1use 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 pub container: Option<String>,
30}
31
32pub 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}