1use lex_core::lex::ast::{
2 Annotation, AstNode, ContentItem, Definition, Document, List, ListItem, Paragraph, Range,
3 Session, Table, 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 for row in table.all_rows() {
127 for cell in &row.cells {
128 if cell.has_block_content() {
129 children.extend(collect_symbols_from_items(cell.children.iter()));
130 }
131 }
132 }
133
134 LexDocumentSymbol {
135 name: format!("Table: {}", summarize_text(&table.subject, "Table")),
136 detail: Some("table".to_string()),
137 kind: SymbolKind::CONSTANT,
138 range: table.range().clone(),
139 selection_range: table
140 .subject
141 .location
142 .clone()
143 .unwrap_or_else(|| table.range().clone()),
144 children,
145 }
146}
147
148fn paragraph_symbol(paragraph: &Paragraph) -> LexDocumentSymbol {
149 let children = annotation_symbol_list(paragraph.annotations());
150 let name = if let Some(ContentItem::TextLine(first_line)) = paragraph.lines.first() {
152 truncate_to_words(&first_line.content, 4, "Paragraph")
153 } else {
154 "Paragraph".to_string()
155 };
156
157 LexDocumentSymbol {
158 name,
159 detail: None,
160 kind: SymbolKind::STRING,
161 range: paragraph.range().clone(),
162 selection_range: paragraph.range().clone(),
163 children,
164 }
165}
166
167fn list_item_symbol(list_item: &ListItem) -> LexDocumentSymbol {
168 let mut children = annotation_symbol_list(list_item.annotations());
169 children.extend(collect_symbols_from_items(list_item.children.iter()));
170
171 let name = if let Some(first_text) = list_item.text.first() {
172 summarize_text(first_text, "List Item")
173 } else {
174 "List Item".to_string()
175 };
176
177 LexDocumentSymbol {
178 name: format!("{} {}", list_item.marker.as_string(), name),
179 detail: None,
180 kind: SymbolKind::ENUM_MEMBER,
181 range: list_item.range().clone(),
182 selection_range: list_item.range().clone(),
183 children,
184 }
185}
186
187fn annotation_symbol(annotation: &Annotation) -> LexDocumentSymbol {
188 let children = collect_symbols_from_items(annotation.children.iter());
189 LexDocumentSymbol {
190 name: format!(":: {} ::", annotation.data.label.value),
191 detail: if annotation.data.parameters.is_empty() {
192 None
193 } else {
194 Some(
195 annotation
196 .data
197 .parameters
198 .iter()
199 .map(|param| format!("{}={}", param.key, param.value))
200 .collect::<Vec<_>>()
201 .join(", "),
202 )
203 },
204 kind: SymbolKind::INTERFACE,
205 range: annotation.range().clone(),
206 selection_range: annotation.header_location().clone(),
207 children,
208 }
209}
210
211fn annotation_symbol_list<'a>(
212 annotations: impl IntoIterator<Item = &'a Annotation>,
213) -> Vec<LexDocumentSymbol> {
214 annotations.into_iter().map(annotation_symbol).collect()
215}
216
217fn summarize_text(text: &TextContent, fallback: &str) -> String {
218 summarize_text_str(text.as_string().trim(), fallback)
219}
220
221fn summarize_text_str(text: &str, fallback: &str) -> String {
222 if text.is_empty() {
223 fallback.to_string()
224 } else {
225 text.to_string()
226 }
227}
228
229fn truncate_to_words(text: &TextContent, max_words: usize, fallback: &str) -> String {
230 let trimmed = text.as_string().trim();
231 if trimmed.is_empty() {
232 return fallback.to_string();
233 }
234 let words: Vec<&str> = trimmed.split_whitespace().collect();
235 if words.len() <= max_words {
236 trimmed.to_string()
237 } else {
238 format!("{}…", words[..max_words].join(" "))
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245 use crate::test_support::sample_document;
246
247 fn find_symbol<'a>(symbols: &'a [LexDocumentSymbol], name: &str) -> &'a LexDocumentSymbol {
248 symbols
249 .iter()
250 .find(|symbol| symbol.name == name)
251 .unwrap_or_else(|| panic!("symbol {name} not found"))
252 }
253
254 #[test]
255 fn builds_session_tree() {
256 let document = sample_document();
257 let symbols = collect_document_symbols(&document);
258 assert!(symbols.iter().any(|s| s.name == ":: doc.note ::"));
259 let session = find_symbol(&symbols, "1. Intro");
260 let child_names: Vec<_> = session
261 .children
262 .iter()
263 .map(|child| child.name.clone())
264 .collect();
265 assert!(child_names.iter().any(|name| name.contains("Cache")));
266 assert!(child_names.iter().any(|name| name.contains("List")));
267 assert!(child_names.iter().any(|name| name.contains("Verbatim")));
268
269 let _verbatim_symbol = session
271 .children
272 .iter()
273 .find(|child| child.name.contains("Cache") && child.kind == SymbolKind::CONSTANT)
274 .expect("verbatim symbol not found");
275 }
276
277 #[test]
278 fn includes_paragraphs_and_list_items() {
279 use lex_core::lex::ast::elements::paragraph::TextLine;
280 use lex_core::lex::ast::{ContentItem, List, ListItem, Paragraph, TextContent};
281
282 let paragraph = Paragraph::new(vec![ContentItem::TextLine(TextLine::new(
284 TextContent::from_string("Hello World".to_string(), None),
285 ))]);
286
287 let list_item = ListItem::new("-".to_string(), "Item 1".to_string());
288 let list = List::new(vec![list_item]);
289
290 let document = Document::with_content(vec![
291 ContentItem::Paragraph(paragraph),
292 ContentItem::List(list),
293 ]);
294
295 let symbols = collect_document_symbols(&document);
296
297 let paragraph_symbol = symbols
299 .iter()
300 .find(|s| s.name.contains("Hello"))
301 .expect("Paragraph symbol not found");
302 assert_eq!(paragraph_symbol.kind, SymbolKind::STRING);
303
304 let list_symbol = symbols
306 .iter()
307 .find(|s| s.name.contains("List"))
308 .expect("List symbol not found");
309
310 let item_symbol = list_symbol
312 .children
313 .iter()
314 .find(|s| s.name.contains("Item 1"));
315 if item_symbol.is_none() {
316 println!("List symbol children: {:#?}", list_symbol.children);
317 }
318 let item_symbol = item_symbol.expect("List item symbol not found");
319 assert!(item_symbol.name.contains("-"));
320 }
321
322 #[test]
323 fn includes_document_level_annotations() {
324 let document = sample_document();
325 let symbols = collect_document_symbols(&document);
326 assert!(symbols.iter().any(|symbol| symbol.name == ":: doc.note ::"));
327 }
329}