Skip to main content

logicaffeine_lsp/
references.rs

1use tower_lsp::lsp_types::{Location, Position, Range, Url};
2
3use crate::document::DocumentState;
4
5/// The name under the cursor, exactly as the reference machinery reads it.
6pub 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
15/// The occurrences of `name` in ANOTHER open document that belong to a
16/// cross-file symbol: unresolved locally (they reach across files), or any
17/// occurrence when this document itself defines the symbol. Definition spans
18/// ride along when `include_defs` (rename edits the definition too).
19pub 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
61/// Is `name` a symbol other files can see from THIS document's point of
62/// view? True when the document doesn't define it (the usage reaches across
63/// files) or defines it as API shape. A local `Let x` is nobody's business.
64pub 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
81/// Handle find-all-references request.
82pub 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    // Find the token at the cursor position
91    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    // Include definition locations if requested
106    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    // Include all references
121    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        // Position on "x" in the Let binding (line 1)
151        let pos = Position { line: 1, character: 8 };
152        let refs = find_references(&doc, pos, &test_uri(), false);
153        // Should find at least the reference in "Show x."
154        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        // Position in whitespace
175        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        // x appears as: "Let x"(def-ref), "Show x"(ref), "Set x"(ref), "x + 1"(ref) → at least 3
186        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            // Each reference range should point to "x" in the source
198            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}