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::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::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::PROPERTY,
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::ENUM,
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::CONSTANT,
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 truncate_to_words(&first_line.content, 4, "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::ENUM_MEMBER,
154 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::INTERFACE,
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 summarize_text_str(text.as_string().trim(), fallback)
192}
193
194fn summarize_text_str(text: &str, fallback: &str) -> String {
195 if text.is_empty() {
196 fallback.to_string()
197 } else {
198 text.to_string()
199 }
200}
201
202fn truncate_to_words(text: &TextContent, max_words: usize, fallback: &str) -> String {
203 let trimmed = text.as_string().trim();
204 if trimmed.is_empty() {
205 return fallback.to_string();
206 }
207 let words: Vec<&str> = trimmed.split_whitespace().collect();
208 if words.len() <= max_words {
209 trimmed.to_string()
210 } else {
211 format!("{}…", words[..max_words].join(" "))
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218 use crate::test_support::sample_document;
219
220 fn find_symbol<'a>(symbols: &'a [LexDocumentSymbol], name: &str) -> &'a LexDocumentSymbol {
221 symbols
222 .iter()
223 .find(|symbol| symbol.name == name)
224 .unwrap_or_else(|| panic!("symbol {name} not found"))
225 }
226
227 #[test]
228 fn builds_session_tree() {
229 let document = sample_document();
230 let symbols = collect_document_symbols(&document);
231 assert!(symbols.iter().any(|s| s.name == ":: doc.note ::"));
232 let session = find_symbol(&symbols, "1. Intro");
233 let child_names: Vec<_> = session
234 .children
235 .iter()
236 .map(|child| child.name.clone())
237 .collect();
238 assert!(child_names.iter().any(|name| name.contains("Cache")));
239 assert!(child_names.iter().any(|name| name.contains("List")));
240 assert!(child_names.iter().any(|name| name.contains("Verbatim")));
241
242 let _verbatim_symbol = session
244 .children
245 .iter()
246 .find(|child| child.name.contains("Cache") && child.kind == SymbolKind::CONSTANT)
247 .expect("verbatim symbol not found");
248 }
249
250 #[test]
251 fn includes_paragraphs_and_list_items() {
252 use lex_core::lex::ast::elements::paragraph::TextLine;
253 use lex_core::lex::ast::{ContentItem, List, ListItem, Paragraph, TextContent};
254
255 let paragraph = Paragraph::new(vec![ContentItem::TextLine(TextLine::new(
257 TextContent::from_string("Hello World".to_string(), None),
258 ))]);
259
260 let list_item = ListItem::new("-".to_string(), "Item 1".to_string());
261 let list = List::new(vec![list_item]);
262
263 let document = Document::with_content(vec![
264 ContentItem::Paragraph(paragraph),
265 ContentItem::List(list),
266 ]);
267
268 let symbols = collect_document_symbols(&document);
269
270 let paragraph_symbol = symbols
272 .iter()
273 .find(|s| s.name.contains("Hello"))
274 .expect("Paragraph symbol not found");
275 assert_eq!(paragraph_symbol.kind, SymbolKind::STRING);
276
277 let list_symbol = symbols
279 .iter()
280 .find(|s| s.name.contains("List"))
281 .expect("List symbol not found");
282
283 let item_symbol = list_symbol
285 .children
286 .iter()
287 .find(|s| s.name.contains("Item 1"));
288 if item_symbol.is_none() {
289 println!("List symbol children: {:#?}", list_symbol.children);
290 }
291 let item_symbol = item_symbol.expect("List item symbol not found");
292 assert!(item_symbol.name.contains("-"));
293 }
294
295 #[test]
296 fn includes_document_level_annotations() {
297 let document = sample_document();
298 let symbols = collect_document_symbols(&document);
299 assert!(symbols.iter().any(|symbol| symbol.name == ":: doc.note ::"));
300 }
302}