1use lex_core::lex::ast::{
2 Annotation, AstNode, ContentItem, Definition, Document, List, ListItem, Paragraph, Range,
3 Session, Table, TableRow, TextContent, Verbatim,
4};
5use lsp_types::SymbolKind;
6
7#[derive(Debug, Clone, PartialEq)]
8pub struct LexDocumentSymbol {
9 pub name: String,
10 pub detail: Option<String>,
11 pub kind: SymbolKind,
12 pub range: Range,
13 pub selection_range: Range,
14 pub children: Vec<LexDocumentSymbol>,
15}
16
17pub fn collect_document_symbols(document: &Document) -> Vec<LexDocumentSymbol> {
18 let mut symbols: Vec<LexDocumentSymbol> = document
19 .annotations()
20 .iter()
21 .map(annotation_symbol)
22 .collect();
23 symbols.extend(session_symbols(&document.root, true));
24 symbols
25}
26
27fn session_symbols(session: &Session, is_root: bool) -> Vec<LexDocumentSymbol> {
28 let mut symbols = Vec::new();
29 if !is_root {
30 let mut children = annotation_symbol_list(session.annotations());
31 children.extend(collect_symbols_from_items(session.children.iter()));
32 let selection_range = session
33 .header_location()
34 .cloned()
35 .unwrap_or_else(|| session.range().clone());
36 symbols.push(LexDocumentSymbol {
37 name: summarize_text(&session.title, "Session"),
38 detail: Some(format!("{} item(s)", session.children.len())),
39 kind: SymbolKind::STRUCT, range: session.range().clone(),
41 selection_range,
42 children,
43 });
44 } else {
45 symbols.extend(collect_symbols_from_items(session.children.iter()));
46 }
47 symbols
48}
49
50fn collect_symbols_from_items<'a>(
51 items: impl Iterator<Item = &'a ContentItem>,
52) -> Vec<LexDocumentSymbol> {
53 let mut symbols = Vec::new();
54 for item in items {
55 match item {
56 ContentItem::Session(session) => symbols.extend(session_symbols(session, false)),
57 ContentItem::Definition(definition) => symbols.push(definition_symbol(definition)),
58 ContentItem::List(list) => symbols.push(list_symbol(list)),
59 ContentItem::Annotation(annotation) => symbols.push(annotation_symbol(annotation)),
60 ContentItem::VerbatimBlock(verbatim) => symbols.push(verbatim_symbol(verbatim)),
61 ContentItem::Table(table) => symbols.push(table_symbol(table)),
62 ContentItem::Paragraph(paragraph) => symbols.push(paragraph_symbol(paragraph)),
63 ContentItem::ListItem(list_item) => symbols.push(list_item_symbol(list_item)),
64 ContentItem::TextLine(_)
65 | ContentItem::VerbatimLine(_)
66 | ContentItem::BlankLineGroup(_) => {}
67 }
68 }
69 symbols
70}
71
72fn definition_symbol(definition: &Definition) -> LexDocumentSymbol {
73 let mut children = annotation_symbol_list(definition.annotations());
74 children.extend(collect_symbols_from_items(definition.children.iter()));
75 let selection_range = definition
76 .header_location()
77 .cloned()
78 .unwrap_or_else(|| definition.range().clone());
79 LexDocumentSymbol {
80 name: summarize_text(&definition.subject, "Definition"),
81 detail: Some("definition".to_string()),
82 kind: SymbolKind::PROPERTY,
83 range: definition.range().clone(),
84 selection_range,
85 children,
86 }
87}
88
89fn list_symbol(list: &List) -> LexDocumentSymbol {
90 let mut children = annotation_symbol_list(list.annotations());
91 children.extend(collect_symbols_from_items(list.items.iter()));
92
93 LexDocumentSymbol {
94 name: format!("List ({} items)", list.items.len()),
95 detail: None,
96 kind: SymbolKind::ENUM,
97 range: list.range().clone(),
98 selection_range: list.range().clone(),
99 children,
100 }
101}
102
103fn verbatim_symbol(verbatim: &Verbatim) -> LexDocumentSymbol {
104 let children = annotation_symbol_list(verbatim.annotations());
105 LexDocumentSymbol {
106 name: format!(
107 "Verbatim: {}",
108 summarize_text(&verbatim.subject, "Verbatim block")
109 ),
110 detail: Some(verbatim.closing_data.label.value.clone()),
111 kind: SymbolKind::CONSTANT,
112 range: verbatim.range().clone(),
113 selection_range: verbatim
114 .subject
115 .location
116 .clone()
117 .unwrap_or_else(|| verbatim.range().clone()),
118 children,
119 }
120}
121
122fn table_symbol(table: &Table) -> LexDocumentSymbol {
123 let mut children = annotation_symbol_list(table.annotations());
124
125 let mut row_index = 0;
127 for row in table.all_rows() {
128 row_index += 1;
129 children.push(row_symbol(row, row_index));
130 }
131
132 LexDocumentSymbol {
133 name: format!("Table: {}", summarize_text(&table.subject, "Table")),
134 detail: Some(format!(
135 "{} row(s), {} col(s)",
136 table.row_count(),
137 table.column_count()
138 )),
139 kind: SymbolKind::CONSTANT,
140 range: table.range().clone(),
141 selection_range: table
142 .subject
143 .location
144 .clone()
145 .unwrap_or_else(|| table.range().clone()),
146 children,
147 }
148}
149
150fn row_symbol(row: &TableRow, index: usize) -> LexDocumentSymbol {
151 let mut children = Vec::new();
152
153 for cell in &row.cells {
154 let cell_text = cell.content.as_string().trim().to_string();
155 let cell_name = if cell_text.is_empty() {
156 "(empty)".to_string()
157 } else {
158 cell_text
159 };
160
161 let cell_children = if cell.has_block_content() {
163 collect_symbols_from_items(cell.children.iter())
164 } else {
165 Vec::new()
166 };
167
168 children.push(LexDocumentSymbol {
169 name: cell_name,
170 detail: if cell.colspan > 1 || cell.rowspan > 1 {
171 Some(format!("{}×{}", cell.colspan, cell.rowspan))
172 } else {
173 None
174 },
175 kind: SymbolKind::FIELD,
176 range: cell.location.clone(),
177 selection_range: cell.location.clone(),
178 children: cell_children,
179 });
180 }
181
182 LexDocumentSymbol {
183 name: format!("Row {index}"),
184 detail: Some(format!("{} cell(s)", row.cells.len())),
185 kind: SymbolKind::ENUM,
186 range: row.location.clone(),
187 selection_range: row.location.clone(),
188 children,
189 }
190}
191
192fn paragraph_symbol(paragraph: &Paragraph) -> LexDocumentSymbol {
193 let children = annotation_symbol_list(paragraph.annotations());
194 let name = if let Some(ContentItem::TextLine(first_line)) = paragraph.lines.first() {
196 truncate_to_words(&first_line.content, 4, "Paragraph")
197 } else {
198 "Paragraph".to_string()
199 };
200
201 LexDocumentSymbol {
202 name,
203 detail: None,
204 kind: SymbolKind::STRING,
205 range: paragraph.range().clone(),
206 selection_range: paragraph.range().clone(),
207 children,
208 }
209}
210
211fn list_item_symbol(list_item: &ListItem) -> LexDocumentSymbol {
212 let mut children = annotation_symbol_list(list_item.annotations());
213 children.extend(collect_symbols_from_items(list_item.children.iter()));
214
215 let name = if let Some(first_text) = list_item.text.first() {
216 summarize_text(first_text, "List Item")
217 } else {
218 "List Item".to_string()
219 };
220
221 LexDocumentSymbol {
222 name: format!("{} {}", list_item.marker.as_string(), name),
223 detail: None,
224 kind: SymbolKind::ENUM_MEMBER,
225 range: list_item.range().clone(),
226 selection_range: list_item.range().clone(),
227 children,
228 }
229}
230
231fn annotation_symbol(annotation: &Annotation) -> LexDocumentSymbol {
232 let children = collect_symbols_from_items(annotation.children.iter());
233 LexDocumentSymbol {
234 name: format!(":: {} ::", annotation.data.label.value),
235 detail: if annotation.data.parameters.is_empty() {
236 None
237 } else {
238 Some(
239 annotation
240 .data
241 .parameters
242 .iter()
243 .map(|param| format!("{}={}", param.key, param.value))
244 .collect::<Vec<_>>()
245 .join(", "),
246 )
247 },
248 kind: SymbolKind::INTERFACE,
249 range: annotation.range().clone(),
250 selection_range: annotation.header_location().clone(),
251 children,
252 }
253}
254
255fn annotation_symbol_list<'a>(
256 annotations: impl IntoIterator<Item = &'a Annotation>,
257) -> Vec<LexDocumentSymbol> {
258 annotations.into_iter().map(annotation_symbol).collect()
259}
260
261fn summarize_text(text: &TextContent, fallback: &str) -> String {
262 summarize_text_str(text.as_string().trim(), fallback)
263}
264
265fn summarize_text_str(text: &str, fallback: &str) -> String {
266 if text.is_empty() {
267 fallback.to_string()
268 } else {
269 text.to_string()
270 }
271}
272
273fn truncate_to_words(text: &TextContent, max_words: usize, fallback: &str) -> String {
274 let trimmed = text.as_string().trim();
275 if trimmed.is_empty() {
276 return fallback.to_string();
277 }
278 let words: Vec<&str> = trimmed.split_whitespace().collect();
279 if words.len() <= max_words {
280 trimmed.to_string()
281 } else {
282 format!("{}…", words[..max_words].join(" "))
283 }
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289 use crate::test_support::sample_document;
290
291 fn find_symbol<'a>(symbols: &'a [LexDocumentSymbol], name: &str) -> &'a LexDocumentSymbol {
292 symbols
293 .iter()
294 .find(|symbol| symbol.name == name)
295 .unwrap_or_else(|| panic!("symbol {name} not found"))
296 }
297
298 #[test]
299 fn builds_session_tree() {
300 let document = sample_document();
301 let symbols = collect_document_symbols(&document);
302 assert!(symbols.iter().any(|s| s.name == ":: test.note ::"));
303 let session = find_symbol(&symbols, "1. Intro");
304 let child_names: Vec<_> = session
305 .children
306 .iter()
307 .map(|child| child.name.clone())
308 .collect();
309 assert!(child_names.iter().any(|name| name.contains("Cache")));
310 assert!(child_names.iter().any(|name| name.contains("List")));
311 assert!(child_names.iter().any(|name| name.contains("Verbatim")));
312
313 let _verbatim_symbol = session
315 .children
316 .iter()
317 .find(|child| child.name.contains("Cache") && child.kind == SymbolKind::CONSTANT)
318 .expect("verbatim symbol not found");
319 }
320
321 #[test]
322 fn includes_paragraphs_and_list_items() {
323 use lex_core::lex::ast::elements::paragraph::TextLine;
324 use lex_core::lex::ast::{ContentItem, List, ListItem, Paragraph, TextContent};
325
326 let paragraph = Paragraph::new(vec![ContentItem::TextLine(TextLine::new(
328 TextContent::from_string("Hello World".to_string(), None),
329 ))]);
330
331 let list_item = ListItem::new("-".to_string(), "Item 1".to_string());
332 let list = List::new(vec![list_item]);
333
334 let document = Document::with_content(vec![
335 ContentItem::Paragraph(paragraph),
336 ContentItem::List(list),
337 ]);
338
339 let symbols = collect_document_symbols(&document);
340
341 let paragraph_symbol = symbols
343 .iter()
344 .find(|s| s.name.contains("Hello"))
345 .expect("Paragraph symbol not found");
346 assert_eq!(paragraph_symbol.kind, SymbolKind::STRING);
347
348 let list_symbol = symbols
350 .iter()
351 .find(|s| s.name.contains("List"))
352 .expect("List symbol not found");
353
354 let item_symbol = list_symbol
356 .children
357 .iter()
358 .find(|s| s.name.contains("Item 1"));
359 if item_symbol.is_none() {
360 println!("List symbol children: {:#?}", list_symbol.children);
361 }
362 let item_symbol = item_symbol.expect("List item symbol not found");
363 assert!(item_symbol.name.contains("-"));
364 }
365
366 #[test]
367 fn table_symbol_includes_rows_and_cells() {
368 use lex_core::lex::ast::elements::table::{TableCell, TableRow};
369 use lex_core::lex::ast::elements::verbatim::VerbatimBlockMode;
370 use lex_core::lex::ast::{Table, TextContent};
371
372 let row1 = TableRow::new(vec![
373 TableCell::new(TextContent::from_string("Header A".to_string(), None)),
374 TableCell::new(TextContent::from_string("Header B".to_string(), None)),
375 ]);
376 let row2 = TableRow::new(vec![
377 TableCell::new(TextContent::from_string("Value 1".to_string(), None)),
378 TableCell::new(TextContent::from_string("Value 2".to_string(), None)),
379 ]);
380 let table = Table::new(
381 TextContent::from_string("My Table".to_string(), None),
382 vec![row1],
383 vec![row2],
384 VerbatimBlockMode::Inflow,
385 );
386
387 let document = Document::with_content(vec![ContentItem::Table(Box::new(table))]);
388 let symbols = collect_document_symbols(&document);
389
390 let table_sym = symbols
392 .iter()
393 .find(|s| s.name.contains("My Table"))
394 .expect("Table symbol not found");
395 assert_eq!(table_sym.kind, SymbolKind::CONSTANT);
396 assert!(table_sym.detail.as_ref().unwrap().contains("2 row(s)"));
397
398 assert_eq!(
400 table_sym.children.len(),
401 2,
402 "Table should have 2 row children"
403 );
404
405 let row1_sym = &table_sym.children[0];
406 assert_eq!(row1_sym.name, "Row 1");
407 assert_eq!(row1_sym.kind, SymbolKind::ENUM);
408 assert_eq!(
409 row1_sym.children.len(),
410 2,
411 "Row 1 should have 2 cell children"
412 );
413
414 assert_eq!(row1_sym.children[0].name, "Header A");
416 assert_eq!(row1_sym.children[1].name, "Header B");
417 assert_eq!(row1_sym.children[0].kind, SymbolKind::FIELD);
418
419 let row2_sym = &table_sym.children[1];
420 assert_eq!(row2_sym.children[0].name, "Value 1");
421 assert_eq!(row2_sym.children[1].name, "Value 2");
422 }
423
424 #[test]
425 fn includes_document_level_annotations() {
426 let document = sample_document();
427 let symbols = collect_document_symbols(&document);
428 assert!(symbols
429 .iter()
430 .any(|symbol| symbol.name == ":: test.note ::"));
431 }
433}