1use std::collections::{BTreeSet, HashMap};
41use std::path::{Path, PathBuf};
42use std::sync::{Arc, LazyLock, Mutex};
43
44use bynk_check::checker::{NamedKind, Ty, TyId, Types};
45use bynk_check::kernel_methods;
46use bynk_check::locals::LocalBinding;
47use bynk_check::store_ops;
48use bynk_syntax::ast::{CommonsItem, ExportKind, FnName, SourceUnit, TypeBody, TypeRef, UsesDecl};
49use bynk_syntax::{keywords, lexer, parser};
50
51use crate::symbols::type_ref_str;
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 pub 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(
107 line_prefix: &str,
108 doc_text: &str,
109 files: Option<&HashMap<PathBuf, String>>,
110) -> Vec<Completion> {
111 if let Some(unit) = consumes_brace_unit(line_prefix) {
113 return capabilities_of_unit(&unit, doc_text, files)
114 .into_iter()
115 .map(|c| {
116 Completion::item(
117 c,
118 CompletionKind::Capability,
119 Some(format!("capability exported by `{unit}`")),
120 )
121 })
122 .collect();
123 }
124 if is_consumes_target(line_prefix) {
126 return consumable_units(doc_text, files);
127 }
128 if is_given_position(line_prefix) {
130 return in_scope_capabilities(doc_text, files);
131 }
132 if let Some(receiver) = member_receiver(line_prefix) {
135 return member_candidates(&receiver, doc_text, files);
136 }
137 if let Some(recv) = record_construction_receiver(line_prefix) {
141 let fields = record_field_names(&recv, doc_text, files);
142 if !fields.is_empty() {
143 return fields;
144 }
145 }
146 if after_clause_keyword(line_prefix, "from") {
148 return protocol_candidates();
149 }
150 if after_clause_keyword(line_prefix, "on") {
152 return handler_kind_candidates();
153 }
154 if after_clause_keyword(line_prefix, "by") {
156 return actor_candidates(doc_text, files);
157 }
158 if after_clause_keyword(line_prefix, "exports") {
160 return export_kind_candidates();
161 }
162 if after_clause_keyword(line_prefix, "provides") {
164 return in_scope_capabilities(doc_text, files);
165 }
166 if after_clause_keyword(line_prefix, "where") && !is_for_all_where(line_prefix) {
174 return predicate_name_candidates();
175 }
176 if is_type_position(line_prefix) {
179 return type_candidates(doc_text, files);
180 }
181 if is_keyword_position(line_prefix) {
184 return keyword_and_snippet_candidates();
185 }
186 if is_expression_position(line_prefix) {
191 return expression_candidates(doc_text, files);
192 }
193 Vec::new()
194}
195
196fn consumes_brace_unit(line: &str) -> Option<String> {
200 let idx = line.rfind("consumes")?;
201 let after = &line[idx + "consumes".len()..];
202 let open = after.find('{')?;
203 if after[open + 1..].contains('}') {
205 return None;
206 }
207 let unit = after[..open].trim();
208 if unit.is_empty() || !is_qualified_name(unit) {
209 return None;
210 }
211 Some(unit.to_string())
212}
213
214fn is_consumes_target(line: &str) -> bool {
216 let Some(idx) = line.rfind("consumes") else {
217 return false;
218 };
219 if !line[..idx]
221 .chars()
222 .last()
223 .map(|c| c.is_whitespace())
224 .unwrap_or(true)
225 {
226 return false;
227 }
228 let after = &line[idx + "consumes".len()..];
229 after.starts_with(char::is_whitespace)
231 && !after.contains('{')
232 && !after.contains('}')
233 && !after.split_whitespace().any(|w| w == "as")
234}
235
236fn is_given_position(line: &str) -> bool {
238 let Some(idx) = line.rfind("given") else {
239 return false;
240 };
241 if !line[..idx]
242 .chars()
243 .last()
244 .map(|c| c.is_whitespace())
245 .unwrap_or(true)
246 {
247 return false;
248 }
249 let after = &line[idx + "given".len()..];
250 if !after.starts_with(char::is_whitespace) {
251 return false;
252 }
253 after
256 .chars()
257 .all(|c| c.is_alphanumeric() || matches!(c, '_' | '.' | ',' | ' ' | '\t'))
258}
259
260fn is_qualified_name(s: &str) -> bool {
261 !s.is_empty()
262 && s.split('.').all(|seg| {
263 !seg.is_empty()
264 && seg.chars().all(|c| c.is_alphanumeric() || c == '_')
265 && !seg.chars().next().unwrap().is_ascii_digit()
266 })
267}
268
269fn is_type_position(line: &str) -> bool {
279 let head = line
280 .trim_end_matches(|c: char| c.is_alphanumeric() || c == '_')
281 .trim_end();
282 head.ends_with("->") || (head.ends_with(':') && !head.ends_with("::")) || in_type_arg_list(head)
283}
284
285fn in_type_arg_list(head: &str) -> bool {
289 let chars: Vec<char> = head.chars().collect();
290 let mut depth = 0i32;
291 let mut opener_after_ident = false;
292 for (i, &c) in chars.iter().enumerate() {
293 match c {
294 '[' => {
295 depth += 1;
296 if depth == 1 {
297 opener_after_ident =
298 i > 0 && (chars[i - 1].is_alphanumeric() || chars[i - 1] == '_');
299 }
300 }
301 ']' => depth -= 1,
302 _ => {}
303 }
304 }
305 depth > 0 && opener_after_ident
306}
307
308pub fn is_keyword_position(line: &str) -> bool {
314 line.trim().chars().all(|c| c.is_alphanumeric() || c == '_')
315}
316
317pub fn is_expression_position(line: &str) -> bool {
322 let head = line
323 .trim_end_matches(|c: char| c.is_alphanumeric() || c == '_')
324 .trim_end();
325 if head.ends_with("->") {
326 return false; }
328 if head.ends_with("=>") {
329 return true; }
331 matches!(
332 head.chars().last(),
333 Some('=' | '(' | ',' | '[' | '+' | '-' | '*' | '/' | '<' | '>' | '&' | '|')
334 )
335}
336
337fn member_receiver(line: &str) -> Option<String> {
345 let head = line
347 .trim_end_matches(|c: char| c.is_alphanumeric() || c == '_')
348 .strip_suffix('.')?;
349 let start = head
353 .char_indices()
354 .rfind(|&(_, c)| !(c.is_alphanumeric() || c == '_'))
355 .map_or(0, |(i, c)| i + c.len_utf8());
356 let recv = &head[start..];
357 let first = recv.chars().next()?;
358 if !first.is_ascii_uppercase() {
359 return None;
360 }
361 if head[..start].ends_with('.') {
363 return None;
364 }
365 Some(recv.to_string())
366}
367
368fn record_construction_receiver(line: &str) -> Option<String> {
374 let bytes = line.as_bytes();
376 let mut depth = 0i32;
377 let mut open = None;
378 for i in (0..bytes.len()).rev() {
379 match bytes[i] {
380 b'}' => depth += 1,
381 b'{' => {
382 if depth == 0 {
383 open = Some(i);
384 break;
385 }
386 depth -= 1;
387 }
388 _ => {}
389 }
390 }
391 let open = open?;
392 let current = line[open + 1..].rsplit(',').next().unwrap_or("");
395 if current.contains(':') {
396 return None;
397 }
398 let head = line[..open].trim_end();
400 let start = head
401 .char_indices()
402 .rfind(|&(_, c)| !(c.is_alphanumeric() || c == '_'))
403 .map_or(0, |(i, c)| i + c.len_utf8());
404 let recv = &head[start..];
405 if recv.chars().next()?.is_ascii_uppercase() {
406 Some(recv.to_string())
407 } else {
408 None
409 }
410}
411
412pub const CORS_FIELDS: &[(&str, &str)] = &[
416 (
417 "origins",
418 "the allowed origins — an exact allowlist, or `[\"*\"]`",
419 ),
420 (
421 "headers",
422 "the `Access-Control-Allow-Headers` a preflight advertises",
423 ),
424 (
425 "credentials",
426 "whether credentialed requests are allowed (`true`/`false`)",
427 ),
428 (
429 "maxAge",
430 "how long a browser may cache the preflight (a `Duration`)",
431 ),
432];
433
434pub const SECURITY_FIELDS: &[(&str, &str)] = &[
437 (
438 "nosniff",
439 "stamp `X-Content-Type-Options: nosniff` (`true`/`false`, default `true`)",
440 ),
441 (
442 "hsts",
443 "opt in to `Strict-Transport-Security` — the `max-age` as a `Duration`",
444 ),
445];
446
447pub const LIMITS_FIELDS: &[(&str, &str)] = &[(
450 "maxBody",
451 "the maximum request body size in bytes (a positive `Int`)",
452)];
453
454pub const CACHE_ARGS: &[(&str, &str)] = &[
459 (
460 "maxAge",
461 "the freshness window — a `Duration` (e.g. `5.minutes`) lowered to `Cache-Control: max-age`",
462 ),
463 (
464 "scope",
465 "`public` or `private` (default `private` — a shared cache stores only on `public`)",
466 ),
467];
468
469pub const LIMIT_ARGS: &[(&str, &str)] = &[(
474 "maxBody",
475 "the maximum request body size in bytes (a positive `Int`) — a `413` is synthesised past it",
476)];
477
478fn innermost_open_brace(text: &str, offset: usize) -> Option<usize> {
482 let bytes = text.as_bytes();
483 let end = offset.min(bytes.len());
484 let mut depth = 0i32;
485 for i in (0..end).rev() {
486 match bytes[i] {
487 b'}' => depth += 1,
488 b'{' => {
489 if depth == 0 {
490 return Some(i);
491 }
492 depth -= 1;
493 }
494 _ => {}
495 }
496 }
497 None
498}
499
500fn word_before_brace(text: &str, open: usize) -> &str {
503 let head = text[..open].trim_end();
504 let start = head
505 .char_indices()
506 .rfind(|&(_, c)| !(c.is_alphanumeric() || c == '_'))
507 .map_or(0, |(i, c)| i + c.len_utf8());
508 &head[start..]
509}
510
511pub fn in_cors_field_position(text: &str, offset: usize) -> bool {
515 let Some(open) = innermost_open_brace(text, offset) else {
516 return false;
517 };
518 if word_before_brace(text, open) != "cors" {
519 return false;
520 }
521 let Some(seg) = text.get(open + 1..offset.min(text.len())) else {
524 return false;
525 };
526 let current = seg.rsplit(['\n', ',']).next().unwrap_or("");
527 !current.contains(':')
528}
529
530pub fn in_security_field_position(text: &str, offset: usize) -> bool {
534 let Some(open) = innermost_open_brace(text, offset) else {
535 return false;
536 };
537 if word_before_brace(text, open) != "security" {
538 return false;
539 }
540 let Some(seg) = text.get(open + 1..offset.min(text.len())) else {
543 return false;
544 };
545 let current = seg.rsplit(['\n', ',']).next().unwrap_or("");
546 !current.contains(':')
547}
548
549pub fn in_limits_field_position(text: &str, offset: usize) -> bool {
553 let Some(open) = innermost_open_brace(text, offset) else {
554 return false;
555 };
556 if word_before_brace(text, open) != "limits" {
557 return false;
558 }
559 let Some(seg) = text.get(open + 1..offset.min(text.len())) else {
562 return false;
563 };
564 let current = seg.rsplit(['\n', ',']).next().unwrap_or("");
565 !current.contains(':')
566}
567
568pub fn in_service_body_item_position(text: &str, offset: usize, line_prefix: &str) -> bool {
573 if !is_keyword_position(line_prefix) {
574 return false;
575 }
576 let Some(open) = innermost_open_brace(text, offset) else {
577 return false;
578 };
579 let header_start = text[..open].rfind('\n').map_or(0, |i| i + 1);
580 text[header_start..open].contains("service ")
581}
582
583fn innermost_open_paren(text: &str, offset: usize) -> Option<usize> {
586 let bytes = text.as_bytes();
587 let end = offset.min(bytes.len());
588 let mut depth = 0i32;
589 for i in (0..end).rev() {
590 match bytes[i] {
591 b')' => depth += 1,
592 b'(' => {
593 if depth == 0 {
594 return Some(i);
595 }
596 depth -= 1;
597 }
598 _ => {}
599 }
600 }
601 None
602}
603
604pub fn in_cache_arg_position(text: &str, offset: usize) -> bool {
609 let Some(open) = innermost_open_paren(text, offset) else {
610 return false;
611 };
612 if word_before_brace(text, open) != "cache" {
613 return false;
614 }
615 let head = text[..open].trim_end();
618 let before_cache = head[..head.len() - "cache".len()].trim_end();
619 if !before_cache.ends_with('@') {
620 return false;
621 }
622 let Some(seg) = text.get(open + 1..offset.min(text.len())) else {
625 return false;
626 };
627 let current = seg.rsplit(['\n', ',']).next().unwrap_or("");
628 !current.contains(':')
629}
630
631pub fn in_limit_arg_position(text: &str, offset: usize) -> bool {
636 let Some(open) = innermost_open_paren(text, offset) else {
637 return false;
638 };
639 if word_before_brace(text, open) != "limit" {
640 return false;
641 }
642 let head = text[..open].trim_end();
645 let before_limit = head[..head.len() - "limit".len()].trim_end();
646 if !before_limit.ends_with('@') {
647 return false;
648 }
649 let Some(seg) = text.get(open + 1..offset.min(text.len())) else {
652 return false;
653 };
654 let current = seg.rsplit(['\n', ',']).next().unwrap_or("");
655 !current.contains(':')
656}
657
658fn after_clause_keyword(line: &str, kw: &str) -> bool {
664 let Some(idx) = line.rfind(kw) else {
665 return false;
666 };
667 if !line[..idx]
668 .chars()
669 .last()
670 .map(char::is_whitespace)
671 .unwrap_or(true)
672 {
673 return false;
674 }
675 let after = &line[idx + kw.len()..];
676 after.starts_with(char::is_whitespace)
677 && after
678 .trim_start()
679 .chars()
680 .all(|c| c.is_alphanumeric() || c == '_')
681}
682
683fn is_for_all_where(line: &str) -> bool {
693 let Some(idx) = line.rfind("where") else {
694 return false;
695 };
696 let head = line[..idx].trim_start();
697 let Some(after_for) = head.strip_prefix("for") else {
698 return false;
699 };
700 if !after_for.starts_with(char::is_whitespace) {
703 return false;
704 }
705 let Some(after_all) = after_for.trim_start().strip_prefix("all") else {
706 return false;
707 };
708 after_all.is_empty() || after_all.starts_with(char::is_whitespace)
710}
711
712pub fn contract_clause_kind(line: &str) -> Option<bool> {
718 let colon = line.rfind(':')?;
719 let clause = line[..colon].trim();
720 for (kw, is_ensures) in [("requires", false), ("ensures", true)] {
721 if let Some(rest) = clause.strip_prefix(kw) {
722 let rest = rest.trim();
723 if !rest.is_empty() && rest.chars().all(|c| c.is_alphanumeric() || c == '_') {
724 return Some(is_ensures);
725 }
726 }
727 }
728 None
729}
730
731fn record_field_names(
741 name: &str,
742 doc_text: &str,
743 files: Option<&HashMap<PathBuf, String>>,
744) -> Vec<Completion> {
745 let mut out: Vec<Completion> = Vec::new();
746 let mut found = false;
747 for_each_unit(doc_text, files, |unit| {
748 if found {
749 return;
750 }
751 let items = match unit {
752 SourceUnit::Commons(c) => &c.items,
753 SourceUnit::Context(c) => &c.items,
754 SourceUnit::Adapter(a) => &a.items,
755 _ => return,
756 };
757 for item in items {
758 if let CommonsItem::Type(t) = item
759 && t.name.name == name
760 && let TypeBody::Record(r) = &t.body
761 {
762 found = true;
763 for f in &r.fields {
764 out.push(Completion::item(
765 f.name.name.clone(),
766 CompletionKind::Field,
767 Some(format!("field of `{name}`")),
768 ));
769 }
770 return;
771 }
772 }
773 });
774 out
775}
776
777pub fn sum_type_variants(
785 name: &str,
786 doc_text: &str,
787 files: Option<&HashMap<PathBuf, String>>,
788) -> Vec<Completion> {
789 let mut out: Vec<Completion> = Vec::new();
790 let mut found = false;
791 for_each_unit(doc_text, files, |unit| {
792 if found {
793 return;
794 }
795 let items = match unit {
796 SourceUnit::Commons(c) => &c.items,
797 SourceUnit::Context(c) => &c.items,
798 SourceUnit::Adapter(a) => &a.items,
799 _ => return,
800 };
801 for item in items {
802 if let CommonsItem::Type(t) = item
803 && t.name.name == name
804 && let TypeBody::Sum(s) = &t.body
805 {
806 found = true;
807 for v in &s.variants {
808 out.push(Completion::item(
809 v.name.name.clone(),
810 CompletionKind::Variant,
811 Some(format!("variant of `{name}`")),
812 ));
813 }
814 return;
815 }
816 }
817 });
818 out
819}
820
821pub fn variants_for_ty(
828 ty: TyId,
829 tys: &Types,
830 doc_text: &str,
831 files: Option<&HashMap<PathBuf, String>>,
832) -> Vec<Completion> {
833 match &*tys.get(ty) {
834 Ty::Named { name, .. } => sum_type_variants(name, doc_text, files),
835 Ty::Result(..) => built_in_variants(&["Ok", "Err"], "Result"),
836 Ty::Option(..) => built_in_variants(&["Some", "None"], "Option"),
837 _ => Vec::new(),
838 }
839}
840
841pub fn nested_variant_completions(
849 ty: TyId,
850 tys: &Types,
851 outer_variant: &str,
852 doc_text: &str,
853 files: Option<&HashMap<PathBuf, String>>,
854) -> Vec<Completion> {
855 match &*tys.get(ty) {
856 Ty::Result(t, e) => match outer_variant {
857 "Ok" => variants_for_ty(*t, tys, doc_text, files),
858 "Err" => variants_for_ty(*e, tys, doc_text, files),
859 _ => Vec::new(),
860 },
861 Ty::HttpResult(t) if outer_variant == "Ok" => variants_for_ty(*t, tys, doc_text, files),
862 Ty::Option(t) if outer_variant == "Some" => variants_for_ty(*t, tys, doc_text, files),
863 Ty::Named {
864 kind: NamedKind::Sum,
865 name,
866 ..
867 } => payload_type_ref_variants(name, outer_variant, doc_text, files),
868 _ => Vec::new(),
869 }
870}
871
872fn built_in_variants(names: &[&str], of: &str) -> Vec<Completion> {
874 names
875 .iter()
876 .map(|v| {
877 Completion::item(
878 (*v).to_string(),
879 CompletionKind::Variant,
880 Some(format!("variant of `{of}`")),
881 )
882 })
883 .collect()
884}
885
886fn payload_type_ref_variants(
891 sum_name: &str,
892 variant: &str,
893 doc_text: &str,
894 files: Option<&HashMap<PathBuf, String>>,
895) -> Vec<Completion> {
896 let mut field_ty: Option<TypeRef> = None;
897 for_each_unit(doc_text, files, |unit| {
898 let items = match unit {
899 SourceUnit::Commons(c) => &c.items,
900 SourceUnit::Context(c) => &c.items,
901 SourceUnit::Adapter(a) => &a.items,
902 _ => return,
903 };
904 for item in items {
905 if let CommonsItem::Type(t) = item
906 && t.name.name == sum_name
907 && let TypeBody::Sum(s) = &t.body
908 && let Some(v) = s.variants.iter().find(|v| v.name.name == variant)
909 && let Some(f) = v.payload.first()
910 {
911 field_ty = Some(f.type_ref.clone());
912 }
913 }
914 });
915 match field_ty {
916 Some(tr) => variants_for_type_ref(&tr, doc_text, files),
917 None => Vec::new(),
918 }
919}
920
921fn variants_for_type_ref(
924 tr: &TypeRef,
925 doc_text: &str,
926 files: Option<&HashMap<PathBuf, String>>,
927) -> Vec<Completion> {
928 match tr {
929 TypeRef::Named(id) => sum_type_variants(&id.name, doc_text, files),
930 TypeRef::Result(..) => built_in_variants(&["Ok", "Err"], "Result"),
931 TypeRef::Option(..) => built_in_variants(&["Some", "None"], "Option"),
932 _ => Vec::new(),
933 }
934}
935
936fn protocol_candidates() -> Vec<Completion> {
938 ["http", "cron", "queue", "websocket"]
939 .into_iter()
940 .map(|p| Completion::item(p, CompletionKind::Keyword, Some("service protocol".into())))
941 .collect()
942}
943
944fn handler_kind_candidates() -> Vec<Completion> {
946 [
947 "call", "GET", "POST", "PUT", "PATCH", "DELETE", "schedule", "message", "open", "close",
948 ]
949 .into_iter()
950 .map(|k| Completion::item(k, CompletionKind::Keyword, Some("handler kind".into())))
951 .collect()
952}
953
954fn export_kind_candidates() -> Vec<Completion> {
956 ["capability", "transparent", "opaque"]
957 .into_iter()
958 .map(|k| Completion::item(k, CompletionKind::Keyword, Some("export kind".into())))
959 .collect()
960}
961
962fn predicate_name_candidates() -> Vec<Completion> {
966 [
967 "Matches",
968 "InRange",
969 "MinLength",
970 "MaxLength",
971 "Length",
972 "NonNegative",
973 "Positive",
974 "NonEmpty",
975 ]
976 .into_iter()
977 .map(|k| {
978 Completion::item(
979 k,
980 CompletionKind::Keyword,
981 Some("refinement predicate".into()),
982 )
983 })
984 .collect()
985}
986
987fn actor_candidates(doc_text: &str, files: Option<&HashMap<PathBuf, String>>) -> Vec<Completion> {
989 let mut out: Vec<Completion> = Vec::new();
990 let mut seen: BTreeSet<String> = BTreeSet::new();
991 for_each_unit(doc_text, files, |unit| {
992 let items = match unit {
993 SourceUnit::Commons(c) => &c.items,
994 SourceUnit::Context(c) => &c.items,
995 SourceUnit::Adapter(a) => &a.items,
996 _ => return,
997 };
998 for item in items {
999 if let CommonsItem::Actor(a) = item
1000 && seen.insert(a.name.name.clone())
1001 {
1002 out.push(Completion::item(
1003 a.name.name.clone(),
1004 CompletionKind::Type,
1005 Some("actor".into()),
1006 ));
1007 }
1008 }
1009 });
1010 out
1011}
1012
1013pub const BUILTIN_STATICS: &[(&str, &[(&str, &str)])] = &[
1020 ("Int", &[("parse", "parse(s: String) -> Option[Int]")]),
1021 ("Float", &[("parse", "parse(s: String) -> Option[Float]")]),
1022 (
1023 "Json",
1024 &[
1025 ("encode", "encode(value) -> String"),
1026 ("decode", "decode[T](s: String) -> Result[T, JsonError]"),
1027 ],
1028 ),
1029 ("List", &[("empty", "empty() -> List[T]")]),
1030 ("Map", &[("empty", "empty() -> Map[K, V]")]),
1031 ("Effect", &[("pure", "pure(value) -> Effect[T]")]),
1032 (
1033 "Bytes",
1034 &[
1035 ("fromUtf8", "fromUtf8(s: String) -> Bytes"),
1036 ("fromBase64", "fromBase64(s: String) -> Option[Bytes]"),
1037 ("empty", "empty() -> Bytes"),
1038 ],
1039 ),
1040];
1041
1042fn builtin_sum_variants(receiver: &str) -> Vec<(String, String)> {
1046 match receiver {
1047 "HttpResult" => bynk_syntax::ast::HTTP_VARIANTS
1048 .iter()
1049 .map(|v| {
1050 (
1051 v.name.to_string(),
1052 format!("variant of `HttpResult` ({})", v.status),
1053 )
1054 })
1055 .collect(),
1056 "QueueResult" => bynk_syntax::ast::QUEUE_VARIANTS
1057 .iter()
1058 .map(|v| (v.name.to_string(), "variant of `QueueResult`".to_string()))
1059 .collect(),
1060 _ => Vec::new(),
1061 }
1062}
1063
1064fn member_candidates(
1070 receiver: &str,
1071 doc_text: &str,
1072 files: Option<&HashMap<PathBuf, String>>,
1073) -> Vec<Completion> {
1074 if let Some((_, statics)) = BUILTIN_STATICS.iter().find(|(name, _)| *name == receiver) {
1075 return statics
1076 .iter()
1077 .map(|(label, sig)| {
1078 Completion::item(*label, CompletionKind::Member, Some(sig.to_string()))
1079 })
1080 .collect();
1081 }
1082 let mut out: Vec<Completion> = Vec::new();
1083 let mut seen: BTreeSet<String> = BTreeSet::new();
1084 for (label, detail) in builtin_sum_variants(receiver) {
1087 if seen.insert(label.clone()) {
1088 out.push(Completion::item(
1089 label,
1090 CompletionKind::Variant,
1091 Some(detail),
1092 ));
1093 }
1094 }
1095 for_each_unit(doc_text, files, |unit| {
1096 let items = match unit {
1097 SourceUnit::Commons(c) => &c.items,
1098 SourceUnit::Context(c) => &c.items,
1099 SourceUnit::Adapter(a) => &a.items,
1100 _ => return,
1101 };
1102 for item in items {
1103 match item {
1104 CommonsItem::Type(t) if t.name.name == receiver => match &t.body {
1105 bynk_syntax::ast::TypeBody::Sum(s) => {
1106 for v in &s.variants {
1107 if seen.insert(v.name.name.clone()) {
1108 out.push(Completion::item(
1109 v.name.name.clone(),
1110 CompletionKind::Variant,
1111 Some(format!("variant of `{receiver}`")),
1112 ));
1113 }
1114 }
1115 }
1116 bynk_syntax::ast::TypeBody::Refined { .. }
1117 | bynk_syntax::ast::TypeBody::Opaque { .. } => {
1118 for (label, sig) in [
1119 (
1120 "of",
1121 format!("of(value) -> Result[{receiver}, ValidationError]"),
1122 ),
1123 ("unsafe", format!("unsafe(value) -> {receiver}")),
1124 ] {
1125 if seen.insert(label.to_string()) {
1126 out.push(Completion::item(
1127 label,
1128 CompletionKind::Member,
1129 Some(sig),
1130 ));
1131 }
1132 }
1133 }
1134 _ => {}
1138 },
1139 CommonsItem::Capability(c) if c.name.name == receiver => {
1140 for op in &c.ops {
1141 if seen.insert(op.name.name.clone()) {
1142 let params = op
1146 .params
1147 .iter()
1148 .map(|p| format!("{}: {}", p.name.name, type_ref_str(&p.type_ref)))
1149 .collect::<Vec<_>>()
1150 .join(", ");
1151 let type_params = if op.type_params.is_empty() {
1153 String::new()
1154 } else {
1155 let names: Vec<&str> = op
1156 .type_params
1157 .iter()
1158 .map(|tp| tp.name.name.as_str())
1159 .collect();
1160 format!("[{}]", names.join(", "))
1161 };
1162 out.push(Completion::item(
1163 op.name.name.clone(),
1164 CompletionKind::Member,
1165 Some(format!(
1166 "{}{type_params}({params}) -> {} — operation of `{receiver}`",
1167 op.name.name,
1168 type_ref_str(&op.return_type)
1169 )),
1170 ));
1171 }
1172 }
1173 }
1174 _ => {}
1175 }
1176 }
1177 });
1178 out
1179}
1180
1181const BUILTIN_TYPES: &[&str] = &[
1187 bynk_check::builtin_names::types::INT,
1188 "Bool",
1189 bynk_check::builtin_names::types::FLOAT,
1190 "String",
1191 "Option",
1192 "Result",
1193 "Effect",
1194 bynk_check::builtin_names::types::LIST,
1195 bynk_check::builtin_names::types::MAP,
1196];
1197
1198pub const SNIPPETS: &[(&str, &str)] = &[
1201 ("context", "context ${1:name} {\n\t$0\n}"),
1203 ("commons", "commons ${1:my.lib}\n\n$0"),
1204 (
1205 "adapter",
1206 "adapter ${1:name} {\n\tbinding \"${2:./module}\"\n\t$0\n}",
1207 ),
1208 ("uses", "uses ${1:module}"),
1210 ("consumes", "consumes ${1:bynk} { ${2:Random} }"),
1211 (
1213 "type record",
1214 "type ${1:Name} = {\n\t${2:field}: ${3:Int},\n}",
1215 ),
1216 ("type enum", "type ${1:Name} = enum {\n\t${2:Variant},\n}"),
1217 (
1218 "type refined",
1219 "type ${1:Name} = ${2:String} where ${3:MinLength(1)}",
1220 ),
1221 (
1222 "type opaque",
1223 "type ${1:Name} = opaque ${2:Int} where ${3:NonNegative}",
1224 ),
1225 (
1227 "fn",
1228 "fn ${1:name}(${2:x}: ${3:Int}) -> ${4:Int} {\n\t$0\n}",
1229 ),
1230 (
1231 "fn contract",
1232 "fn ${1:name}(${2:x}: ${3:Int}) -> ${4:Int}\n\trequires ${5:in_range}: ${6:x >= 0}\n\tensures ${7:non_negative}: ${8:result >= 0}\n{\n\t$0\n}",
1233 ),
1234 (
1236 "capability",
1237 "capability ${1:Name} {\n\tfn ${2:op}() -> Effect[${3:Unit}]\n}",
1238 ),
1239 (
1240 "provides",
1241 "provides ${1:Cap} = ${2:Impl} {\n\tfn ${3:op}(${4}) -> Effect[${5:()}] {\n\t\tEffect.pure(${6:()})\n\t}\n}",
1242 ),
1243 (
1245 "actor",
1246 "actor ${1:Name} { auth = ${2:Bearer(secret = \"AUTH_JWT_SECRET\")}, identity = ${3:UserId} }",
1247 ),
1248 (
1249 "agent",
1250 "agent ${1:Name} {\n\tkey ${2:id}: ${3:String}\n\n\tstore ${4:status}: Cell[${5:Int}] = ${6:0}\n\n\tinvariant ${7:non_negative}: ${8:status >= 0}\n\n\ttransition ${9:monotonic}: ${10:new.status >= old.status}\n\n\ton call ${11:op}(${12}) -> Effect[Result[${13:()}, String]] {\n\t\tOk(${14:()})\n\t}\n}",
1251 ),
1252 (
1254 "service",
1255 "service ${1:name} {\n\ton call(${2}) -> Effect[${3:Unit}] {\n\t\t$0\n\t}\n}",
1256 ),
1257 ("on call", "on call(${1}) -> Effect[${2:Unit}] {\n\t$0\n}"),
1258 (
1259 "on http",
1260 "on ${1|GET,POST,PUT,DELETE,PATCH|}(\"${2:/path}\") (${3:body}: ${4:Req}) -> Effect[HttpResult[${5:Res}]] given ${6:Cap} {\n\t$0\n}",
1261 ),
1262 (
1263 "on cron",
1264 "on schedule(\"${1:0 * * * *}\") () -> Effect[Result[(), String]] {\n\t$0\n\tOk(())\n}",
1265 ),
1266 (
1268 "suite",
1269 "suite ${1:target}\n\ncase \"${2:it works}\" {\n\tlet ${3:actual} = ${4:0}\n\texpect ${5:actual == 0}\n}",
1270 ),
1271 (
1272 "property",
1273 "property \"${1:invariant holds}\" {\n\tfor all ${2:x}: ${3:Int} {\n\t\texpect ${4:x == x}\n\t}\n}",
1274 ),
1275];
1276
1277const CONSTRUCTORS: &[&str] = &["Ok", "Err", "Some", "None", "true", "false"];
1282
1283fn expression_candidates(
1289 doc_text: &str,
1290 files: Option<&HashMap<PathBuf, String>>,
1291) -> Vec<Completion> {
1292 let mut out: Vec<Completion> = CONSTRUCTORS
1293 .iter()
1294 .map(|&name| {
1295 Completion::item(
1296 name,
1297 CompletionKind::Constructor,
1298 keyword_doc(name).map(str::to_string),
1299 )
1300 })
1301 .collect();
1302 out.extend(type_candidates(doc_text, files));
1305 out.extend(free_function_candidates(doc_text, files));
1308 out
1309}
1310
1311fn unit_items_and_uses(unit: &SourceUnit) -> (&[CommonsItem], &[UsesDecl]) {
1314 match unit {
1315 SourceUnit::Commons(c) => (&c.items, &c.uses),
1316 SourceUnit::Context(c) => (&c.items, &c.uses),
1317 SourceUnit::Adapter(a) => (&a.items, &a.uses),
1318 _ => (&[], &[]),
1319 }
1320}
1321
1322fn current_unit_name(doc_text: &str) -> Option<String> {
1326 let tokens = lexer::tokenize(doc_text).ok()?;
1327 let (unit, _errs) = parser::parse_unit_with_recovery(&tokens, doc_text);
1328 Some(unit?.name().joined())
1329}
1330
1331fn free_fn_signature(name: &str, f: &bynk_syntax::ast::FnDecl) -> String {
1335 let params = f
1336 .params
1337 .iter()
1338 .map(|p| format!("{}: {}", p.name.name, type_ref_str(&p.type_ref)))
1339 .collect::<Vec<_>>()
1340 .join(", ");
1341 format!("{name}({params}) -> {}", type_ref_str(&f.return_type))
1342}
1343
1344fn free_function_candidates(
1349 doc_text: &str,
1350 files: Option<&HashMap<PathBuf, String>>,
1351) -> Vec<Completion> {
1352 let Some(current) = current_unit_name(doc_text) else {
1353 return Vec::new();
1354 };
1355 struct UnitFns {
1358 name: String,
1359 fns: Vec<(String, String)>,
1360 uses: Vec<String>,
1361 }
1362 let mut units: Vec<UnitFns> = Vec::new();
1363 for_each_unit(doc_text, files, |unit| {
1364 let (items, uses) = unit_items_and_uses(unit);
1365 let fns = items
1366 .iter()
1367 .filter_map(|it| match it {
1368 CommonsItem::Fn(f) => match &f.name {
1369 FnName::Free(id) => Some((id.name.clone(), free_fn_signature(&id.name, f))),
1370 FnName::Method { .. } => None,
1371 },
1372 _ => None,
1373 })
1374 .collect();
1375 units.push(UnitFns {
1376 name: unit.name().joined(),
1377 fns,
1378 uses: uses.iter().map(|u| u.target.joined()).collect(),
1379 });
1380 });
1381 let mut imported: BTreeSet<String> = BTreeSet::new();
1384 for u in &units {
1385 if u.name == current {
1386 imported.extend(u.uses.iter().cloned());
1387 }
1388 }
1389 let mut out: Vec<Completion> = Vec::new();
1391 let mut seen: BTreeSet<String> = BTreeSet::new();
1392 for u in &units {
1393 let own = u.name == current;
1394 if !own && !imported.contains(&u.name) {
1395 continue;
1396 }
1397 let origin = if own { "this unit" } else { u.name.as_str() };
1398 for (name, sig) in &u.fns {
1399 if seen.insert(name.clone()) {
1400 out.push(Completion::item(
1401 name.clone(),
1402 CompletionKind::Function,
1403 Some(format!("{sig} — `{origin}`")),
1404 ));
1405 }
1406 }
1407 }
1408 out
1409}
1410
1411pub fn keyword_doc(word: &str) -> Option<&'static str> {
1415 keywords::KEYWORDS
1416 .iter()
1417 .find(|k| k.word == word)
1418 .map(|k| k.meaning)
1419}
1420
1421fn type_candidates(doc_text: &str, files: Option<&HashMap<PathBuf, String>>) -> Vec<Completion> {
1425 let mut out: Vec<Completion> = Vec::new();
1426 let mut seen: BTreeSet<String> = BTreeSet::new();
1427 for &name in BUILTIN_TYPES {
1428 if seen.insert(name.to_string()) {
1429 let detail = keyword_doc(name)
1430 .map(str::to_string)
1431 .or_else(|| match name {
1432 "List" => Some("The built-in list type, `List[T]`.".to_string()),
1433 "Map" => Some("The built-in map type, `Map[K, V]`.".to_string()),
1434 _ => Some("built-in type".to_string()),
1435 });
1436 out.push(Completion::item(name, CompletionKind::Type, detail));
1437 }
1438 }
1439 for_each_unit(doc_text, files, |unit| {
1440 let items = match unit {
1441 SourceUnit::Commons(c) => &c.items,
1442 SourceUnit::Context(c) => &c.items,
1443 SourceUnit::Adapter(a) => &a.items,
1444 _ => return,
1445 };
1446 for item in items {
1447 if let CommonsItem::Type(t) = item
1448 && seen.insert(t.name.name.clone())
1449 {
1450 out.push(Completion::item(
1451 t.name.name.clone(),
1452 CompletionKind::Type,
1453 Some("type".to_string()),
1454 ));
1455 }
1456 }
1457 });
1458 out
1459}
1460
1461fn keyword_and_snippet_candidates() -> Vec<Completion> {
1466 let mut out: Vec<Completion> = keywords::KEYWORDS
1467 .iter()
1468 .filter(|k| k.word.chars().next().is_some_and(char::is_lowercase))
1469 .map(|k| Completion::item(k.word, CompletionKind::Keyword, Some(k.meaning.to_string())))
1470 .collect();
1471 for &(label, body) in SNIPPETS {
1472 out.push(Completion::snippet(label, body));
1473 }
1474 out
1475}
1476
1477fn parse_source_unit(src: &str) -> Option<SourceUnit> {
1482 let tokens = lexer::tokenize(src).ok()?;
1483 parser::parse_unit_with_recovery(&tokens, src).0
1484}
1485
1486static EMBEDDED_UNITS: LazyLock<Vec<Arc<SourceUnit>>> = LazyLock::new(|| {
1495 bynk_check::firstparty::FIRSTPARTY_SOURCES
1499 .iter()
1500 .filter_map(|(_, src)| parse_source_unit(src).map(Arc::new))
1501 .collect()
1502});
1503
1504struct CachedUnit {
1511 content: Arc<str>,
1512 unit: Option<Arc<SourceUnit>>,
1513}
1514
1515static PROJECT_UNIT_CACHE: LazyLock<Mutex<HashMap<PathBuf, CachedUnit>>> =
1521 LazyLock::new(|| Mutex::new(HashMap::new()));
1522
1523const PROJECT_UNIT_CACHE_CAP: usize = 4096;
1531
1532fn cached_project_unit(path: &Path, content: &str) -> Option<Arc<SourceUnit>> {
1538 {
1539 let cache = PROJECT_UNIT_CACHE.lock().unwrap();
1540 if let Some(entry) = cache.get(path)
1541 && &*entry.content == content
1542 {
1543 return entry.unit.clone();
1544 }
1545 }
1546 let unit = parse_source_unit(content).map(Arc::new);
1547 let mut cache = PROJECT_UNIT_CACHE.lock().unwrap();
1548 if cache.len() >= PROJECT_UNIT_CACHE_CAP && !cache.contains_key(path) {
1551 cache.clear();
1552 }
1553 cache.insert(
1554 path.to_path_buf(),
1555 CachedUnit {
1556 content: Arc::from(content),
1557 unit: unit.clone(),
1558 },
1559 );
1560 unit
1561}
1562
1563pub fn for_each_unit(
1579 doc_text: &str,
1580 files: Option<&HashMap<PathBuf, String>>,
1581 mut f: impl FnMut(&SourceUnit),
1582) {
1583 for unit in EMBEDDED_UNITS.iter() {
1584 f(unit);
1585 }
1586 if let Some(unit) = parse_source_unit(doc_text) {
1587 f(&unit);
1588 }
1589 if let Some(content) = files {
1594 for (path, text) in content {
1595 if let Some(unit) = cached_project_unit(path, text) {
1596 f(&unit);
1597 }
1598 }
1599 }
1600}
1601
1602fn consumable_units(doc_text: &str, files: Option<&HashMap<PathBuf, String>>) -> Vec<Completion> {
1604 let mut seen: BTreeSet<String> = BTreeSet::new();
1605 let mut out: Vec<Completion> = Vec::new();
1606 for_each_unit(doc_text, files, |unit| {
1607 let (name, kind) = match unit {
1608 SourceUnit::Context(c) => (c.name.joined(), "context"),
1609 SourceUnit::Adapter(a) => (a.name.joined(), "adapter"),
1610 _ => return,
1611 };
1612 if seen.insert(name.clone()) {
1613 out.push(Completion::item(
1614 name,
1615 CompletionKind::Unit,
1616 Some(kind.to_string()),
1617 ));
1618 }
1619 });
1620 out
1621}
1622
1623fn capabilities_of_unit(
1629 unit: &str,
1630 doc_text: &str,
1631 files: Option<&HashMap<PathBuf, String>>,
1632) -> Vec<String> {
1633 let mut out: BTreeSet<String> = BTreeSet::new();
1634 let mut found = false;
1635 for_each_unit(doc_text, files, |u| {
1636 if found {
1637 return;
1638 }
1639 let (name, exports) = match u {
1640 SourceUnit::Context(c) => (c.name.joined(), &c.exports),
1641 SourceUnit::Adapter(a) => (a.name.joined(), &a.exports),
1642 _ => return,
1643 };
1644 if name != unit {
1645 return;
1646 }
1647 found = true;
1648 for clause in exports {
1649 if clause.kind == ExportKind::Capability {
1650 for n in &clause.names {
1651 out.insert(n.name.clone());
1652 }
1653 }
1654 }
1655 });
1656 out.into_iter().collect()
1657}
1658
1659fn in_scope_capabilities(
1663 doc_text: &str,
1664 files: Option<&HashMap<PathBuf, String>>,
1665) -> Vec<Completion> {
1666 let mut labels: BTreeSet<String> = BTreeSet::new();
1667 let Ok(tokens) = lexer::tokenize(doc_text) else {
1668 return Vec::new();
1669 };
1670 let (Some(unit), _errs) = parser::parse_unit_with_recovery(&tokens, doc_text) else {
1671 return Vec::new();
1672 };
1673 let (items, consumes) = match &unit {
1674 SourceUnit::Context(c) => (&c.items, &c.consumes),
1675 SourceUnit::Adapter(a) => (&a.items, &EMPTY_CONSUMES),
1676 _ => return Vec::new(),
1677 };
1678 for item in items {
1680 if let bynk_syntax::ast::CommonsItem::Capability(c) = item {
1681 labels.insert(c.name.name.clone());
1682 }
1683 }
1684 for c in consumes {
1686 let unit_name = c.target.joined();
1687 match &c.selected {
1688 Some(names) => {
1689 for n in names {
1690 labels.insert(n.name.clone());
1691 }
1692 }
1693 None => {
1694 let prefix = c
1695 .alias
1696 .as_ref()
1697 .map(|a| a.name.clone())
1698 .unwrap_or_else(|| unit_name.clone());
1699 for cap in capabilities_of_unit(&unit_name, doc_text, files) {
1700 labels.insert(format!("{prefix}.{cap}"));
1701 }
1702 }
1703 }
1704 }
1705 labels
1706 .into_iter()
1707 .map(|label| {
1708 Completion::item(
1709 label,
1710 CompletionKind::Capability,
1711 Some("capability in scope".to_string()),
1712 )
1713 })
1714 .collect()
1715}
1716
1717pub fn value_receiver_rewrite(text: &str, offset: usize) -> Option<(String, usize)> {
1729 let prefix = text.get(..offset)?;
1730 let head = prefix
1731 .trim_end_matches(|c: char| c.is_alphanumeric() || c == '_')
1732 .strip_suffix('.')?;
1733 let (recv, start) = ident_ending_at(head, head.len())?;
1734 let first = recv.chars().next()?;
1735 if !(first.is_ascii_lowercase() || first == '_') {
1736 return None; }
1738 if head[..start].ends_with('.') {
1739 return None; }
1741 let dot = head.len(); let rewritten = format!("{}{}", &text[..dot], &text[offset..]);
1743 Some((rewritten, dot.saturating_sub(1)))
1744}
1745
1746pub fn ident_ending_at(text: &str, end: usize) -> Option<(&str, usize)> {
1757 let before = text.get(..end)?;
1758 let start = before
1759 .char_indices()
1760 .rfind(|&(_, c)| !(c.is_alphanumeric() || c == '_'))
1761 .map_or(0, |(i, c)| i + c.len_utf8());
1762 let name = &before[start..];
1763 (!name.is_empty()).then_some((name, start))
1764}
1765
1766pub fn value_member_candidates(
1769 ty: TyId,
1770 tys: &Types,
1771 doc_text: &str,
1772 files: Option<&HashMap<PathBuf, String>>,
1773) -> Vec<Completion> {
1774 let mut out: Vec<Completion> = kernel_methods::methods_for(ty, tys)
1775 .iter()
1776 .map(|km| {
1777 Completion::item(
1778 km.name,
1779 CompletionKind::Member,
1780 Some(km.signature.to_string()),
1781 )
1782 })
1783 .collect();
1784 if let Ty::Named { name, .. } = &*tys.get(ty) {
1786 let mut seen: BTreeSet<String> = BTreeSet::new();
1787 for_each_unit(doc_text, files, |unit| {
1788 let items = match unit {
1789 SourceUnit::Commons(c) => &c.items,
1790 SourceUnit::Context(c) => &c.items,
1791 SourceUnit::Adapter(a) => &a.items,
1792 _ => return,
1793 };
1794 for item in items {
1795 if let CommonsItem::Type(t) = item
1796 && &t.name.name == name
1797 && let TypeBody::Record(r) = &t.body
1798 {
1799 for f in &r.fields {
1800 if seen.insert(f.name.name.clone()) {
1801 out.push(Completion::item(
1802 f.name.name.clone(),
1803 CompletionKind::Field,
1804 Some(format!("field of `{name}`")),
1805 ));
1806 }
1807 }
1808 }
1809 }
1810 });
1811 }
1812 out
1813}
1814
1815pub fn store_field_member_candidates(
1830 rewritten: &str,
1831 recv_offset: usize,
1832 locals: &[LocalBinding],
1833) -> Vec<Completion> {
1834 let Some((kind_head, held)) =
1835 crate::symbols::store_field_kind_at(rewritten, recv_offset + 1, locals)
1836 else {
1837 return Vec::new();
1838 };
1839 let mut out: Vec<Completion> = store_ops::ops_for(&kind_head)
1840 .iter()
1841 .map(|o| {
1842 Completion::item(
1843 o.name,
1844 CompletionKind::Member,
1845 Some(o.signature.to_string()),
1846 )
1847 })
1848 .collect();
1849 if kind_head == "Map" && !held {
1850 out.extend(store_ops::MAP_QUERY_ACCESSORS.iter().map(|a| {
1851 Completion::item(a.name, CompletionKind::Field, Some(a.signature.to_string()))
1852 }));
1853 }
1854 out
1855}
1856
1857static EMPTY_CONSUMES: Vec<bynk_syntax::ast::ConsumesDecl> = Vec::new();
1858
1859#[cfg(test)]
1860mod tests {
1861 use super::*;
1862 use bynk_check::firstparty::BYNK_LIST_SRC;
1863
1864 fn labels(line: &str, doc: &str) -> Vec<String> {
1865 complete(line, doc, None)
1866 .into_iter()
1867 .map(|c| c.label)
1868 .collect()
1869 }
1870
1871 #[test]
1872 fn consumes_target_suggests_units_including_bynk() {
1873 let doc = "adapter tokens {\n binding \"./b.ts\"\n capability Jwt { fn f() -> Effect[Int] }\n provides Jwt = X\n}\n";
1875 let got = labels(" consumes ", doc);
1876 assert!(got.contains(&"bynk".to_string()), "{got:?}");
1877 assert!(got.contains(&"tokens".to_string()), "{got:?}");
1878 }
1879
1880 #[test]
1881 fn consumes_brace_suggests_that_units_capabilities() {
1882 let got = labels(" consumes bynk { ", "context a.b\n");
1883 assert!(got.contains(&"Clock".to_string()), "{got:?}");
1885 assert!(got.contains(&"Random".to_string()), "{got:?}");
1886 assert!(got.contains(&"Logger".to_string()), "{got:?}");
1887 }
1888
1889 #[test]
1890 fn given_suggests_local_and_flattened_capabilities() {
1891 let doc = "context a.b\n\
1892 consumes bynk { Clock }\n\
1893 capability Local { fn f() -> Effect[Int] }\n\
1894 service s {\n\
1895 on call() -> Effect[Int] given Clock {\n\
1896 1\n\
1897 }\n\
1898 }\n";
1899 let got = labels(" on call() -> Effect[Int] given ", doc);
1900 assert!(got.contains(&"Clock".to_string()), "flattened: {got:?}");
1901 assert!(got.contains(&"Local".to_string()), "local: {got:?}");
1902 }
1903
1904 #[test]
1905 fn expression_position_offers_constructors_and_types() {
1906 let doc = "commons m {\n type Order = { id: Int }\n}\n";
1911 let items = complete(" let x = ", doc, None);
1912 for &c in CONSTRUCTORS {
1913 assert!(
1914 find(&items, c, CompletionKind::Constructor).is_some(),
1915 "constructor {c}: {:?}",
1916 items.iter().map(|i| &i.label).collect::<Vec<_>>()
1917 );
1918 }
1919 assert!(
1920 find(&items, "Int", CompletionKind::Type).is_some(),
1921 "builtin type"
1922 );
1923 assert!(
1924 find(&items, "Order", CompletionKind::Type).is_some(),
1925 "project type"
1926 );
1927 }
1928
1929 #[test]
1930 fn value_receiver_and_decimal_are_not_expression_positions() {
1931 assert!(complete(" let p = q.", "context a.b\n", None).is_empty());
1935 assert!(complete(" let n = 1.", "context a.b\n", None).is_empty());
1936 }
1937
1938 fn free_fn_names(src: &str) -> Vec<String> {
1940 let tokens = lexer::tokenize(src).unwrap();
1941 let (unit, _) = parser::parse_unit_with_recovery(&tokens, src);
1942 let unit = unit.unwrap();
1943 let (items, _) = unit_items_and_uses(&unit);
1944 items
1945 .iter()
1946 .filter_map(|it| match it {
1947 CommonsItem::Fn(f) => match &f.name {
1948 FnName::Free(id) => Some(id.name.clone()),
1949 FnName::Method { .. } => None,
1950 },
1951 _ => None,
1952 })
1953 .collect()
1954 }
1955
1956 #[test]
1957 fn free_functions_offered_for_own_unit_and_used_modules() {
1958 let doc = "commons app {\n uses bynk.list\n fn helper(x: Int) -> Int { x }\n}\n";
1961 let items = complete(" let y = ", doc, None);
1962 assert!(
1964 find(&items, "helper", CompletionKind::Function).is_some(),
1965 "own fn: {:?}",
1966 items.iter().map(|i| &i.label).collect::<Vec<_>>()
1967 );
1968 for name in free_fn_names(BYNK_LIST_SRC) {
1971 assert!(
1972 find(&items, &name, CompletionKind::Function).is_some(),
1973 "bynk.list.{name}: {:?}",
1974 items.iter().map(|i| &i.label).collect::<Vec<_>>()
1975 );
1976 }
1977 assert!(
1979 find(&items, "values", CompletionKind::Function).is_none(),
1980 "bynk.map.values leaked without `uses bynk.map`"
1981 );
1982 }
1983
1984 #[test]
1985 fn locale_functions_offered_when_bynk_locale_is_used() {
1986 let doc = "commons app {\n uses bynk.locale\n}\n";
1991 let items = complete(" let y = ", doc, None);
1992 for name in [
1993 "render",
1994 "message",
1995 "withText",
1996 "withWhole",
1997 "withNum",
1998 "withMoment",
1999 ] {
2000 assert!(
2001 find(&items, name, CompletionKind::Function).is_some(),
2002 "bynk.locale.{name} missing from completion: {:?}",
2003 items.iter().map(|i| &i.label).collect::<Vec<_>>()
2004 );
2005 }
2006 }
2007
2008 #[test]
2009 fn free_functions_require_a_uses_import() {
2010 let doc = "commons app {\n fn helper(x: Int) -> Int { x }\n}\n";
2012 let items = complete(" let y = ", doc, None);
2013 assert!(find(&items, "helper", CompletionKind::Function).is_some());
2014 for name in ["map", "filter", "reverse"] {
2015 assert!(
2016 find(&items, name, CompletionKind::Function).is_none(),
2017 "bynk.list.{name} offered without `uses bynk.list`"
2018 );
2019 }
2020 }
2021
2022 #[test]
2023 fn member_completion_reaches_inside_an_interpolation_hole() {
2024 let doc = "context a.b\n capability Timer { fn now() -> Effect[Int] }\n";
2028 let in_hole = complete(" \"the time is \\(Timer.", doc, None);
2029 assert!(
2030 find(&in_hole, "now", CompletionKind::Member).is_some(),
2031 "capability op not offered inside a hole: {:?}",
2032 in_hole.iter().map(|c| &c.label).collect::<Vec<_>>()
2033 );
2034 let statics = complete(" \"n=\\(Int.", "context a.b\n", None);
2036 assert!(find(&statics, "parse", CompletionKind::Member).is_some());
2037 }
2038
2039 #[test]
2040 fn consumes_with_as_is_not_a_target_completion() {
2041 assert!(!is_consumes_target("consumes platform.time as "));
2043 assert!(is_consumes_target("consumes platform"));
2044 }
2045
2046 fn find<'a>(
2047 items: &'a [Completion],
2048 label: &str,
2049 kind: CompletionKind,
2050 ) -> Option<&'a Completion> {
2051 items.iter().find(|c| c.label == label && c.kind == kind)
2052 }
2053
2054 #[test]
2055 fn type_annotation_suggests_builtins_surface_and_project_types() {
2056 let doc = "commons m {\n type Order = { id: Int }\n}\n";
2057 let got = labels(" let x: ", doc);
2058 for want in ["Int", "Option", "Result", "Effect", "List", "Map"] {
2061 assert!(got.contains(&want.to_string()), "built-in {want}: {got:?}");
2062 }
2063 assert!(got.contains(&"Uuid".to_string()), "surface: {got:?}");
2064 assert!(got.contains(&"Order".to_string()), "project: {got:?}");
2065 }
2066
2067 #[test]
2068 fn return_type_and_type_args_are_type_positions() {
2069 assert!(is_type_position(" on call() -> "));
2070 assert!(is_type_position(" let x: Option["));
2071 assert!(is_type_position(" let x: Result[Int, "));
2072 assert!(is_type_position(" -> Eff"));
2074 }
2075
2076 #[test]
2077 fn list_literal_is_not_a_type_position() {
2078 assert!(!is_type_position(" let xs = ["));
2080 let items = complete(" let xs = [", "context a.b\n", None);
2084 assert!(
2085 find(&items, "Some", CompletionKind::Constructor).is_some(),
2086 "{:?}",
2087 items.iter().map(|c| &c.label).collect::<Vec<_>>()
2088 );
2089 }
2090
2091 #[test]
2092 fn builtin_type_carries_its_registry_doc() {
2093 let items = complete(" let x: ", "context a.b\n", None);
2094 let int = find(&items, "Int", CompletionKind::Type).expect("Int present");
2095 assert_eq!(int.detail.as_deref(), keyword_doc("Int"));
2096 assert!(int.detail.is_some(), "Int should have a doc");
2097 }
2098
2099 #[test]
2100 fn keyword_position_suggests_keywords_and_snippets() {
2101 let items = complete(" ", "context a.b\n", None);
2102 assert!(find(&items, "capability", CompletionKind::Keyword).is_some());
2104 assert!(find(&items, "fn", CompletionKind::Keyword).is_some());
2105 assert!(find(&items, "let", CompletionKind::Keyword).is_some());
2106 assert!(find(&items, "Int", CompletionKind::Keyword).is_none());
2108 assert!(find(&items, "Some", CompletionKind::Keyword).is_none());
2109 let snip = find(&items, "service", CompletionKind::Snippet).expect("service snippet");
2111 let body = snip.insert_text.as_deref().unwrap_or("");
2112 assert!(body.contains("on call"), "snippet body: {body:?}");
2113 assert!(body.contains("${1"), "snippet tab stop: {body:?}");
2114 }
2115
2116 #[test]
2117 fn keyword_position_fires_on_an_empty_line() {
2118 assert!(is_keyword_position(""));
2119 assert!(is_keyword_position(" cap"));
2120 assert!(!is_keyword_position(" let x ="));
2121 assert!(!is_keyword_position(" x: "));
2122 assert!(!complete("", "context a.b\n", None).is_empty());
2123 }
2124
2125 #[test]
2126 fn member_receiver_is_a_single_upper_ident_before_a_dot() {
2127 assert_eq!(member_receiver(" Color."), Some("Color".to_string()));
2128 assert_eq!(
2129 member_receiver(" let e = Email.o"),
2130 Some("Email".to_string())
2131 );
2132 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); }
2137
2138 #[test]
2139 fn receiver_extraction_survives_a_multibyte_char_before_the_receiver() {
2140 assert_eq!(member_receiver("\"Foo."), Some("Foo".to_string()));
2144 assert_eq!(member_receiver("€Color."), Some("Color".to_string()));
2145 assert_eq!(member_receiver("—Bar."), Some("Bar".to_string()));
2146 assert_eq!(word_before_brace("\"cors {", 6), "cors");
2147 assert_eq!(
2148 record_construction_receiver("\"€Order {"),
2149 Some("Order".to_string())
2150 );
2151 let _ = complete("let x = \"Foo.", "commons m {}\n", None);
2154 let _ = complete(" \"€42.", "commons m {}\n", None);
2155 assert_eq!(
2156 value_receiver_rewrite("\"email.", 7).map(|(_, r)| r),
2157 Some(5),
2158 );
2159 }
2160
2161 #[test]
2162 fn sum_member_suggests_variants() {
2163 let doc = "commons m {\n type Color = enum { Red, Green, Blue }\n}\n";
2164 let items = complete(" let c = Color.", doc, None);
2165 for v in ["Red", "Green", "Blue"] {
2166 assert!(
2167 find(&items, v, CompletionKind::Variant).is_some(),
2168 "variant {v}: {:?}",
2169 items.iter().map(|c| &c.label).collect::<Vec<_>>()
2170 );
2171 }
2172 }
2173
2174 #[test]
2175 fn refined_and_plain_alias_members_are_of_and_unsafe() {
2176 let doc = "commons m {\n type Email = String where NonEmpty\n}\n";
2178 let items = complete(" Email.", doc, None);
2179 assert!(find(&items, "of", CompletionKind::Member).is_some());
2180 assert!(find(&items, "unsafe", CompletionKind::Member).is_some());
2181 let doc = "commons m {\n type Id = Int\n}\n";
2184 assert!(find(&complete(" Id.", doc, None), "of", CompletionKind::Member).is_some());
2185 }
2186
2187 #[test]
2188 fn capability_member_suggests_ops() {
2189 let doc = "context a.b\n capability Timer { fn now() -> Effect[Int]\n fn at(t: Int) -> Effect[()] }\n";
2190 let items = complete(" Timer.", doc, None);
2191 let now = find(&items, "now", CompletionKind::Member).expect("`now` op offered");
2192 assert_eq!(
2195 now.detail.as_deref(),
2196 Some("now() -> Effect[Int] — operation of `Timer`")
2197 );
2198 let at = find(&items, "at", CompletionKind::Member).expect("`at` op offered");
2199 assert_eq!(
2200 at.detail.as_deref(),
2201 Some("at(t: Int) -> Effect[()] — operation of `Timer`")
2202 );
2203 }
2204
2205 #[test]
2206 fn builtin_type_statics_are_offered() {
2207 assert!(
2208 find(
2209 &complete(" Int.", "context a.b\n", None),
2210 "parse",
2211 CompletionKind::Member
2212 )
2213 .is_some()
2214 );
2215 let j = complete(" Json.", "context a.b\n", None);
2216 assert!(find(&j, "encode", CompletionKind::Member).is_some());
2217 assert!(find(&j, "decode", CompletionKind::Member).is_some());
2218 }
2219
2220 #[test]
2221 fn builtin_sum_variants_are_complete() {
2222 let http: Vec<&str> = bynk_syntax::ast::HTTP_VARIANTS
2227 .iter()
2228 .map(|v| v.name)
2229 .collect();
2230 let queue: Vec<&str> = bynk_syntax::ast::QUEUE_VARIANTS
2231 .iter()
2232 .map(|v| v.name)
2233 .collect();
2234 for (recv, names) in [("HttpResult", http), ("QueueResult", queue)] {
2235 let items = complete(&format!(" {recv}."), "context a.b\n", None);
2236 for name in names {
2237 assert!(
2238 find(&items, name, CompletionKind::Variant).is_some(),
2239 "{recv}.{name} missing: {:?}",
2240 items.iter().map(|c| &c.label).collect::<Vec<_>>()
2241 );
2242 }
2243 }
2244 }
2245
2246 #[test]
2247 fn builtin_statics_are_reachable() {
2248 for &(recv, members) in BUILTIN_STATICS {
2252 let items = complete(&format!(" {recv}."), "context a.b\n", None);
2253 for &(member, _) in members {
2254 assert!(
2255 find(&items, member, CompletionKind::Member).is_some(),
2256 "{recv}.{member} unreachable: {:?}",
2257 items.iter().map(|c| &c.label).collect::<Vec<_>>()
2258 );
2259 }
2260 }
2261 for (recv, member) in [("List", "empty"), ("Map", "empty"), ("Effect", "pure")] {
2264 let items = complete(&format!(" {recv}."), "context a.b\n", None);
2265 assert!(
2266 find(&items, member, CompletionKind::Member).is_some(),
2267 "{recv}.{member} missing from the statics table"
2268 );
2269 }
2270 }
2271
2272 #[test]
2273 fn record_value_and_decimal_receivers_yield_nothing() {
2274 let doc = "commons m {\n type Point = { x: Int }\n}\n";
2276 assert!(complete(" Point.", doc, None).is_empty(), "record");
2277 assert!(complete(" let p = q.", doc, None).is_empty(), "value");
2279 assert!(complete(" let n = 1.", doc, None).is_empty(), "decimal");
2281 }
2282
2283 #[test]
2284 fn value_receiver_rewrite_drops_the_dot_for_lowercase_receivers() {
2285 let text = " let x = email.\n";
2286 let offset = text.find('.').unwrap() + 1; let (rewritten, recv) = value_receiver_rewrite(text, offset).expect("value receiver");
2288 assert_eq!(
2289 rewritten, " let x = email\n",
2290 "the trailing dot is dropped"
2291 );
2292 assert!(
2293 text.get(recv..=recv).is_some_and(|c| c == "l"),
2294 "the receiver offset lands inside `email`"
2295 );
2296 let text2 = " let x = email.ma\n";
2298 let off2 = text2.find(".ma").unwrap() + 3;
2299 assert_eq!(
2300 value_receiver_rewrite(text2, off2).map(|(r, _)| r),
2301 Some(" let x = email\n".to_string())
2302 );
2303 assert!(value_receiver_rewrite(" Email.", 8).is_none());
2305 assert!(value_receiver_rewrite(" let n = 1.", 12).is_none());
2306 assert!(value_receiver_rewrite(" email", 7).is_none());
2307 }
2308
2309 static TYS: std::sync::LazyLock<Types> = std::sync::LazyLock::new(Types::new);
2313
2314 #[test]
2315 fn value_member_candidates_lists_kernel_methods() {
2316 use bynk_syntax::ast::BaseType;
2317 let list = TYS.intern(Ty::List(TYS.intern(Ty::Base(BaseType::Int))));
2318 let items = value_member_candidates(list, &TYS, "context a.b\n", None);
2319 assert!(find(&items, "fold", CompletionKind::Member).is_some());
2320 assert!(find(&items, "get", CompletionKind::Member).is_some());
2321
2322 let string = TYS.intern(Ty::Base(BaseType::String));
2323 let items = value_member_candidates(string, &TYS, "context a.b\n", None);
2324 assert!(find(&items, "split", CompletionKind::Member).is_some());
2325 assert!(find(&items, "trim", CompletionKind::Member).is_some());
2326 }
2327
2328 #[test]
2329 fn value_member_candidates_lists_refined_inherited_kernel_methods() {
2330 use bynk_check::checker::NamedKind;
2333 use bynk_syntax::ast::BaseType;
2334 let name = TYS.intern(Ty::Named {
2335 name: "Name".to_string(),
2336 kind: NamedKind::Refined(BaseType::String),
2337 args: Vec::new(),
2338 });
2339 let items = value_member_candidates(
2340 name,
2341 &TYS,
2342 "commons m {\n type Name = String where NonEmpty\n}\n",
2343 None,
2344 );
2345 assert!(find(&items, "toUpper", CompletionKind::Member).is_some());
2346 assert!(find(&items, "length", CompletionKind::Member).is_some());
2347 }
2348
2349 #[test]
2350 fn expression_position_offers_locals() {
2351 assert!(is_expression_position(" let y = "));
2353 assert!(is_expression_position(" let y = a + lo")); assert!(is_expression_position(" f("));
2355 assert!(is_expression_position(" g(a, "));
2356 assert!(is_expression_position(" xs.fold(0, (acc, x) => ac")); assert!(is_expression_position(" let y = foo"));
2359 assert!(!is_expression_position(" let y: ")); assert!(!is_expression_position(" on call() -> ")); assert!(!is_expression_position(" tot")); }
2364
2365 #[test]
2366 fn value_member_candidates_lists_record_fields() {
2367 use bynk_check::checker::NamedKind;
2368 let order = TYS.intern(Ty::Named {
2369 name: "Order".to_string(),
2370 kind: NamedKind::Record,
2371 args: Vec::new(),
2372 });
2373 let doc = "commons m {\n type Order = { id: Int, total: Int }\n}\n";
2374 let items = value_member_candidates(order, &TYS, doc, None);
2375 assert!(
2376 find(&items, "id", CompletionKind::Field).is_some(),
2377 "{items:?}",
2378 items = items.iter().map(|c| &c.label).collect::<Vec<_>>()
2379 );
2380 assert!(find(&items, "total", CompletionKind::Field).is_some());
2381 }
2382
2383 #[test]
2384 fn store_field_member_candidates_offers_entry_ops_and_query_accessors() {
2385 let doc = "context shop\n\nagent Inventory {\n key id: String\n store items: Map[String, Int]\n\n on call f() -> Effect[()] {\n items.\n }\n}\n";
2390 let offset = doc.find("items.").unwrap() + "items.".len();
2391 let (rewritten, recv_offset) = value_receiver_rewrite(doc, offset).expect("bare receiver");
2392 let items = store_field_member_candidates(&rewritten, recv_offset, &[]);
2393 assert!(
2394 find(&items, "put", CompletionKind::Member).is_some(),
2395 "{items:?}",
2396 items = items.iter().map(|c| &c.label).collect::<Vec<_>>()
2397 );
2398 assert!(find(&items, "get", CompletionKind::Member).is_some());
2399 assert!(find(&items, "update", CompletionKind::Member).is_some());
2400 assert!(find(&items, "entries", CompletionKind::Field).is_some());
2401 assert!(find(&items, "keys", CompletionKind::Field).is_some());
2402 assert!(find(&items, "values", CompletionKind::Field).is_some());
2403 }
2404
2405 #[test]
2406 fn store_field_member_candidates_empty_for_ordinary_local() {
2407 let doc = "context shop\n\nagent Inventory {\n key id: String\n\n on call f() -> Effect[()] {\n let items = 1\n items.\n }\n}\n";
2410 let offset = doc.rfind("items.").unwrap() + "items.".len();
2411 let (rewritten, recv_offset) = value_receiver_rewrite(doc, offset).expect("bare receiver");
2412 let items = store_field_member_candidates(&rewritten, recv_offset, &[]);
2413 assert!(items.is_empty());
2414 }
2415
2416 #[test]
2417 fn store_field_member_candidates_skips_query_accessors_on_held_map() {
2418 let doc = "context shop\n\nagent Room {\n key id: String\n store conns: Map[String, Connection[String]]\n\n on call f() -> Effect[()] {\n conns.\n }\n}\n";
2421 let offset = doc.find("conns.").unwrap() + "conns.".len();
2422 let (rewritten, recv_offset) = value_receiver_rewrite(doc, offset).expect("bare receiver");
2423 let items = store_field_member_candidates(&rewritten, recv_offset, &[]);
2424 assert!(find(&items, "put", CompletionKind::Member).is_some());
2425 assert!(find(&items, "entries", CompletionKind::Field).is_none());
2426 assert!(find(&items, "keys", CompletionKind::Field).is_none());
2427 assert!(find(&items, "values", CompletionKind::Field).is_none());
2428 }
2429
2430 #[test]
2431 fn store_field_member_candidates_offers_set_and_cache_vocabularies() {
2432 let doc = "context shop\n\nagent Inventory {\n key id: String\n store tags: Set[String]\n\n on call f() -> Effect[()] {\n tags.\n }\n}\n";
2435 let offset = doc.find("tags.").unwrap() + "tags.".len();
2436 let (rewritten, recv_offset) = value_receiver_rewrite(doc, offset).expect("bare receiver");
2437 let items = store_field_member_candidates(&rewritten, recv_offset, &[]);
2438 assert!(find(&items, "add", CompletionKind::Member).is_some());
2439 assert!(find(&items, "entries", CompletionKind::Field).is_none());
2440 }
2441
2442 #[test]
2445 fn record_construction_offers_field_names() {
2446 let doc = "commons m {\n type Order = { id: Int, total: Int }\n}\n";
2447 let got = labels(" let o = Order { ", doc);
2448 assert!(got.contains(&"id".to_string()), "{got:?}");
2449 assert!(got.contains(&"total".to_string()), "{got:?}");
2450 let got2 = labels(" let o = Order { id: 1, ", doc);
2452 assert!(got2.contains(&"total".to_string()), "{got2:?}");
2453 assert!(record_construction_receiver(" let o = Order { id: ").is_none());
2455 assert!(record_construction_receiver(" if x { ").is_none());
2457 }
2458
2459 #[test]
2460 fn from_offers_protocols() {
2461 let got = labels(" service s from ", "context a.b\n");
2462 assert!(got.contains(&"http".to_string()), "{got:?}");
2463 assert!(got.contains(&"cron".to_string()) && got.contains(&"queue".to_string()));
2464 }
2465
2466 #[test]
2467 fn on_offers_handler_kinds() {
2468 let got = labels(" on ", "context a.b\n");
2469 assert!(got.contains(&"call".to_string()), "{got:?}");
2470 assert!(got.contains(&"GET".to_string()) && got.contains(&"schedule".to_string()));
2471 }
2472
2473 #[test]
2474 fn by_offers_project_actors() {
2475 let doc = "context a.b\n\nactor Caller { auth = Bearer }\n";
2476 let got = labels(" by ", doc);
2477 assert!(got.contains(&"Caller".to_string()), "{got:?}");
2478 }
2479
2480 #[test]
2481 fn exports_offers_export_kinds() {
2482 let got = labels(" exports ", "adapter t {\n binding \"./b.ts\"\n}\n");
2483 assert!(got.contains(&"capability".to_string()), "{got:?}");
2484 assert!(got.contains(&"transparent".to_string()));
2485 }
2486
2487 #[test]
2488 fn provides_offers_in_scope_capabilities() {
2489 let doc = "context a.b\n\ncapability Store { fn get() -> Effect[Int] }\n";
2490 let got = labels(" provides ", doc);
2491 assert!(got.contains(&"Store".to_string()), "{got:?}");
2492 }
2493
2494 #[test]
2495 fn where_offers_predicate_names() {
2496 let got = labels(" type Code = Int where ", "context a.b\n");
2498 assert!(got.contains(&"InRange".to_string()), "{got:?}");
2499 assert!(got.contains(&"NonNegative".to_string()));
2500 let got = labels(" _ where ", "context a.b\n");
2502 assert!(got.contains(&"Matches".to_string()), "{got:?}");
2503 assert!(got.contains(&"NonEmpty".to_string()));
2504 }
2505
2506 #[test]
2507 fn is_for_all_where_matches_only_the_for_all_binder() {
2508 assert!(is_for_all_where("\tfor all x: Int, y: Int where "));
2509 assert!(is_for_all_where("for all x: Int where "));
2510 assert!(!is_for_all_where("format(x) where "));
2513 assert!(!is_for_all_where("for allocate x where "));
2514 assert!(!is_for_all_where(" type Code = Int where "));
2516 assert!(!is_for_all_where(" _ where "));
2517 assert!(!is_for_all_where("no where here"));
2518 }
2519
2520 #[test]
2521 fn for_all_where_falls_through_to_expression_position_not_predicates() {
2522 let got = labels("\tfor all x: Int where ", "context a.b\n");
2526 assert!(!got.contains(&"InRange".to_string()), "{got:?}");
2527 assert!(!got.contains(&"NonNegative".to_string()), "{got:?}");
2528 }
2529
2530 #[test]
2531 fn clause_detectors_do_not_over_fire() {
2532 assert!(!after_clause_keyword(" session ", "on"));
2534 assert!(!after_clause_keyword(" let from = ", "from"));
2535 assert!(after_clause_keyword(" service s from ", "from"));
2537 assert!(after_clause_keyword(" by ", "by"));
2538 }
2539
2540 #[test]
2541 fn contract_clause_kind_detects_requires_and_ensures() {
2542 assert_eq!(contract_clause_kind(" requires positive: "), Some(false));
2543 assert_eq!(contract_clause_kind(" ensures never_neg: "), Some(true));
2544 assert_eq!(contract_clause_kind(" id: Int"), None);
2546 assert_eq!(contract_clause_kind(" let x = 1"), None);
2547 }
2548
2549 #[test]
2550 fn cors_field_position_inside_cors_block() {
2551 let doc = "service api from http {\n cors {\n ";
2553 assert!(in_cors_field_position(doc, doc.len()));
2554 let doc2 = "service api from http {\n cors {\n origins: ";
2556 assert!(!in_cors_field_position(doc2, doc2.len()));
2557 let doc3 = "let x = Order {\n ";
2559 assert!(!in_cors_field_position(doc3, doc3.len()));
2560 }
2561
2562 #[test]
2563 fn security_field_position_inside_security_block() {
2564 let doc = "service api from http {\n security {\n ";
2566 assert!(in_security_field_position(doc, doc.len()));
2567 let doc2 = "service api from http {\n security {\n hsts: ";
2569 assert!(!in_security_field_position(doc2, doc2.len()));
2570 let doc3 = "service api from http {\n cors {\n ";
2572 assert!(!in_security_field_position(doc3, doc3.len()));
2573 }
2574
2575 #[test]
2579 fn field_position_probes_survive_non_ascii() {
2580 let doc = "service api from http {\n cors { -- café\n ";
2581 assert!(in_cors_field_position(doc, doc.len()));
2582 let mid = doc.find('é').unwrap() + 1;
2584 assert!(!doc.is_char_boundary(mid));
2585 let _ = in_cors_field_position(doc, mid);
2586 let _ = in_security_field_position(doc, mid);
2587 let _ = in_limits_field_position(doc, mid);
2588 }
2589
2590 #[test]
2591 fn limits_field_position_inside_limits_block() {
2592 let doc = "service api from http {\n limits {\n ";
2594 assert!(in_limits_field_position(doc, doc.len()));
2595 let doc2 = "service api from http {\n limits {\n maxBody: ";
2597 assert!(!in_limits_field_position(doc2, doc2.len()));
2598 let doc3 = "service api from http {\n security {\n ";
2600 assert!(!in_limits_field_position(doc3, doc3.len()));
2601 }
2602
2603 #[test]
2604 fn service_body_item_position_offers_cors() {
2605 let doc = "service api from http {\n ";
2607 assert!(in_service_body_item_position(doc, doc.len(), " "));
2608 let doc2 = "service api from http {\n cors {\n ";
2611 assert!(!in_service_body_item_position(doc2, doc2.len(), " "));
2612 }
2613
2614 #[test]
2615 fn cache_arg_position_inside_cache_annotation() {
2616 let doc = "service api from http {\n @cache(";
2618 assert!(in_cache_arg_position(doc, doc.len()));
2619 let doc2 = "service api from http {\n @cache(maxAge: 5.minutes, ";
2621 assert!(in_cache_arg_position(doc2, doc2.len()));
2622 let doc3 = "service api from http {\n @cache(maxAge: ";
2624 assert!(!in_cache_arg_position(doc3, doc3.len()));
2625 let doc4 = "let x = cache(";
2627 assert!(!in_cache_arg_position(doc4, doc4.len()));
2628 let doc5 = "on GET(\"/x\") by v: Visitor (";
2630 assert!(!in_cache_arg_position(doc5, doc5.len()));
2631 }
2632
2633 #[test]
2634 fn limit_arg_position_inside_limit_annotation() {
2635 let doc = "service api from http {\n @limit(";
2637 assert!(in_limit_arg_position(doc, doc.len()));
2638 let doc2 = "service api from http {\n @limit(maxBody: 1048576, ";
2640 assert!(in_limit_arg_position(doc2, doc2.len()));
2641 let doc3 = "service api from http {\n @limit(maxBody: ";
2643 assert!(!in_limit_arg_position(doc3, doc3.len()));
2644 let doc4 = "let x = limit(";
2646 assert!(!in_limit_arg_position(doc4, doc4.len()));
2647 let doc5 = "on GET(\"/x\") by v: Visitor (";
2649 assert!(!in_limit_arg_position(doc5, doc5.len()));
2650 }
2651
2652 #[test]
2653 fn sum_type_variants_lists_variants() {
2654 let doc = "commons m {\n type Status = enum { Pending, Shipped }\n}\n";
2655 let got: Vec<String> = sum_type_variants("Status", doc, None)
2656 .into_iter()
2657 .map(|c| c.label)
2658 .collect();
2659 assert!(got.contains(&"Pending".to_string()), "{got:?}");
2660 assert!(got.contains(&"Shipped".to_string()), "{got:?}");
2661 }
2662
2663 #[test]
2664 fn variants_for_ty_offers_built_in_result_and_option() {
2665 use bynk_syntax::ast::BaseType;
2670 let result = TYS.intern(Ty::Result(
2671 TYS.intern(Ty::Base(BaseType::Int)),
2672 TYS.intern(Ty::Base(BaseType::String)),
2673 ));
2674 let got: Vec<String> = variants_for_ty(result, &TYS, "", None)
2675 .into_iter()
2676 .map(|c| c.label)
2677 .collect();
2678 assert_eq!(got, vec!["Ok".to_string(), "Err".to_string()]);
2679
2680 let option = TYS.intern(Ty::Option(TYS.intern(Ty::Base(BaseType::Int))));
2681 let got: Vec<String> = variants_for_ty(option, &TYS, "", None)
2682 .into_iter()
2683 .map(|c| c.label)
2684 .collect();
2685 assert_eq!(got, vec!["Some".to_string(), "None".to_string()]);
2686 }
2687
2688 #[test]
2689 fn nested_variant_completions_resolves_the_payload_type() {
2690 use bynk_check::checker::NamedKind;
2691 use bynk_syntax::ast::BaseType;
2692 let payload = TYS.intern(Ty::Result(
2695 TYS.intern(Ty::Base(BaseType::Int)),
2696 TYS.intern(Ty::Named {
2697 name: "E".to_string(),
2698 kind: NamedKind::Sum,
2699 args: Vec::new(),
2700 }),
2701 ));
2702 let scrut = TYS.intern(Ty::Option(payload));
2703 let got: Vec<String> = nested_variant_completions(scrut, &TYS, "Some", "", None)
2704 .into_iter()
2705 .map(|c| c.label)
2706 .collect();
2707 assert_eq!(got, vec!["Ok".to_string(), "Err".to_string()]);
2708
2709 let opt_int = TYS.intern(Ty::Option(TYS.intern(Ty::Base(BaseType::Int))));
2711 let inner = TYS.intern(Ty::Result(TYS.intern(Ty::Base(BaseType::Int)), opt_int));
2712 let got: Vec<String> = nested_variant_completions(inner, &TYS, "Err", "", None)
2713 .into_iter()
2714 .map(|c| c.label)
2715 .collect();
2716 assert_eq!(got, vec!["Some".to_string(), "None".to_string()]);
2717
2718 let doc = "commons m {\n type Inner = enum { A, B }\n \
2721 type Outer = | Wrap(inner: Inner) | Bare\n}\n";
2722 let outer = TYS.intern(Ty::Named {
2723 name: "Outer".to_string(),
2724 kind: NamedKind::Sum,
2725 args: Vec::new(),
2726 });
2727 let got: Vec<String> = nested_variant_completions(outer, &TYS, "Wrap", doc, None)
2728 .into_iter()
2729 .map(|c| c.label)
2730 .collect();
2731 assert!(got.contains(&"A".to_string()), "{got:?}");
2732 assert!(got.contains(&"B".to_string()), "{got:?}");
2733 }
2734
2735 #[test]
2736 fn literal_kind_scrutinee_suggests_no_variants() {
2737 let doc = "commons m {\n type Quantity = Int where InRange(1, 99)\n}\n";
2743 assert!(sum_type_variants("Quantity", doc, None).is_empty());
2744 assert!(sum_type_variants("Int", doc, None).is_empty());
2745 }
2746
2747 fn enumerated_units(doc_text: &str, files: Option<&HashMap<PathBuf, String>>) -> Vec<String> {
2751 let mut names = Vec::new();
2752 for_each_unit(doc_text, files, |u| names.push(u.name().joined()));
2753 names
2754 }
2755
2756 fn synthetic_path(name: &str) -> PathBuf {
2762 PathBuf::from(format!("/synthetic/{name}.bynk"))
2763 }
2764
2765 #[test]
2766 fn for_each_unit_yields_embedded_buffer_and_project_files() {
2767 let sibling = synthetic_path("sibling");
2768 let files = HashMap::from([(
2769 sibling,
2770 "commons proj.sibling {\n fn s() -> Int { 1 }\n}\n".to_string(),
2771 )]);
2772
2773 let names = enumerated_units(
2774 "commons proj.buffer {\n fn b() -> Int { 1 }\n}\n",
2775 Some(&files),
2776 );
2777 assert!(names.iter().any(|n| n == "bynk"), "embedded: {names:?}");
2779 assert!(
2780 names.iter().any(|n| n == "proj.buffer"),
2781 "buffer: {names:?}"
2782 );
2783 assert!(
2784 names.iter().any(|n| n == "proj.sibling"),
2785 "project file: {names:?}"
2786 );
2787 }
2788
2789 #[test]
2796 fn same_named_declarations_do_not_union_across_units() {
2797 let stale = HashMap::from([(
2798 synthetic_path("stale"),
2799 "commons m {\n \
2800 type Rec = { total: Int, old_field: Int }\n \
2801 type Status = enum { Pending, Retired }\n\
2802 }\n"
2803 .to_string(),
2804 )]);
2805
2806 let buffer = "commons m {\n \
2807 type Rec = { total: Int }\n \
2808 type Status = enum { Pending, Shipped }\n\
2809 }\n";
2810 let files = Some(&stale);
2811
2812 let fields: Vec<String> = record_field_names("Rec", buffer, files)
2813 .into_iter()
2814 .map(|c| c.label)
2815 .collect();
2816 assert_eq!(fields, vec!["total".to_string()], "{fields:?}");
2817
2818 let variants: Vec<String> = sum_type_variants("Status", buffer, files)
2819 .into_iter()
2820 .map(|c| c.label)
2821 .collect();
2822 assert_eq!(
2823 variants,
2824 vec!["Pending".to_string(), "Shipped".to_string()],
2825 "{variants:?}"
2826 );
2827 }
2828
2829 #[test]
2834 fn capabilities_of_unit_does_not_union_across_units() {
2835 let stale = HashMap::from([(
2836 synthetic_path("stale"),
2837 "adapter tokens {\n \
2838 exports capability { Jwt, Retired }\n\
2839 }\n"
2840 .to_string(),
2841 )]);
2842
2843 let buffer = "adapter tokens {\n exports capability { Jwt }\n}\n";
2844 let files = Some(&stale);
2845
2846 let caps = capabilities_of_unit("tokens", buffer, files);
2847 assert_eq!(caps, vec!["Jwt".to_string()], "{caps:?}");
2848 }
2849
2850 #[test]
2851 fn project_unit_cache_invalidates_on_change() {
2852 let path = synthetic_path("unit");
2853
2854 let first_files = HashMap::from([(
2855 path.clone(),
2856 "commons proj.first {\n fn a() -> Int { 1 }\n}\n".to_string(),
2857 )]);
2858 let first = enumerated_units("context a.b\n", Some(&first_files));
2859 assert!(
2860 first.iter().any(|n| n == "proj.first"),
2861 "first read: {first:?}"
2862 );
2863
2864 let second_files = HashMap::from([(
2869 path,
2870 "commons proj.second.longer {\n fn a() -> Int { 1 }\n fn c() -> Int { 3 }\n}\n"
2871 .to_string(),
2872 )]);
2873 let second = enumerated_units("context a.b\n", Some(&second_files));
2874 assert!(
2875 second.iter().any(|n| n == "proj.second.longer"),
2876 "after change: {second:?}"
2877 );
2878 assert!(
2879 !second.iter().any(|n| n == "proj.first"),
2880 "stale served: {second:?}"
2881 );
2882 }
2883
2884 #[test]
2885 fn unparseable_project_file_is_skipped() {
2886 let files = HashMap::from([(synthetic_path("broken"), "not bynk source {{{".to_string())]);
2893 let names = enumerated_units("context a.b\n", Some(&files));
2894 let baseline = enumerated_units("context a.b\n", None);
2895 assert_eq!(names, baseline, "{names:?}");
2901 }
2902}