1use bynk_syntax::ast::{BaseType, CommonsItem, FnName, SourceUnit, TypeBody};
17use std::path::Path;
18
19use crate::completion::{BUILTIN_STATICS, for_each_unit};
20use crate::symbols::type_ref_str;
21
22#[derive(Debug, PartialEq, Eq)]
25pub struct CallContext {
26 pub callee: String,
27 pub active_param: usize,
28 pub open_paren: usize,
29}
30
31pub fn call_context(text: &str, offset: usize) -> Option<CallContext> {
35 let prefix = text.get(..offset)?;
36 let open = innermost_unclosed_paren(prefix)?;
37 let callee = callee_before(&prefix[..open])?;
38 let active = top_level_commas(&prefix[open + 1..]);
39 Some(CallContext {
40 callee,
41 active_param: active,
42 open_paren: open,
43 })
44}
45
46pub fn value_receiver_method(callee: &str) -> Option<(&str, &str)> {
50 let (recv, method) = callee.rsplit_once('.')?;
51 let first = recv.chars().next()?;
52 if (first.is_ascii_lowercase() || first == '_') && !recv.contains('.') {
53 Some((recv, method))
54 } else {
55 None
56 }
57}
58
59pub fn value_receiver_rewrite(
64 text: &str,
65 callee: &str,
66 open_paren: usize,
67 cursor: usize,
68) -> Option<(String, usize)> {
69 let (recv, _) = value_receiver_method(callee)?;
70 let callee_start = open_paren.checked_sub(callee.len())?;
71 let dot = callee_start + recv.len();
72 let rewritten = format!("{}{}", &text[..dot], &text[cursor..]);
73 Some((rewritten, dot.saturating_sub(1)))
74}
75
76pub fn kernel_method_signature(ty: &bynk_check::checker::Ty, method: &str) -> Option<String> {
78 bynk_check::kernel_methods::methods_for(ty)
79 .iter()
80 .find(|m| m.name == method)
81 .map(|m| m.signature.to_string())
82}
83
84fn innermost_unclosed_paren(prefix: &str) -> Option<usize> {
87 let b = prefix.as_bytes();
88 let mut depth = 0i32;
89 for i in (0..b.len()).rev() {
90 match b[i] {
91 b')' | b']' | b'}' => depth += 1,
92 b'(' => {
93 if depth == 0 {
94 return Some(i);
95 }
96 depth -= 1;
97 }
98 b'[' | b'{' => {
99 if depth == 0 {
100 return None;
101 }
102 depth -= 1;
103 }
104 _ => {}
105 }
106 }
107 None
108}
109
110fn top_level_commas(s: &str) -> usize {
112 let mut depth = 0i32;
113 let mut n = 0;
114 for c in s.chars() {
115 match c {
116 '(' | '[' | '{' => depth += 1,
117 ')' | ']' | '}' => depth -= 1,
118 ',' if depth == 0 => n += 1,
119 _ => {}
120 }
121 }
122 n
123}
124
125fn callee_before(s: &str) -> Option<String> {
127 let s = s.trim_end();
128 let start = s
129 .rfind(|c: char| !(c.is_alphanumeric() || c == '_' || c == '.'))
130 .map_or(0, |i| i + 1);
131 let callee = &s[start..];
132 if callee.is_empty() || callee.starts_with('.') || callee.ends_with('.') {
133 return None;
134 }
135 Some(callee.to_string())
136}
137
138pub fn resolve_label(callee: &str, doc_text: &str, src_root: Option<&Path>) -> Option<String> {
141 if let Some((recv, member)) = callee.rsplit_once('.') {
142 if let Some((_, statics)) = BUILTIN_STATICS.iter().find(|(n, _)| *n == recv)
144 && let Some((_, sig)) = statics.iter().find(|(n, _)| *n == member)
145 {
146 return Some((*sig).to_string());
147 }
148 return resolve_qualified(recv, member, doc_text, src_root);
150 }
151 match callee {
153 "Ok" => return Some("Ok(value: T) -> Result[T, E]".to_string()),
154 "Err" => return Some("Err(error: E) -> Result[T, E]".to_string()),
155 "Some" => return Some("Some(value: T) -> Option[T]".to_string()),
156 _ => {}
157 }
158 let mut found = None;
160 for_each_unit(doc_text, src_root, |unit| {
161 if found.is_some() {
162 return;
163 }
164 let items = match unit {
165 SourceUnit::Commons(c) => &c.items,
166 SourceUnit::Context(c) => &c.items,
167 SourceUnit::Adapter(a) => &a.items,
168 _ => return,
169 };
170 for item in items {
171 if let CommonsItem::Fn(f) = item
172 && let FnName::Free(id) = &f.name
173 && id.name == callee
174 {
175 let params: Vec<String> = f
176 .params
177 .iter()
178 .map(|p| format!("{}: {}", p.name.name, type_ref_str(&p.type_ref)))
179 .collect();
180 found = Some(format!(
181 "{callee}({}) -> {}",
182 params.join(", "),
183 type_ref_str(&f.return_type)
184 ));
185 return;
186 }
187 }
188 });
189 found
190}
191
192fn resolve_qualified(
193 recv: &str,
194 member: &str,
195 doc_text: &str,
196 src_root: Option<&Path>,
197) -> Option<String> {
198 let mut out = None;
199 for_each_unit(doc_text, src_root, |unit| {
200 if out.is_some() {
201 return;
202 }
203 let items = match unit {
204 SourceUnit::Commons(c) => &c.items,
205 SourceUnit::Context(c) => &c.items,
206 SourceUnit::Adapter(a) => &a.items,
207 _ => return,
208 };
209 for item in items {
210 match item {
211 CommonsItem::Type(t)
213 if t.name.name == recv && (member == "of" || member == "unsafe") =>
214 {
215 let base = match &t.body {
216 TypeBody::Refined { base, .. } | TypeBody::Opaque { base, .. } => {
217 base_name(*base)
218 }
219 _ => return,
220 };
221 out = Some(if member == "of" {
222 format!("of(value: {base}) -> Result[{recv}, ValidationError]")
223 } else {
224 format!("unsafe(value: {base}) -> {recv}")
225 });
226 return;
227 }
228 CommonsItem::Capability(c) if c.name.name == recv => {
230 if let Some(op) = c.ops.iter().find(|o| o.name.name == member) {
231 let params: Vec<String> = op
232 .params
233 .iter()
234 .map(|p| format!("{}: {}", p.name.name, type_ref_str(&p.type_ref)))
235 .collect();
236 out = Some(format!(
237 "{member}({}) -> {}",
238 params.join(", "),
239 type_ref_str(&op.return_type)
240 ));
241 return;
242 }
243 }
244 _ => {}
245 }
246 }
247 });
248 out
249}
250
251fn base_name(b: BaseType) -> &'static str {
252 match b {
253 BaseType::Int => "Int",
254 BaseType::Float => "Float",
255 BaseType::String => "String",
256 BaseType::Bool => "Bool",
257 BaseType::Duration => "Duration",
258 BaseType::Instant => "Instant",
259 BaseType::Bytes => "Bytes",
260 }
261}
262
263pub fn param_ranges(label: &str) -> Vec<(usize, usize)> {
266 let Some(open) = label.find('(') else {
267 return Vec::new();
268 };
269 let mut ranges = Vec::new();
270 let mut depth = 0i32;
271 let mut seg_start = open + 1;
272 let bytes = label.as_bytes();
273 let mut i = open;
274 while i < bytes.len() {
275 match bytes[i] {
276 b'(' | b'[' | b'{' => depth += 1,
277 b')' | b']' | b'}' => {
278 depth -= 1;
279 if depth == 0 {
280 push_trimmed(label, seg_start, i, &mut ranges);
281 break;
282 }
283 }
284 b',' if depth == 1 => {
285 push_trimmed(label, seg_start, i, &mut ranges);
286 seg_start = i + 1;
287 }
288 _ => {}
289 }
290 i += 1;
291 }
292 ranges
293}
294
295fn push_trimmed(label: &str, start: usize, end: usize, out: &mut Vec<(usize, usize)>) {
296 let seg = &label[start..end];
297 let trimmed = seg.trim();
298 if trimmed.is_empty() {
299 return;
300 }
301 let s = start + (seg.len() - seg.trim_start().len());
302 out.push((s, s + trimmed.len()));
303}
304
305#[cfg(test)]
306mod tests {
307 use super::*;
308
309 #[test]
310 fn call_context_finds_callee_and_active_param() {
311 let t = " let x = f(a, b";
312 let ctx = call_context(t, t.len()).unwrap();
313 assert_eq!(ctx.callee, "f");
314 assert_eq!(ctx.active_param, 1); }
316
317 #[test]
318 fn innermost_call_wins_in_nested_calls() {
319 let t = " outer(g(x";
320 let ctx = call_context(t, t.len()).unwrap();
321 assert_eq!(ctx.callee, "g");
322 assert_eq!(ctx.active_param, 0);
323 let t2 = " outer(g(x), ";
325 let ctx2 = call_context(t2, t2.len()).unwrap();
326 assert_eq!(ctx2.callee, "outer");
327 assert_eq!(ctx2.active_param, 1);
328 }
329
330 #[test]
331 fn commas_inside_nested_brackets_dont_count() {
332 let t = " f(g(a, b), ";
333 assert_eq!(call_context(t, t.len()).unwrap().active_param, 1);
334 }
335
336 #[test]
337 fn qualified_callee_and_no_call_context() {
338 let t = " Clock.now(";
339 assert_eq!(call_context(t, t.len()).unwrap().callee, "Clock.now");
340 assert!(call_context(" let x = 1", 11).is_none()); assert!(call_context(" xs[", 4).is_none()); }
343
344 #[test]
345 fn builtin_static_and_constructor_labels() {
346 assert_eq!(
347 resolve_label("Int.parse", "context a.b\n", None).as_deref(),
348 Some("parse(s: String) -> Option[Int]")
349 );
350 assert!(
351 resolve_label("Ok", "context a.b\n", None)
352 .unwrap()
353 .starts_with("Ok(value")
354 );
355 }
356
357 #[test]
358 fn free_fn_and_capability_op_and_refined_labels() {
359 let doc = "commons m {\n fn add(a: Int, b: Int) -> Int { a }\n}\n";
360 assert_eq!(
361 resolve_label("add", doc, None).as_deref(),
362 Some("add(a: Int, b: Int) -> Int")
363 );
364 let cap = "context a.b\n capability Timer { fn after(label: String) -> Effect[Int] }\n";
365 assert_eq!(
366 resolve_label("Timer.after", cap, None).as_deref(),
367 Some("after(label: String) -> Effect[Int]")
368 );
369 let refined = "commons m {\n type Email = String where NonEmpty\n}\n";
370 assert_eq!(
371 resolve_label("Email.of", refined, None).as_deref(),
372 Some("of(value: String) -> Result[Email, ValidationError]")
373 );
374 }
375
376 #[test]
377 fn value_receiver_callee_detection_and_rewrite() {
378 assert_eq!(value_receiver_method("xs.fold"), Some(("xs", "fold")));
379 assert_eq!(value_receiver_method("Int.parse"), None); assert_eq!(value_receiver_method("a.b.fold"), None); assert_eq!(value_receiver_method("bar"), None); let text = " let r = xs.fold(0, ";
384 let open = text.find('(').unwrap();
385 let (rw, off) = value_receiver_rewrite(text, "xs.fold", open, text.len()).unwrap();
386 assert_eq!(rw, " let r = xs", "the `.fold(0, ` is dropped");
387 assert_eq!(&text[off..=off], "s", "offset lands inside `xs`");
388 }
389
390 #[test]
391 fn kernel_method_signature_lookup() {
392 use bynk_check::checker::Ty;
393 use bynk_syntax::ast::BaseType;
394 let list = Ty::List(Box::new(Ty::Base(BaseType::Int)));
395 assert!(
396 kernel_method_signature(&list, "fold")
397 .unwrap()
398 .starts_with("fold(")
399 );
400 let string = Ty::Base(BaseType::String);
401 assert!(
402 kernel_method_signature(&string, "split")
403 .unwrap()
404 .starts_with("split(")
405 );
406 assert!(kernel_method_signature(&string, "nope").is_none());
407 }
408
409 #[test]
410 fn param_ranges_split_top_level_only() {
411 let label = "fold(init: U, step: (U, T) -> U) -> U";
412 let r = param_ranges(label);
413 assert_eq!(r.len(), 2, "two params, not split inside (U, T): {r:?}");
414 assert_eq!(&label[r[0].0..r[0].1], "init: U");
415 assert_eq!(&label[r[1].0..r[1].1], "step: (U, T) -> U");
416 }
417}