Skip to main content

ide/inlay_hints/
chaining.rs

1//! Implementation of "chaining" inlay hints.
2use hir::DisplayTarget;
3use ide_db::famous_defs::FamousDefs;
4use syntax::{
5    Direction, NodeOrToken, SyntaxKind, T, TextRange,
6    ast::{self, AstNode},
7};
8
9use crate::{InlayHint, InlayHintPosition, InlayHintsConfig, InlayKind};
10
11use super::{TypeHintsPlacement, label_of_ty};
12
13pub(super) fn hints(
14    acc: &mut Vec<InlayHint>,
15    famous_defs @ FamousDefs(sema, _): &FamousDefs<'_, '_>,
16    config: &InlayHintsConfig<'_>,
17    display_target: DisplayTarget,
18    expr: &ast::Expr,
19) -> Option<()> {
20    if !config.chaining_hints {
21        return None;
22    }
23
24    if matches!(expr, ast::Expr::RecordExpr(_)) {
25        return None;
26    }
27
28    let descended = sema.descend_node_into_attributes(expr.clone()).pop();
29    let desc_expr = descended.as_ref().unwrap_or(expr);
30
31    let mut tokens = expr
32        .syntax()
33        .siblings_with_tokens(Direction::Next)
34        .filter_map(NodeOrToken::into_token)
35        .filter(|t| match t.kind() {
36            SyntaxKind::WHITESPACE if !t.text().contains('\n') => false,
37            SyntaxKind::COMMENT | SyntaxKind::OUTER_DOC_COMMENT | SyntaxKind::INNER_DOC_COMMENT => {
38                false
39            }
40            _ => true,
41        });
42
43    // Chaining can be defined as an expression whose next sibling tokens are newline and dot
44    // Ignoring extra whitespace and comments
45    let next_token = tokens.next()?;
46    if next_token.kind() == SyntaxKind::WHITESPACE {
47        let newline_token = next_token;
48        let mut next_next = tokens.next()?;
49        while next_next.kind() == SyntaxKind::WHITESPACE {
50            next_next = tokens.next()?;
51        }
52        if next_next.kind() == T![.] {
53            let ty = sema.type_of_expr(desc_expr)?.original;
54            if ty.is_unknown() {
55                return None;
56            }
57            if matches!(expr, ast::Expr::PathExpr(_))
58                && let Some(hir::Adt::Struct(st)) = ty.as_adt()
59                && st.fields(sema.db).is_empty()
60            {
61                return None;
62            }
63            let label = label_of_ty(famous_defs, config, &ty, display_target)?;
64            let range = {
65                let mut range = expr.syntax().text_range();
66                if config.type_hints_placement == TypeHintsPlacement::EndOfLine {
67                    range = TextRange::new(
68                        range.start(),
69                        newline_token.text_range().start().max(range.end()),
70                    );
71                }
72                range
73            };
74            acc.push(InlayHint {
75                range,
76                kind: InlayKind::Chaining,
77                label,
78                text_edit: None,
79                position: InlayHintPosition::After,
80                pad_left: true,
81                pad_right: false,
82                resolve_parent: Some(expr.syntax().text_range()),
83            });
84        }
85    }
86    Some(())
87}
88
89#[cfg(test)]
90mod tests {
91    use expect_test::{Expect, expect};
92    use ide_db::text_edit::{TextRange, TextSize};
93
94    use crate::{
95        InlayHintsConfig, TypeHintsPlacement, fixture,
96        inlay_hints::{
97            LazyProperty,
98            tests::{DISABLED_CONFIG, TEST_CONFIG, check_expect, check_with_config},
99        },
100    };
101
102    #[track_caller]
103    fn check_chains(#[rust_analyzer::rust_fixture] ra_fixture: &str) {
104        check_with_config(InlayHintsConfig { chaining_hints: true, ..DISABLED_CONFIG }, ra_fixture);
105    }
106
107    #[track_caller]
108    pub(super) fn check_expect_clear_loc(
109        config: InlayHintsConfig<'_>,
110        #[rust_analyzer::rust_fixture] ra_fixture: &str,
111        expect: Expect,
112    ) {
113        let (analysis, file_id) = fixture::file(ra_fixture);
114        let mut inlay_hints = analysis.inlay_hints(&config, file_id, None).unwrap();
115        inlay_hints.iter_mut().flat_map(|hint| &mut hint.label.parts).for_each(|hint| {
116            if let Some(LazyProperty::Computed(loc)) = &mut hint.linked_location {
117                loc.range = TextRange::empty(TextSize::from(0));
118            }
119        });
120        let filtered =
121            inlay_hints.into_iter().map(|hint| (hint.range, hint.label)).collect::<Vec<_>>();
122        expect.assert_debug_eq(&filtered)
123    }
124
125    #[test]
126    fn chaining_hints_ignore_comments() {
127        check_expect(
128            InlayHintsConfig { type_hints: false, chaining_hints: true, ..DISABLED_CONFIG },
129            r#"
130struct A(B);
131impl A { fn into_b(self) -> B { self.0 } }
132struct B(C);
133impl B { fn into_c(self) -> C { self.0 } }
134struct C;
135
136fn main() {
137    let c = A(B(C))
138        .into_b() // This is a comment
139        // This is another comment
140        .into_c();
141}
142"#,
143            expect![[r#"
144                [
145                    (
146                        147..172,
147                        [
148                            InlayHintLabelPart {
149                                text: "B",
150                                linked_location: Some(
151                                    Computed(
152                                        FileRangeWrapper {
153                                            file_id: FileId(
154                                                0,
155                                            ),
156                                            range: 63..64,
157                                        },
158                                    ),
159                                ),
160                                tooltip: "",
161                            },
162                        ],
163                    ),
164                    (
165                        147..154,
166                        [
167                            InlayHintLabelPart {
168                                text: "A",
169                                linked_location: Some(
170                                    Computed(
171                                        FileRangeWrapper {
172                                            file_id: FileId(
173                                                0,
174                                            ),
175                                            range: 7..8,
176                                        },
177                                    ),
178                                ),
179                                tooltip: "",
180                            },
181                        ],
182                    ),
183                ]
184            "#]],
185        );
186    }
187
188    #[test]
189    fn chaining_hints_without_newlines() {
190        check_chains(
191            r#"
192struct A(B);
193impl A { fn into_b(self) -> B { self.0 } }
194struct B(C);
195impl B { fn into_c(self) -> C { self.0 } }
196struct C;
197
198fn main() {
199    let c = A(B(C)).into_b().into_c();
200}"#,
201        );
202    }
203
204    #[test]
205    fn disabled_location_links() {
206        check_expect(
207            InlayHintsConfig { chaining_hints: true, ..DISABLED_CONFIG },
208            r#"
209    struct A { pub b: B }
210    struct B { pub c: C }
211    struct C(pub bool);
212    struct D;
213
214    impl D {
215        fn foo(&self) -> i32 { 42 }
216    }
217
218    fn main() {
219        let x = A { b: B { c: C(true) } }
220            .b
221            .c
222            .0;
223        let x = D
224            .foo();
225    }"#,
226            expect![[r#"
227                [
228                    (
229                        143..190,
230                        [
231                            InlayHintLabelPart {
232                                text: "C",
233                                linked_location: Some(
234                                    Computed(
235                                        FileRangeWrapper {
236                                            file_id: FileId(
237                                                0,
238                                            ),
239                                            range: 51..52,
240                                        },
241                                    ),
242                                ),
243                                tooltip: "",
244                            },
245                        ],
246                    ),
247                    (
248                        143..179,
249                        [
250                            InlayHintLabelPart {
251                                text: "B",
252                                linked_location: Some(
253                                    Computed(
254                                        FileRangeWrapper {
255                                            file_id: FileId(
256                                                0,
257                                            ),
258                                            range: 29..30,
259                                        },
260                                    ),
261                                ),
262                                tooltip: "",
263                            },
264                        ],
265                    ),
266                ]
267            "#]],
268        );
269    }
270
271    #[test]
272    fn struct_access_chaining_hints() {
273        check_expect(
274            InlayHintsConfig { chaining_hints: true, ..DISABLED_CONFIG },
275            r#"
276struct A { pub b: B }
277struct B { pub c: C }
278struct C(pub bool);
279struct D;
280
281impl D {
282    fn foo(&self) -> i32 { 42 }
283}
284
285fn main() {
286    let x = A { b: B { c: C(true) } }
287        .b
288        .c
289        .0;
290    let x = D
291        .foo();
292}"#,
293            expect![[r#"
294                [
295                    (
296                        143..190,
297                        [
298                            InlayHintLabelPart {
299                                text: "C",
300                                linked_location: Some(
301                                    Computed(
302                                        FileRangeWrapper {
303                                            file_id: FileId(
304                                                0,
305                                            ),
306                                            range: 51..52,
307                                        },
308                                    ),
309                                ),
310                                tooltip: "",
311                            },
312                        ],
313                    ),
314                    (
315                        143..179,
316                        [
317                            InlayHintLabelPart {
318                                text: "B",
319                                linked_location: Some(
320                                    Computed(
321                                        FileRangeWrapper {
322                                            file_id: FileId(
323                                                0,
324                                            ),
325                                            range: 29..30,
326                                        },
327                                    ),
328                                ),
329                                tooltip: "",
330                            },
331                        ],
332                    ),
333                ]
334            "#]],
335        );
336    }
337
338    #[test]
339    fn generic_chaining_hints() {
340        check_expect(
341            InlayHintsConfig { chaining_hints: true, ..DISABLED_CONFIG },
342            r#"
343struct A<T>(T);
344struct B<T>(T);
345struct C<T>(T);
346struct X<T,R>(T, R);
347
348impl<T> A<T> {
349    fn new(t: T) -> Self { A(t) }
350    fn into_b(self) -> B<T> { B(self.0) }
351}
352impl<T> B<T> {
353    fn into_c(self) -> C<T> { C(self.0) }
354}
355fn main() {
356    let c = A::new(X(42, true))
357        .into_b()
358        .into_c();
359}
360"#,
361            expect![[r#"
362                [
363                    (
364                        246..283,
365                        [
366                            InlayHintLabelPart {
367                                text: "B",
368                                linked_location: Some(
369                                    Computed(
370                                        FileRangeWrapper {
371                                            file_id: FileId(
372                                                0,
373                                            ),
374                                            range: 23..24,
375                                        },
376                                    ),
377                                ),
378                                tooltip: "",
379                            },
380                            "<",
381                            InlayHintLabelPart {
382                                text: "X",
383                                linked_location: Some(
384                                    Computed(
385                                        FileRangeWrapper {
386                                            file_id: FileId(
387                                                0,
388                                            ),
389                                            range: 55..56,
390                                        },
391                                    ),
392                                ),
393                                tooltip: "",
394                            },
395                            "<i32, bool>>",
396                        ],
397                    ),
398                    (
399                        246..265,
400                        [
401                            InlayHintLabelPart {
402                                text: "A",
403                                linked_location: Some(
404                                    Computed(
405                                        FileRangeWrapper {
406                                            file_id: FileId(
407                                                0,
408                                            ),
409                                            range: 7..8,
410                                        },
411                                    ),
412                                ),
413                                tooltip: "",
414                            },
415                            "<",
416                            InlayHintLabelPart {
417                                text: "X",
418                                linked_location: Some(
419                                    Computed(
420                                        FileRangeWrapper {
421                                            file_id: FileId(
422                                                0,
423                                            ),
424                                            range: 55..56,
425                                        },
426                                    ),
427                                ),
428                                tooltip: "",
429                            },
430                            "<i32, bool>>",
431                        ],
432                    ),
433                ]
434            "#]],
435        );
436    }
437
438    #[test]
439    fn shorten_iterator_chaining_hints() {
440        check_expect_clear_loc(
441            InlayHintsConfig { chaining_hints: true, ..DISABLED_CONFIG },
442            r#"
443//- minicore: iterators
444use core::iter;
445
446struct MyIter;
447
448impl Iterator for MyIter {
449    type Item = ();
450    fn next(&mut self) -> Option<Self::Item> {
451        None
452    }
453}
454
455fn main() {
456    let _x = MyIter.by_ref()
457        .take(5)
458        .by_ref()
459        .take(5)
460        .by_ref();
461}
462"#,
463            expect![[r#"
464                [
465                    (
466                        174..241,
467                        [
468                            "impl ",
469                            InlayHintLabelPart {
470                                text: "Iterator",
471                                linked_location: Some(
472                                    Computed(
473                                        FileRangeWrapper {
474                                            file_id: FileId(
475                                                1,
476                                            ),
477                                            range: 0..0,
478                                        },
479                                    ),
480                                ),
481                                tooltip: "",
482                            },
483                            "<",
484                            InlayHintLabelPart {
485                                text: "Item",
486                                linked_location: Some(
487                                    Computed(
488                                        FileRangeWrapper {
489                                            file_id: FileId(
490                                                1,
491                                            ),
492                                            range: 0..0,
493                                        },
494                                    ),
495                                ),
496                                tooltip: "",
497                            },
498                            " = ()>",
499                        ],
500                    ),
501                    (
502                        174..224,
503                        [
504                            "impl ",
505                            InlayHintLabelPart {
506                                text: "Iterator",
507                                linked_location: Some(
508                                    Computed(
509                                        FileRangeWrapper {
510                                            file_id: FileId(
511                                                1,
512                                            ),
513                                            range: 0..0,
514                                        },
515                                    ),
516                                ),
517                                tooltip: "",
518                            },
519                            "<",
520                            InlayHintLabelPart {
521                                text: "Item",
522                                linked_location: Some(
523                                    Computed(
524                                        FileRangeWrapper {
525                                            file_id: FileId(
526                                                1,
527                                            ),
528                                            range: 0..0,
529                                        },
530                                    ),
531                                ),
532                                tooltip: "",
533                            },
534                            " = ()>",
535                        ],
536                    ),
537                    (
538                        174..206,
539                        [
540                            "impl ",
541                            InlayHintLabelPart {
542                                text: "Iterator",
543                                linked_location: Some(
544                                    Computed(
545                                        FileRangeWrapper {
546                                            file_id: FileId(
547                                                1,
548                                            ),
549                                            range: 0..0,
550                                        },
551                                    ),
552                                ),
553                                tooltip: "",
554                            },
555                            "<",
556                            InlayHintLabelPart {
557                                text: "Item",
558                                linked_location: Some(
559                                    Computed(
560                                        FileRangeWrapper {
561                                            file_id: FileId(
562                                                1,
563                                            ),
564                                            range: 0..0,
565                                        },
566                                    ),
567                                ),
568                                tooltip: "",
569                            },
570                            " = ()>",
571                        ],
572                    ),
573                    (
574                        174..189,
575                        [
576                            "&mut ",
577                            InlayHintLabelPart {
578                                text: "MyIter",
579                                linked_location: Some(
580                                    Computed(
581                                        FileRangeWrapper {
582                                            file_id: FileId(
583                                                0,
584                                            ),
585                                            range: 0..0,
586                                        },
587                                    ),
588                                ),
589                                tooltip: "",
590                            },
591                        ],
592                    ),
593                ]
594            "#]],
595        );
596    }
597
598    #[test]
599    fn hints_in_attr_call() {
600        check_expect(
601            TEST_CONFIG,
602            r#"
603//- proc_macros: identity, input_replace
604struct Struct;
605impl Struct {
606    fn chain(self) -> Self {
607        self
608    }
609}
610#[proc_macros::identity]
611fn main() {
612    let strukt = Struct;
613    strukt
614        .chain()
615        .chain()
616        .chain();
617    Struct::chain(strukt);
618}
619"#,
620            expect![[r#"
621                [
622                    (
623                        124..130,
624                        [
625                            InlayHintLabelPart {
626                                text: "Struct",
627                                linked_location: Some(
628                                    Computed(
629                                        FileRangeWrapper {
630                                            file_id: FileId(
631                                                0,
632                                            ),
633                                            range: 7..13,
634                                        },
635                                    ),
636                                ),
637                                tooltip: "",
638                            },
639                        ],
640                    ),
641                    (
642                        145..185,
643                        [
644                            InlayHintLabelPart {
645                                text: "Struct",
646                                linked_location: Some(
647                                    Computed(
648                                        FileRangeWrapper {
649                                            file_id: FileId(
650                                                0,
651                                            ),
652                                            range: 7..13,
653                                        },
654                                    ),
655                                ),
656                                tooltip: "",
657                            },
658                        ],
659                    ),
660                    (
661                        145..168,
662                        [
663                            InlayHintLabelPart {
664                                text: "Struct",
665                                linked_location: Some(
666                                    Computed(
667                                        FileRangeWrapper {
668                                            file_id: FileId(
669                                                0,
670                                            ),
671                                            range: 7..13,
672                                        },
673                                    ),
674                                ),
675                                tooltip: "",
676                            },
677                        ],
678                    ),
679                    (
680                        222..228,
681                        [
682                            InlayHintLabelPart {
683                                text: "self",
684                                linked_location: Some(
685                                    Computed(
686                                        FileRangeWrapper {
687                                            file_id: FileId(
688                                                0,
689                                            ),
690                                            range: 42..46,
691                                        },
692                                    ),
693                                ),
694                                tooltip: "",
695                            },
696                        ],
697                    ),
698                ]
699            "#]],
700        );
701    }
702
703    #[test]
704    fn chaining_hints_end_of_line_placement() {
705        check_expect(
706            InlayHintsConfig {
707                chaining_hints: true,
708                type_hints_placement: TypeHintsPlacement::EndOfLine,
709                ..DISABLED_CONFIG
710            },
711            r#"
712fn main() {
713    let baz = make()
714        .into_bar()
715        .into_baz();
716}
717
718struct Foo;
719struct Bar;
720struct Baz;
721
722impl Foo {
723    fn into_bar(self) -> Bar { Bar }
724}
725
726impl Bar {
727    fn into_baz(self) -> Baz { Baz }
728}
729
730fn make() -> Foo {
731    Foo
732}
733"#,
734            expect![[r#"
735                [
736                    (
737                        26..52,
738                        [
739                            InlayHintLabelPart {
740                                text: "Bar",
741                                linked_location: Some(
742                                    Computed(
743                                        FileRangeWrapper {
744                                            file_id: FileId(
745                                                0,
746                                            ),
747                                            range: 96..99,
748                                        },
749                                    ),
750                                ),
751                                tooltip: "",
752                            },
753                        ],
754                    ),
755                    (
756                        26..32,
757                        [
758                            InlayHintLabelPart {
759                                text: "Foo",
760                                linked_location: Some(
761                                    Computed(
762                                        FileRangeWrapper {
763                                            file_id: FileId(
764                                                0,
765                                            ),
766                                            range: 84..87,
767                                        },
768                                    ),
769                                ),
770                                tooltip: "",
771                            },
772                        ],
773                    ),
774                ]
775            "#]],
776        );
777    }
778}