1use gruel_parser::ast::{Ast, BlockExpr, CallArg, Expr, Function, Item, Statement, TypeExpr};
7use gruel_util::FileId;
8use lasso::{Spur, ThreadedRodeo};
9
10#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct SignatureHelpResult {
13 pub label: String,
15 pub parameters: Vec<(u32, u32)>,
17 pub active_parameter: usize,
19}
20
21pub 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}