Skip to main content

logicaffeine_lsp/
document_symbols.rs

1use tower_lsp::lsp_types::{DocumentSymbol, Range, SymbolKind};
2
3use crate::document::DocumentState;
4use crate::index::DefinitionKind;
5use crate::line_index::LineIndex;
6
7#[allow(deprecated)]
8pub fn document_symbols(doc: &DocumentState) -> Vec<DocumentSymbol> {
9    let mut symbols = Vec::new();
10
11    // Add block-level symbols (## Main, ## To, etc.)
12    for (name, block_type, span) in &doc.symbol_index.block_spans {
13        let kind = match block_type {
14            logicaffeine_language::token::BlockType::Main => SymbolKind::FUNCTION,
15            logicaffeine_language::token::BlockType::Function => SymbolKind::FUNCTION,
16            logicaffeine_language::token::BlockType::Theorem => SymbolKind::CLASS,
17            logicaffeine_language::token::BlockType::Definition
18            | logicaffeine_language::token::BlockType::TypeDef => SymbolKind::STRUCT,
19            logicaffeine_language::token::BlockType::Policy => SymbolKind::INTERFACE,
20            logicaffeine_language::token::BlockType::Proof => SymbolKind::METHOD,
21            _ => SymbolKind::NAMESPACE,
22        };
23
24        let range = span_to_range(span, &doc.line_index);
25        let selection_range = Range {
26            start: doc.line_index.position(span.start),
27            end: doc.line_index.position(span.start + name.len().min(span.end - span.start)),
28        };
29
30        symbols.push(DocumentSymbol {
31            name: name.clone(),
32            detail: None,
33            kind,
34            tags: None,
35            deprecated: None,
36            range,
37            selection_range,
38            children: None,
39        });
40    }
41
42    // Add definitions from the symbol index, nesting children under parents.
43    // Track index of most recent parent symbol (Struct, Enum, Function) for nesting.
44    let mut current_parent_idx: Option<usize> = None;
45
46    for def in &doc.symbol_index.definitions {
47        if def.span == logicaffeine_language::token::Span::default() {
48            continue; // Skip definitions without known positions
49        }
50
51        let kind = match def.kind {
52            DefinitionKind::Function => SymbolKind::FUNCTION,
53            DefinitionKind::Variable => SymbolKind::VARIABLE,
54            DefinitionKind::Struct => SymbolKind::STRUCT,
55            DefinitionKind::Enum => SymbolKind::ENUM,
56            DefinitionKind::Field => SymbolKind::FIELD,
57            DefinitionKind::Parameter => SymbolKind::VARIABLE,
58            DefinitionKind::Variant => SymbolKind::ENUM_MEMBER,
59            DefinitionKind::Block => continue, // Already added from block_spans
60            DefinitionKind::Theorem => SymbolKind::CLASS,
61        };
62
63        let range = span_to_range(&def.span, &doc.line_index);
64
65        let symbol = DocumentSymbol {
66            name: def.name.clone(),
67            detail: def.detail.clone(),
68            kind,
69            tags: None,
70            deprecated: None,
71            range,
72            selection_range: range,
73            children: None,
74        };
75
76        // Decide whether this is a child of the current parent
77        let is_child = matches!(
78            def.kind,
79            DefinitionKind::Field | DefinitionKind::Parameter | DefinitionKind::Variant
80        );
81
82        if is_child {
83            if let Some(parent_idx) = current_parent_idx {
84                let parent = &mut symbols[parent_idx];
85                parent.children.get_or_insert_with(Vec::new).push(symbol);
86                continue;
87            }
88        }
89
90        // This is a top-level symbol; track it as a potential parent
91        if matches!(
92            def.kind,
93            DefinitionKind::Struct | DefinitionKind::Enum | DefinitionKind::Function
94        ) {
95            current_parent_idx = Some(symbols.len());
96        } else {
97            // Non-parent top-level (Variable, Theorem, etc.) — reset parent tracking
98            current_parent_idx = None;
99        }
100
101        // Try nesting Variable/Theorem under their containing block
102        if matches!(def.kind, DefinitionKind::Variable | DefinitionKind::Theorem) {
103            if let Some(block_idx) = find_containing_block(&symbols, &def.span, &doc.line_index) {
104                symbols[block_idx].children.get_or_insert_with(Vec::new).push(symbol);
105                continue;
106            }
107        }
108
109        symbols.push(symbol);
110    }
111
112    symbols
113}
114
115/// Find the block symbol whose range contains the given span.
116#[allow(deprecated)]
117fn find_containing_block(
118    symbols: &[DocumentSymbol],
119    span: &logicaffeine_language::token::Span,
120    line_index: &LineIndex,
121) -> Option<usize> {
122    let pos = line_index.position(span.start);
123    for (i, sym) in symbols.iter().enumerate() {
124        let is_block = matches!(
125            sym.kind,
126            SymbolKind::FUNCTION | SymbolKind::CLASS | SymbolKind::STRUCT
127            | SymbolKind::INTERFACE | SymbolKind::METHOD | SymbolKind::NAMESPACE
128        );
129        if is_block
130            && pos.line >= sym.range.start.line
131            && pos.line <= sym.range.end.line
132        {
133            return Some(i);
134        }
135    }
136    None
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142    use crate::document::DocumentState;
143
144    fn make_doc(source: &str) -> DocumentState {
145        DocumentState::new(source.to_string(), 1)
146    }
147
148    #[test]
149    #[allow(deprecated)]
150    fn symbols_include_block_headers() {
151        let doc = make_doc("## Main\n    Let x be 5.\n");
152        let symbols = document_symbols(&doc);
153        let block_names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
154        assert!(
155            block_names.iter().any(|n| n.contains("Main")),
156            "Should include 'Main' block symbol: {:?}",
157            block_names
158        );
159    }
160
161    #[test]
162    #[allow(deprecated)]
163    fn symbols_include_variables() {
164        let doc = make_doc("## Main\n    Let x be 5.\n");
165        let symbols = document_symbols(&doc);
166        // Variables may be nested under blocks, so search recursively
167        fn find_var(symbols: &[DocumentSymbol], name: &str) -> bool {
168            for sym in symbols {
169                if sym.kind == SymbolKind::VARIABLE && sym.name == name {
170                    return true;
171                }
172                if let Some(children) = &sym.children {
173                    if find_var(children, name) {
174                        return true;
175                    }
176                }
177            }
178            false
179        }
180        assert!(
181            find_var(&symbols, "x"),
182            "Should include variable 'x' (possibly nested): {:?}",
183            symbols.iter().map(|s| &s.name).collect::<Vec<_>>()
184        );
185    }
186
187    #[test]
188    #[allow(deprecated)]
189    fn symbols_empty_for_empty_doc() {
190        let doc = make_doc("");
191        let symbols = document_symbols(&doc);
192        // Empty doc should have no variable/function symbols
193        let non_block_syms: Vec<_> = symbols.iter()
194            .filter(|s| s.kind == SymbolKind::VARIABLE || s.kind == SymbolKind::FUNCTION)
195            .collect();
196        assert!(non_block_syms.is_empty(),
197            "Empty doc should have no variable/function symbols: {:?}",
198            non_block_syms.iter().map(|s| &s.name).collect::<Vec<_>>());
199    }
200
201    #[test]
202    #[allow(deprecated)]
203    fn symbols_have_valid_ranges() {
204        let doc = make_doc("## Main\n    Let x be 5.\n");
205        let symbols = document_symbols(&doc);
206        for sym in &symbols {
207            assert!(
208                sym.range.start.line <= sym.range.end.line,
209                "Symbol '{}' has invalid range: start {:?} > end {:?}",
210                sym.name, sym.range.start, sym.range.end
211            );
212        }
213    }
214
215    #[test]
216    #[allow(deprecated)]
217    fn symbol_detail_populated() {
218        let doc = make_doc("## Main\n    Let x be 5.\n");
219        let symbols = document_symbols(&doc);
220        // Variables are nested under blocks, so search recursively
221        fn collect_vars(symbols: &[DocumentSymbol], out: &mut Vec<String>) {
222            for sym in symbols {
223                if sym.kind == SymbolKind::VARIABLE {
224                    out.push(sym.name.clone());
225                    assert!(sym.detail.is_some(),
226                        "Variable symbol '{}' should have a non-None detail", sym.name);
227                }
228                if let Some(children) = &sym.children {
229                    collect_vars(children, out);
230                }
231            }
232        }
233        let mut vars = Vec::new();
234        collect_vars(&symbols, &mut vars);
235        assert!(!vars.is_empty(), "Should find at least one variable");
236    }
237
238    #[test]
239    #[allow(deprecated)]
240    fn block_kind_skipped_in_definitions() {
241        let doc = make_doc("## Main\n    Let x be 5.\n");
242        let symbols = document_symbols(&doc);
243        // Block definitions should not appear duplicated — they come from block_spans,
244        // and DefinitionKind::Block is `continue`d in the loop
245        let block_syms: Vec<_> = symbols.iter()
246            .filter(|s| s.name.contains("Main"))
247            .collect();
248        assert_eq!(block_syms.len(), 1,
249            "Main should appear exactly once, got {}: {:?}",
250            block_syms.len(), block_syms.iter().map(|s| &s.name).collect::<Vec<_>>());
251    }
252
253    #[test]
254    #[allow(deprecated)]
255    fn variable_symbols_have_no_children() {
256        let doc = make_doc("## Main\n    Let x be 5.\n    Let y be 10.\n");
257        let symbols = document_symbols(&doc);
258        // Variables are nested under blocks; check they have no children of their own
259        fn check_vars(symbols: &[DocumentSymbol]) {
260            for sym in symbols {
261                if sym.kind == SymbolKind::VARIABLE {
262                    assert!(
263                        sym.children.is_none() || sym.children.as_ref().unwrap().is_empty(),
264                        "Variable '{}' should have no children",
265                        sym.name
266                    );
267                }
268                if let Some(children) = &sym.children {
269                    check_vars(children);
270                }
271            }
272        }
273        check_vars(&symbols);
274    }
275
276    #[test]
277    #[allow(deprecated)]
278    fn symbols_block_has_correct_kind() {
279        let doc = make_doc("## Main\n    Let x be 5.\n");
280        let symbols = document_symbols(&doc);
281        let main_sym = symbols.iter().find(|s| s.name.contains("Main"));
282        assert!(main_sym.is_some(), "Should have a Main symbol");
283        assert_eq!(main_sym.unwrap().kind, SymbolKind::FUNCTION, "Main block should be FUNCTION kind");
284    }
285
286    #[test]
287    #[allow(deprecated)]
288    fn variables_nested_under_block() {
289        let doc = make_doc("## Main\n    Let x be 5.\n    Let y be 10.\n");
290        let symbols = document_symbols(&doc);
291        let main_sym = symbols.iter().find(|s| s.name.contains("Main"));
292        assert!(main_sym.is_some(), "Should have Main block");
293        let main = main_sym.unwrap();
294        let children = main.children.as_ref();
295        assert!(children.is_some(), "Main block should have children");
296        let child_names: Vec<&str> = children.unwrap().iter().map(|c| c.name.as_str()).collect();
297        assert!(child_names.contains(&"x"), "Main should contain 'x': {:?}", child_names);
298        assert!(child_names.contains(&"y"), "Main should contain 'y': {:?}", child_names);
299    }
300
301    #[test]
302    #[allow(deprecated)]
303    fn function_params_nested() {
304        let doc = make_doc("## To greet (name: Text):\n    Show name.\n");
305        let symbols = document_symbols(&doc);
306        let func_syms: Vec<_> = symbols.iter()
307            .filter(|s| s.kind == SymbolKind::FUNCTION)
308            .collect();
309        let has_children = func_syms.iter().any(|s| {
310            s.children.as_ref().map(|c| !c.is_empty()).unwrap_or(false)
311        });
312        if has_children {
313            let parent = func_syms.iter().find(|s| {
314                s.children.as_ref().map(|c| !c.is_empty()).unwrap_or(false)
315            }).unwrap();
316            let child_names: Vec<&str> = parent.children.as_ref().unwrap()
317                .iter().map(|c| c.name.as_str()).collect();
318            assert!(child_names.contains(&"name"),
319                "Function should have 'name' param as child: {:?}", child_names);
320        }
321    }
322}
323
324fn span_to_range(span: &logicaffeine_language::token::Span, line_index: &LineIndex) -> Range {
325    Range {
326        start: line_index.position(span.start),
327        end: line_index.position(span.end),
328    }
329}