1use std::collections::{BTreeSet, HashMap};
41use std::path::{Path, PathBuf};
42use std::sync::{Arc, LazyLock};
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 whole-second `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
1504fn cached_project_unit(path: &Path, content: &str) -> Option<Arc<SourceUnit>> {
1525 let (_file_id, result) = bynk_project::parse_cache::cached_parse(path, content);
1526 match result {
1527 Ok((units, _warnings)) => units.first().cloned().map(Arc::new),
1528 Err(_) => parse_source_unit(content).map(Arc::new),
1529 }
1530}
1531
1532pub fn for_each_unit(
1548 doc_text: &str,
1549 files: Option<&HashMap<PathBuf, String>>,
1550 mut f: impl FnMut(&SourceUnit),
1551) {
1552 for unit in EMBEDDED_UNITS.iter() {
1553 f(unit);
1554 }
1555 if let Some(unit) = parse_source_unit(doc_text) {
1556 f(&unit);
1557 }
1558 if let Some(content) = files {
1563 for (path, text) in content {
1564 if let Some(unit) = cached_project_unit(path, text) {
1565 f(&unit);
1566 }
1567 }
1568 }
1569}
1570
1571fn consumable_units(doc_text: &str, files: Option<&HashMap<PathBuf, String>>) -> Vec<Completion> {
1573 let mut seen: BTreeSet<String> = BTreeSet::new();
1574 let mut out: Vec<Completion> = Vec::new();
1575 for_each_unit(doc_text, files, |unit| {
1576 let (name, kind) = match unit {
1577 SourceUnit::Context(c) => (c.name.joined(), "context"),
1578 SourceUnit::Adapter(a) => (a.name.joined(), "adapter"),
1579 _ => return,
1580 };
1581 if seen.insert(name.clone()) {
1582 out.push(Completion::item(
1583 name,
1584 CompletionKind::Unit,
1585 Some(kind.to_string()),
1586 ));
1587 }
1588 });
1589 out
1590}
1591
1592fn capabilities_of_unit(
1598 unit: &str,
1599 doc_text: &str,
1600 files: Option<&HashMap<PathBuf, String>>,
1601) -> Vec<String> {
1602 let mut out: BTreeSet<String> = BTreeSet::new();
1603 let mut found = false;
1604 for_each_unit(doc_text, files, |u| {
1605 if found {
1606 return;
1607 }
1608 let (name, exports) = match u {
1609 SourceUnit::Context(c) => (c.name.joined(), &c.exports),
1610 SourceUnit::Adapter(a) => (a.name.joined(), &a.exports),
1611 _ => return,
1612 };
1613 if name != unit {
1614 return;
1615 }
1616 found = true;
1617 for clause in exports {
1618 if clause.kind == ExportKind::Capability {
1619 for n in clause.names.iter().map(|e| &e.name) {
1620 out.insert(n.name.clone());
1621 }
1622 }
1623 }
1624 });
1625 out.into_iter().collect()
1626}
1627
1628fn in_scope_capabilities(
1632 doc_text: &str,
1633 files: Option<&HashMap<PathBuf, String>>,
1634) -> Vec<Completion> {
1635 let mut labels: BTreeSet<String> = BTreeSet::new();
1636 let Ok(tokens) = lexer::tokenize(doc_text) else {
1637 return Vec::new();
1638 };
1639 let (Some(unit), _errs) = parser::parse_unit_with_recovery(&tokens, doc_text) else {
1640 return Vec::new();
1641 };
1642 let (items, consumes) = match &unit {
1643 SourceUnit::Context(c) => (&c.items, &c.consumes),
1644 SourceUnit::Adapter(a) => (&a.items, &EMPTY_CONSUMES),
1645 _ => return Vec::new(),
1646 };
1647 for item in items {
1649 if let bynk_syntax::ast::CommonsItem::Capability(c) = item {
1650 labels.insert(c.name.name.clone());
1651 }
1652 }
1653 for c in consumes {
1655 let unit_name = c.target.joined();
1656 match &c.selected {
1657 Some(names) => {
1658 for n in names {
1659 labels.insert(n.name.clone());
1660 }
1661 }
1662 None => {
1663 let prefix = c
1664 .alias
1665 .as_ref()
1666 .map(|a| a.name.clone())
1667 .unwrap_or_else(|| unit_name.clone());
1668 for cap in capabilities_of_unit(&unit_name, doc_text, files) {
1669 labels.insert(format!("{prefix}.{cap}"));
1670 }
1671 }
1672 }
1673 }
1674 labels
1675 .into_iter()
1676 .map(|label| {
1677 Completion::item(
1678 label,
1679 CompletionKind::Capability,
1680 Some("capability in scope".to_string()),
1681 )
1682 })
1683 .collect()
1684}
1685
1686pub fn value_receiver_rewrite(text: &str, offset: usize) -> Option<(String, usize)> {
1698 let prefix = text.get(..offset)?;
1699 let head = prefix
1700 .trim_end_matches(|c: char| c.is_alphanumeric() || c == '_')
1701 .strip_suffix('.')?;
1702 let (recv, start) = ident_ending_at(head, head.len())?;
1703 let first = recv.chars().next()?;
1704 if !(first.is_ascii_lowercase() || first == '_') {
1705 return None; }
1707 if head[..start].ends_with('.') {
1708 return None; }
1710 let dot = head.len(); let rewritten = format!("{}{}", &text[..dot], &text[offset..]);
1712 Some((rewritten, dot.saturating_sub(1)))
1713}
1714
1715pub fn ident_ending_at(text: &str, end: usize) -> Option<(&str, usize)> {
1726 let before = text.get(..end)?;
1727 let start = before
1728 .char_indices()
1729 .rfind(|&(_, c)| !(c.is_alphanumeric() || c == '_'))
1730 .map_or(0, |(i, c)| i + c.len_utf8());
1731 let name = &before[start..];
1732 (!name.is_empty()).then_some((name, start))
1733}
1734
1735pub fn value_member_candidates(
1738 ty: TyId,
1739 tys: &Types,
1740 doc_text: &str,
1741 files: Option<&HashMap<PathBuf, String>>,
1742) -> Vec<Completion> {
1743 let mut out: Vec<Completion> = kernel_methods::methods_for(ty, tys)
1744 .iter()
1745 .map(|km| {
1746 Completion::item(
1747 km.name,
1748 CompletionKind::Member,
1749 Some(km.signature.to_string()),
1750 )
1751 })
1752 .collect();
1753 if let Ty::Named { name, .. } = &*tys.get(ty) {
1755 let mut seen: BTreeSet<String> = BTreeSet::new();
1756 for_each_unit(doc_text, files, |unit| {
1757 let items = match unit {
1758 SourceUnit::Commons(c) => &c.items,
1759 SourceUnit::Context(c) => &c.items,
1760 SourceUnit::Adapter(a) => &a.items,
1761 _ => return,
1762 };
1763 for item in items {
1764 if let CommonsItem::Type(t) = item
1765 && &t.name.name == name
1766 && let TypeBody::Record(r) = &t.body
1767 {
1768 for f in &r.fields {
1769 if seen.insert(f.name.name.clone()) {
1770 out.push(Completion::item(
1771 f.name.name.clone(),
1772 CompletionKind::Field,
1773 Some(format!("field of `{name}`")),
1774 ));
1775 }
1776 }
1777 }
1778 }
1779 });
1780 }
1781 out
1782}
1783
1784pub fn store_field_member_candidates(
1799 rewritten: &str,
1800 recv_offset: usize,
1801 locals: &[LocalBinding],
1802) -> Vec<Completion> {
1803 let Some((kind_head, held)) =
1804 crate::symbols::store_field_kind_at(rewritten, recv_offset + 1, locals)
1805 else {
1806 return Vec::new();
1807 };
1808 let mut out: Vec<Completion> = store_ops::ops_for(&kind_head)
1809 .iter()
1810 .map(|o| {
1811 Completion::item(
1812 o.name,
1813 CompletionKind::Member,
1814 Some(o.signature.to_string()),
1815 )
1816 })
1817 .collect();
1818 if kind_head == "Map" && !held {
1819 out.extend(store_ops::MAP_QUERY_ACCESSORS.iter().map(|a| {
1820 Completion::item(a.name, CompletionKind::Field, Some(a.signature.to_string()))
1821 }));
1822 }
1823 out
1824}
1825
1826static EMPTY_CONSUMES: Vec<bynk_syntax::ast::ConsumesDecl> = Vec::new();
1827
1828#[cfg(test)]
1829mod tests {
1830 use super::*;
1831 use bynk_check::firstparty::BYNK_LIST_SRC;
1832
1833 fn labels(line: &str, doc: &str) -> Vec<String> {
1834 complete(line, doc, None)
1835 .into_iter()
1836 .map(|c| c.label)
1837 .collect()
1838 }
1839
1840 #[test]
1841 fn consumes_target_suggests_units_including_bynk() {
1842 let doc = "adapter tokens {\n binding \"./b.ts\"\n capability Jwt { fn f() -> Effect[Int] }\n provides Jwt = X\n}\n";
1844 let got = labels(" consumes ", doc);
1845 assert!(got.contains(&"bynk".to_string()), "{got:?}");
1846 assert!(got.contains(&"tokens".to_string()), "{got:?}");
1847 }
1848
1849 #[test]
1850 fn consumes_brace_suggests_that_units_capabilities() {
1851 let got = labels(" consumes bynk { ", "context a.b\n");
1852 assert!(got.contains(&"Clock".to_string()), "{got:?}");
1854 assert!(got.contains(&"Random".to_string()), "{got:?}");
1855 assert!(got.contains(&"Logger".to_string()), "{got:?}");
1856 }
1857
1858 #[test]
1859 fn given_suggests_local_and_flattened_capabilities() {
1860 let doc = "context a.b\n\
1861 consumes bynk { Clock }\n\
1862 capability Local { fn f() -> Effect[Int] }\n\
1863 service s {\n\
1864 on call() -> Effect[Int] given Clock {\n\
1865 1\n\
1866 }\n\
1867 }\n";
1868 let got = labels(" on call() -> Effect[Int] given ", doc);
1869 assert!(got.contains(&"Clock".to_string()), "flattened: {got:?}");
1870 assert!(got.contains(&"Local".to_string()), "local: {got:?}");
1871 }
1872
1873 #[test]
1874 fn expression_position_offers_constructors_and_types() {
1875 let doc = "commons m {\n type Order = { id: Int }\n}\n";
1880 let items = complete(" let x = ", doc, None);
1881 for &c in CONSTRUCTORS {
1882 assert!(
1883 find(&items, c, CompletionKind::Constructor).is_some(),
1884 "constructor {c}: {:?}",
1885 items.iter().map(|i| &i.label).collect::<Vec<_>>()
1886 );
1887 }
1888 assert!(
1889 find(&items, "Int", CompletionKind::Type).is_some(),
1890 "builtin type"
1891 );
1892 assert!(
1893 find(&items, "Order", CompletionKind::Type).is_some(),
1894 "project type"
1895 );
1896 }
1897
1898 #[test]
1899 fn value_receiver_and_decimal_are_not_expression_positions() {
1900 assert!(complete(" let p = q.", "context a.b\n", None).is_empty());
1904 assert!(complete(" let n = 1.", "context a.b\n", None).is_empty());
1905 }
1906
1907 fn free_fn_names(src: &str) -> Vec<String> {
1909 let tokens = lexer::tokenize(src).unwrap();
1910 let (unit, _) = parser::parse_unit_with_recovery(&tokens, src);
1911 let unit = unit.unwrap();
1912 let (items, _) = unit_items_and_uses(&unit);
1913 items
1914 .iter()
1915 .filter_map(|it| match it {
1916 CommonsItem::Fn(f) => match &f.name {
1917 FnName::Free(id) => Some(id.name.clone()),
1918 FnName::Method { .. } => None,
1919 },
1920 _ => None,
1921 })
1922 .collect()
1923 }
1924
1925 #[test]
1926 fn free_functions_offered_for_own_unit_and_used_modules() {
1927 let doc = "commons app {\n uses bynk.list\n fn helper(x: Int) -> Int { x }\n}\n";
1930 let items = complete(" let y = ", doc, None);
1931 assert!(
1933 find(&items, "helper", CompletionKind::Function).is_some(),
1934 "own fn: {:?}",
1935 items.iter().map(|i| &i.label).collect::<Vec<_>>()
1936 );
1937 for name in free_fn_names(BYNK_LIST_SRC) {
1940 assert!(
1941 find(&items, &name, CompletionKind::Function).is_some(),
1942 "bynk.list.{name}: {:?}",
1943 items.iter().map(|i| &i.label).collect::<Vec<_>>()
1944 );
1945 }
1946 assert!(
1948 find(&items, "values", CompletionKind::Function).is_none(),
1949 "bynk.map.values leaked without `uses bynk.map`"
1950 );
1951 }
1952
1953 #[test]
1954 fn locale_functions_offered_when_bynk_locale_is_used() {
1955 let doc = "commons app {\n uses bynk.locale\n}\n";
1960 let items = complete(" let y = ", doc, None);
1961 for name in [
1962 "render",
1963 "message",
1964 "withText",
1965 "withWhole",
1966 "withNum",
1967 "withMoment",
1968 ] {
1969 assert!(
1970 find(&items, name, CompletionKind::Function).is_some(),
1971 "bynk.locale.{name} missing from completion: {:?}",
1972 items.iter().map(|i| &i.label).collect::<Vec<_>>()
1973 );
1974 }
1975 }
1976
1977 #[test]
1978 fn free_functions_require_a_uses_import() {
1979 let doc = "commons app {\n fn helper(x: Int) -> Int { x }\n}\n";
1981 let items = complete(" let y = ", doc, None);
1982 assert!(find(&items, "helper", CompletionKind::Function).is_some());
1983 for name in ["map", "filter", "reverse"] {
1984 assert!(
1985 find(&items, name, CompletionKind::Function).is_none(),
1986 "bynk.list.{name} offered without `uses bynk.list`"
1987 );
1988 }
1989 }
1990
1991 #[test]
1992 fn member_completion_reaches_inside_an_interpolation_hole() {
1993 let doc = "context a.b\n capability Timer { fn now() -> Effect[Int] }\n";
1997 let in_hole = complete(" \"the time is \\(Timer.", doc, None);
1998 assert!(
1999 find(&in_hole, "now", CompletionKind::Member).is_some(),
2000 "capability op not offered inside a hole: {:?}",
2001 in_hole.iter().map(|c| &c.label).collect::<Vec<_>>()
2002 );
2003 let statics = complete(" \"n=\\(Int.", "context a.b\n", None);
2005 assert!(find(&statics, "parse", CompletionKind::Member).is_some());
2006 }
2007
2008 #[test]
2009 fn consumes_with_as_is_not_a_target_completion() {
2010 assert!(!is_consumes_target("consumes platform.time as "));
2012 assert!(is_consumes_target("consumes platform"));
2013 }
2014
2015 fn find<'a>(
2016 items: &'a [Completion],
2017 label: &str,
2018 kind: CompletionKind,
2019 ) -> Option<&'a Completion> {
2020 items.iter().find(|c| c.label == label && c.kind == kind)
2021 }
2022
2023 #[test]
2024 fn type_annotation_suggests_builtins_surface_and_project_types() {
2025 let doc = "commons m {\n type Order = { id: Int }\n}\n";
2026 let got = labels(" let x: ", doc);
2027 for want in ["Int", "Option", "Result", "Effect", "List", "Map"] {
2030 assert!(got.contains(&want.to_string()), "built-in {want}: {got:?}");
2031 }
2032 assert!(got.contains(&"Uuid".to_string()), "surface: {got:?}");
2033 assert!(got.contains(&"Order".to_string()), "project: {got:?}");
2034 }
2035
2036 #[test]
2037 fn return_type_and_type_args_are_type_positions() {
2038 assert!(is_type_position(" on call() -> "));
2039 assert!(is_type_position(" let x: Option["));
2040 assert!(is_type_position(" let x: Result[Int, "));
2041 assert!(is_type_position(" -> Eff"));
2043 }
2044
2045 #[test]
2046 fn list_literal_is_not_a_type_position() {
2047 assert!(!is_type_position(" let xs = ["));
2049 let items = complete(" let xs = [", "context a.b\n", None);
2053 assert!(
2054 find(&items, "Some", CompletionKind::Constructor).is_some(),
2055 "{:?}",
2056 items.iter().map(|c| &c.label).collect::<Vec<_>>()
2057 );
2058 }
2059
2060 #[test]
2061 fn builtin_type_carries_its_registry_doc() {
2062 let items = complete(" let x: ", "context a.b\n", None);
2063 let int = find(&items, "Int", CompletionKind::Type).expect("Int present");
2064 assert_eq!(int.detail.as_deref(), keyword_doc("Int"));
2065 assert!(int.detail.is_some(), "Int should have a doc");
2066 }
2067
2068 #[test]
2069 fn keyword_position_suggests_keywords_and_snippets() {
2070 let items = complete(" ", "context a.b\n", None);
2071 assert!(find(&items, "capability", CompletionKind::Keyword).is_some());
2073 assert!(find(&items, "fn", CompletionKind::Keyword).is_some());
2074 assert!(find(&items, "let", CompletionKind::Keyword).is_some());
2075 assert!(find(&items, "Int", CompletionKind::Keyword).is_none());
2077 assert!(find(&items, "Some", CompletionKind::Keyword).is_none());
2078 let snip = find(&items, "service", CompletionKind::Snippet).expect("service snippet");
2080 let body = snip.insert_text.as_deref().unwrap_or("");
2081 assert!(body.contains("on call"), "snippet body: {body:?}");
2082 assert!(body.contains("${1"), "snippet tab stop: {body:?}");
2083 }
2084
2085 #[test]
2086 fn keyword_position_fires_on_an_empty_line() {
2087 assert!(is_keyword_position(""));
2088 assert!(is_keyword_position(" cap"));
2089 assert!(!is_keyword_position(" let x ="));
2090 assert!(!is_keyword_position(" x: "));
2091 assert!(!complete("", "context a.b\n", None).is_empty());
2092 }
2093
2094 #[test]
2095 fn member_receiver_is_a_single_upper_ident_before_a_dot() {
2096 assert_eq!(member_receiver(" Color."), Some("Color".to_string()));
2097 assert_eq!(
2098 member_receiver(" let e = Email.o"),
2099 Some("Email".to_string())
2100 );
2101 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); }
2106
2107 #[test]
2108 fn receiver_extraction_survives_a_multibyte_char_before_the_receiver() {
2109 assert_eq!(member_receiver("\"Foo."), Some("Foo".to_string()));
2113 assert_eq!(member_receiver("€Color."), Some("Color".to_string()));
2114 assert_eq!(member_receiver("—Bar."), Some("Bar".to_string()));
2115 assert_eq!(word_before_brace("\"cors {", 6), "cors");
2116 assert_eq!(
2117 record_construction_receiver("\"€Order {"),
2118 Some("Order".to_string())
2119 );
2120 let _ = complete("let x = \"Foo.", "commons m {}\n", None);
2123 let _ = complete(" \"€42.", "commons m {}\n", None);
2124 assert_eq!(
2125 value_receiver_rewrite("\"email.", 7).map(|(_, r)| r),
2126 Some(5),
2127 );
2128 }
2129
2130 #[test]
2131 fn sum_member_suggests_variants() {
2132 let doc = "commons m {\n type Color = enum { Red, Green, Blue }\n}\n";
2133 let items = complete(" let c = Color.", doc, None);
2134 for v in ["Red", "Green", "Blue"] {
2135 assert!(
2136 find(&items, v, CompletionKind::Variant).is_some(),
2137 "variant {v}: {:?}",
2138 items.iter().map(|c| &c.label).collect::<Vec<_>>()
2139 );
2140 }
2141 }
2142
2143 #[test]
2144 fn refined_and_plain_alias_members_are_of_and_unsafe() {
2145 let doc = "commons m {\n type Email = String where NonEmpty\n}\n";
2147 let items = complete(" Email.", doc, None);
2148 assert!(find(&items, "of", CompletionKind::Member).is_some());
2149 assert!(find(&items, "unsafe", CompletionKind::Member).is_some());
2150 let doc = "commons m {\n type Id = Int\n}\n";
2153 assert!(find(&complete(" Id.", doc, None), "of", CompletionKind::Member).is_some());
2154 }
2155
2156 #[test]
2157 fn capability_member_suggests_ops() {
2158 let doc = "context a.b\n capability Timer { fn now() -> Effect[Int]\n fn at(t: Int) -> Effect[()] }\n";
2159 let items = complete(" Timer.", doc, None);
2160 let now = find(&items, "now", CompletionKind::Member).expect("`now` op offered");
2161 assert_eq!(
2164 now.detail.as_deref(),
2165 Some("now() -> Effect[Int] — operation of `Timer`")
2166 );
2167 let at = find(&items, "at", CompletionKind::Member).expect("`at` op offered");
2168 assert_eq!(
2169 at.detail.as_deref(),
2170 Some("at(t: Int) -> Effect[()] — operation of `Timer`")
2171 );
2172 }
2173
2174 #[test]
2175 fn builtin_type_statics_are_offered() {
2176 assert!(
2177 find(
2178 &complete(" Int.", "context a.b\n", None),
2179 "parse",
2180 CompletionKind::Member
2181 )
2182 .is_some()
2183 );
2184 let j = complete(" Json.", "context a.b\n", None);
2185 assert!(find(&j, "encode", CompletionKind::Member).is_some());
2186 assert!(find(&j, "decode", CompletionKind::Member).is_some());
2187 }
2188
2189 #[test]
2190 fn builtin_sum_variants_are_complete() {
2191 let http: Vec<&str> = bynk_syntax::ast::HTTP_VARIANTS
2196 .iter()
2197 .map(|v| v.name)
2198 .collect();
2199 let queue: Vec<&str> = bynk_syntax::ast::QUEUE_VARIANTS
2200 .iter()
2201 .map(|v| v.name)
2202 .collect();
2203 for (recv, names) in [("HttpResult", http), ("QueueResult", queue)] {
2204 let items = complete(&format!(" {recv}."), "context a.b\n", None);
2205 for name in names {
2206 assert!(
2207 find(&items, name, CompletionKind::Variant).is_some(),
2208 "{recv}.{name} missing: {:?}",
2209 items.iter().map(|c| &c.label).collect::<Vec<_>>()
2210 );
2211 }
2212 }
2213 }
2214
2215 #[test]
2216 fn builtin_statics_are_reachable() {
2217 for &(recv, members) in BUILTIN_STATICS {
2221 let items = complete(&format!(" {recv}."), "context a.b\n", None);
2222 for &(member, _) in members {
2223 assert!(
2224 find(&items, member, CompletionKind::Member).is_some(),
2225 "{recv}.{member} unreachable: {:?}",
2226 items.iter().map(|c| &c.label).collect::<Vec<_>>()
2227 );
2228 }
2229 }
2230 for (recv, member) in [("List", "empty"), ("Map", "empty"), ("Effect", "pure")] {
2233 let items = complete(&format!(" {recv}."), "context a.b\n", None);
2234 assert!(
2235 find(&items, member, CompletionKind::Member).is_some(),
2236 "{recv}.{member} missing from the statics table"
2237 );
2238 }
2239 }
2240
2241 #[test]
2242 fn record_value_and_decimal_receivers_yield_nothing() {
2243 let doc = "commons m {\n type Point = { x: Int }\n}\n";
2245 assert!(complete(" Point.", doc, None).is_empty(), "record");
2246 assert!(complete(" let p = q.", doc, None).is_empty(), "value");
2248 assert!(complete(" let n = 1.", doc, None).is_empty(), "decimal");
2250 }
2251
2252 #[test]
2253 fn value_receiver_rewrite_drops_the_dot_for_lowercase_receivers() {
2254 let text = " let x = email.\n";
2255 let offset = text.find('.').unwrap() + 1; let (rewritten, recv) = value_receiver_rewrite(text, offset).expect("value receiver");
2257 assert_eq!(
2258 rewritten, " let x = email\n",
2259 "the trailing dot is dropped"
2260 );
2261 assert!(
2262 text.get(recv..=recv).is_some_and(|c| c == "l"),
2263 "the receiver offset lands inside `email`"
2264 );
2265 let text2 = " let x = email.ma\n";
2267 let off2 = text2.find(".ma").unwrap() + 3;
2268 assert_eq!(
2269 value_receiver_rewrite(text2, off2).map(|(r, _)| r),
2270 Some(" let x = email\n".to_string())
2271 );
2272 assert!(value_receiver_rewrite(" Email.", 8).is_none());
2274 assert!(value_receiver_rewrite(" let n = 1.", 12).is_none());
2275 assert!(value_receiver_rewrite(" email", 7).is_none());
2276 }
2277
2278 static TYS: std::sync::LazyLock<Types> = std::sync::LazyLock::new(Types::new);
2282
2283 #[test]
2284 fn value_member_candidates_lists_kernel_methods() {
2285 use bynk_syntax::ast::BaseType;
2286 let list = TYS.intern(Ty::List(TYS.intern(Ty::Base(BaseType::Int))));
2287 let items = value_member_candidates(list, &TYS, "context a.b\n", None);
2288 assert!(find(&items, "fold", CompletionKind::Member).is_some());
2289 assert!(find(&items, "get", CompletionKind::Member).is_some());
2290
2291 let string = TYS.intern(Ty::Base(BaseType::String));
2292 let items = value_member_candidates(string, &TYS, "context a.b\n", None);
2293 assert!(find(&items, "split", CompletionKind::Member).is_some());
2294 assert!(find(&items, "trim", CompletionKind::Member).is_some());
2295 }
2296
2297 #[test]
2298 fn value_member_candidates_lists_refined_inherited_kernel_methods() {
2299 use bynk_check::checker::NamedKind;
2302 use bynk_syntax::ast::BaseType;
2303 let name = TYS.intern(Ty::Named {
2304 name: "Name".to_string(),
2305 kind: NamedKind::Refined(BaseType::String),
2306 args: Vec::new(),
2307 });
2308 let items = value_member_candidates(
2309 name,
2310 &TYS,
2311 "commons m {\n type Name = String where NonEmpty\n}\n",
2312 None,
2313 );
2314 assert!(find(&items, "toUpper", CompletionKind::Member).is_some());
2315 assert!(find(&items, "length", CompletionKind::Member).is_some());
2316 }
2317
2318 #[test]
2319 fn expression_position_offers_locals() {
2320 assert!(is_expression_position(" let y = "));
2322 assert!(is_expression_position(" let y = a + lo")); assert!(is_expression_position(" f("));
2324 assert!(is_expression_position(" g(a, "));
2325 assert!(is_expression_position(" xs.fold(0, (acc, x) => ac")); assert!(is_expression_position(" let y = foo"));
2328 assert!(!is_expression_position(" let y: ")); assert!(!is_expression_position(" on call() -> ")); assert!(!is_expression_position(" tot")); }
2333
2334 #[test]
2335 fn value_member_candidates_lists_record_fields() {
2336 use bynk_check::checker::NamedKind;
2337 let order = TYS.intern(Ty::Named {
2338 name: "Order".to_string(),
2339 kind: NamedKind::Record,
2340 args: Vec::new(),
2341 });
2342 let doc = "commons m {\n type Order = { id: Int, total: Int }\n}\n";
2343 let items = value_member_candidates(order, &TYS, doc, None);
2344 assert!(
2345 find(&items, "id", CompletionKind::Field).is_some(),
2346 "{items:?}",
2347 items = items.iter().map(|c| &c.label).collect::<Vec<_>>()
2348 );
2349 assert!(find(&items, "total", CompletionKind::Field).is_some());
2350 }
2351
2352 #[test]
2353 fn store_field_member_candidates_offers_entry_ops_and_query_accessors() {
2354 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";
2359 let offset = doc.find("items.").unwrap() + "items.".len();
2360 let (rewritten, recv_offset) = value_receiver_rewrite(doc, offset).expect("bare receiver");
2361 let items = store_field_member_candidates(&rewritten, recv_offset, &[]);
2362 assert!(
2363 find(&items, "put", CompletionKind::Member).is_some(),
2364 "{items:?}",
2365 items = items.iter().map(|c| &c.label).collect::<Vec<_>>()
2366 );
2367 assert!(find(&items, "get", CompletionKind::Member).is_some());
2368 assert!(find(&items, "update", CompletionKind::Member).is_some());
2369 assert!(find(&items, "entries", CompletionKind::Field).is_some());
2370 assert!(find(&items, "keys", CompletionKind::Field).is_some());
2371 assert!(find(&items, "values", CompletionKind::Field).is_some());
2372 }
2373
2374 #[test]
2375 fn store_field_member_candidates_empty_for_ordinary_local() {
2376 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";
2379 let offset = doc.rfind("items.").unwrap() + "items.".len();
2380 let (rewritten, recv_offset) = value_receiver_rewrite(doc, offset).expect("bare receiver");
2381 let items = store_field_member_candidates(&rewritten, recv_offset, &[]);
2382 assert!(items.is_empty());
2383 }
2384
2385 #[test]
2386 fn store_field_member_candidates_skips_query_accessors_on_held_map() {
2387 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";
2390 let offset = doc.find("conns.").unwrap() + "conns.".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!(find(&items, "put", CompletionKind::Member).is_some());
2394 assert!(find(&items, "entries", CompletionKind::Field).is_none());
2395 assert!(find(&items, "keys", CompletionKind::Field).is_none());
2396 assert!(find(&items, "values", CompletionKind::Field).is_none());
2397 }
2398
2399 #[test]
2400 fn store_field_member_candidates_offers_set_and_cache_vocabularies() {
2401 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";
2404 let offset = doc.find("tags.").unwrap() + "tags.".len();
2405 let (rewritten, recv_offset) = value_receiver_rewrite(doc, offset).expect("bare receiver");
2406 let items = store_field_member_candidates(&rewritten, recv_offset, &[]);
2407 assert!(find(&items, "add", CompletionKind::Member).is_some());
2408 assert!(find(&items, "entries", CompletionKind::Field).is_none());
2409 }
2410
2411 #[test]
2414 fn record_construction_offers_field_names() {
2415 let doc = "commons m {\n type Order = { id: Int, total: Int }\n}\n";
2416 let got = labels(" let o = Order { ", doc);
2417 assert!(got.contains(&"id".to_string()), "{got:?}");
2418 assert!(got.contains(&"total".to_string()), "{got:?}");
2419 let got2 = labels(" let o = Order { id: 1, ", doc);
2421 assert!(got2.contains(&"total".to_string()), "{got2:?}");
2422 assert!(record_construction_receiver(" let o = Order { id: ").is_none());
2424 assert!(record_construction_receiver(" if x { ").is_none());
2426 }
2427
2428 #[test]
2429 fn from_offers_protocols() {
2430 let got = labels(" service s from ", "context a.b\n");
2431 assert!(got.contains(&"http".to_string()), "{got:?}");
2432 assert!(got.contains(&"cron".to_string()) && got.contains(&"queue".to_string()));
2433 }
2434
2435 #[test]
2436 fn on_offers_handler_kinds() {
2437 let got = labels(" on ", "context a.b\n");
2438 assert!(got.contains(&"call".to_string()), "{got:?}");
2439 assert!(got.contains(&"GET".to_string()) && got.contains(&"schedule".to_string()));
2440 }
2441
2442 #[test]
2443 fn by_offers_project_actors() {
2444 let doc = "context a.b\n\nactor Caller { auth = Bearer }\n";
2445 let got = labels(" by ", doc);
2446 assert!(got.contains(&"Caller".to_string()), "{got:?}");
2447 }
2448
2449 #[test]
2450 fn exports_offers_export_kinds() {
2451 let got = labels(" exports ", "adapter t {\n binding \"./b.ts\"\n}\n");
2452 assert!(got.contains(&"capability".to_string()), "{got:?}");
2453 assert!(got.contains(&"transparent".to_string()));
2454 }
2455
2456 #[test]
2457 fn provides_offers_in_scope_capabilities() {
2458 let doc = "context a.b\n\ncapability Store { fn get() -> Effect[Int] }\n";
2459 let got = labels(" provides ", doc);
2460 assert!(got.contains(&"Store".to_string()), "{got:?}");
2461 }
2462
2463 #[test]
2464 fn where_offers_predicate_names() {
2465 let got = labels(" type Code = Int where ", "context a.b\n");
2467 assert!(got.contains(&"InRange".to_string()), "{got:?}");
2468 assert!(got.contains(&"NonNegative".to_string()));
2469 let got = labels(" _ where ", "context a.b\n");
2471 assert!(got.contains(&"Matches".to_string()), "{got:?}");
2472 assert!(got.contains(&"NonEmpty".to_string()));
2473 }
2474
2475 #[test]
2476 fn is_for_all_where_matches_only_the_for_all_binder() {
2477 assert!(is_for_all_where("\tfor all x: Int, y: Int where "));
2478 assert!(is_for_all_where("for all x: Int where "));
2479 assert!(!is_for_all_where("format(x) where "));
2482 assert!(!is_for_all_where("for allocate x where "));
2483 assert!(!is_for_all_where(" type Code = Int where "));
2485 assert!(!is_for_all_where(" _ where "));
2486 assert!(!is_for_all_where("no where here"));
2487 }
2488
2489 #[test]
2490 fn for_all_where_falls_through_to_expression_position_not_predicates() {
2491 let got = labels("\tfor all x: Int where ", "context a.b\n");
2495 assert!(!got.contains(&"InRange".to_string()), "{got:?}");
2496 assert!(!got.contains(&"NonNegative".to_string()), "{got:?}");
2497 }
2498
2499 #[test]
2500 fn clause_detectors_do_not_over_fire() {
2501 assert!(!after_clause_keyword(" session ", "on"));
2503 assert!(!after_clause_keyword(" let from = ", "from"));
2504 assert!(after_clause_keyword(" service s from ", "from"));
2506 assert!(after_clause_keyword(" by ", "by"));
2507 }
2508
2509 #[test]
2510 fn contract_clause_kind_detects_requires_and_ensures() {
2511 assert_eq!(contract_clause_kind(" requires positive: "), Some(false));
2512 assert_eq!(contract_clause_kind(" ensures never_neg: "), Some(true));
2513 assert_eq!(contract_clause_kind(" id: Int"), None);
2515 assert_eq!(contract_clause_kind(" let x = 1"), None);
2516 }
2517
2518 #[test]
2519 fn cors_field_position_inside_cors_block() {
2520 let doc = "service api from http {\n cors {\n ";
2522 assert!(in_cors_field_position(doc, doc.len()));
2523 let doc2 = "service api from http {\n cors {\n origins: ";
2525 assert!(!in_cors_field_position(doc2, doc2.len()));
2526 let doc3 = "let x = Order {\n ";
2528 assert!(!in_cors_field_position(doc3, doc3.len()));
2529 }
2530
2531 #[test]
2532 fn security_field_position_inside_security_block() {
2533 let doc = "service api from http {\n security {\n ";
2535 assert!(in_security_field_position(doc, doc.len()));
2536 let doc2 = "service api from http {\n security {\n hsts: ";
2538 assert!(!in_security_field_position(doc2, doc2.len()));
2539 let doc3 = "service api from http {\n cors {\n ";
2541 assert!(!in_security_field_position(doc3, doc3.len()));
2542 }
2543
2544 #[test]
2548 fn field_position_probes_survive_non_ascii() {
2549 let doc = "service api from http {\n cors { -- café\n ";
2550 assert!(in_cors_field_position(doc, doc.len()));
2551 let mid = doc.find('é').unwrap() + 1;
2553 assert!(!doc.is_char_boundary(mid));
2554 let _ = in_cors_field_position(doc, mid);
2555 let _ = in_security_field_position(doc, mid);
2556 let _ = in_limits_field_position(doc, mid);
2557 }
2558
2559 #[test]
2560 fn limits_field_position_inside_limits_block() {
2561 let doc = "service api from http {\n limits {\n ";
2563 assert!(in_limits_field_position(doc, doc.len()));
2564 let doc2 = "service api from http {\n limits {\n maxBody: ";
2566 assert!(!in_limits_field_position(doc2, doc2.len()));
2567 let doc3 = "service api from http {\n security {\n ";
2569 assert!(!in_limits_field_position(doc3, doc3.len()));
2570 }
2571
2572 #[test]
2573 fn service_body_item_position_offers_cors() {
2574 let doc = "service api from http {\n ";
2576 assert!(in_service_body_item_position(doc, doc.len(), " "));
2577 let doc2 = "service api from http {\n cors {\n ";
2580 assert!(!in_service_body_item_position(doc2, doc2.len(), " "));
2581 }
2582
2583 #[test]
2584 fn cache_arg_position_inside_cache_annotation() {
2585 let doc = "service api from http {\n @cache(";
2587 assert!(in_cache_arg_position(doc, doc.len()));
2588 let doc2 = "service api from http {\n @cache(maxAge: 5.minutes, ";
2590 assert!(in_cache_arg_position(doc2, doc2.len()));
2591 let doc3 = "service api from http {\n @cache(maxAge: ";
2593 assert!(!in_cache_arg_position(doc3, doc3.len()));
2594 let doc4 = "let x = cache(";
2596 assert!(!in_cache_arg_position(doc4, doc4.len()));
2597 let doc5 = "on GET(\"/x\") by v: Visitor (";
2599 assert!(!in_cache_arg_position(doc5, doc5.len()));
2600 }
2601
2602 #[test]
2603 fn limit_arg_position_inside_limit_annotation() {
2604 let doc = "service api from http {\n @limit(";
2606 assert!(in_limit_arg_position(doc, doc.len()));
2607 let doc2 = "service api from http {\n @limit(maxBody: 1048576, ";
2609 assert!(in_limit_arg_position(doc2, doc2.len()));
2610 let doc3 = "service api from http {\n @limit(maxBody: ";
2612 assert!(!in_limit_arg_position(doc3, doc3.len()));
2613 let doc4 = "let x = limit(";
2615 assert!(!in_limit_arg_position(doc4, doc4.len()));
2616 let doc5 = "on GET(\"/x\") by v: Visitor (";
2618 assert!(!in_limit_arg_position(doc5, doc5.len()));
2619 }
2620
2621 #[test]
2622 fn sum_type_variants_lists_variants() {
2623 let doc = "commons m {\n type Status = enum { Pending, Shipped }\n}\n";
2624 let got: Vec<String> = sum_type_variants("Status", doc, None)
2625 .into_iter()
2626 .map(|c| c.label)
2627 .collect();
2628 assert!(got.contains(&"Pending".to_string()), "{got:?}");
2629 assert!(got.contains(&"Shipped".to_string()), "{got:?}");
2630 }
2631
2632 #[test]
2633 fn variants_for_ty_offers_built_in_result_and_option() {
2634 use bynk_syntax::ast::BaseType;
2639 let result = TYS.intern(Ty::Result(
2640 TYS.intern(Ty::Base(BaseType::Int)),
2641 TYS.intern(Ty::Base(BaseType::String)),
2642 ));
2643 let got: Vec<String> = variants_for_ty(result, &TYS, "", None)
2644 .into_iter()
2645 .map(|c| c.label)
2646 .collect();
2647 assert_eq!(got, vec!["Ok".to_string(), "Err".to_string()]);
2648
2649 let option = TYS.intern(Ty::Option(TYS.intern(Ty::Base(BaseType::Int))));
2650 let got: Vec<String> = variants_for_ty(option, &TYS, "", None)
2651 .into_iter()
2652 .map(|c| c.label)
2653 .collect();
2654 assert_eq!(got, vec!["Some".to_string(), "None".to_string()]);
2655 }
2656
2657 #[test]
2658 fn nested_variant_completions_resolves_the_payload_type() {
2659 use bynk_check::checker::NamedKind;
2660 use bynk_syntax::ast::BaseType;
2661 let payload = TYS.intern(Ty::Result(
2664 TYS.intern(Ty::Base(BaseType::Int)),
2665 TYS.intern(Ty::Named {
2666 name: "E".to_string(),
2667 kind: NamedKind::Sum,
2668 args: Vec::new(),
2669 }),
2670 ));
2671 let scrut = TYS.intern(Ty::Option(payload));
2672 let got: Vec<String> = nested_variant_completions(scrut, &TYS, "Some", "", None)
2673 .into_iter()
2674 .map(|c| c.label)
2675 .collect();
2676 assert_eq!(got, vec!["Ok".to_string(), "Err".to_string()]);
2677
2678 let opt_int = TYS.intern(Ty::Option(TYS.intern(Ty::Base(BaseType::Int))));
2680 let inner = TYS.intern(Ty::Result(TYS.intern(Ty::Base(BaseType::Int)), opt_int));
2681 let got: Vec<String> = nested_variant_completions(inner, &TYS, "Err", "", None)
2682 .into_iter()
2683 .map(|c| c.label)
2684 .collect();
2685 assert_eq!(got, vec!["Some".to_string(), "None".to_string()]);
2686
2687 let doc = "commons m {\n type Inner = enum { A, B }\n \
2690 type Outer = | Wrap(inner: Inner) | Bare\n}\n";
2691 let outer = TYS.intern(Ty::Named {
2692 name: "Outer".to_string(),
2693 kind: NamedKind::Sum,
2694 args: Vec::new(),
2695 });
2696 let got: Vec<String> = nested_variant_completions(outer, &TYS, "Wrap", doc, None)
2697 .into_iter()
2698 .map(|c| c.label)
2699 .collect();
2700 assert!(got.contains(&"A".to_string()), "{got:?}");
2701 assert!(got.contains(&"B".to_string()), "{got:?}");
2702 }
2703
2704 #[test]
2705 fn literal_kind_scrutinee_suggests_no_variants() {
2706 let doc = "commons m {\n type Quantity = Int where InRange(1, 99)\n}\n";
2712 assert!(sum_type_variants("Quantity", doc, None).is_empty());
2713 assert!(sum_type_variants("Int", doc, None).is_empty());
2714 }
2715
2716 fn enumerated_units(doc_text: &str, files: Option<&HashMap<PathBuf, String>>) -> Vec<String> {
2720 let mut names = Vec::new();
2721 for_each_unit(doc_text, files, |u| names.push(u.name().joined()));
2722 names
2723 }
2724
2725 fn synthetic_path(name: &str) -> PathBuf {
2731 PathBuf::from(format!("/synthetic/{name}.bynk"))
2732 }
2733
2734 #[test]
2735 fn for_each_unit_yields_embedded_buffer_and_project_files() {
2736 let sibling = synthetic_path("sibling");
2737 let files = HashMap::from([(
2738 sibling,
2739 "commons proj.sibling {\n fn s() -> Int { 1 }\n}\n".to_string(),
2740 )]);
2741
2742 let names = enumerated_units(
2743 "commons proj.buffer {\n fn b() -> Int { 1 }\n}\n",
2744 Some(&files),
2745 );
2746 assert!(names.iter().any(|n| n == "bynk"), "embedded: {names:?}");
2748 assert!(
2749 names.iter().any(|n| n == "proj.buffer"),
2750 "buffer: {names:?}"
2751 );
2752 assert!(
2753 names.iter().any(|n| n == "proj.sibling"),
2754 "project file: {names:?}"
2755 );
2756 }
2757
2758 #[test]
2765 fn same_named_declarations_do_not_union_across_units() {
2766 let stale = HashMap::from([(
2767 synthetic_path("stale"),
2768 "commons m {\n \
2769 type Rec = { total: Int, old_field: Int }\n \
2770 type Status = enum { Pending, Retired }\n\
2771 }\n"
2772 .to_string(),
2773 )]);
2774
2775 let buffer = "commons m {\n \
2776 type Rec = { total: Int }\n \
2777 type Status = enum { Pending, Shipped }\n\
2778 }\n";
2779 let files = Some(&stale);
2780
2781 let fields: Vec<String> = record_field_names("Rec", buffer, files)
2782 .into_iter()
2783 .map(|c| c.label)
2784 .collect();
2785 assert_eq!(fields, vec!["total".to_string()], "{fields:?}");
2786
2787 let variants: Vec<String> = sum_type_variants("Status", buffer, files)
2788 .into_iter()
2789 .map(|c| c.label)
2790 .collect();
2791 assert_eq!(
2792 variants,
2793 vec!["Pending".to_string(), "Shipped".to_string()],
2794 "{variants:?}"
2795 );
2796 }
2797
2798 #[test]
2803 fn capabilities_of_unit_does_not_union_across_units() {
2804 let stale = HashMap::from([(
2805 synthetic_path("stale"),
2806 "adapter tokens {\n \
2807 exports capability { Jwt, Retired }\n\
2808 }\n"
2809 .to_string(),
2810 )]);
2811
2812 let buffer = "adapter tokens {\n exports capability { Jwt }\n}\n";
2813 let files = Some(&stale);
2814
2815 let caps = capabilities_of_unit("tokens", buffer, files);
2816 assert_eq!(caps, vec!["Jwt".to_string()], "{caps:?}");
2817 }
2818
2819 #[test]
2820 fn project_unit_cache_invalidates_on_change() {
2821 let path = synthetic_path("unit");
2822
2823 let first_files = HashMap::from([(
2824 path.clone(),
2825 "commons proj.first {\n fn a() -> Int { 1 }\n}\n".to_string(),
2826 )]);
2827 let first = enumerated_units("context a.b\n", Some(&first_files));
2828 assert!(
2829 first.iter().any(|n| n == "proj.first"),
2830 "first read: {first:?}"
2831 );
2832
2833 let second_files = HashMap::from([(
2838 path,
2839 "commons proj.second.longer {\n fn a() -> Int { 1 }\n fn c() -> Int { 3 }\n}\n"
2840 .to_string(),
2841 )]);
2842 let second = enumerated_units("context a.b\n", Some(&second_files));
2843 assert!(
2844 second.iter().any(|n| n == "proj.second.longer"),
2845 "after change: {second:?}"
2846 );
2847 assert!(
2848 !second.iter().any(|n| n == "proj.first"),
2849 "stale served: {second:?}"
2850 );
2851 }
2852
2853 #[test]
2854 fn unparseable_project_file_is_skipped() {
2855 let files = HashMap::from([(synthetic_path("broken"), "not bynk source {{{".to_string())]);
2862 let names = enumerated_units("context a.b\n", Some(&files));
2863 let baseline = enumerated_units("context a.b\n", None);
2864 assert_eq!(names, baseline, "{names:?}");
2870 }
2871}