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}