Skip to main content

gruel_lsp/
workspace_symbols.rs

1//! Workspace symbols (ADR-0091 Phase 5).
2//!
3//! Walks the merged AST and emits a `SymbolInformation`-shaped entry per
4//! top-level item. Filtered by substring match against the LSP query.
5
6use gruel_parser::ast::{Ast, Ident, Item};
7use gruel_util::Span;
8use lasso::ThreadedRodeo;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub enum SymbolKind {
12    Function,
13    Struct,
14    Enum,
15    Interface,
16    Derive,
17    Constant,
18    Field,
19    EnumMember,
20    Method,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct WorkspaceSymbol {
25    pub name: String,
26    pub kind: SymbolKind,
27    pub span: Span,
28    /// Optional container name (e.g. struct/enum that owns a method).
29    pub container: Option<String>,
30}
31
32/// Collect every top-level (and nested method/field/variant) symbol
33/// matching `query` (substring match, case-insensitive).
34pub fn workspace_symbols(ast: &Ast, interner: &ThreadedRodeo, query: &str) -> Vec<WorkspaceSymbol> {
35    let query_lower = query.to_lowercase();
36    let mut out = Vec::new();
37    for item in &ast.items {
38        emit_item(item, interner, &query_lower, &mut out);
39    }
40    out
41}
42
43fn emit_item(item: &Item, interner: &ThreadedRodeo, query: &str, out: &mut Vec<WorkspaceSymbol>) {
44    match item {
45        Item::Function(f) => {
46            push_if_match(&f.name, SymbolKind::Function, None, interner, query, out);
47        }
48        Item::Struct(s) => {
49            push_if_match(&s.name, SymbolKind::Struct, None, interner, query, out);
50            let container = interner.resolve(&s.name.name).to_string();
51            for field in &s.fields {
52                push_if_match(
53                    &field.name,
54                    SymbolKind::Field,
55                    Some(container.clone()),
56                    interner,
57                    query,
58                    out,
59                );
60            }
61            for m in &s.methods {
62                push_if_match(
63                    &m.name,
64                    SymbolKind::Method,
65                    Some(container.clone()),
66                    interner,
67                    query,
68                    out,
69                );
70            }
71        }
72        Item::Enum(e) => {
73            push_if_match(&e.name, SymbolKind::Enum, None, interner, query, out);
74            let container = interner.resolve(&e.name.name).to_string();
75            for v in &e.variants {
76                push_if_match(
77                    &v.name,
78                    SymbolKind::EnumMember,
79                    Some(container.clone()),
80                    interner,
81                    query,
82                    out,
83                );
84            }
85            for m in &e.methods {
86                push_if_match(
87                    &m.name,
88                    SymbolKind::Method,
89                    Some(container.clone()),
90                    interner,
91                    query,
92                    out,
93                );
94            }
95        }
96        Item::Interface(i) => {
97            push_if_match(&i.name, SymbolKind::Interface, None, interner, query, out);
98            let container = interner.resolve(&i.name.name).to_string();
99            for sig in &i.methods {
100                push_if_match(
101                    &sig.name,
102                    SymbolKind::Method,
103                    Some(container.clone()),
104                    interner,
105                    query,
106                    out,
107                );
108            }
109        }
110        Item::Derive(d) => {
111            push_if_match(&d.name, SymbolKind::Derive, None, interner, query, out);
112            let container = interner.resolve(&d.name.name).to_string();
113            for m in &d.methods {
114                push_if_match(
115                    &m.name,
116                    SymbolKind::Method,
117                    Some(container.clone()),
118                    interner,
119                    query,
120                    out,
121                );
122            }
123        }
124        Item::Const(c) => {
125            push_if_match(&c.name, SymbolKind::Constant, None, interner, query, out);
126        }
127        Item::LinkExtern(b) => {
128            for ext in &b.items {
129                push_if_match(&ext.name, SymbolKind::Function, None, interner, query, out);
130            }
131        }
132        Item::Error(_) => {}
133    }
134}
135
136fn push_if_match(
137    ident: &Ident,
138    kind: SymbolKind,
139    container: Option<String>,
140    interner: &ThreadedRodeo,
141    query: &str,
142    out: &mut Vec<WorkspaceSymbol>,
143) {
144    let name = interner.resolve(&ident.name);
145    if !query.is_empty() && !name.to_lowercase().contains(query) {
146        return;
147    }
148    out.push(WorkspaceSymbol {
149        name: name.to_string(),
150        kind,
151        span: ident.span,
152        container,
153    });
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159    use gruel_compiler::{
160        FileId, PreviewFeatures, SourceFile, merge_symbols, parse_all_files_with_preview,
161    };
162
163    fn parse(source: &str) -> (Ast, ThreadedRodeo) {
164        let sources = vec![SourceFile::new("main.gruel", source, FileId::new(1))];
165        let parsed = parse_all_files_with_preview(&sources, &PreviewFeatures::default()).unwrap();
166        let merged = merge_symbols(parsed).unwrap();
167        (merged.ast, merged.interner)
168    }
169
170    #[test]
171    fn all_top_level_items() {
172        let src = "fn foo() -> i32 { 0 }\nstruct Bar { x: i32 }\nconst N: i32 = 1;";
173        let (ast, interner) = parse(src);
174        let syms = workspace_symbols(&ast, &interner, "");
175        let names: Vec<_> = syms.iter().map(|s| s.name.as_str()).collect();
176        assert!(names.contains(&"foo"));
177        assert!(names.contains(&"Bar"));
178        assert!(names.contains(&"N"));
179        assert!(names.contains(&"x"));
180    }
181
182    #[test]
183    fn filter_by_substring() {
184        let src = "fn foo() -> i32 { 0 }\nstruct Bar { x: i32 }";
185        let (ast, interner) = parse(src);
186        let syms = workspace_symbols(&ast, &interner, "bar");
187        assert_eq!(syms.len(), 1);
188        assert_eq!(syms[0].name, "Bar");
189    }
190}