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