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