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 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 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; }
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, 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 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 if matches!(
92 def.kind,
93 DefinitionKind::Struct | DefinitionKind::Enum | DefinitionKind::Function
94 ) {
95 current_parent_idx = Some(symbols.len());
96 } else {
97 current_parent_idx = None;
99 }
100
101 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#[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 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 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 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 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 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}