nifty_lsp/features/
navigation.rs1use lsp_types::{DocumentHighlight, Location};
6
7use crate::document::Encoding;
8use crate::index::{Occurrence, Role, SymbolId};
9use crate::project::Cursor;
10
11pub(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
18pub(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
31pub(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}