Skip to main content

gruel_lsp/
signature.rs

1//! Signature help (ADR-0091 Phase 4).
2//!
3//! When the cursor sits after `(` or `,` inside a call, return the
4//! callee's parameter list with the active parameter index.
5
6use gruel_parser::ast::{Ast, BlockExpr, CallArg, Expr, Function, Item, Statement, TypeExpr};
7use gruel_util::FileId;
8use lasso::{Spur, ThreadedRodeo};
9
10/// Result returned to the editor.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct SignatureHelpResult {
13    /// One signature label, e.g. `"fn foo(x: i32, y: bool) -> i32"`.
14    pub label: String,
15    /// Parameter labels — `(start, end)` byte offsets into `label`.
16    pub parameters: Vec<(u32, u32)>,
17    /// Active parameter index (0-based).
18    pub active_parameter: usize,
19}
20
21/// Find the enclosing call at byte `byte` and produce signature help.
22pub fn signature_help(
23    ast: &Ast,
24    interner: &ThreadedRodeo,
25    file_id: FileId,
26    byte: u32,
27) -> Option<SignatureHelpResult> {
28    let info = find_enclosing_call(ast, file_id, byte)?;
29    let target_fn = find_function(ast, info.callee)?;
30    Some(build_signature(target_fn, interner, info.active_parameter))
31}
32
33#[derive(Clone, Debug)]
34struct CallInfo {
35    callee: Spur,
36    active_parameter: usize,
37}
38
39fn active_param(args: &[CallArg], byte: u32) -> usize {
40    let mut idx = 0usize;
41    for arg in args {
42        if byte <= arg.span.end {
43            return idx;
44        }
45        idx += 1;
46    }
47    idx
48}
49
50fn find_enclosing_call(ast: &Ast, file_id: FileId, byte: u32) -> Option<CallInfo> {
51    let mut best: Option<(u32, CallInfo)> = None;
52    for item in &ast.items {
53        match item {
54            Item::Function(f) => visit_function(f, file_id, byte, &mut best),
55            Item::Struct(s) => {
56                for m in &s.methods {
57                    visit_expr(&m.body, file_id, byte, &mut best);
58                }
59            }
60            Item::Enum(e) => {
61                for m in &e.methods {
62                    visit_expr(&m.body, file_id, byte, &mut best);
63                }
64            }
65            Item::Derive(d) => {
66                for m in &d.methods {
67                    visit_expr(&m.body, file_id, byte, &mut best);
68                }
69            }
70            Item::Const(c) => visit_expr(&c.init, file_id, byte, &mut best),
71            _ => {}
72        }
73    }
74    best.map(|(_, c)| c)
75}
76
77fn visit_function(f: &Function, file_id: FileId, byte: u32, best: &mut Option<(u32, CallInfo)>) {
78    visit_expr(&f.body, file_id, byte, best);
79}
80
81fn visit_block(b: &BlockExpr, file_id: FileId, byte: u32, best: &mut Option<(u32, CallInfo)>) {
82    for stmt in &b.statements {
83        match stmt {
84            Statement::Let(l) => visit_expr(&l.init, file_id, byte, best),
85            Statement::Assign(a) => visit_expr(&a.value, file_id, byte, best),
86            Statement::Expr(e) => visit_expr(e, file_id, byte, best),
87        }
88    }
89    visit_expr(&b.expr, file_id, byte, best);
90}
91
92fn visit_expr(e: &Expr, file_id: FileId, byte: u32, best: &mut Option<(u32, CallInfo)>) {
93    match e {
94        Expr::Call(c) => {
95            if c.span.file_id == file_id && byte >= c.span.start && byte <= c.span.end {
96                let size = c.span.end.saturating_sub(c.span.start);
97                if best.as_ref().map_or(true, |(b, _)| size <= *b) {
98                    *best = Some((
99                        size,
100                        CallInfo {
101                            callee: c.name.name,
102                            active_parameter: active_param(&c.args, byte),
103                        },
104                    ));
105                }
106            }
107            for arg in &c.args {
108                visit_expr(&arg.expr, file_id, byte, best);
109            }
110        }
111        Expr::MethodCall(m) => {
112            if m.span.file_id == file_id && byte >= m.span.start && byte <= m.span.end {
113                let size = m.span.end.saturating_sub(m.span.start);
114                if best.as_ref().map_or(true, |(b, _)| size <= *b) {
115                    *best = Some((
116                        size,
117                        CallInfo {
118                            callee: m.method.name,
119                            active_parameter: active_param(&m.args, byte),
120                        },
121                    ));
122                }
123            }
124            visit_expr(&m.receiver, file_id, byte, best);
125            for arg in &m.args {
126                visit_expr(&arg.expr, file_id, byte, best);
127            }
128        }
129        Expr::Block(b) => visit_block(b, file_id, byte, best),
130        Expr::If(i) => {
131            visit_expr(&i.cond, file_id, byte, best);
132            visit_block(&i.then_block, file_id, byte, best);
133            if let Some(b) = &i.else_block {
134                visit_block(b, file_id, byte, best);
135            }
136        }
137        Expr::While(w) => {
138            visit_expr(&w.cond, file_id, byte, best);
139            visit_block(&w.body, file_id, byte, best);
140        }
141        Expr::For(f) => {
142            visit_expr(&f.iterable, file_id, byte, best);
143            visit_block(&f.body, file_id, byte, best);
144        }
145        Expr::Loop(l) => visit_block(&l.body, file_id, byte, best),
146        Expr::Match(m) => {
147            visit_expr(&m.scrutinee, file_id, byte, best);
148            for arm in &m.arms {
149                visit_expr(&arm.body, file_id, byte, best);
150            }
151        }
152        Expr::Binary(b) => {
153            visit_expr(&b.left, file_id, byte, best);
154            visit_expr(&b.right, file_id, byte, best);
155        }
156        Expr::Unary(u) => visit_expr(&u.operand, file_id, byte, best),
157        Expr::Paren(p) => visit_expr(&p.inner, file_id, byte, best),
158        Expr::Field(fa) => visit_expr(&fa.base, file_id, byte, best),
159        Expr::Return(r) => {
160            if let Some(e) = &r.value {
161                visit_expr(e, file_id, byte, best);
162            }
163        }
164        Expr::Tuple(t) => {
165            for e in &t.elems {
166                visit_expr(e, file_id, byte, best);
167            }
168        }
169        Expr::ArrayLit(a) => {
170            for e in &a.elements {
171                visit_expr(e, file_id, byte, best);
172            }
173        }
174        Expr::Index(i) => {
175            visit_expr(&i.base, file_id, byte, best);
176            visit_expr(&i.index, file_id, byte, best);
177        }
178        _ => {}
179    }
180}
181
182fn find_function(ast: &Ast, name: Spur) -> Option<Function> {
183    for item in &ast.items {
184        if let Item::Function(f) = item {
185            if f.name.name == name {
186                return Some(f.clone());
187            }
188        }
189    }
190    None
191}
192
193fn build_signature(f: Function, interner: &ThreadedRodeo, active: usize) -> SignatureHelpResult {
194    let mut label = String::from("fn ");
195    label.push_str(interner.resolve(&f.name.name));
196    label.push('(');
197    let mut parameters = Vec::new();
198    for (i, p) in f.params.iter().enumerate() {
199        if i > 0 {
200            label.push_str(", ");
201        }
202        let start = label.len() as u32;
203        label.push_str(interner.resolve(&p.name.name));
204        label.push_str(": ");
205        label.push_str(&type_expr_display(&p.ty, interner));
206        let end = label.len() as u32;
207        parameters.push((start, end));
208    }
209    label.push(')');
210    if let Some(rt) = &f.return_type {
211        label.push_str(" -> ");
212        label.push_str(&type_expr_display(rt, interner));
213    }
214    let clamped_active = if f.params.is_empty() {
215        0
216    } else {
217        active.min(f.params.len() - 1)
218    };
219    SignatureHelpResult {
220        label,
221        parameters,
222        active_parameter: clamped_active,
223    }
224}
225
226fn type_expr_display(ty: &TypeExpr, interner: &ThreadedRodeo) -> String {
227    match ty {
228        TypeExpr::Named(ident) => interner.resolve(&ident.name).to_string(),
229        TypeExpr::Unit(_) => "()".to_string(),
230        TypeExpr::Never(_) => "!".to_string(),
231        TypeExpr::Array {
232            element, length, ..
233        } => {
234            format!("[{}; {}]", type_expr_display(element, interner), length)
235        }
236        TypeExpr::Tuple { elems, .. } => {
237            let mut s = String::from("(");
238            for (i, e) in elems.iter().enumerate() {
239                if i > 0 {
240                    s.push_str(", ");
241                }
242                s.push_str(&type_expr_display(e, interner));
243            }
244            if elems.len() == 1 {
245                s.push(',');
246            }
247            s.push(')');
248            s
249        }
250        _ => "_".to_string(),
251    }
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257    use gruel_compiler::{
258        PreviewFeatures, SourceFile, merge_symbols, parse_all_files_with_preview,
259    };
260
261    fn parse(source: &str) -> (Ast, ThreadedRodeo) {
262        let sources = vec![SourceFile::new("main.gruel", source, FileId::new(1))];
263        let parsed = parse_all_files_with_preview(&sources, &PreviewFeatures::default()).unwrap();
264        let merged = merge_symbols(parsed).unwrap();
265        (merged.ast, merged.interner)
266    }
267
268    #[test]
269    fn signature_help_for_call() {
270        let src = "fn add(x: i32, y: i32) -> i32 { x + y }\nfn main() -> i32 { add(1, 2) }";
271        let (ast, interner) = parse(src);
272        let pos = src.find("add(1").unwrap() as u32 + 4;
273        let sig = signature_help(&ast, &interner, FileId::new(1), pos).unwrap();
274        assert!(sig.label.starts_with("fn add(x: i32, y: i32)"));
275        assert_eq!(sig.active_parameter, 0);
276        assert_eq!(sig.parameters.len(), 2);
277    }
278
279    #[test]
280    fn signature_help_active_param_advances_after_comma() {
281        let src = "fn add(x: i32, y: i32) -> i32 { x + y }\nfn main() -> i32 { add(1, 2) }";
282        let (ast, interner) = parse(src);
283        let pos = src.find("add(1, 2)").unwrap() as u32 + 7;
284        let sig = signature_help(&ast, &interner, FileId::new(1), pos).unwrap();
285        assert_eq!(sig.active_parameter, 1);
286    }
287}