1use std::collections::BTreeSet;
41use std::path::Path;
42
43use bynk_check::checker::Ty;
44use bynk_check::firstparty::{
45 BYNK_ADAPTER_SRC, BYNK_LIST_SRC, BYNK_MAP_SRC, BYNK_STRING_SRC, CLOUDFLARE_ADAPTER_SRC,
46};
47use bynk_check::kernel_methods;
48use bynk_syntax::ast::{CommonsItem, ExportKind, FnName, SourceUnit, TypeBody, UsesDecl};
49use bynk_syntax::{keywords, lexer, parser};
50
51use crate::symbols::{type_ref_str, walk_bynk_files};
52
53#[derive(Clone, Copy, PartialEq, Eq)]
55pub enum CompletionKind {
56 Unit,
57 Capability,
58 Type,
59 Keyword,
60 Snippet,
61 Variant,
63 Member,
66 Field,
68 Constructor,
70 Function,
73}
74
75pub struct Completion {
76 pub label: String,
77 pub kind: CompletionKind,
78 pub detail: Option<String>,
79 pub insert_text: Option<String>,
82}
83
84impl Completion {
85 fn item(label: impl Into<String>, kind: CompletionKind, detail: Option<String>) -> Self {
86 Completion {
87 label: label.into(),
88 kind,
89 detail,
90 insert_text: None,
91 }
92 }
93
94 fn snippet(label: &str, body: &str) -> Self {
95 Completion {
96 label: label.to_string(),
97 kind: CompletionKind::Snippet,
98 detail: Some(format!("{label} scaffold")),
99 insert_text: Some(body.to_string()),
100 }
101 }
102}
103
104pub fn complete(line_prefix: &str, doc_text: &str, src_root: Option<&Path>) -> Vec<Completion> {
107 if let Some(unit) = consumes_brace_unit(line_prefix) {
109 return capabilities_of_unit(&unit, doc_text, src_root)
110 .into_iter()
111 .map(|c| {
112 Completion::item(
113 c,
114 CompletionKind::Capability,
115 Some(format!("capability exported by `{unit}`")),
116 )
117 })
118 .collect();
119 }
120 if is_consumes_target(line_prefix) {
122 return consumable_units(doc_text, src_root);
123 }
124 if is_given_position(line_prefix) {
126 return in_scope_capabilities(doc_text, src_root);
127 }
128 if let Some(receiver) = member_receiver(line_prefix) {
131 return member_candidates(&receiver, doc_text, src_root);
132 }
133 if is_type_position(line_prefix) {
136 return type_candidates(doc_text, src_root);
137 }
138 if is_keyword_position(line_prefix) {
141 return keyword_and_snippet_candidates();
142 }
143 if is_expression_position(line_prefix) {
148 return expression_candidates(doc_text, src_root);
149 }
150 Vec::new()
151}
152
153fn consumes_brace_unit(line: &str) -> Option<String> {
157 let idx = line.rfind("consumes")?;
158 let after = &line[idx + "consumes".len()..];
159 let open = after.find('{')?;
160 if after[open + 1..].contains('}') {
162 return None;
163 }
164 let unit = after[..open].trim();
165 if unit.is_empty() || !is_qualified_name(unit) {
166 return None;
167 }
168 Some(unit.to_string())
169}
170
171fn is_consumes_target(line: &str) -> bool {
173 let Some(idx) = line.rfind("consumes") else {
174 return false;
175 };
176 if !line[..idx]
178 .chars()
179 .last()
180 .map(|c| c.is_whitespace())
181 .unwrap_or(true)
182 {
183 return false;
184 }
185 let after = &line[idx + "consumes".len()..];
186 after.starts_with(char::is_whitespace)
188 && !after.contains('{')
189 && !after.contains('}')
190 && !after.split_whitespace().any(|w| w == "as")
191}
192
193fn is_given_position(line: &str) -> bool {
195 let Some(idx) = line.rfind("given") else {
196 return false;
197 };
198 if !line[..idx]
199 .chars()
200 .last()
201 .map(|c| c.is_whitespace())
202 .unwrap_or(true)
203 {
204 return false;
205 }
206 let after = &line[idx + "given".len()..];
207 if !after.starts_with(char::is_whitespace) {
208 return false;
209 }
210 after
213 .chars()
214 .all(|c| c.is_alphanumeric() || matches!(c, '_' | '.' | ',' | ' ' | '\t'))
215}
216
217fn is_qualified_name(s: &str) -> bool {
218 !s.is_empty()
219 && s.split('.').all(|seg| {
220 !seg.is_empty()
221 && seg.chars().all(|c| c.is_alphanumeric() || c == '_')
222 && !seg.chars().next().unwrap().is_ascii_digit()
223 })
224}
225
226fn is_type_position(line: &str) -> bool {
236 let head = line
237 .trim_end_matches(|c: char| c.is_alphanumeric() || c == '_')
238 .trim_end();
239 head.ends_with("->") || (head.ends_with(':') && !head.ends_with("::")) || in_type_arg_list(head)
240}
241
242fn in_type_arg_list(head: &str) -> bool {
246 let chars: Vec<char> = head.chars().collect();
247 let mut depth = 0i32;
248 let mut opener_after_ident = false;
249 for (i, &c) in chars.iter().enumerate() {
250 match c {
251 '[' => {
252 depth += 1;
253 if depth == 1 {
254 opener_after_ident =
255 i > 0 && (chars[i - 1].is_alphanumeric() || chars[i - 1] == '_');
256 }
257 }
258 ']' => depth -= 1,
259 _ => {}
260 }
261 }
262 depth > 0 && opener_after_ident
263}
264
265pub fn is_keyword_position(line: &str) -> bool {
270 line.trim().chars().all(|c| c.is_alphanumeric() || c == '_')
271}
272
273pub fn is_expression_position(line: &str) -> bool {
278 let head = line
279 .trim_end_matches(|c: char| c.is_alphanumeric() || c == '_')
280 .trim_end();
281 if head.ends_with("->") {
282 return false; }
284 if head.ends_with("=>") {
285 return true; }
287 matches!(
288 head.chars().last(),
289 Some('=' | '(' | ',' | '[' | '+' | '-' | '*' | '/' | '<' | '>' | '&' | '|')
290 )
291}
292
293fn member_receiver(line: &str) -> Option<String> {
301 let head = line
303 .trim_end_matches(|c: char| c.is_alphanumeric() || c == '_')
304 .strip_suffix('.')?;
305 let start = head
307 .rfind(|c: char| !(c.is_alphanumeric() || c == '_'))
308 .map_or(0, |i| i + 1);
309 let recv = &head[start..];
310 let first = recv.chars().next()?;
311 if !first.is_ascii_uppercase() {
312 return None;
313 }
314 if head[..start].ends_with('.') {
316 return None;
317 }
318 Some(recv.to_string())
319}
320
321pub(crate) const BUILTIN_STATICS: &[(&str, &[(&str, &str)])] = &[
328 ("Int", &[("parse", "parse(s: String) -> Option[Int]")]),
329 ("Float", &[("parse", "parse(s: String) -> Option[Float]")]),
330 (
331 "Json",
332 &[
333 ("encode", "encode(value) -> String"),
334 ("decode", "decode[T](s: String) -> Result[T, JsonError]"),
335 ],
336 ),
337 ("List", &[("empty", "empty() -> List[T]")]),
338 ("Map", &[("empty", "empty() -> Map[K, V]")]),
339 ("Effect", &[("pure", "pure(value) -> Effect[T]")]),
340 (
341 "Bytes",
342 &[
343 ("fromUtf8", "fromUtf8(s: String) -> Bytes"),
344 ("fromBase64", "fromBase64(s: String) -> Option[Bytes]"),
345 ("empty", "empty() -> Bytes"),
346 ],
347 ),
348];
349
350fn builtin_sum_variants(receiver: &str) -> Vec<(String, String)> {
354 match receiver {
355 "HttpResult" => bynk_syntax::ast::HTTP_VARIANTS
356 .iter()
357 .map(|v| {
358 (
359 v.name.to_string(),
360 format!("variant of `HttpResult` ({})", v.status),
361 )
362 })
363 .collect(),
364 "QueueResult" => bynk_syntax::ast::QUEUE_VARIANTS
365 .iter()
366 .map(|v| (v.name.to_string(), "variant of `QueueResult`".to_string()))
367 .collect(),
368 _ => Vec::new(),
369 }
370}
371
372fn member_candidates(receiver: &str, doc_text: &str, src_root: Option<&Path>) -> Vec<Completion> {
378 if let Some((_, statics)) = BUILTIN_STATICS.iter().find(|(name, _)| *name == receiver) {
379 return statics
380 .iter()
381 .map(|(label, sig)| {
382 Completion::item(*label, CompletionKind::Member, Some(sig.to_string()))
383 })
384 .collect();
385 }
386 let mut out: Vec<Completion> = Vec::new();
387 let mut seen: BTreeSet<String> = BTreeSet::new();
388 for (label, detail) in builtin_sum_variants(receiver) {
391 if seen.insert(label.clone()) {
392 out.push(Completion::item(
393 label,
394 CompletionKind::Variant,
395 Some(detail),
396 ));
397 }
398 }
399 for_each_unit(doc_text, src_root, |unit| {
400 let items = match unit {
401 SourceUnit::Commons(c) => &c.items,
402 SourceUnit::Context(c) => &c.items,
403 SourceUnit::Adapter(a) => &a.items,
404 _ => return,
405 };
406 for item in items {
407 match item {
408 CommonsItem::Type(t) if t.name.name == receiver => match &t.body {
409 bynk_syntax::ast::TypeBody::Sum(s) => {
410 for v in &s.variants {
411 if seen.insert(v.name.name.clone()) {
412 out.push(Completion::item(
413 v.name.name.clone(),
414 CompletionKind::Variant,
415 Some(format!("variant of `{receiver}`")),
416 ));
417 }
418 }
419 }
420 bynk_syntax::ast::TypeBody::Refined { .. }
421 | bynk_syntax::ast::TypeBody::Opaque { .. } => {
422 for (label, sig) in [
423 (
424 "of",
425 format!("of(value) -> Result[{receiver}, ValidationError]"),
426 ),
427 ("unsafe", format!("unsafe(value) -> {receiver}")),
428 ] {
429 if seen.insert(label.to_string()) {
430 out.push(Completion::item(
431 label,
432 CompletionKind::Member,
433 Some(sig),
434 ));
435 }
436 }
437 }
438 _ => {}
442 },
443 CommonsItem::Capability(c) if c.name.name == receiver => {
444 for op in &c.ops {
445 if seen.insert(op.name.name.clone()) {
446 let params = op
450 .params
451 .iter()
452 .map(|p| format!("{}: {}", p.name.name, type_ref_str(&p.type_ref)))
453 .collect::<Vec<_>>()
454 .join(", ");
455 out.push(Completion::item(
456 op.name.name.clone(),
457 CompletionKind::Member,
458 Some(format!(
459 "{}({params}) -> {} — operation of `{receiver}`",
460 op.name.name,
461 type_ref_str(&op.return_type)
462 )),
463 ));
464 }
465 }
466 }
467 _ => {}
468 }
469 }
470 });
471 out
472}
473
474const BUILTIN_TYPES: &[&str] = &[
480 bynk_check::builtin_names::types::INT,
481 "Bool",
482 bynk_check::builtin_names::types::FLOAT,
483 "String",
484 "Option",
485 "Result",
486 "Effect",
487 bynk_check::builtin_names::types::LIST,
488 bynk_check::builtin_names::types::MAP,
489];
490
491const SNIPPETS: &[(&str, &str)] = &[
493 ("context", "context ${1:name} {\n\t$0\n}"),
494 (
495 "adapter",
496 "adapter ${1:name} {\n\tbinding \"${2:./module}\"\n\t$0\n}",
497 ),
498 (
499 "capability",
500 "capability ${1:Name} {\n\tfn ${2:op}() -> Effect[${3:Unit}]\n}",
501 ),
502 (
503 "service",
504 "service ${1:name} {\n\ton call(${2}) -> Effect[${3:Unit}] {\n\t\t$0\n\t}\n}",
505 ),
506 ("on call", "on call(${1}) -> Effect[${2:Unit}] {\n\t$0\n}"),
507 ("test", "test \"${1:description}\" {\n\t$0\n}"),
508];
509
510const CONSTRUCTORS: &[&str] = &["Ok", "Err", "Some", "None", "true", "false"];
515
516fn expression_candidates(doc_text: &str, src_root: Option<&Path>) -> Vec<Completion> {
522 let mut out: Vec<Completion> = CONSTRUCTORS
523 .iter()
524 .map(|&name| {
525 Completion::item(
526 name,
527 CompletionKind::Constructor,
528 keyword_doc(name).map(str::to_string),
529 )
530 })
531 .collect();
532 out.extend(type_candidates(doc_text, src_root));
535 out.extend(free_function_candidates(doc_text, src_root));
538 out
539}
540
541fn unit_items_and_uses(unit: &SourceUnit) -> (&[CommonsItem], &[UsesDecl]) {
544 match unit {
545 SourceUnit::Commons(c) => (&c.items, &c.uses),
546 SourceUnit::Context(c) => (&c.items, &c.uses),
547 SourceUnit::Adapter(a) => (&a.items, &a.uses),
548 _ => (&[], &[]),
549 }
550}
551
552fn current_unit_name(doc_text: &str) -> Option<String> {
556 let tokens = lexer::tokenize(doc_text).ok()?;
557 let (unit, _errs) = parser::parse_unit_with_recovery(&tokens, doc_text);
558 Some(unit?.name().joined())
559}
560
561fn free_fn_signature(name: &str, f: &bynk_syntax::ast::FnDecl) -> String {
565 let params = f
566 .params
567 .iter()
568 .map(|p| format!("{}: {}", p.name.name, type_ref_str(&p.type_ref)))
569 .collect::<Vec<_>>()
570 .join(", ");
571 format!("{name}({params}) -> {}", type_ref_str(&f.return_type))
572}
573
574fn free_function_candidates(doc_text: &str, src_root: Option<&Path>) -> Vec<Completion> {
579 let Some(current) = current_unit_name(doc_text) else {
580 return Vec::new();
581 };
582 struct UnitFns {
585 name: String,
586 fns: Vec<(String, String)>,
587 uses: Vec<String>,
588 }
589 let mut units: Vec<UnitFns> = Vec::new();
590 for_each_unit(doc_text, src_root, |unit| {
591 let (items, uses) = unit_items_and_uses(unit);
592 let fns = items
593 .iter()
594 .filter_map(|it| match it {
595 CommonsItem::Fn(f) => match &f.name {
596 FnName::Free(id) => Some((id.name.clone(), free_fn_signature(&id.name, f))),
597 FnName::Method { .. } => None,
598 },
599 _ => None,
600 })
601 .collect();
602 units.push(UnitFns {
603 name: unit.name().joined(),
604 fns,
605 uses: uses.iter().map(|u| u.target.joined()).collect(),
606 });
607 });
608 let mut imported: BTreeSet<String> = BTreeSet::new();
611 for u in &units {
612 if u.name == current {
613 imported.extend(u.uses.iter().cloned());
614 }
615 }
616 let mut out: Vec<Completion> = Vec::new();
618 let mut seen: BTreeSet<String> = BTreeSet::new();
619 for u in &units {
620 let own = u.name == current;
621 if !own && !imported.contains(&u.name) {
622 continue;
623 }
624 let origin = if own { "this unit" } else { u.name.as_str() };
625 for (name, sig) in &u.fns {
626 if seen.insert(name.clone()) {
627 out.push(Completion::item(
628 name.clone(),
629 CompletionKind::Function,
630 Some(format!("{sig} — `{origin}`")),
631 ));
632 }
633 }
634 }
635 out
636}
637
638fn keyword_doc(word: &str) -> Option<&'static str> {
640 keywords::KEYWORDS
641 .iter()
642 .find(|k| k.word == word)
643 .map(|k| k.meaning)
644}
645
646fn type_candidates(doc_text: &str, src_root: Option<&Path>) -> Vec<Completion> {
650 let mut out: Vec<Completion> = Vec::new();
651 let mut seen: BTreeSet<String> = BTreeSet::new();
652 for &name in BUILTIN_TYPES {
653 if seen.insert(name.to_string()) {
654 let detail = keyword_doc(name)
655 .map(str::to_string)
656 .or_else(|| match name {
657 "List" => Some("The built-in list type, `List[T]`.".to_string()),
658 "Map" => Some("The built-in map type, `Map[K, V]`.".to_string()),
659 _ => Some("built-in type".to_string()),
660 });
661 out.push(Completion::item(name, CompletionKind::Type, detail));
662 }
663 }
664 for_each_unit(doc_text, src_root, |unit| {
665 let items = match unit {
666 SourceUnit::Commons(c) => &c.items,
667 SourceUnit::Context(c) => &c.items,
668 SourceUnit::Adapter(a) => &a.items,
669 _ => return,
670 };
671 for item in items {
672 if let CommonsItem::Type(t) = item
673 && seen.insert(t.name.name.clone())
674 {
675 out.push(Completion::item(
676 t.name.name.clone(),
677 CompletionKind::Type,
678 Some("type".to_string()),
679 ));
680 }
681 }
682 });
683 out
684}
685
686fn keyword_and_snippet_candidates() -> Vec<Completion> {
691 let mut out: Vec<Completion> = keywords::KEYWORDS
692 .iter()
693 .filter(|k| k.word.chars().next().is_some_and(char::is_lowercase))
694 .map(|k| Completion::item(k.word, CompletionKind::Keyword, Some(k.meaning.to_string())))
695 .collect();
696 for &(label, body) in SNIPPETS {
697 out.push(Completion::snippet(label, body));
698 }
699 out
700}
701
702pub(crate) fn for_each_unit(
708 doc_text: &str,
709 src_root: Option<&Path>,
710 mut f: impl FnMut(&SourceUnit),
711) {
712 let mut sources: Vec<String> = vec![
713 BYNK_ADAPTER_SRC.to_string(),
714 CLOUDFLARE_ADAPTER_SRC.to_string(),
715 BYNK_LIST_SRC.to_string(),
721 BYNK_MAP_SRC.to_string(),
722 BYNK_STRING_SRC.to_string(),
723 doc_text.to_string(),
724 ];
725 if let Some(root) = src_root {
726 for path in walk_bynk_files(root) {
727 if let Ok(s) = std::fs::read_to_string(&path) {
728 sources.push(s);
729 }
730 }
731 }
732 for src in &sources {
733 let Ok(tokens) = lexer::tokenize(src) else {
734 continue;
735 };
736 let (unit, _errs) = parser::parse_unit_with_recovery(&tokens, src);
737 if let Some(unit) = unit {
738 f(&unit);
739 }
740 }
741}
742
743fn consumable_units(doc_text: &str, src_root: Option<&Path>) -> Vec<Completion> {
745 let mut seen: BTreeSet<String> = BTreeSet::new();
746 let mut out: Vec<Completion> = Vec::new();
747 for_each_unit(doc_text, src_root, |unit| {
748 let (name, kind) = match unit {
749 SourceUnit::Context(c) => (c.name.joined(), "context"),
750 SourceUnit::Adapter(a) => (a.name.joined(), "adapter"),
751 _ => return,
752 };
753 if seen.insert(name.clone()) {
754 out.push(Completion::item(
755 name,
756 CompletionKind::Unit,
757 Some(kind.to_string()),
758 ));
759 }
760 });
761 out
762}
763
764fn capabilities_of_unit(unit: &str, doc_text: &str, src_root: Option<&Path>) -> Vec<String> {
766 let mut out: BTreeSet<String> = BTreeSet::new();
767 for_each_unit(doc_text, src_root, |u| {
768 let (name, exports) = match u {
769 SourceUnit::Context(c) => (c.name.joined(), &c.exports),
770 SourceUnit::Adapter(a) => (a.name.joined(), &a.exports),
771 _ => return,
772 };
773 if name != unit {
774 return;
775 }
776 for clause in exports {
777 if clause.kind == ExportKind::Capability {
778 for n in &clause.names {
779 out.insert(n.name.clone());
780 }
781 }
782 }
783 });
784 out.into_iter().collect()
785}
786
787fn in_scope_capabilities(doc_text: &str, src_root: Option<&Path>) -> Vec<Completion> {
791 let mut labels: BTreeSet<String> = BTreeSet::new();
792 let Ok(tokens) = lexer::tokenize(doc_text) else {
793 return Vec::new();
794 };
795 let (Some(unit), _errs) = parser::parse_unit_with_recovery(&tokens, doc_text) else {
796 return Vec::new();
797 };
798 let (items, consumes) = match &unit {
799 SourceUnit::Context(c) => (&c.items, &c.consumes),
800 SourceUnit::Adapter(a) => (&a.items, &EMPTY_CONSUMES),
801 _ => return Vec::new(),
802 };
803 for item in items {
805 if let bynk_syntax::ast::CommonsItem::Capability(c) = item {
806 labels.insert(c.name.name.clone());
807 }
808 }
809 for c in consumes {
811 let unit_name = c.target.joined();
812 match &c.selected {
813 Some(names) => {
814 for n in names {
815 labels.insert(n.name.clone());
816 }
817 }
818 None => {
819 let prefix = c
820 .alias
821 .as_ref()
822 .map(|a| a.name.clone())
823 .unwrap_or_else(|| unit_name.clone());
824 for cap in capabilities_of_unit(&unit_name, doc_text, src_root) {
825 labels.insert(format!("{prefix}.{cap}"));
826 }
827 }
828 }
829 }
830 labels
831 .into_iter()
832 .map(|label| {
833 Completion::item(
834 label,
835 CompletionKind::Capability,
836 Some("capability in scope".to_string()),
837 )
838 })
839 .collect()
840}
841
842pub fn value_receiver_rewrite(text: &str, offset: usize) -> Option<(String, usize)> {
854 let prefix = text.get(..offset)?;
855 let head = prefix
856 .trim_end_matches(|c: char| c.is_alphanumeric() || c == '_')
857 .strip_suffix('.')?;
858 let start = head
859 .rfind(|c: char| !(c.is_alphanumeric() || c == '_'))
860 .map_or(0, |i| i + 1);
861 let recv = &head[start..];
862 let first = recv.chars().next()?;
863 if !(first.is_ascii_lowercase() || first == '_') {
864 return None; }
866 if head[..start].ends_with('.') {
867 return None; }
869 let dot = head.len(); let rewritten = format!("{}{}", &text[..dot], &text[offset..]);
871 Some((rewritten, dot.saturating_sub(1)))
872}
873
874pub fn value_member_candidates(
877 ty: &Ty,
878 doc_text: &str,
879 src_root: Option<&Path>,
880) -> Vec<Completion> {
881 let mut out: Vec<Completion> = kernel_methods::methods_for(ty)
882 .iter()
883 .map(|km| {
884 Completion::item(
885 km.name,
886 CompletionKind::Member,
887 Some(km.signature.to_string()),
888 )
889 })
890 .collect();
891 if let Ty::Named { name, .. } = ty {
893 let mut seen: BTreeSet<String> = BTreeSet::new();
894 for_each_unit(doc_text, src_root, |unit| {
895 let items = match unit {
896 SourceUnit::Commons(c) => &c.items,
897 SourceUnit::Context(c) => &c.items,
898 SourceUnit::Adapter(a) => &a.items,
899 _ => return,
900 };
901 for item in items {
902 if let CommonsItem::Type(t) = item
903 && &t.name.name == name
904 && let TypeBody::Record(r) = &t.body
905 {
906 for f in &r.fields {
907 if seen.insert(f.name.name.clone()) {
908 out.push(Completion::item(
909 f.name.name.clone(),
910 CompletionKind::Field,
911 Some(format!("field of `{name}`")),
912 ));
913 }
914 }
915 }
916 }
917 });
918 }
919 out
920}
921
922static EMPTY_CONSUMES: Vec<bynk_syntax::ast::ConsumesDecl> = Vec::new();
923
924#[cfg(test)]
925mod tests {
926 use super::*;
927
928 fn labels(line: &str, doc: &str) -> Vec<String> {
929 complete(line, doc, None)
930 .into_iter()
931 .map(|c| c.label)
932 .collect()
933 }
934
935 #[test]
936 fn consumes_target_suggests_units_including_bynk() {
937 let doc = "adapter tokens {\n binding \"./b.ts\"\n capability Jwt { fn f() -> Effect[Int] }\n provides Jwt = X\n}\n";
939 let got = labels(" consumes ", doc);
940 assert!(got.contains(&"bynk".to_string()), "{got:?}");
941 assert!(got.contains(&"tokens".to_string()), "{got:?}");
942 }
943
944 #[test]
945 fn consumes_brace_suggests_that_units_capabilities() {
946 let got = labels(" consumes bynk { ", "context a.b\n");
947 assert!(got.contains(&"Clock".to_string()), "{got:?}");
949 assert!(got.contains(&"Random".to_string()), "{got:?}");
950 assert!(got.contains(&"Logger".to_string()), "{got:?}");
951 }
952
953 #[test]
954 fn given_suggests_local_and_flattened_capabilities() {
955 let doc = "context a.b\n\
956 consumes bynk { Clock }\n\
957 capability Local { fn f() -> Effect[Int] }\n\
958 service s {\n\
959 on call() -> Effect[Int] given Clock {\n\
960 1\n\
961 }\n\
962 }\n";
963 let got = labels(" on call() -> Effect[Int] given ", doc);
964 assert!(got.contains(&"Clock".to_string()), "flattened: {got:?}");
965 assert!(got.contains(&"Local".to_string()), "local: {got:?}");
966 }
967
968 #[test]
969 fn expression_position_offers_constructors_and_types() {
970 let doc = "commons m {\n type Order = { id: Int }\n}\n";
975 let items = complete(" let x = ", doc, None);
976 for &c in CONSTRUCTORS {
977 assert!(
978 find(&items, c, CompletionKind::Constructor).is_some(),
979 "constructor {c}: {:?}",
980 items.iter().map(|i| &i.label).collect::<Vec<_>>()
981 );
982 }
983 assert!(
984 find(&items, "Int", CompletionKind::Type).is_some(),
985 "builtin type"
986 );
987 assert!(
988 find(&items, "Order", CompletionKind::Type).is_some(),
989 "project type"
990 );
991 }
992
993 #[test]
994 fn value_receiver_and_decimal_are_not_expression_positions() {
995 assert!(complete(" let p = q.", "context a.b\n", None).is_empty());
999 assert!(complete(" let n = 1.", "context a.b\n", None).is_empty());
1000 }
1001
1002 fn free_fn_names(src: &str) -> Vec<String> {
1004 let tokens = lexer::tokenize(src).unwrap();
1005 let (unit, _) = parser::parse_unit_with_recovery(&tokens, src);
1006 let unit = unit.unwrap();
1007 let (items, _) = unit_items_and_uses(&unit);
1008 items
1009 .iter()
1010 .filter_map(|it| match it {
1011 CommonsItem::Fn(f) => match &f.name {
1012 FnName::Free(id) => Some(id.name.clone()),
1013 FnName::Method { .. } => None,
1014 },
1015 _ => None,
1016 })
1017 .collect()
1018 }
1019
1020 #[test]
1021 fn free_functions_offered_for_own_unit_and_used_modules() {
1022 let doc = "commons app {\n uses bynk.list\n fn helper(x: Int) -> Int { x }\n}\n";
1025 let items = complete(" let y = ", doc, None);
1026 assert!(
1028 find(&items, "helper", CompletionKind::Function).is_some(),
1029 "own fn: {:?}",
1030 items.iter().map(|i| &i.label).collect::<Vec<_>>()
1031 );
1032 for name in free_fn_names(BYNK_LIST_SRC) {
1035 assert!(
1036 find(&items, &name, CompletionKind::Function).is_some(),
1037 "bynk.list.{name}: {:?}",
1038 items.iter().map(|i| &i.label).collect::<Vec<_>>()
1039 );
1040 }
1041 assert!(
1043 find(&items, "values", CompletionKind::Function).is_none(),
1044 "bynk.map.values leaked without `uses bynk.map`"
1045 );
1046 }
1047
1048 #[test]
1049 fn free_functions_require_a_uses_import() {
1050 let doc = "commons app {\n fn helper(x: Int) -> Int { x }\n}\n";
1052 let items = complete(" let y = ", doc, None);
1053 assert!(find(&items, "helper", CompletionKind::Function).is_some());
1054 for name in ["map", "filter", "reverse"] {
1055 assert!(
1056 find(&items, name, CompletionKind::Function).is_none(),
1057 "bynk.list.{name} offered without `uses bynk.list`"
1058 );
1059 }
1060 }
1061
1062 #[test]
1063 fn member_completion_reaches_inside_an_interpolation_hole() {
1064 let doc = "context a.b\n capability Timer { fn now() -> Effect[Int] }\n";
1068 let in_hole = complete(" \"the time is \\(Timer.", doc, None);
1069 assert!(
1070 find(&in_hole, "now", CompletionKind::Member).is_some(),
1071 "capability op not offered inside a hole: {:?}",
1072 in_hole.iter().map(|c| &c.label).collect::<Vec<_>>()
1073 );
1074 let statics = complete(" \"n=\\(Int.", "context a.b\n", None);
1076 assert!(find(&statics, "parse", CompletionKind::Member).is_some());
1077 }
1078
1079 #[test]
1080 fn consumes_with_as_is_not_a_target_completion() {
1081 assert!(!is_consumes_target("consumes platform.time as "));
1083 assert!(is_consumes_target("consumes platform"));
1084 }
1085
1086 fn find<'a>(
1087 items: &'a [Completion],
1088 label: &str,
1089 kind: CompletionKind,
1090 ) -> Option<&'a Completion> {
1091 items.iter().find(|c| c.label == label && c.kind == kind)
1092 }
1093
1094 #[test]
1095 fn type_annotation_suggests_builtins_surface_and_project_types() {
1096 let doc = "commons m {\n type Order = { id: Int }\n}\n";
1097 let got = labels(" let x: ", doc);
1098 for want in ["Int", "Option", "Result", "Effect", "List", "Map"] {
1101 assert!(got.contains(&want.to_string()), "built-in {want}: {got:?}");
1102 }
1103 assert!(got.contains(&"Uuid".to_string()), "surface: {got:?}");
1104 assert!(got.contains(&"Order".to_string()), "project: {got:?}");
1105 }
1106
1107 #[test]
1108 fn return_type_and_type_args_are_type_positions() {
1109 assert!(is_type_position(" on call() -> "));
1110 assert!(is_type_position(" let x: Option["));
1111 assert!(is_type_position(" let x: Result[Int, "));
1112 assert!(is_type_position(" -> Eff"));
1114 }
1115
1116 #[test]
1117 fn list_literal_is_not_a_type_position() {
1118 assert!(!is_type_position(" let xs = ["));
1120 let items = complete(" let xs = [", "context a.b\n", None);
1124 assert!(
1125 find(&items, "Some", CompletionKind::Constructor).is_some(),
1126 "{:?}",
1127 items.iter().map(|c| &c.label).collect::<Vec<_>>()
1128 );
1129 }
1130
1131 #[test]
1132 fn builtin_type_carries_its_registry_doc() {
1133 let items = complete(" let x: ", "context a.b\n", None);
1134 let int = find(&items, "Int", CompletionKind::Type).expect("Int present");
1135 assert_eq!(int.detail.as_deref(), keyword_doc("Int"));
1136 assert!(int.detail.is_some(), "Int should have a doc");
1137 }
1138
1139 #[test]
1140 fn keyword_position_suggests_keywords_and_snippets() {
1141 let items = complete(" ", "context a.b\n", None);
1142 assert!(find(&items, "capability", CompletionKind::Keyword).is_some());
1144 assert!(find(&items, "fn", CompletionKind::Keyword).is_some());
1145 assert!(find(&items, "let", CompletionKind::Keyword).is_some());
1146 assert!(find(&items, "Int", CompletionKind::Keyword).is_none());
1148 assert!(find(&items, "Some", CompletionKind::Keyword).is_none());
1149 let snip = find(&items, "service", CompletionKind::Snippet).expect("service snippet");
1151 let body = snip.insert_text.as_deref().unwrap_or("");
1152 assert!(body.contains("on call"), "snippet body: {body:?}");
1153 assert!(body.contains("${1"), "snippet tab stop: {body:?}");
1154 }
1155
1156 #[test]
1157 fn keyword_position_fires_on_an_empty_line() {
1158 assert!(is_keyword_position(""));
1159 assert!(is_keyword_position(" cap"));
1160 assert!(!is_keyword_position(" let x ="));
1161 assert!(!is_keyword_position(" x: "));
1162 assert!(!complete("", "context a.b\n", None).is_empty());
1163 }
1164
1165 #[test]
1166 fn member_receiver_is_a_single_upper_ident_before_a_dot() {
1167 assert_eq!(member_receiver(" Color."), Some("Color".to_string()));
1168 assert_eq!(
1169 member_receiver(" let e = Email.o"),
1170 Some("Email".to_string())
1171 );
1172 assert_eq!(member_receiver(" x."), None); assert_eq!(member_receiver(" 1."), None); assert_eq!(member_receiver(" a.B."), None); assert_eq!(member_receiver(" Color"), None); }
1177
1178 #[test]
1179 fn sum_member_suggests_variants() {
1180 let doc = "commons m {\n type Color = enum { Red, Green, Blue }\n}\n";
1181 let items = complete(" let c = Color.", doc, None);
1182 for v in ["Red", "Green", "Blue"] {
1183 assert!(
1184 find(&items, v, CompletionKind::Variant).is_some(),
1185 "variant {v}: {:?}",
1186 items.iter().map(|c| &c.label).collect::<Vec<_>>()
1187 );
1188 }
1189 }
1190
1191 #[test]
1192 fn refined_and_plain_alias_members_are_of_and_unsafe() {
1193 let doc = "commons m {\n type Email = String where NonEmpty\n}\n";
1195 let items = complete(" Email.", doc, None);
1196 assert!(find(&items, "of", CompletionKind::Member).is_some());
1197 assert!(find(&items, "unsafe", CompletionKind::Member).is_some());
1198 let doc = "commons m {\n type Id = Int\n}\n";
1201 assert!(find(&complete(" Id.", doc, None), "of", CompletionKind::Member).is_some());
1202 }
1203
1204 #[test]
1205 fn capability_member_suggests_ops() {
1206 let doc = "context a.b\n capability Timer { fn now() -> Effect[Int]\n fn at(t: Int) -> Effect[()] }\n";
1207 let items = complete(" Timer.", doc, None);
1208 let now = find(&items, "now", CompletionKind::Member).expect("`now` op offered");
1209 assert_eq!(
1212 now.detail.as_deref(),
1213 Some("now() -> Effect[Int] — operation of `Timer`")
1214 );
1215 let at = find(&items, "at", CompletionKind::Member).expect("`at` op offered");
1216 assert_eq!(
1217 at.detail.as_deref(),
1218 Some("at(t: Int) -> Effect[()] — operation of `Timer`")
1219 );
1220 }
1221
1222 #[test]
1223 fn builtin_type_statics_are_offered() {
1224 assert!(
1225 find(
1226 &complete(" Int.", "context a.b\n", None),
1227 "parse",
1228 CompletionKind::Member
1229 )
1230 .is_some()
1231 );
1232 let j = complete(" Json.", "context a.b\n", None);
1233 assert!(find(&j, "encode", CompletionKind::Member).is_some());
1234 assert!(find(&j, "decode", CompletionKind::Member).is_some());
1235 }
1236
1237 #[test]
1238 fn builtin_sum_variants_are_complete() {
1239 let http: Vec<&str> = bynk_syntax::ast::HTTP_VARIANTS
1244 .iter()
1245 .map(|v| v.name)
1246 .collect();
1247 let queue: Vec<&str> = bynk_syntax::ast::QUEUE_VARIANTS
1248 .iter()
1249 .map(|v| v.name)
1250 .collect();
1251 for (recv, names) in [("HttpResult", http), ("QueueResult", queue)] {
1252 let items = complete(&format!(" {recv}."), "context a.b\n", None);
1253 for name in names {
1254 assert!(
1255 find(&items, name, CompletionKind::Variant).is_some(),
1256 "{recv}.{name} missing: {:?}",
1257 items.iter().map(|c| &c.label).collect::<Vec<_>>()
1258 );
1259 }
1260 }
1261 }
1262
1263 #[test]
1264 fn builtin_statics_are_reachable() {
1265 for &(recv, members) in BUILTIN_STATICS {
1269 let items = complete(&format!(" {recv}."), "context a.b\n", None);
1270 for &(member, _) in members {
1271 assert!(
1272 find(&items, member, CompletionKind::Member).is_some(),
1273 "{recv}.{member} unreachable: {:?}",
1274 items.iter().map(|c| &c.label).collect::<Vec<_>>()
1275 );
1276 }
1277 }
1278 for (recv, member) in [("List", "empty"), ("Map", "empty"), ("Effect", "pure")] {
1281 let items = complete(&format!(" {recv}."), "context a.b\n", None);
1282 assert!(
1283 find(&items, member, CompletionKind::Member).is_some(),
1284 "{recv}.{member} missing from the statics table"
1285 );
1286 }
1287 }
1288
1289 #[test]
1290 fn record_value_and_decimal_receivers_yield_nothing() {
1291 let doc = "commons m {\n type Point = { x: Int }\n}\n";
1293 assert!(complete(" Point.", doc, None).is_empty(), "record");
1294 assert!(complete(" let p = q.", doc, None).is_empty(), "value");
1296 assert!(complete(" let n = 1.", doc, None).is_empty(), "decimal");
1298 }
1299
1300 #[test]
1301 fn value_receiver_rewrite_drops_the_dot_for_lowercase_receivers() {
1302 let text = " let x = email.\n";
1303 let offset = text.find('.').unwrap() + 1; let (rewritten, recv) = value_receiver_rewrite(text, offset).expect("value receiver");
1305 assert_eq!(
1306 rewritten, " let x = email\n",
1307 "the trailing dot is dropped"
1308 );
1309 assert!(
1310 text.get(recv..=recv).is_some_and(|c| c == "l"),
1311 "the receiver offset lands inside `email`"
1312 );
1313 let text2 = " let x = email.ma\n";
1315 let off2 = text2.find(".ma").unwrap() + 3;
1316 assert_eq!(
1317 value_receiver_rewrite(text2, off2).map(|(r, _)| r),
1318 Some(" let x = email\n".to_string())
1319 );
1320 assert!(value_receiver_rewrite(" Email.", 8).is_none());
1322 assert!(value_receiver_rewrite(" let n = 1.", 12).is_none());
1323 assert!(value_receiver_rewrite(" email", 7).is_none());
1324 }
1325
1326 #[test]
1327 fn value_member_candidates_lists_kernel_methods() {
1328 use bynk_syntax::ast::BaseType;
1329 let list = Ty::List(Box::new(Ty::Base(BaseType::Int)));
1330 let items = value_member_candidates(&list, "context a.b\n", None);
1331 assert!(find(&items, "fold", CompletionKind::Member).is_some());
1332 assert!(find(&items, "get", CompletionKind::Member).is_some());
1333
1334 let string = Ty::Base(BaseType::String);
1335 let items = value_member_candidates(&string, "context a.b\n", None);
1336 assert!(find(&items, "split", CompletionKind::Member).is_some());
1337 assert!(find(&items, "trim", CompletionKind::Member).is_some());
1338 }
1339
1340 #[test]
1341 fn expression_position_offers_locals() {
1342 assert!(is_expression_position(" let y = "));
1344 assert!(is_expression_position(" let y = a + lo")); assert!(is_expression_position(" f("));
1346 assert!(is_expression_position(" g(a, "));
1347 assert!(is_expression_position(" xs.fold(0, (acc, x) => ac")); assert!(is_expression_position(" let y = foo"));
1350 assert!(!is_expression_position(" let y: ")); assert!(!is_expression_position(" on call() -> ")); assert!(!is_expression_position(" tot")); }
1355
1356 #[test]
1357 fn value_member_candidates_lists_record_fields() {
1358 use bynk_check::checker::NamedKind;
1359 let order = Ty::Named {
1360 name: "Order".to_string(),
1361 kind: NamedKind::Record,
1362 };
1363 let doc = "commons m {\n type Order = { id: Int, total: Int }\n}\n";
1364 let items = value_member_candidates(&order, doc, None);
1365 assert!(
1366 find(&items, "id", CompletionKind::Field).is_some(),
1367 "{items:?}",
1368 items = items.iter().map(|c| &c.label).collect::<Vec<_>>()
1369 );
1370 assert!(find(&items, "total", CompletionKind::Field).is_some());
1371 }
1372}