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}