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