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}