1use lex_core::lex::ast::{
2 Annotation, AstNode, ContentItem, Definition, Document, List, ListItem, Paragraph, Range,
3 Session, 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::NAMESPACE,
40 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::Paragraph(paragraph) => symbols.push(paragraph_symbol(paragraph)),
62 ContentItem::ListItem(list_item) => symbols.push(list_item_symbol(list_item)),
63 ContentItem::TextLine(_)
64 | ContentItem::VerbatimLine(_)
65 | ContentItem::BlankLineGroup(_) => {}
66 }
67 }
68 symbols
69}
70
71fn definition_symbol(definition: &Definition) -> LexDocumentSymbol {
72 let mut children = annotation_symbol_list(definition.annotations());
73 children.extend(collect_symbols_from_items(definition.children.iter()));
74 let selection_range = definition
75 .header_location()
76 .cloned()
77 .unwrap_or_else(|| definition.range().clone());
78 LexDocumentSymbol {
79 name: summarize_text(&definition.subject, "Definition"),
80 detail: Some("definition".to_string()),
81 kind: SymbolKind::STRUCT,
82 range: definition.range().clone(),
83 selection_range,
84 children,
85 }
86}
87
88fn list_symbol(list: &List) -> LexDocumentSymbol {
89 let mut children = annotation_symbol_list(list.annotations());
90 children.extend(collect_symbols_from_items(list.items.iter()));
91
92 LexDocumentSymbol {
93 name: format!("List ({} items)", list.items.len()),
94 detail: None,
95 kind: SymbolKind::ARRAY,
96 range: list.range().clone(),
97 selection_range: list.range().clone(),
98 children,
99 }
100}
101
102fn verbatim_symbol(verbatim: &Verbatim) -> LexDocumentSymbol {
103 let children = annotation_symbol_list(verbatim.annotations());
104 LexDocumentSymbol {
105 name: format!(
106 "Verbatim: {}",
107 summarize_text(&verbatim.subject, "Verbatim block")
108 ),
109 detail: Some(verbatim.closing_data.label.value.clone()),
110 kind: SymbolKind::OBJECT,
111 range: verbatim.range().clone(),
112 selection_range: verbatim
113 .subject
114 .location
115 .clone()
116 .unwrap_or_else(|| verbatim.range().clone()),
117 children,
118 }
119}
120
121fn paragraph_symbol(paragraph: &Paragraph) -> LexDocumentSymbol {
122 let children = annotation_symbol_list(paragraph.annotations());
123 let name = if let Some(ContentItem::TextLine(first_line)) = paragraph.lines.first() {
125 summarize_text(&first_line.content, "Paragraph")
126 } else {
127 "Paragraph".to_string()
128 };
129
130 LexDocumentSymbol {
131 name,
132 detail: None,
133 kind: SymbolKind::STRING,
134 range: paragraph.range().clone(),
135 selection_range: paragraph.range().clone(),
136 children,
137 }
138}
139
140fn list_item_symbol(list_item: &ListItem) -> LexDocumentSymbol {
141 let mut children = annotation_symbol_list(list_item.annotations());
142 children.extend(collect_symbols_from_items(list_item.children.iter()));
143
144 let name = if let Some(first_text) = list_item.text.first() {
145 summarize_text(first_text, "List Item")
146 } else {
147 "List Item".to_string()
148 };
149
150 LexDocumentSymbol {
151 name: format!("{} {}", list_item.marker.as_string(), name),
152 detail: None,
153 kind: SymbolKind::FIELD, range: list_item.range().clone(),
155 selection_range: list_item.range().clone(),
156 children,
157 }
158}
159
160fn annotation_symbol(annotation: &Annotation) -> LexDocumentSymbol {
161 let children = collect_symbols_from_items(annotation.children.iter());
162 LexDocumentSymbol {
163 name: format!(":: {} ::", annotation.data.label.value),
164 detail: if annotation.data.parameters.is_empty() {
165 None
166 } else {
167 Some(
168 annotation
169 .data
170 .parameters
171 .iter()
172 .map(|param| format!("{}={}", param.key, param.value))
173 .collect::<Vec<_>>()
174 .join(", "),
175 )
176 },
177 kind: SymbolKind::EVENT,
178 range: annotation.range().clone(),
179 selection_range: annotation.header_location().clone(),
180 children,
181 }
182}
183
184fn annotation_symbol_list<'a>(
185 annotations: impl IntoIterator<Item = &'a Annotation>,
186) -> Vec<LexDocumentSymbol> {
187 annotations.into_iter().map(annotation_symbol).collect()
188}
189
190fn summarize_text(text: &TextContent, fallback: &str) -> String {
191 let trimmed = text.as_string().trim();
192 if trimmed.is_empty() {
193 fallback.to_string()
194 } else {
195 trimmed.to_string()
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202 use crate::test_support::sample_document;
203
204 fn find_symbol<'a>(symbols: &'a [LexDocumentSymbol], name: &str) -> &'a LexDocumentSymbol {
205 symbols
206 .iter()
207 .find(|symbol| symbol.name == name)
208 .unwrap_or_else(|| panic!("symbol {name} not found"))
209 }
210
211 #[test]
212 fn builds_session_tree() {
213 let document = sample_document();
214 let symbols = collect_document_symbols(&document);
215 assert!(symbols.iter().any(|s| s.name == ":: doc.note ::"));
216 let session = find_symbol(&symbols, "1. Intro");
217 let child_names: Vec<_> = session
218 .children
219 .iter()
220 .map(|child| child.name.clone())
221 .collect();
222 assert!(child_names.iter().any(|name| name.contains("Cache")));
223 assert!(child_names.iter().any(|name| name.contains("List")));
224 assert!(child_names.iter().any(|name| name.contains("Verbatim")));
225
226 let _verbatim_symbol = session
228 .children
229 .iter()
230 .find(|child| child.name.contains("Cache") && child.kind == SymbolKind::OBJECT)
231 .expect("verbatim symbol not found");
232 }
233
234 #[test]
235 fn includes_paragraphs_and_list_items() {
236 use lex_core::lex::ast::elements::paragraph::TextLine;
237 use lex_core::lex::ast::{ContentItem, List, ListItem, Paragraph, TextContent};
238
239 let paragraph = Paragraph::new(vec![ContentItem::TextLine(TextLine::new(
241 TextContent::from_string("Hello World".to_string(), None),
242 ))]);
243
244 let list_item = ListItem::new("-".to_string(), "Item 1".to_string());
245 let list = List::new(vec![list_item]);
246
247 let document = Document::with_content(vec![
248 ContentItem::Paragraph(paragraph),
249 ContentItem::List(list),
250 ]);
251
252 let symbols = collect_document_symbols(&document);
253
254 let paragraph_symbol = symbols
256 .iter()
257 .find(|s| s.name == "Hello World")
258 .expect("Paragraph symbol not found");
259 assert_eq!(paragraph_symbol.kind, SymbolKind::STRING);
260
261 let list_symbol = symbols
263 .iter()
264 .find(|s| s.name.contains("List"))
265 .expect("List symbol not found");
266
267 let item_symbol = list_symbol
269 .children
270 .iter()
271 .find(|s| s.name.contains("Item 1"));
272 if item_symbol.is_none() {
273 println!("List symbol children: {:#?}", list_symbol.children);
274 }
275 let item_symbol = item_symbol.expect("List item symbol not found");
276 assert!(item_symbol.name.contains("-"));
277 }
278
279 #[test]
280 fn includes_document_level_annotations() {
281 let document = sample_document();
282 let symbols = collect_document_symbols(&document);
283 assert!(symbols.iter().any(|symbol| symbol.name == ":: doc.note ::"));
284 }
286}