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