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