ide/annotations.rs
1use hir::{HasSource, InFile, InRealFile, Semantics};
2use ide_db::{
3 FileId, FilePosition, FileRange, FxIndexSet, RootDatabase, defs::Definition,
4 helpers::visit_file_defs,
5};
6use itertools::Itertools;
7use syntax::{AstNode, TextRange, ast::HasName};
8
9use crate::{
10 NavigationTarget, RunnableKind,
11 annotations::fn_references::find_all_methods,
12 goto_implementation::goto_implementation,
13 references::find_all_refs,
14 runnables::{Runnable, runnables},
15};
16
17mod fn_references;
18
19// Feature: Annotations
20//
21// Provides user with annotations above items for looking up references or impl blocks
22// and running/debugging binaries.
23//
24// 
25#[derive(Debug, Hash, PartialEq, Eq)]
26pub struct Annotation {
27 pub range: TextRange,
28 pub kind: AnnotationKind,
29}
30
31#[derive(Debug, Hash, PartialEq, Eq)]
32pub enum AnnotationKind {
33 Runnable(Runnable),
34 HasImpls { pos: FilePosition, data: Option<Vec<NavigationTarget>> },
35 HasReferences { pos: FilePosition, data: Option<Vec<FileRange>> },
36}
37
38pub struct AnnotationConfig {
39 pub binary_target: bool,
40 pub annotate_runnables: bool,
41 pub annotate_impls: bool,
42 pub annotate_references: bool,
43 pub annotate_method_references: bool,
44 pub annotate_enum_variant_references: bool,
45 pub location: AnnotationLocation,
46}
47
48pub enum AnnotationLocation {
49 AboveName,
50 AboveWholeItem,
51}
52
53pub(crate) fn annotations(
54 db: &RootDatabase,
55 config: &AnnotationConfig,
56 file_id: FileId,
57) -> Vec<Annotation> {
58 let mut annotations = FxIndexSet::default();
59
60 if config.annotate_runnables {
61 for runnable in runnables(db, file_id) {
62 if should_skip_runnable(&runnable.kind, config.binary_target) {
63 continue;
64 }
65
66 let range = runnable.nav.focus_or_full_range();
67
68 annotations.insert(Annotation { range, kind: AnnotationKind::Runnable(runnable) });
69 }
70 }
71
72 let mk_ranges = |(range, focus): (_, Option<_>)| {
73 let cmd_target: TextRange = focus.unwrap_or(range);
74 let annotation_range = match config.location {
75 AnnotationLocation::AboveName => cmd_target,
76 AnnotationLocation::AboveWholeItem => range,
77 };
78 let target_pos = FilePosition { file_id, offset: cmd_target.start() };
79 (annotation_range, target_pos)
80 };
81
82 visit_file_defs(&Semantics::new(db), file_id, &mut |def| {
83 let range = match def {
84 Definition::Const(konst) if config.annotate_references => {
85 konst.source(db).and_then(|node| name_range(db, node, file_id))
86 }
87 Definition::Trait(trait_) if config.annotate_references || config.annotate_impls => {
88 trait_.source(db).and_then(|node| name_range(db, node, file_id))
89 }
90 Definition::Adt(adt) => match adt {
91 hir::Adt::Enum(enum_) => {
92 if config.annotate_enum_variant_references {
93 enum_
94 .variants(db)
95 .into_iter()
96 .filter_map(|variant| {
97 variant.source(db).and_then(|node| name_range(db, node, file_id))
98 })
99 .for_each(|range| {
100 let (annotation_range, target_position) = mk_ranges(range);
101 annotations.insert(Annotation {
102 range: annotation_range,
103 kind: AnnotationKind::HasReferences {
104 pos: target_position,
105 data: None,
106 },
107 });
108 })
109 }
110 if config.annotate_references || config.annotate_impls {
111 enum_.source(db).and_then(|node| name_range(db, node, file_id))
112 } else {
113 None
114 }
115 }
116 _ => {
117 if config.annotate_references || config.annotate_impls {
118 adt.source(db).and_then(|node| name_range(db, node, file_id))
119 } else {
120 None
121 }
122 }
123 },
124 _ => None,
125 };
126
127 let range = match range {
128 Some(range) => range,
129 None => return,
130 };
131 let (annotation_range, target_pos) = mk_ranges(range);
132 if config.annotate_impls && !matches!(def, Definition::Const(_)) {
133 annotations.insert(Annotation {
134 range: annotation_range,
135 kind: AnnotationKind::HasImpls { pos: target_pos, data: None },
136 });
137 }
138
139 if config.annotate_references {
140 annotations.insert(Annotation {
141 range: annotation_range,
142 kind: AnnotationKind::HasReferences { pos: target_pos, data: None },
143 });
144 }
145
146 fn name_range<T: HasName>(
147 db: &RootDatabase,
148 node: InFile<T>,
149 source_file_id: FileId,
150 ) -> Option<(TextRange, Option<TextRange>)> {
151 if let Some(InRealFile { file_id, value }) = node.original_ast_node_rooted(db) {
152 if file_id.file_id(db) == source_file_id {
153 return Some((
154 value.syntax().text_range(),
155 value.name().map(|name| name.syntax().text_range()),
156 ));
157 }
158 }
159 None
160 }
161 });
162
163 if config.annotate_method_references {
164 annotations.extend(find_all_methods(db, file_id).into_iter().map(|range| {
165 let (annotation_range, target_range) = mk_ranges(range);
166 Annotation {
167 range: annotation_range,
168 kind: AnnotationKind::HasReferences { pos: target_range, data: None },
169 }
170 }));
171 }
172
173 annotations
174 .into_iter()
175 .sorted_by_key(|a| {
176 (a.range.start(), a.range.end(), matches!(a.kind, AnnotationKind::Runnable(..)))
177 })
178 .collect()
179}
180
181pub(crate) fn resolve_annotation(db: &RootDatabase, mut annotation: Annotation) -> Annotation {
182 match annotation.kind {
183 AnnotationKind::HasImpls { pos, ref mut data } => {
184 *data = goto_implementation(db, pos).map(|range| range.info);
185 }
186 AnnotationKind::HasReferences { pos, ref mut data } => {
187 *data = find_all_refs(&Semantics::new(db), pos, None).map(|result| {
188 result
189 .into_iter()
190 .flat_map(|res| res.references)
191 .flat_map(|(file_id, access)| {
192 access.into_iter().map(move |(range, _)| FileRange { file_id, range })
193 })
194 .collect()
195 });
196 }
197 _ => {}
198 };
199
200 annotation
201}
202
203fn should_skip_runnable(kind: &RunnableKind, binary_target: bool) -> bool {
204 match kind {
205 RunnableKind::Bin => !binary_target,
206 _ => false,
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use expect_test::{Expect, expect};
213
214 use crate::{Annotation, AnnotationConfig, fixture};
215
216 use super::AnnotationLocation;
217
218 const DEFAULT_CONFIG: AnnotationConfig = AnnotationConfig {
219 binary_target: true,
220 annotate_runnables: true,
221 annotate_impls: true,
222 annotate_references: true,
223 annotate_method_references: true,
224 annotate_enum_variant_references: true,
225 location: AnnotationLocation::AboveName,
226 };
227
228 fn check_with_config(
229 #[rust_analyzer::rust_fixture] ra_fixture: &str,
230 expect: Expect,
231 config: &AnnotationConfig,
232 ) {
233 let (analysis, file_id) = fixture::file(ra_fixture);
234
235 let annotations: Vec<Annotation> = analysis
236 .annotations(config, file_id)
237 .unwrap()
238 .into_iter()
239 .map(|annotation| analysis.resolve_annotation(annotation).unwrap())
240 .collect();
241
242 expect.assert_debug_eq(&annotations);
243 }
244
245 fn check(#[rust_analyzer::rust_fixture] ra_fixture: &str, expect: Expect) {
246 check_with_config(ra_fixture, expect, &DEFAULT_CONFIG);
247 }
248
249 #[test]
250 fn const_annotations() {
251 check(
252 r#"
253const DEMO: i32 = 123;
254
255const UNUSED: i32 = 123;
256
257fn main() {
258 let hello = DEMO;
259}
260 "#,
261 expect![[r#"
262 [
263 Annotation {
264 range: 6..10,
265 kind: HasReferences {
266 pos: FilePositionWrapper {
267 file_id: FileId(
268 0,
269 ),
270 offset: 6,
271 },
272 data: Some(
273 [
274 FileRangeWrapper {
275 file_id: FileId(
276 0,
277 ),
278 range: 78..82,
279 },
280 ],
281 ),
282 },
283 },
284 Annotation {
285 range: 30..36,
286 kind: HasReferences {
287 pos: FilePositionWrapper {
288 file_id: FileId(
289 0,
290 ),
291 offset: 30,
292 },
293 data: Some(
294 [],
295 ),
296 },
297 },
298 Annotation {
299 range: 53..57,
300 kind: HasReferences {
301 pos: FilePositionWrapper {
302 file_id: FileId(
303 0,
304 ),
305 offset: 53,
306 },
307 data: Some(
308 [],
309 ),
310 },
311 },
312 Annotation {
313 range: 53..57,
314 kind: Runnable(
315 Runnable {
316 use_name_in_title: false,
317 nav: NavigationTarget {
318 file_id: FileId(
319 0,
320 ),
321 full_range: 50..85,
322 focus_range: 53..57,
323 name: "main",
324 kind: Function,
325 },
326 kind: Bin,
327 cfg: None,
328 update_test: UpdateTest {
329 expect_test: false,
330 insta: false,
331 snapbox: false,
332 },
333 },
334 ),
335 },
336 ]
337 "#]],
338 );
339 }
340
341 #[test]
342 fn struct_references_annotations() {
343 check(
344 r#"
345struct Test;
346
347fn main() {
348 let test = Test;
349}
350 "#,
351 expect![[r#"
352 [
353 Annotation {
354 range: 7..11,
355 kind: HasImpls {
356 pos: FilePositionWrapper {
357 file_id: FileId(
358 0,
359 ),
360 offset: 7,
361 },
362 data: Some(
363 [],
364 ),
365 },
366 },
367 Annotation {
368 range: 7..11,
369 kind: HasReferences {
370 pos: FilePositionWrapper {
371 file_id: FileId(
372 0,
373 ),
374 offset: 7,
375 },
376 data: Some(
377 [
378 FileRangeWrapper {
379 file_id: FileId(
380 0,
381 ),
382 range: 41..45,
383 },
384 ],
385 ),
386 },
387 },
388 Annotation {
389 range: 17..21,
390 kind: HasReferences {
391 pos: FilePositionWrapper {
392 file_id: FileId(
393 0,
394 ),
395 offset: 17,
396 },
397 data: Some(
398 [],
399 ),
400 },
401 },
402 Annotation {
403 range: 17..21,
404 kind: Runnable(
405 Runnable {
406 use_name_in_title: false,
407 nav: NavigationTarget {
408 file_id: FileId(
409 0,
410 ),
411 full_range: 14..48,
412 focus_range: 17..21,
413 name: "main",
414 kind: Function,
415 },
416 kind: Bin,
417 cfg: None,
418 update_test: UpdateTest {
419 expect_test: false,
420 insta: false,
421 snapbox: false,
422 },
423 },
424 ),
425 },
426 ]
427 "#]],
428 );
429 }
430
431 #[test]
432 fn struct_and_trait_impls_annotations() {
433 check(
434 r#"
435struct Test;
436
437trait MyCoolTrait {}
438
439impl MyCoolTrait for Test {}
440
441fn main() {
442 let test = Test;
443}
444 "#,
445 expect![[r#"
446 [
447 Annotation {
448 range: 7..11,
449 kind: HasImpls {
450 pos: FilePositionWrapper {
451 file_id: FileId(
452 0,
453 ),
454 offset: 7,
455 },
456 data: Some(
457 [
458 NavigationTarget {
459 file_id: FileId(
460 0,
461 ),
462 full_range: 36..64,
463 focus_range: 57..61,
464 name: "impl",
465 kind: Impl,
466 },
467 ],
468 ),
469 },
470 },
471 Annotation {
472 range: 7..11,
473 kind: HasReferences {
474 pos: FilePositionWrapper {
475 file_id: FileId(
476 0,
477 ),
478 offset: 7,
479 },
480 data: Some(
481 [
482 FileRangeWrapper {
483 file_id: FileId(
484 0,
485 ),
486 range: 57..61,
487 },
488 FileRangeWrapper {
489 file_id: FileId(
490 0,
491 ),
492 range: 93..97,
493 },
494 ],
495 ),
496 },
497 },
498 Annotation {
499 range: 20..31,
500 kind: HasImpls {
501 pos: FilePositionWrapper {
502 file_id: FileId(
503 0,
504 ),
505 offset: 20,
506 },
507 data: Some(
508 [
509 NavigationTarget {
510 file_id: FileId(
511 0,
512 ),
513 full_range: 36..64,
514 focus_range: 57..61,
515 name: "impl",
516 kind: Impl,
517 },
518 ],
519 ),
520 },
521 },
522 Annotation {
523 range: 20..31,
524 kind: HasReferences {
525 pos: FilePositionWrapper {
526 file_id: FileId(
527 0,
528 ),
529 offset: 20,
530 },
531 data: Some(
532 [
533 FileRangeWrapper {
534 file_id: FileId(
535 0,
536 ),
537 range: 41..52,
538 },
539 ],
540 ),
541 },
542 },
543 Annotation {
544 range: 69..73,
545 kind: HasReferences {
546 pos: FilePositionWrapper {
547 file_id: FileId(
548 0,
549 ),
550 offset: 69,
551 },
552 data: Some(
553 [],
554 ),
555 },
556 },
557 Annotation {
558 range: 69..73,
559 kind: Runnable(
560 Runnable {
561 use_name_in_title: false,
562 nav: NavigationTarget {
563 file_id: FileId(
564 0,
565 ),
566 full_range: 66..100,
567 focus_range: 69..73,
568 name: "main",
569 kind: Function,
570 },
571 kind: Bin,
572 cfg: None,
573 update_test: UpdateTest {
574 expect_test: false,
575 insta: false,
576 snapbox: false,
577 },
578 },
579 ),
580 },
581 ]
582 "#]],
583 );
584 }
585
586 #[test]
587 fn runnable_annotation() {
588 check(
589 r#"
590fn main() {}
591 "#,
592 expect![[r#"
593 [
594 Annotation {
595 range: 3..7,
596 kind: HasReferences {
597 pos: FilePositionWrapper {
598 file_id: FileId(
599 0,
600 ),
601 offset: 3,
602 },
603 data: Some(
604 [],
605 ),
606 },
607 },
608 Annotation {
609 range: 3..7,
610 kind: Runnable(
611 Runnable {
612 use_name_in_title: false,
613 nav: NavigationTarget {
614 file_id: FileId(
615 0,
616 ),
617 full_range: 0..12,
618 focus_range: 3..7,
619 name: "main",
620 kind: Function,
621 },
622 kind: Bin,
623 cfg: None,
624 update_test: UpdateTest {
625 expect_test: false,
626 insta: false,
627 snapbox: false,
628 },
629 },
630 ),
631 },
632 ]
633 "#]],
634 );
635 }
636
637 #[test]
638 fn method_annotations() {
639 check(
640 r#"
641struct Test;
642
643impl Test {
644 fn self_by_ref(&self) {}
645}
646
647fn main() {
648 Test.self_by_ref();
649}
650 "#,
651 expect![[r#"
652 [
653 Annotation {
654 range: 7..11,
655 kind: HasImpls {
656 pos: FilePositionWrapper {
657 file_id: FileId(
658 0,
659 ),
660 offset: 7,
661 },
662 data: Some(
663 [
664 NavigationTarget {
665 file_id: FileId(
666 0,
667 ),
668 full_range: 14..56,
669 focus_range: 19..23,
670 name: "impl",
671 kind: Impl,
672 },
673 ],
674 ),
675 },
676 },
677 Annotation {
678 range: 7..11,
679 kind: HasReferences {
680 pos: FilePositionWrapper {
681 file_id: FileId(
682 0,
683 ),
684 offset: 7,
685 },
686 data: Some(
687 [
688 FileRangeWrapper {
689 file_id: FileId(
690 0,
691 ),
692 range: 19..23,
693 },
694 FileRangeWrapper {
695 file_id: FileId(
696 0,
697 ),
698 range: 74..78,
699 },
700 ],
701 ),
702 },
703 },
704 Annotation {
705 range: 33..44,
706 kind: HasReferences {
707 pos: FilePositionWrapper {
708 file_id: FileId(
709 0,
710 ),
711 offset: 33,
712 },
713 data: Some(
714 [
715 FileRangeWrapper {
716 file_id: FileId(
717 0,
718 ),
719 range: 79..90,
720 },
721 ],
722 ),
723 },
724 },
725 Annotation {
726 range: 61..65,
727 kind: HasReferences {
728 pos: FilePositionWrapper {
729 file_id: FileId(
730 0,
731 ),
732 offset: 61,
733 },
734 data: Some(
735 [],
736 ),
737 },
738 },
739 Annotation {
740 range: 61..65,
741 kind: Runnable(
742 Runnable {
743 use_name_in_title: false,
744 nav: NavigationTarget {
745 file_id: FileId(
746 0,
747 ),
748 full_range: 58..95,
749 focus_range: 61..65,
750 name: "main",
751 kind: Function,
752 },
753 kind: Bin,
754 cfg: None,
755 update_test: UpdateTest {
756 expect_test: false,
757 insta: false,
758 snapbox: false,
759 },
760 },
761 ),
762 },
763 ]
764 "#]],
765 );
766 }
767
768 #[test]
769 fn test_annotations() {
770 check(
771 r#"
772fn main() {}
773
774mod tests {
775 #[test]
776 fn my_cool_test() {}
777}
778 "#,
779 expect![[r#"
780 [
781 Annotation {
782 range: 3..7,
783 kind: HasReferences {
784 pos: FilePositionWrapper {
785 file_id: FileId(
786 0,
787 ),
788 offset: 3,
789 },
790 data: Some(
791 [],
792 ),
793 },
794 },
795 Annotation {
796 range: 3..7,
797 kind: Runnable(
798 Runnable {
799 use_name_in_title: false,
800 nav: NavigationTarget {
801 file_id: FileId(
802 0,
803 ),
804 full_range: 0..12,
805 focus_range: 3..7,
806 name: "main",
807 kind: Function,
808 },
809 kind: Bin,
810 cfg: None,
811 update_test: UpdateTest {
812 expect_test: false,
813 insta: false,
814 snapbox: false,
815 },
816 },
817 ),
818 },
819 Annotation {
820 range: 18..23,
821 kind: Runnable(
822 Runnable {
823 use_name_in_title: false,
824 nav: NavigationTarget {
825 file_id: FileId(
826 0,
827 ),
828 full_range: 14..64,
829 focus_range: 18..23,
830 name: "tests",
831 kind: Module,
832 description: "mod tests",
833 },
834 kind: TestMod {
835 path: "tests",
836 },
837 cfg: None,
838 update_test: UpdateTest {
839 expect_test: false,
840 insta: false,
841 snapbox: false,
842 },
843 },
844 ),
845 },
846 Annotation {
847 range: 45..57,
848 kind: Runnable(
849 Runnable {
850 use_name_in_title: false,
851 nav: NavigationTarget {
852 file_id: FileId(
853 0,
854 ),
855 full_range: 30..62,
856 focus_range: 45..57,
857 name: "my_cool_test",
858 kind: Function,
859 },
860 kind: Test {
861 test_id: Path(
862 "tests::my_cool_test",
863 ),
864 attr: TestAttr {
865 ignore: false,
866 },
867 },
868 cfg: None,
869 update_test: UpdateTest {
870 expect_test: false,
871 insta: false,
872 snapbox: false,
873 },
874 },
875 ),
876 },
877 ]
878 "#]],
879 );
880 }
881
882 #[test]
883 fn test_no_annotations_outside_module_tree() {
884 check(
885 r#"
886//- /foo.rs
887struct Foo;
888//- /lib.rs
889// this file comes last since `check` checks the first file only
890"#,
891 expect![[r#"
892 []
893 "#]],
894 );
895 }
896
897 #[test]
898 fn test_no_annotations_macro_struct_def() {
899 check(
900 r#"
901//- /lib.rs
902macro_rules! m {
903 () => {
904 struct A {}
905 };
906}
907
908m!();
909"#,
910 expect![[r#"
911 []
912 "#]],
913 );
914 }
915
916 #[test]
917 fn test_annotations_appear_above_whole_item_when_configured_to_do_so() {
918 check_with_config(
919 r#"
920/// This is a struct named Foo, obviously.
921#[derive(Clone)]
922struct Foo;
923"#,
924 expect![[r#"
925 [
926 Annotation {
927 range: 0..71,
928 kind: HasImpls {
929 pos: FilePositionWrapper {
930 file_id: FileId(
931 0,
932 ),
933 offset: 67,
934 },
935 data: Some(
936 [],
937 ),
938 },
939 },
940 Annotation {
941 range: 0..71,
942 kind: HasReferences {
943 pos: FilePositionWrapper {
944 file_id: FileId(
945 0,
946 ),
947 offset: 67,
948 },
949 data: Some(
950 [],
951 ),
952 },
953 },
954 ]
955 "#]],
956 &AnnotationConfig { location: AnnotationLocation::AboveWholeItem, ..DEFAULT_CONFIG },
957 );
958 }
959}