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