logicaffeine_lsp/
references.rs1use tower_lsp::lsp_types::{Location, Position, Range, Url};
2
3use crate::document::DocumentState;
4
5pub fn name_at(doc: &DocumentState, position: Position) -> Option<String> {
7 let offset = doc.line_index.offset(position);
8 let token = doc
9 .tokens
10 .iter()
11 .find(|t| offset >= t.span.start && offset < t.span.end)?;
12 doc.source.get(token.span.start..token.span.end).map(str::to_string)
13}
14
15pub fn cross_file_candidates(
20 doc: &DocumentState,
21 name: &str,
22 include_defs: bool,
23) -> Vec<Range> {
24 let local_defs = doc.symbol_index.definitions_of(name);
25 let doc_defines = local_defs.iter().any(|d| {
26 matches!(
27 d.kind,
28 crate::index::DefinitionKind::Function
29 | crate::index::DefinitionKind::Struct
30 | crate::index::DefinitionKind::Enum
31 | crate::index::DefinitionKind::Variant
32 | crate::index::DefinitionKind::Theorem
33 )
34 });
35
36 let mut ranges = Vec::new();
37 if include_defs && doc_defines {
38 for def in &local_defs {
39 if def.span != logicaffeine_language::token::Span::default() {
40 ranges.push(Range {
41 start: doc.line_index.position(def.span.start),
42 end: doc.line_index.position(def.span.end),
43 });
44 }
45 }
46 }
47 for reference in &doc.symbol_index.references {
48 if reference.name != name {
49 continue;
50 }
51 if reference.definition_idx.is_none() || doc_defines {
52 ranges.push(Range {
53 start: doc.line_index.position(reference.span.start),
54 end: doc.line_index.position(reference.span.end),
55 });
56 }
57 }
58 ranges
59}
60
61pub fn is_cross_file_symbol(doc: &DocumentState, name: &str) -> bool {
65 let defs = doc.symbol_index.definitions_of(name);
66 if defs.is_empty() {
67 return true;
68 }
69 defs.iter().any(|d| {
70 matches!(
71 d.kind,
72 crate::index::DefinitionKind::Function
73 | crate::index::DefinitionKind::Struct
74 | crate::index::DefinitionKind::Enum
75 | crate::index::DefinitionKind::Variant
76 | crate::index::DefinitionKind::Theorem
77 )
78 })
79}
80
81pub fn find_references(
83 doc: &DocumentState,
84 position: Position,
85 uri: &Url,
86 include_declaration: bool,
87) -> Vec<Location> {
88 let offset = doc.line_index.offset(position);
89
90 let token = match doc.tokens.iter().find(|t| {
92 offset >= t.span.start && offset < t.span.end
93 }) {
94 Some(t) => t,
95 None => return vec![],
96 };
97
98 let name = match doc.source.get(token.span.start..token.span.end) {
99 Some(n) => n.to_string(),
100 None => return vec![],
101 };
102
103 let mut locations = Vec::new();
104
105 if include_declaration {
107 for def in doc.symbol_index.definitions_of(&name) {
108 if def.span != logicaffeine_language::token::Span::default() {
109 locations.push(Location {
110 uri: uri.clone(),
111 range: Range {
112 start: doc.line_index.position(def.span.start),
113 end: doc.line_index.position(def.span.end),
114 },
115 });
116 }
117 }
118 }
119
120 for reference in doc.symbol_index.references_to(&name) {
122 locations.push(Location {
123 uri: uri.clone(),
124 range: Range {
125 start: doc.line_index.position(reference.span.start),
126 end: doc.line_index.position(reference.span.end),
127 },
128 });
129 }
130
131 locations
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137 use crate::document::DocumentState;
138
139 fn make_doc(source: &str) -> DocumentState {
140 DocumentState::new(source.to_string(), 1)
141 }
142
143 fn test_uri() -> Url {
144 Url::parse("file:///test.logos").unwrap()
145 }
146
147 #[test]
148 fn find_references_includes_usages() {
149 let doc = make_doc("## Main\n Let x be 5.\n Show x.\n");
150 let pos = Position { line: 1, character: 8 };
152 let refs = find_references(&doc, pos, &test_uri(), false);
153 assert!(!refs.is_empty(), "Expected at least one reference to 'x'");
155 }
156
157 #[test]
158 fn find_references_with_declaration() {
159 let doc = make_doc("## Main\n Let x be 5.\n Show x.\n");
160 let pos = Position { line: 1, character: 8 };
161 let refs_with = find_references(&doc, pos, &test_uri(), true);
162 let refs_without = find_references(&doc, pos, &test_uri(), false);
163 assert!(
164 refs_with.len() >= refs_without.len(),
165 "With declarations should return >= without: {} vs {}",
166 refs_with.len(),
167 refs_without.len()
168 );
169 }
170
171 #[test]
172 fn find_references_unknown_returns_empty() {
173 let doc = make_doc("## Main\n Let x be 5.\n");
174 let pos = Position { line: 0, character: 50 };
176 let refs = find_references(&doc, pos, &test_uri(), true);
177 assert!(refs.is_empty(), "Expected empty for out-of-range position");
178 }
179
180 #[test]
181 fn find_references_exact_count() {
182 let doc = make_doc("## Main\n Let x be 5.\n Show x.\n Set x to x + 1.\n");
183 let pos = Position { line: 1, character: 8 };
184 let refs = find_references(&doc, pos, &test_uri(), false);
185 assert!(refs.len() >= 3,
187 "Expected at least 3 references to 'x', got {}", refs.len());
188 }
189
190 #[test]
191 fn find_references_positions_are_correct() {
192 let source = "## Main\n Let x be 5.\n Show x.\n";
193 let doc = make_doc(source);
194 let pos = Position { line: 2, character: 9 };
195 let refs = find_references(&doc, pos, &test_uri(), true);
196 for r in &refs {
197 let start = doc.line_index.offset(r.range.start);
199 let end = doc.line_index.offset(r.range.end);
200 let text = &source[start..end];
201 assert_eq!(text, "x", "Reference range should point to 'x', got '{}'", text);
202 }
203 }
204
205 #[test]
206 fn find_references_include_declaration_adds_one() {
207 let doc = make_doc("## Main\n Let x be 5.\n Show x.\n");
208 let pos = Position { line: 1, character: 8 };
209 let refs_with = find_references(&doc, pos, &test_uri(), true);
210 let refs_without = find_references(&doc, pos, &test_uri(), false);
211 assert!(refs_with.len() > refs_without.len(),
212 "With declaration should have more refs: {} vs {}",
213 refs_with.len(), refs_without.len());
214 }
215
216 #[test]
217 fn find_references_correct_uri() {
218 let doc = make_doc("## Main\n Let x be 5.\n Show x.\n");
219 let uri = test_uri();
220 let pos = Position { line: 2, character: 9 };
221 let refs = find_references(&doc, pos, &uri, true);
222 for r in &refs {
223 assert_eq!(r.uri, uri, "All references should use the provided URI");
224 }
225 }
226}