1use std::path::PathBuf;
26
27use bock_ast::{
28 ClassDecl, EffectDecl, EnumDecl, EnumVariant, FnDecl, ImplBlock, Item, RecordDecl,
29 RecordDeclField, TraitDecl, TypeExpr, TypePath,
30};
31use bock_errors::{FileId, Span};
32use bock_lexer::Lexer;
33use bock_parser::Parser;
34use bock_source::{SourceFile, SourceMap};
35use tower_lsp::lsp_types::{DocumentSymbol, SymbolKind};
36
37use crate::diagnostics::span_to_range;
38
39pub struct SymbolNode {
44 pub name: String,
47 pub detail: Option<String>,
49 pub kind: SymbolKind,
51 pub span: Span,
53 pub selection_span: Span,
55 pub children: Vec<SymbolNode>,
57}
58
59pub struct DocumentSymbolsResult {
61 pub source_map: SourceMap,
64 pub file_id: FileId,
66 pub symbols: Vec<SymbolNode>,
69}
70
71#[must_use]
73pub fn document_symbols(path: PathBuf, content: String) -> DocumentSymbolsResult {
74 let mut source_map = SourceMap::new();
75 let file_id = source_map.add_file(path, content);
76 let source_file = source_map.get_file(file_id);
77
78 let mut lexer = Lexer::new(source_file);
81 let tokens = lexer.tokenize();
82 let mut parser = Parser::new(tokens, source_file);
83 let module = parser.parse_module();
84
85 let items: Vec<SymbolNode> = module.items.iter().filter_map(item_symbol).collect();
86
87 let symbols = match &module.path {
88 Some(path) => {
89 let mut full = enclosing_span(module.span, path.span);
92 for item in &items {
93 full = enclosing_span(full, item.span);
94 }
95 vec![SymbolNode {
96 name: render_segments(&path.segments),
97 detail: None,
98 kind: SymbolKind::MODULE,
99 span: full,
100 selection_span: path.span,
101 children: items,
102 }]
103 }
104 None => items,
105 };
106
107 DocumentSymbolsResult {
108 source_map,
109 file_id,
110 symbols,
111 }
112}
113
114#[must_use]
117pub fn to_lsp_symbols(nodes: &[SymbolNode], source: &SourceFile) -> Vec<DocumentSymbol> {
118 nodes
119 .iter()
120 .map(|node| {
121 let full = enclosing_span(node.span, node.selection_span);
124 let children = to_lsp_symbols(&node.children, source);
125 #[allow(deprecated)] DocumentSymbol {
127 name: node.name.clone(),
128 detail: node.detail.clone(),
129 kind: node.kind,
130 tags: None,
131 deprecated: None,
132 range: span_to_range(full, source),
133 selection_range: span_to_range(node.selection_span, source),
134 children: if children.is_empty() {
135 None
136 } else {
137 Some(children)
138 },
139 }
140 })
141 .collect()
142}
143
144fn enclosing_span(outer: Span, inner: Span) -> Span {
146 Span {
147 file: outer.file,
148 start: outer.start.min(inner.start),
149 end: outer.end.max(inner.end),
150 }
151}
152
153fn item_symbol(item: &Item) -> Option<SymbolNode> {
154 match item {
155 Item::Fn(d) => Some(fn_symbol(d, SymbolKind::FUNCTION)),
156 Item::Record(d) => Some(record_symbol(d)),
157 Item::Enum(d) => Some(enum_symbol(d)),
158 Item::Class(d) => Some(class_symbol(d)),
159 Item::Trait(d) | Item::PlatformTrait(d) => Some(trait_symbol(d)),
160 Item::Impl(d) => Some(impl_symbol(d)),
161 Item::Effect(d) => Some(effect_symbol(d)),
162 Item::TypeAlias(d) => Some(SymbolNode {
163 name: d.name.name.clone(),
164 detail: None,
165 kind: SymbolKind::TYPE_PARAMETER,
166 span: d.span,
167 selection_span: d.name.span,
168 children: Vec::new(),
169 }),
170 Item::Const(d) => Some(SymbolNode {
171 name: d.name.name.clone(),
172 detail: Some(render_type_expr(&d.ty)),
173 kind: SymbolKind::CONSTANT,
174 span: d.span,
175 selection_span: d.name.span,
176 children: Vec::new(),
177 }),
178 Item::ModuleHandle(d) => Some(SymbolNode {
179 name: format!("handle {}", render_type_path(&d.effect)),
180 detail: None,
181 kind: SymbolKind::EVENT,
182 span: d.span,
183 selection_span: d.effect.span,
184 children: Vec::new(),
185 }),
186 Item::PropertyTest(d) => Some(SymbolNode {
187 name: d.name.clone(),
188 detail: Some("property test".to_string()),
189 kind: SymbolKind::FUNCTION,
190 span: d.span,
191 selection_span: d.span,
192 children: Vec::new(),
193 }),
194 Item::Error { .. } => None,
195 }
196}
197
198fn fn_symbol(d: &FnDecl, kind: SymbolKind) -> SymbolNode {
199 SymbolNode {
200 name: d.name.name.clone(),
201 detail: None,
202 kind,
203 span: d.span,
204 selection_span: d.name.span,
205 children: Vec::new(),
206 }
207}
208
209fn field_symbol(f: &RecordDeclField) -> SymbolNode {
210 SymbolNode {
211 name: f.name.name.clone(),
212 detail: Some(render_type_expr(&f.ty)),
213 kind: SymbolKind::FIELD,
214 span: f.span,
215 selection_span: f.name.span,
216 children: Vec::new(),
217 }
218}
219
220fn record_symbol(d: &RecordDecl) -> SymbolNode {
221 SymbolNode {
222 name: d.name.name.clone(),
223 detail: None,
224 kind: SymbolKind::STRUCT,
225 span: d.span,
226 selection_span: d.name.span,
227 children: d.fields.iter().map(field_symbol).collect(),
228 }
229}
230
231fn enum_symbol(d: &EnumDecl) -> SymbolNode {
232 let children = d
233 .variants
234 .iter()
235 .map(|v| {
236 let (name, span) = match v {
237 EnumVariant::Unit { name, span, .. }
238 | EnumVariant::Struct { name, span, .. }
239 | EnumVariant::Tuple { name, span, .. } => (name, span),
240 };
241 SymbolNode {
242 name: name.name.clone(),
243 detail: None,
244 kind: SymbolKind::ENUM_MEMBER,
245 span: *span,
246 selection_span: name.span,
247 children: Vec::new(),
248 }
249 })
250 .collect();
251 SymbolNode {
252 name: d.name.name.clone(),
253 detail: None,
254 kind: SymbolKind::ENUM,
255 span: d.span,
256 selection_span: d.name.span,
257 children,
258 }
259}
260
261fn class_symbol(d: &ClassDecl) -> SymbolNode {
262 let mut children: Vec<SymbolNode> = d.fields.iter().map(field_symbol).collect();
263 children.extend(d.methods.iter().map(|m| fn_symbol(m, SymbolKind::METHOD)));
264 SymbolNode {
265 name: d.name.name.clone(),
266 detail: None,
267 kind: SymbolKind::CLASS,
268 span: d.span,
269 selection_span: d.name.span,
270 children,
271 }
272}
273
274fn trait_symbol(d: &TraitDecl) -> SymbolNode {
275 SymbolNode {
276 name: d.name.name.clone(),
277 detail: None,
278 kind: SymbolKind::INTERFACE,
279 span: d.span,
280 selection_span: d.name.span,
281 children: d
282 .methods
283 .iter()
284 .map(|m| fn_symbol(m, SymbolKind::METHOD))
285 .collect(),
286 }
287}
288
289fn impl_symbol(d: &ImplBlock) -> SymbolNode {
290 let target = render_type_expr(&d.target);
291 let (name, selection_span) = match &d.trait_path {
292 Some(trait_path) => (
293 format!("impl {} for {}", render_type_path(trait_path), target),
294 trait_path.span,
295 ),
296 None => (format!("impl {target}"), d.target.span()),
297 };
298 SymbolNode {
299 name,
300 detail: None,
301 kind: SymbolKind::OBJECT,
302 span: d.span,
303 selection_span,
304 children: d
305 .methods
306 .iter()
307 .map(|m| fn_symbol(m, SymbolKind::METHOD))
308 .collect(),
309 }
310}
311
312fn effect_symbol(d: &EffectDecl) -> SymbolNode {
313 SymbolNode {
314 name: d.name.name.clone(),
315 detail: None,
316 kind: SymbolKind::EVENT,
317 span: d.span,
318 selection_span: d.name.span,
319 children: d
320 .operations
321 .iter()
322 .map(|op| fn_symbol(op, SymbolKind::METHOD))
323 .collect(),
324 }
325}
326
327fn render_segments(segments: &[bock_ast::Ident]) -> String {
330 segments
331 .iter()
332 .map(|s| s.name.as_str())
333 .collect::<Vec<_>>()
334 .join(".")
335}
336
337fn render_type_path(path: &TypePath) -> String {
338 render_segments(&path.segments)
339}
340
341fn render_type_expr(ty: &TypeExpr) -> String {
342 match ty {
343 TypeExpr::Named { path, args, .. } => {
344 let base = render_type_path(path);
345 if args.is_empty() {
346 base
347 } else {
348 let rendered: Vec<String> = args.iter().map(render_type_expr).collect();
349 format!("{base}[{}]", rendered.join(", "))
350 }
351 }
352 TypeExpr::Tuple { elems, .. } => {
353 let rendered: Vec<String> = elems.iter().map(render_type_expr).collect();
354 format!("({})", rendered.join(", "))
355 }
356 TypeExpr::Function { params, ret, .. } => {
357 let rendered: Vec<String> = params.iter().map(render_type_expr).collect();
358 format!("Fn({}) -> {}", rendered.join(", "), render_type_expr(ret))
359 }
360 TypeExpr::Optional { inner, .. } => format!("{}?", render_type_expr(inner)),
361 TypeExpr::SelfType { .. } => "Self".to_string(),
362 }
363}
364
365#[cfg(test)]
366mod tests {
367 use super::*;
368
369 fn run(src: &str) -> DocumentSymbolsResult {
370 document_symbols(PathBuf::from("test.bock"), src.to_string())
371 }
372
373 fn find<'a>(nodes: &'a [SymbolNode], name: &str) -> &'a SymbolNode {
376 nodes
377 .iter()
378 .find(|n| n.name == name)
379 .unwrap_or_else(|| panic!("symbol `{name}` not found"))
380 }
381
382 const FULL_SRC: &str = "\
383module demo.app
384
385public record Point { x: Int, y: Int }
386
387public enum Color { Red, Green, Blue }
388
389trait Shape {
390 fn area(self) -> Int
391}
392
393impl Shape for Point {
394 public fn area(self) -> Int {
395 self.x
396 }
397}
398
399effect Log {
400 fn log(message: String) -> Void
401}
402
403const MAX: Int = 10
404
405public fn main() -> Int {
406 MAX
407}
408";
409
410 #[test]
411 fn module_declaration_becomes_root_symbol() {
412 let result = run(FULL_SRC);
413 assert_eq!(result.symbols.len(), 1, "one MODULE root expected");
414 let root = &result.symbols[0];
415 assert_eq!(root.name, "demo.app");
416 assert_eq!(root.kind, SymbolKind::MODULE);
417 assert_eq!(root.children.len(), 7, "all items nested under module");
418 }
419
420 #[test]
421 fn record_has_field_children() {
422 let result = run(FULL_SRC);
423 let point = find(&result.symbols[0].children, "Point");
424 assert_eq!(point.kind, SymbolKind::STRUCT);
425 let names: Vec<_> = point.children.iter().map(|c| c.name.as_str()).collect();
426 assert_eq!(names, vec!["x", "y"]);
427 assert!(point.children.iter().all(|c| c.kind == SymbolKind::FIELD));
428 assert_eq!(point.children[0].detail.as_deref(), Some("Int"));
429 }
430
431 #[test]
432 fn enum_has_variant_children() {
433 let result = run(FULL_SRC);
434 let color = find(&result.symbols[0].children, "Color");
435 assert_eq!(color.kind, SymbolKind::ENUM);
436 let names: Vec<_> = color.children.iter().map(|c| c.name.as_str()).collect();
437 assert_eq!(names, vec!["Red", "Green", "Blue"]);
438 assert!(color
439 .children
440 .iter()
441 .all(|c| c.kind == SymbolKind::ENUM_MEMBER));
442 }
443
444 #[test]
445 fn trait_and_impl_have_method_children() {
446 let result = run(FULL_SRC);
447 let shape = find(&result.symbols[0].children, "Shape");
448 assert_eq!(shape.kind, SymbolKind::INTERFACE);
449 assert_eq!(shape.children.len(), 1);
450 assert_eq!(shape.children[0].kind, SymbolKind::METHOD);
451
452 let imp = find(&result.symbols[0].children, "impl Shape for Point");
453 assert_eq!(imp.kind, SymbolKind::OBJECT);
454 assert_eq!(imp.children.len(), 1);
455 assert_eq!(imp.children[0].name, "area");
456 assert_eq!(imp.children[0].kind, SymbolKind::METHOD);
457 }
458
459 #[test]
460 fn effect_const_and_fn_kinds() {
461 let result = run(FULL_SRC);
462 let children = &result.symbols[0].children;
463 assert_eq!(find(children, "Log").kind, SymbolKind::EVENT);
464 assert_eq!(find(children, "Log").children[0].name, "log");
465 assert_eq!(find(children, "MAX").kind, SymbolKind::CONSTANT);
466 assert_eq!(find(children, "MAX").detail.as_deref(), Some("Int"));
467 assert_eq!(find(children, "main").kind, SymbolKind::FUNCTION);
468 }
469
470 #[test]
471 fn selection_span_is_contained_in_span() {
472 fn check(node: &SymbolNode) {
473 let full = enclosing_span(node.span, node.selection_span);
474 assert!(
475 full.start <= node.selection_span.start && node.selection_span.end <= full.end,
476 "selection span must sit inside the full span for `{}`",
477 node.name,
478 );
479 for child in &node.children {
480 check(child);
481 }
482 }
483 let result = run(FULL_SRC);
484 for node in &result.symbols {
485 check(node);
486 }
487 }
488
489 #[test]
490 fn selection_span_covers_the_name() {
491 let result = run(FULL_SRC);
492 let file = result.source_map.get_file(result.file_id);
493 let point = find(&result.symbols[0].children, "Point");
494 assert_eq!(file.slice(point.selection_span), "Point");
495 let main_fn = find(&result.symbols[0].children, "main");
496 assert_eq!(file.slice(main_fn.selection_span), "main");
497 }
498
499 #[test]
500 fn to_lsp_symbols_produces_nested_tree() {
501 let result = run(FULL_SRC);
502 let file = result.source_map.get_file(result.file_id);
503 let lsp = to_lsp_symbols(&result.symbols, file);
504 assert_eq!(lsp.len(), 1);
505 let root = &lsp[0];
506 assert_eq!(root.kind, SymbolKind::MODULE);
507 let children = root.children.as_ref().expect("module children");
508 assert_eq!(children.len(), 7);
509 for child in children {
511 assert!(child.range.start <= child.selection_range.start);
512 assert!(child.selection_range.end <= child.range.end);
513 }
514 let main_fn = children
516 .iter()
517 .find(|c| c.name == "main")
518 .expect("main present");
519 assert!(main_fn.children.is_none());
520 }
521
522 #[test]
523 fn empty_module_has_no_symbols() {
524 let result = run("module m\n");
525 assert_eq!(result.symbols.len(), 1, "module root only");
526 assert!(result.symbols[0].children.is_empty());
527 }
528
529 #[test]
530 fn file_without_module_declaration_yields_flat_list() {
531 let src = "\
532fn lonely() -> Int {
533 1
534}
535";
536 let result = run(src);
537 assert_eq!(result.symbols.len(), 1);
538 assert_eq!(result.symbols[0].name, "lonely");
539 assert_eq!(result.symbols[0].kind, SymbolKind::FUNCTION);
540 }
541}