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