Skip to main content

bock_lsp/
document_symbol.rs

1//! `textDocument/documentSymbol` support — a hierarchical outline of the
2//! buffer's declarations.
3//!
4//! Only lex + parse are needed: the outline is purely syntactic. Items map
5//! to LSP `SymbolKind`s as follows:
6//!
7//! | Bock item            | SymbolKind       | children                  |
8//! |----------------------|------------------|---------------------------|
9//! | `module` declaration | `MODULE`         | all items                 |
10//! | `fn`                 | `FUNCTION`       | —                         |
11//! | `record`             | `STRUCT`         | fields (`FIELD`)          |
12//! | `enum`               | `ENUM`           | variants (`ENUM_MEMBER`)  |
13//! | `class`              | `CLASS`          | fields + methods          |
14//! | `trait`              | `INTERFACE`      | methods (`METHOD`)        |
15//! | `impl`               | `OBJECT`         | methods (`METHOD`)        |
16//! | `effect`             | `EVENT`          | operations (`METHOD`)     |
17//! | `type` alias         | `TYPE_PARAMETER` | —                         |
18//! | `const`              | `CONSTANT`       | —                         |
19//! | `handle` (module)    | `EVENT`          | —                         |
20//! | `property` test      | `FUNCTION`       | —                         |
21//!
22//! Each symbol's `selection_span` is the declared name; its `span` is the
23//! whole item. Parse-error recovery items are skipped.
24
25use 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
39/// One node in the buffer's symbol outline, in source-span form.
40///
41/// Conversion to LSP ranges happens in [`to_lsp_symbols`]; keeping spans
42/// here lets tests assert on the tree without a position back-end.
43pub struct SymbolNode {
44    /// Display name (the declared identifier, or a rendered header for
45    /// nameless items like `impl` blocks).
46    pub name: String,
47    /// Optional extra detail shown next to the name.
48    pub detail: Option<String>,
49    /// LSP symbol kind.
50    pub kind: SymbolKind,
51    /// Span of the whole item.
52    pub span: Span,
53    /// Span of the name. Always contained within [`SymbolNode::span`].
54    pub selection_span: Span,
55    /// Nested symbols (fields, variants, methods…).
56    pub children: Vec<SymbolNode>,
57}
58
59/// Result of building the outline for one document.
60pub struct DocumentSymbolsResult {
61    /// Owned source map containing the document (keeps `SourceFile`
62    /// borrows valid for the lifetime of the result).
63    pub source_map: SourceMap,
64    /// Id of the added file inside [`DocumentSymbolsResult::source_map`].
65    pub file_id: FileId,
66    /// Root symbols. If the buffer declares `module <path>`, a single
67    /// `MODULE` root wraps every item.
68    pub symbols: Vec<SymbolNode>,
69}
70
71/// Lex + parse `content` and build its symbol outline.
72#[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    // Tolerate diagnostics: a partial AST still produces a useful outline
79    // for the well-formed items.
80    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            // The root's range must enclose the module path *and* every
90            // child item, so clients can rely on the containment invariant.
91            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/// Convert an outline tree into LSP [`DocumentSymbol`]s using `source` for
115/// position lookup.
116#[must_use]
117pub fn to_lsp_symbols(nodes: &[SymbolNode], source: &SourceFile) -> Vec<DocumentSymbol> {
118    nodes
119        .iter()
120        .map(|node| {
121            // The LSP requires `selection_range` ⊆ `range`; guard against
122            // any item span that fails to cover its name span.
123            let full = enclosing_span(node.span, node.selection_span);
124            let children = to_lsp_symbols(&node.children, source);
125            #[allow(deprecated)] // `deprecated` is a required struct field.
126            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
144/// The smallest span covering both `outer` and `inner`.
145fn 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
327// ─── Type rendering (compact, for names/details) ─────────────────────────────
328
329fn 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    /// Find a child by name within a symbol list, panicking with a useful
374    /// message when absent.
375    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        // Ranges must satisfy the LSP containment invariant.
510        for child in children {
511            assert!(child.range.start <= child.selection_range.start);
512            assert!(child.selection_range.end <= child.range.end);
513        }
514        // Leaf symbols have no children array at all.
515        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}