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