Skip to main content

gruel_lsp/
completion.rs

1//! Completion (ADR-0091 Phase 6).
2//!
3//! Trigger characters: `.`, `@`, `:`, `(`. The phase-6 model:
4//!
5//! - After `.` on a receiver expression: fields and methods on the
6//!   receiver's type (when we have it from the AIR side-table).
7//! - After `@`: intrinsic names from the [`gruel_intrinsics`] registry.
8//! - Otherwise: locals in the enclosing function (from RIR's `Local`
9//!   sites — recovered via AST walk), plus every top-level item.
10
11use std::collections::HashSet;
12
13use gruel_parser::ast::{
14    AssignTarget, Ast, BlockExpr, Expr, Function, Ident, Item, Method, Pattern, Statement,
15};
16use lasso::ThreadedRodeo;
17
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub enum CompletionKind {
20    Function,
21    Struct,
22    Enum,
23    Interface,
24    Derive,
25    Constant,
26    Field,
27    EnumMember,
28    Variable,
29    Method,
30    Keyword,
31    Intrinsic,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct CompletionItem {
36    pub label: String,
37    pub kind: CompletionKind,
38    pub detail: Option<String>,
39}
40
41const KEYWORDS: &[&str] = &[
42    "fn",
43    "let",
44    "mut",
45    "struct",
46    "enum",
47    "interface",
48    "derive",
49    "const",
50    "pub",
51    "if",
52    "else",
53    "match",
54    "while",
55    "for",
56    "loop",
57    "in",
58    "break",
59    "continue",
60    "return",
61    "true",
62    "false",
63    "self",
64    "Self",
65];
66
67/// Completion at the given byte position. `trigger` is `Some('.')` /
68/// `Some('@')` if the editor invoked us via a trigger character.
69pub fn complete_at(
70    ast: &Ast,
71    interner: &ThreadedRodeo,
72    file_id: gruel_util::FileId,
73    byte: u32,
74    trigger: Option<char>,
75) -> Vec<CompletionItem> {
76    let mut items = Vec::new();
77    let mut seen: HashSet<String> = HashSet::new();
78    let mut push = |items: &mut Vec<CompletionItem>, item: CompletionItem| {
79        if seen.insert(item.label.clone()) {
80            items.push(item);
81        }
82    };
83
84    match trigger {
85        Some('@') => {
86            for def in gruel_intrinsics::INTRINSICS.iter() {
87                push(
88                    &mut items,
89                    CompletionItem {
90                        label: format!("@{}", def.name),
91                        kind: CompletionKind::Intrinsic,
92                        detail: Some(def.summary.to_string()),
93                    },
94                );
95            }
96            return items;
97        }
98        Some('.') => {
99            // Dot completion: surface every field/method name in the workspace.
100            // Without sema-level expression type info routed here we can't
101            // restrict to the receiver's type; this is the simplest correct
102            // option (over-suggests, never under-suggests).
103            push_fields_and_methods(ast, interner, &mut items, &mut seen);
104            return items;
105        }
106        _ => {}
107    }
108
109    // Generic context: locals in scope + top-level items + keywords.
110    let enclosing = enclosing_function(ast, file_id, byte);
111    if let Some(f) = enclosing {
112        for p in &f.params {
113            push(
114                &mut items,
115                CompletionItem {
116                    label: interner.resolve(&p.name.name).to_string(),
117                    kind: CompletionKind::Variable,
118                    detail: None,
119                },
120            );
121        }
122        collect_lets(&f.body, interner, &mut |label| {
123            push(
124                &mut items,
125                CompletionItem {
126                    label,
127                    kind: CompletionKind::Variable,
128                    detail: None,
129                },
130            )
131        });
132    }
133
134    for item in &ast.items {
135        match item {
136            Item::Function(f) => push(
137                &mut items,
138                CompletionItem {
139                    label: interner.resolve(&f.name.name).to_string(),
140                    kind: CompletionKind::Function,
141                    detail: None,
142                },
143            ),
144            Item::Struct(s) => push(
145                &mut items,
146                CompletionItem {
147                    label: interner.resolve(&s.name.name).to_string(),
148                    kind: CompletionKind::Struct,
149                    detail: None,
150                },
151            ),
152            Item::Enum(e) => push(
153                &mut items,
154                CompletionItem {
155                    label: interner.resolve(&e.name.name).to_string(),
156                    kind: CompletionKind::Enum,
157                    detail: None,
158                },
159            ),
160            Item::Interface(i) => push(
161                &mut items,
162                CompletionItem {
163                    label: interner.resolve(&i.name.name).to_string(),
164                    kind: CompletionKind::Interface,
165                    detail: None,
166                },
167            ),
168            Item::Derive(d) => push(
169                &mut items,
170                CompletionItem {
171                    label: interner.resolve(&d.name.name).to_string(),
172                    kind: CompletionKind::Derive,
173                    detail: None,
174                },
175            ),
176            Item::Const(c) => push(
177                &mut items,
178                CompletionItem {
179                    label: interner.resolve(&c.name.name).to_string(),
180                    kind: CompletionKind::Constant,
181                    detail: None,
182                },
183            ),
184            _ => {}
185        }
186    }
187
188    for kw in KEYWORDS {
189        push(
190            &mut items,
191            CompletionItem {
192                label: (*kw).to_string(),
193                kind: CompletionKind::Keyword,
194                detail: None,
195            },
196        );
197    }
198
199    items
200}
201
202fn enclosing_function(ast: &Ast, file_id: gruel_util::FileId, byte: u32) -> Option<&Function> {
203    for item in &ast.items {
204        if let Item::Function(f) = item {
205            if f.span.file_id == file_id && byte >= f.span.start && byte <= f.span.end {
206                return Some(f);
207            }
208        }
209    }
210    None
211}
212
213fn collect_lets(expr: &Expr, interner: &ThreadedRodeo, push: &mut impl FnMut(String)) {
214    match expr {
215        Expr::Block(b) => collect_lets_block(b, interner, push),
216        _ => {}
217    }
218}
219
220fn collect_lets_block(b: &BlockExpr, interner: &ThreadedRodeo, push: &mut impl FnMut(String)) {
221    for stmt in &b.statements {
222        match stmt {
223            Statement::Let(l) => {
224                if let Pattern::Ident { name, .. } = &l.pattern {
225                    push(interner.resolve(&name.name).to_string());
226                }
227                if let Expr::Block(_) = &*l.init {
228                    collect_lets(&l.init, interner, push);
229                }
230            }
231            Statement::Assign(_) => {}
232            Statement::Expr(e) => collect_lets(e, interner, push),
233        }
234    }
235    collect_lets(&b.expr, interner, push);
236}
237
238fn push_fields_and_methods(
239    ast: &Ast,
240    interner: &ThreadedRodeo,
241    items: &mut Vec<CompletionItem>,
242    seen: &mut HashSet<String>,
243) {
244    let mut push = |items: &mut Vec<CompletionItem>, item: CompletionItem| {
245        if seen.insert(item.label.clone()) {
246            items.push(item);
247        }
248    };
249
250    for item in &ast.items {
251        match item {
252            Item::Struct(s) => {
253                for f in &s.fields {
254                    push(
255                        items,
256                        CompletionItem {
257                            label: interner.resolve(&f.name.name).to_string(),
258                            kind: CompletionKind::Field,
259                            detail: None,
260                        },
261                    );
262                }
263                for m in &s.methods {
264                    push(
265                        items,
266                        CompletionItem {
267                            label: interner.resolve(&m.name.name).to_string(),
268                            kind: CompletionKind::Method,
269                            detail: None,
270                        },
271                    );
272                }
273            }
274            Item::Enum(e) => {
275                for v in &e.variants {
276                    push(
277                        items,
278                        CompletionItem {
279                            label: interner.resolve(&v.name.name).to_string(),
280                            kind: CompletionKind::EnumMember,
281                            detail: None,
282                        },
283                    );
284                }
285                for m in &e.methods {
286                    push(
287                        items,
288                        CompletionItem {
289                            label: interner.resolve(&m.name.name).to_string(),
290                            kind: CompletionKind::Method,
291                            detail: None,
292                        },
293                    );
294                }
295            }
296            Item::Derive(d) => {
297                for m in &d.methods {
298                    push(
299                        items,
300                        CompletionItem {
301                            label: interner.resolve(&m.name.name).to_string(),
302                            kind: CompletionKind::Method,
303                            detail: None,
304                        },
305                    );
306                }
307            }
308            Item::Interface(i) => {
309                for sig in &i.methods {
310                    push(
311                        items,
312                        CompletionItem {
313                            label: interner.resolve(&sig.name.name).to_string(),
314                            kind: CompletionKind::Method,
315                            detail: None,
316                        },
317                    );
318                }
319            }
320            _ => {}
321        }
322    }
323}
324
325// Silence unused-import warning when method-walking is no longer used after
326// future refactors.
327#[allow(dead_code)]
328fn _suppress() {
329    let _: Option<Ident> = None;
330    let _: Option<&Method> = None;
331    let _: Option<&dyn Fn(&AssignTarget) -> ()> = None;
332}
333
334#[cfg(test)]
335mod tests {
336    use super::*;
337    use gruel_compiler::{
338        FileId, PreviewFeatures, SourceFile, merge_symbols, parse_all_files_with_preview,
339    };
340
341    fn parse(source: &str) -> (Ast, ThreadedRodeo) {
342        let sources = vec![SourceFile::new("main.gruel", source, FileId::new(1))];
343        let parsed = parse_all_files_with_preview(&sources, &PreviewFeatures::default()).unwrap();
344        let merged = merge_symbols(parsed).unwrap();
345        (merged.ast, merged.interner)
346    }
347
348    #[test]
349    fn intrinsic_completion_after_at() {
350        let src = "fn main() -> i32 { 0 }";
351        let (ast, interner) = parse(src);
352        let items = complete_at(&ast, &interner, FileId::new(1), 19, Some('@'));
353        assert!(!items.is_empty());
354        assert!(items.iter().all(|i| i.label.starts_with('@')));
355        assert!(items.iter().any(|i| i.kind == CompletionKind::Intrinsic));
356    }
357
358    #[test]
359    fn dot_completion_surfaces_fields_and_methods() {
360        let src = r#"struct Point { x: i32, y: i32, fn sum(self) -> i32 { self.x + self.y } }
361fn main() -> i32 { 0 }"#;
362        let (ast, interner) = parse(src);
363        let items = complete_at(&ast, &interner, FileId::new(1), 30, Some('.'));
364        let labels: Vec<_> = items.iter().map(|i| i.label.as_str()).collect();
365        assert!(labels.contains(&"x"));
366        assert!(labels.contains(&"y"));
367        assert!(labels.contains(&"sum"));
368    }
369
370    #[test]
371    fn generic_context_includes_top_level_items() {
372        let src = "fn foo() -> i32 { 0 }\nstruct Bar { x: i32 }\nfn main() -> i32 { 0 }";
373        let (ast, interner) = parse(src);
374        // Cursor inside main's body.
375        let byte = src.find("0 }").unwrap() as u32;
376        let items = complete_at(&ast, &interner, FileId::new(1), byte, None);
377        let labels: Vec<_> = items.iter().map(|i| i.label.as_str()).collect();
378        assert!(labels.contains(&"foo"));
379        assert!(labels.contains(&"Bar"));
380        assert!(labels.contains(&"main"));
381        // Keywords too.
382        assert!(labels.contains(&"if"));
383        assert!(labels.contains(&"let"));
384    }
385
386    #[test]
387    fn generic_context_includes_locals_in_function() {
388        let src = "fn main() -> i32 { let answer = 42; 0 }";
389        let (ast, interner) = parse(src);
390        let byte = src.find("0 }").unwrap() as u32;
391        let items = complete_at(&ast, &interner, FileId::new(1), byte, None);
392        assert!(
393            items
394                .iter()
395                .any(|i| i.label == "answer" && i.kind == CompletionKind::Variable),
396            "expected `answer` in completion, got: {:?}",
397            items.iter().map(|i| &i.label).collect::<Vec<_>>()
398        );
399    }
400}