Skip to main content

nifty_lsp/features/
navigation.rs

1//! Go to definition, find references and highlighting the name under the cursor. Top-level
2//! names are shared by every file of a project, so definitions and references can be in
3//! other files.
4
5use lsp_types::{DocumentHighlight, Location};
6
7use crate::document::Encoding;
8use crate::index::{Occurrence, Role, SymbolId};
9use crate::project::Cursor;
10
11/// Where the name at the cursor is declared.
12pub(crate) fn definition(cursor: Cursor, encoding: Encoding) -> Option<Location> {
13    let symbol = symbol_at(cursor)?;
14    let declaration = cursor.project.index.declaration_of(symbol)?;
15    Some(cursor.project.location(declaration, encoding))
16}
17
18/// Every use of the name at the cursor, in any file, with its declaration when asked for.
19pub(crate) fn references(
20    cursor: Cursor,
21    include_declaration: bool,
22    encoding: Encoding,
23) -> Option<Vec<Location>> {
24    let locations = occurrences(cursor)?
25        .filter(|occurrence| include_declaration || matches!(occurrence.role, Role::Reference(_)))
26        .map(|occurrence| cursor.project.location(occurrence.span, encoding))
27        .collect();
28    Some(locations)
29}
30
31/// The declaration and uses of the name at the cursor within its own file.
32pub(crate) fn highlights(cursor: Cursor, encoding: Encoding) -> Option<Vec<DocumentHighlight>> {
33    let document = cursor.document();
34    let highlights = occurrences(cursor)?
35        .filter(|occurrence| occurrence.span.file == cursor.file)
36        .map(|occurrence| DocumentHighlight {
37            range: document.range(occurrence.span, encoding),
38            kind: None,
39        })
40        .collect();
41    Some(highlights)
42}
43
44fn occurrences(cursor: Cursor) -> Option<impl Iterator<Item = Occurrence>> {
45    let symbol = symbol_at(cursor)?;
46    Some(cursor.project.index.occurrences_of(symbol))
47}
48
49fn symbol_at(cursor: Cursor) -> Option<SymbolId> {
50    cursor
51        .project
52        .index
53        .occurrence_at(cursor.file, cursor.offset)?
54        .role
55        .symbol()
56}
57
58#[cfg(test)]
59mod tests {
60    use lsp_types::{Position, Range, Uri};
61    use nifty_syntax::FileId;
62
63    use super::*;
64    use crate::project::{Project, Source, test_uri};
65
66    const SOURCE: &str = "state n = 0\nfn f() { n += 1 }";
67
68    fn start(location: Location) -> (Uri, u32, u32) {
69        let Range { start, .. } = location.range;
70        (location.uri, start.line, start.character)
71    }
72
73    fn cursor(project: &Project, file: usize, position: Position) -> Cursor<'_> {
74        let file = FileId::new(file);
75        let offset = project.document(file).offset(position, Encoding::Utf16);
76        Cursor {
77            project,
78            file,
79            offset,
80        }
81    }
82
83    #[test]
84    fn jumps_from_a_reference_to_its_declaration() {
85        let project = Project::single(SOURCE);
86        let found = definition(cursor(&project, 0, Position::new(1, 9)), Encoding::Utf16);
87        assert_eq!(found.map(start), Some((test_uri("main.nf"), 0, 6)));
88    }
89
90    #[test]
91    fn lists_references_with_or_without_the_declaration() {
92        let project = Project::single(SOURCE);
93        let starts = |include_declaration| {
94            references(project.cursor(6), include_declaration, Encoding::Utf16).map(|found| {
95                found
96                    .into_iter()
97                    .map(|location| {
98                        let (_, line, column) = start(location);
99                        (line, column)
100                    })
101                    .collect::<Vec<_>>()
102            })
103        };
104        assert_eq!(starts(true), Some(vec![(0, 6), (1, 9)]));
105        assert_eq!(starts(false), Some(vec![(1, 9)]));
106    }
107
108    #[test]
109    fn follows_names_into_other_files() {
110        let project = Project::new(vec![
111            Source {
112                uri: test_uri("main.nf"),
113                text: "window(\"T\") {\n  Header()\n}".to_owned(),
114            },
115            Source {
116                uri: test_uri("ui/header.nf"),
117                text: "view Header() {}".to_owned(),
118            },
119        ]);
120        let use_site = cursor(&project, 0, Position::new(1, 3));
121        assert_eq!(
122            definition(use_site, Encoding::Utf16).map(start),
123            Some((test_uri("ui/header.nf"), 0, 5))
124        );
125        let everywhere = references(use_site, true, Encoding::Utf16)
126            .map(|found| found.into_iter().map(start).collect::<Vec<_>>());
127        assert_eq!(
128            everywhere,
129            Some(vec![
130                (test_uri("main.nf"), 1, 2),
131                (test_uri("ui/header.nf"), 0, 5)
132            ])
133        );
134        let in_main = highlights(use_site, Encoding::Utf16).map(|found| found.len());
135        assert_eq!(in_main, Some(1));
136    }
137
138    #[test]
139    fn finds_nothing_for_built_ins() {
140        let project = Project::single("label(\"hi\")");
141        assert_eq!(definition(project.cursor(2), Encoding::Utf16), None);
142    }
143}