1use std::borrow::Cow;
41use std::collections::{BTreeMap, BTreeSet, HashMap};
42use std::path::{Path, PathBuf};
43use std::sync::{Arc, LazyLock};
44
45use bynk_check::checker::{NamedKind, Ty, TyId, Types};
46use bynk_check::kernel_methods;
47use bynk_check::locals::LocalBinding;
48use bynk_check::store_ops;
49use bynk_syntax::ast::{CommonsItem, ExportKind, FnName, SourceUnit, TypeBody, TypeRef, UsesDecl};
50use bynk_syntax::{keywords, lexer, parser};
51
52use crate::symbols::type_ref_str;
53
54#[derive(Clone, Copy, PartialEq, Eq)]
56pub enum CompletionKind {
57 Unit,
58 Capability,
59 Type,
60 Keyword,
61 Snippet,
62 Variant,
64 Member,
67 Field,
69 Constructor,
71 Function,
74 Constant,
77}
78
79pub struct Completion {
80 pub label: String,
81 pub kind: CompletionKind,
82 pub detail: Option<String>,
83 pub insert_text: Option<String>,
86 pub documentation: Option<String>,
89}
90
91impl Completion {
92 pub fn item(label: impl Into<String>, kind: CompletionKind, detail: Option<String>) -> Self {
93 Completion {
94 label: label.into(),
95 kind,
96 detail,
97 insert_text: None,
98 documentation: None,
99 }
100 }
101
102 fn with_doc(mut self, doc: Option<&str>) -> Self {
104 self.documentation = doc.map(str::to_owned);
105 self
106 }
107
108 fn snippet(label: &str, body: &str) -> Self {
109 Completion {
110 label: label.to_string(),
111 kind: CompletionKind::Snippet,
112 detail: Some(format!("{label} scaffold")),
113 insert_text: Some(body.to_string()),
114 documentation: None,
115 }
116 }
117}
118
119pub fn complete(
125 line_prefix: &str,
126 doc_text: &str,
127 files: Option<&HashMap<PathBuf, String>>,
128) -> Vec<Completion> {
129 complete_in(None, line_prefix, doc_text, files)
130}
131
132pub fn complete_in(
135 project_root: Option<&Path>,
136 line_prefix: &str,
137 doc_text: &str,
138 files: Option<&HashMap<PathBuf, String>>,
139) -> Vec<Completion> {
140 let split = || split_dependency_files(files, project_root);
143 if let Some(unit) = consumes_brace_unit(line_prefix) {
145 return capabilities_of_unit(&unit, doc_text, &split())
146 .into_iter()
147 .map(|c| {
148 let detail = format!(
149 "capability exported by `{unit}`{}",
150 stability_suffix(experimental(&format!("{unit}.{c}")))
151 );
152 Completion::item(c, CompletionKind::Capability, Some(detail))
153 })
154 .collect();
155 }
156 if is_consumes_target(line_prefix) {
158 return consumable_units(doc_text, &split());
159 }
160 if is_uses_target(line_prefix) {
164 return usable_commons(doc_text, &split());
165 }
166 if is_given_position(line_prefix) {
168 return in_scope_capabilities(doc_text, &split());
169 }
170 if let Some(receiver) = member_receiver(line_prefix) {
173 return member_candidates(&receiver, doc_text, files);
174 }
175 if let Some(recv) = record_construction_receiver(line_prefix) {
179 let fields = record_field_names(&recv, doc_text, files);
180 if !fields.is_empty() {
181 return fields;
182 }
183 }
184 if after_clause_keyword(line_prefix, "from") {
186 return protocol_candidates();
187 }
188 if after_clause_keyword(line_prefix, "on") {
190 return handler_kind_candidates();
191 }
192 if after_clause_keyword(line_prefix, "by") {
194 return actor_candidates(doc_text, files);
195 }
196 if after_clause_keyword(line_prefix, "exports") {
198 return export_kind_candidates();
199 }
200 if after_clause_keyword(line_prefix, "provides") {
202 return in_scope_capabilities(doc_text, &split());
203 }
204 if after_clause_keyword(line_prefix, "where") && !is_for_all_where(line_prefix) {
212 return predicate_name_candidates();
213 }
214 if is_type_position(line_prefix) {
217 return type_candidates(doc_text, files);
218 }
219 if is_keyword_position(line_prefix) {
222 return keyword_and_snippet_candidates();
223 }
224 if is_expression_position(line_prefix) {
229 return expression_candidates(doc_text, files);
230 }
231 Vec::new()
232}
233
234fn consumes_brace_unit(line: &str) -> Option<String> {
238 let idx = line.rfind("consumes")?;
239 let after = &line[idx + "consumes".len()..];
240 let open = after.find('{')?;
241 if after[open + 1..].contains('}') {
243 return None;
244 }
245 let unit = after[..open].trim();
246 if unit.is_empty() || !is_qualified_name(unit) {
247 return None;
248 }
249 Some(unit.to_string())
250}
251
252fn is_consumes_target(line: &str) -> bool {
254 let Some(idx) = line.rfind("consumes") else {
255 return false;
256 };
257 if !line[..idx]
259 .chars()
260 .last()
261 .map(|c| c.is_whitespace())
262 .unwrap_or(true)
263 {
264 return false;
265 }
266 let after = &line[idx + "consumes".len()..];
267 after.starts_with(char::is_whitespace)
269 && !after.contains('{')
270 && !after.contains('}')
271 && !after.split_whitespace().any(|w| w == "as")
272}
273
274fn is_uses_target(line: &str) -> bool {
276 let trimmed = line.trim_start();
277 let Some(after) = trimmed.strip_prefix("uses") else {
278 return false;
279 };
280 after.starts_with(char::is_whitespace) && after.split_whitespace().count() <= 1
281}
282
283fn is_given_position(line: &str) -> bool {
285 let Some(idx) = line.rfind("given") else {
286 return false;
287 };
288 if !line[..idx]
289 .chars()
290 .last()
291 .map(|c| c.is_whitespace())
292 .unwrap_or(true)
293 {
294 return false;
295 }
296 let after = &line[idx + "given".len()..];
297 if !after.starts_with(char::is_whitespace) {
298 return false;
299 }
300 after
303 .chars()
304 .all(|c| c.is_alphanumeric() || matches!(c, '_' | '.' | ',' | ' ' | '\t'))
305}
306
307fn is_qualified_name(s: &str) -> bool {
308 !s.is_empty()
309 && s.split('.').all(|seg| {
310 !seg.is_empty()
311 && seg.chars().all(|c| c.is_alphanumeric() || c == '_')
312 && !seg.chars().next().unwrap().is_ascii_digit()
313 })
314}
315
316fn is_type_position(line: &str) -> bool {
326 let head = line
327 .trim_end_matches(|c: char| c.is_alphanumeric() || c == '_')
328 .trim_end();
329 head.ends_with("->") || (head.ends_with(':') && !head.ends_with("::")) || in_type_arg_list(head)
330}
331
332fn in_type_arg_list(head: &str) -> bool {
336 let chars: Vec<char> = head.chars().collect();
337 let mut depth = 0i32;
338 let mut opener_after_ident = false;
339 for (i, &c) in chars.iter().enumerate() {
340 match c {
341 '[' => {
342 depth += 1;
343 if depth == 1 {
344 opener_after_ident =
345 i > 0 && (chars[i - 1].is_alphanumeric() || chars[i - 1] == '_');
346 }
347 }
348 ']' => depth -= 1,
349 _ => {}
350 }
351 }
352 depth > 0 && opener_after_ident
353}
354
355pub fn is_keyword_position(line: &str) -> bool {
361 line.trim().chars().all(|c| c.is_alphanumeric() || c == '_')
362}
363
364pub fn is_expression_position(line: &str) -> bool {
369 let head = line
370 .trim_end_matches(|c: char| c.is_alphanumeric() || c == '_')
371 .trim_end();
372 if head.ends_with("->") {
373 return false; }
375 if head.ends_with("=>") {
376 return true; }
378 matches!(
379 head.chars().last(),
380 Some('=' | '(' | ',' | '[' | '+' | '-' | '*' | '/' | '<' | '>' | '&' | '|')
381 )
382}
383
384fn member_receiver(line: &str) -> Option<String> {
392 let head = line
394 .trim_end_matches(|c: char| c.is_alphanumeric() || c == '_')
395 .strip_suffix('.')?;
396 let start = head
400 .char_indices()
401 .rfind(|&(_, c)| !(c.is_alphanumeric() || c == '_'))
402 .map_or(0, |(i, c)| i + c.len_utf8());
403 let recv = &head[start..];
404 let first = recv.chars().next()?;
405 if !first.is_ascii_uppercase() {
406 return None;
407 }
408 if head[..start].ends_with('.') {
410 return None;
411 }
412 Some(recv.to_string())
413}
414
415fn record_construction_receiver(line: &str) -> Option<String> {
421 let bytes = line.as_bytes();
423 let mut depth = 0i32;
424 let mut open = None;
425 for i in (0..bytes.len()).rev() {
426 match bytes[i] {
427 b'}' => depth += 1,
428 b'{' => {
429 if depth == 0 {
430 open = Some(i);
431 break;
432 }
433 depth -= 1;
434 }
435 _ => {}
436 }
437 }
438 let open = open?;
439 let current = line[open + 1..].rsplit(',').next().unwrap_or("");
442 if current.contains(':') {
443 return None;
444 }
445 let head = line[..open].trim_end();
447 let start = head
448 .char_indices()
449 .rfind(|&(_, c)| !(c.is_alphanumeric() || c == '_'))
450 .map_or(0, |(i, c)| i + c.len_utf8());
451 let recv = &head[start..];
452 if recv.chars().next()?.is_ascii_uppercase() {
453 Some(recv.to_string())
454 } else {
455 None
456 }
457}
458
459pub const CORS_FIELDS: &[(&str, &str)] = &[
463 (
464 "origins",
465 "the allowed origins — an exact allowlist, or `[\"*\"]`",
466 ),
467 (
468 "headers",
469 "the `Access-Control-Allow-Headers` a preflight advertises",
470 ),
471 (
472 "credentials",
473 "whether credentialed requests are allowed (`true`/`false`)",
474 ),
475 (
476 "maxAge",
477 "how long a browser may cache the preflight (a `Duration`)",
478 ),
479];
480
481pub const SECURITY_FIELDS: &[(&str, &str)] = &[
484 (
485 "nosniff",
486 "stamp `X-Content-Type-Options: nosniff` (`true`/`false`, default `true`)",
487 ),
488 (
489 "hsts",
490 "opt in to `Strict-Transport-Security` — the `max-age` as a `Duration`",
491 ),
492];
493
494pub const LIMITS_FIELDS: &[(&str, &str)] = &[(
497 "maxBody",
498 "the maximum request body size in bytes (a positive `Int`)",
499)];
500
501pub const CACHE_ARGS: &[(&str, &str)] = &[
507 (
508 "maxAge",
509 "the freshness window — a whole-second `Duration` (e.g. `5.minutes`) lowered to `Cache-Control: max-age`",
510 ),
511 (
512 "scope",
513 "`public` or `private` (default `private` — a shared cache stores only on `public`)",
514 ),
515 (
516 "immutable",
517 "`true` promises the response never changes (a content-addressed path) — appends `immutable` to `Cache-Control`; purge it through `EdgeCache` if it can change; default `false`",
518 ),
519];
520
521pub const LIMIT_ARGS: &[(&str, &str)] = &[(
526 "maxBody",
527 "the maximum request body size in bytes (a positive `Int`) — a `413` is synthesised past it",
528)];
529
530pub const RAW_ARGS: &[(&str, &str)] = &[(
534 "contentType",
535 "the media type the route accepts, a string literal (`\"*/*\"` for any) — another `Content-Type` is a `415`",
536)];
537
538fn innermost_open_brace(text: &str, offset: usize) -> Option<usize> {
542 let bytes = text.as_bytes();
543 let end = offset.min(bytes.len());
544 let mut depth = 0i32;
545 for i in (0..end).rev() {
546 match bytes[i] {
547 b'}' => depth += 1,
548 b'{' => {
549 if depth == 0 {
550 return Some(i);
551 }
552 depth -= 1;
553 }
554 _ => {}
555 }
556 }
557 None
558}
559
560fn word_before_brace(text: &str, open: usize) -> &str {
563 let head = text[..open].trim_end();
564 let start = head
565 .char_indices()
566 .rfind(|&(_, c)| !(c.is_alphanumeric() || c == '_'))
567 .map_or(0, |(i, c)| i + c.len_utf8());
568 &head[start..]
569}
570
571pub fn in_cors_field_position(text: &str, offset: usize) -> bool {
575 let Some(open) = innermost_open_brace(text, offset) else {
576 return false;
577 };
578 if word_before_brace(text, open) != "cors" {
579 return false;
580 }
581 let Some(seg) = text.get(open + 1..offset.min(text.len())) else {
584 return false;
585 };
586 let current = seg.rsplit(['\n', ',']).next().unwrap_or("");
587 !current.contains(':')
588}
589
590pub fn in_security_field_position(text: &str, offset: usize) -> bool {
594 let Some(open) = innermost_open_brace(text, offset) else {
595 return false;
596 };
597 if word_before_brace(text, open) != "security" {
598 return false;
599 }
600 let Some(seg) = text.get(open + 1..offset.min(text.len())) else {
603 return false;
604 };
605 let current = seg.rsplit(['\n', ',']).next().unwrap_or("");
606 !current.contains(':')
607}
608
609pub fn in_limits_field_position(text: &str, offset: usize) -> bool {
613 let Some(open) = innermost_open_brace(text, offset) else {
614 return false;
615 };
616 if word_before_brace(text, open) != "limits" {
617 return false;
618 }
619 let Some(seg) = text.get(open + 1..offset.min(text.len())) else {
622 return false;
623 };
624 let current = seg.rsplit(['\n', ',']).next().unwrap_or("");
625 !current.contains(':')
626}
627
628pub fn in_env_name_position(text: &str, offset: usize) -> bool {
632 let Some(open) = innermost_open_brace(text, offset) else {
633 return false;
634 };
635 if word_before_brace(text, open) != "env" {
636 return false;
637 }
638 let Some(seg) = text.get(open + 1..offset.min(text.len())) else {
639 return false;
640 };
641 let current = seg.rsplit(['\n', ',']).next().unwrap_or("");
642 !current.contains('=')
643}
644
645pub fn env_name_candidates(
650 doc_text: &str,
651 files: Option<&HashMap<PathBuf, String>>,
652) -> Vec<Completion> {
653 let mut names: BTreeMap<String, &'static str> = BTreeMap::new();
654 let mut scan = |src: &str| {
655 for (marker, detail) in [
656 ("secret = \"", "a secret an actor declares"),
657 ("Secrets.get(\"", "a name `Secrets.get` reads"),
658 ] {
659 let mut rest = src;
660 while let Some(i) = rest.find(marker) {
661 rest = &rest[i + marker.len()..];
662 if let Some(end) = rest.find('"') {
663 let name = &rest[..end];
664 if !name.is_empty() && !name.contains(char::is_whitespace) {
665 names.entry(name.to_string()).or_insert(detail);
666 }
667 }
668 }
669 }
670 };
671 scan(doc_text);
672 if let Some(files) = files {
673 for src in files.values() {
674 scan(src);
675 }
676 }
677 names
678 .into_iter()
679 .map(|(name, detail)| {
680 let mut c = Completion::item(name.clone(), CompletionKind::Field, Some(detail.into()));
681 c.insert_text = Some(format!("{name} = \"\""));
682 c
683 })
684 .collect()
685}
686
687pub fn in_service_body_item_position(text: &str, offset: usize, line_prefix: &str) -> bool {
692 if !is_keyword_position(line_prefix) {
693 return false;
694 }
695 let Some(open) = innermost_open_brace(text, offset) else {
696 return false;
697 };
698 let header_start = text[..open].rfind('\n').map_or(0, |i| i + 1);
699 text[header_start..open].contains("service ")
700}
701
702fn innermost_open_paren(text: &str, offset: usize) -> Option<usize> {
705 let bytes = text.as_bytes();
706 let end = offset.min(bytes.len());
707 let mut depth = 0i32;
708 for i in (0..end).rev() {
709 match bytes[i] {
710 b')' => depth += 1,
711 b'(' => {
712 if depth == 0 {
713 return Some(i);
714 }
715 depth -= 1;
716 }
717 _ => {}
718 }
719 }
720 None
721}
722
723pub fn in_cache_arg_position(text: &str, offset: usize) -> bool {
728 let Some(open) = innermost_open_paren(text, offset) else {
729 return false;
730 };
731 if word_before_brace(text, open) != "cache" {
732 return false;
733 }
734 let head = text[..open].trim_end();
737 let before_cache = head[..head.len() - "cache".len()].trim_end();
738 if !before_cache.ends_with('@') {
739 return false;
740 }
741 let Some(seg) = text.get(open + 1..offset.min(text.len())) else {
744 return false;
745 };
746 let current = seg.rsplit(['\n', ',']).next().unwrap_or("");
747 !current.contains(':')
748}
749
750pub fn in_limit_arg_position(text: &str, offset: usize) -> bool {
755 let Some(open) = innermost_open_paren(text, offset) else {
756 return false;
757 };
758 if word_before_brace(text, open) != "limit" {
759 return false;
760 }
761 let head = text[..open].trim_end();
764 let before_limit = head[..head.len() - "limit".len()].trim_end();
765 if !before_limit.ends_with('@') {
766 return false;
767 }
768 if on_capability_declaration(text, open) {
771 return false;
772 }
773 let Some(seg) = text.get(open + 1..offset.min(text.len())) else {
776 return false;
777 };
778 let current = seg.rsplit(['\n', ',']).next().unwrap_or("");
779 !current.contains(':')
780}
781
782fn on_capability_declaration(text: &str, open: usize) -> bool {
785 let line_start = text[..open].rfind('\n').map_or(0, |i| i + 1);
786 let line = text[line_start..open].trim_start();
787 line.starts_with("capability ") && line.contains('=')
788}
789
790pub fn instance_option_values(
794 text: &str,
795 offset: usize,
796) -> &'static [(&'static str, &'static str)] {
797 let Some(open) = innermost_open_paren(text, offset) else {
798 return &[];
799 };
800 if !on_capability_declaration(text, open) {
801 return &[];
802 }
803 let Some(seg) = text.get(open + 1..offset.min(text.len())) else {
804 return &[];
805 };
806 if !seg.trim().is_empty() {
807 return &[];
808 }
809 let head = text[..open].trim_end();
810 let before = head[..head.len() - word_before_brace(text, open).len()].trim_end();
811 if !before.ends_with('@') {
812 return &[];
813 }
814 match word_before_brace(text, open) {
815 "period" => RATELIMIT_PERIODS,
816 _ => &[],
817 }
818}
819
820pub fn instance_declaration_completions(line: &str) -> Vec<(String, String, String)> {
826 let line = line.trim_start();
827 let Some(rest) = line.strip_prefix("capability ") else {
828 return Vec::new();
829 };
830 let Some((_, after)) = rest.split_once('=') else {
831 return Vec::new();
832 };
833 let kinds = || {
834 bynk_check::firstparty::UNITS
835 .iter()
836 .flat_map(|u| u.instantiable.iter().map(move |i| (u.name, i)))
837 };
838 let typed = after.trim_start();
839 if !typed.contains(char::is_whitespace) {
840 return kinds()
842 .filter(|(_, i)| i.capability.starts_with(typed))
843 .map(|(unit, i)| {
844 (
845 i.capability.to_string(),
846 format!("an instance of `{unit}`'s `{}`", i.capability),
847 i.capability.to_string(),
848 )
849 })
850 .collect();
851 }
852 let (base, tail) = typed.split_once(char::is_whitespace).unwrap_or((typed, ""));
853 if !tail.trim().is_empty() {
854 return Vec::new();
855 }
856 let Some((_, kind)) = kinds().find(|(_, i)| i.capability == base) else {
857 return Vec::new();
858 };
859 if kind.required.is_empty() {
860 return Vec::new();
861 }
862 let snippet: Vec<String> = kind
863 .required
864 .iter()
865 .enumerate()
866 .map(|(n, o)| format!("@{o}(${{{}:{}}})", n + 1, option_sample(o)))
867 .collect();
868 let label: Vec<String> = kind.required.iter().map(|o| format!("@{o}(…)")).collect();
869 vec![(
870 label.join(" "),
871 format!("the options every `{base}` instance needs"),
872 snippet.join(" "),
873 )]
874}
875
876fn option_sample(option: &str) -> &'static str {
878 match option {
879 "limit" => "100",
880 "period" => "60.seconds",
881 "jurisdiction" => "\"eu\"",
882 _ => "\"legacy-billing\"",
883 }
884}
885
886pub const RATELIMIT_PERIODS: &[(&str, &str)] = &[
888 (
889 "10.seconds",
890 "a 10-second window — one of the two the platform offers",
891 ),
892 (
893 "60.seconds",
894 "a 60-second window — one of the two the platform offers",
895 ),
896];
897
898pub fn in_raw_arg_position(text: &str, offset: usize) -> bool {
901 let Some(open) = innermost_open_paren(text, offset) else {
902 return false;
903 };
904 if word_before_brace(text, open) != "raw" {
905 return false;
906 }
907 let head = text[..open].trim_end();
908 let before_raw = head[..head.len() - "raw".len()].trim_end();
909 if !before_raw.ends_with('@') {
910 return false;
911 }
912 let Some(seg) = text.get(open + 1..offset.min(text.len())) else {
913 return false;
914 };
915 let current = seg.rsplit(['\n', ',']).next().unwrap_or("");
916 !current.contains(':')
917}
918
919fn after_clause_keyword(line: &str, kw: &str) -> bool {
925 let Some(idx) = line.rfind(kw) else {
926 return false;
927 };
928 if !line[..idx]
929 .chars()
930 .last()
931 .map(char::is_whitespace)
932 .unwrap_or(true)
933 {
934 return false;
935 }
936 let after = &line[idx + kw.len()..];
937 after.starts_with(char::is_whitespace)
938 && after
939 .trim_start()
940 .chars()
941 .all(|c| c.is_alphanumeric() || c == '_')
942}
943
944fn is_for_all_where(line: &str) -> bool {
954 let Some(idx) = line.rfind("where") else {
955 return false;
956 };
957 let head = line[..idx].trim_start();
958 let Some(after_for) = head.strip_prefix("for") else {
959 return false;
960 };
961 if !after_for.starts_with(char::is_whitespace) {
964 return false;
965 }
966 let Some(after_all) = after_for.trim_start().strip_prefix("all") else {
967 return false;
968 };
969 after_all.is_empty() || after_all.starts_with(char::is_whitespace)
971}
972
973pub fn contract_clause_kind(line: &str) -> Option<bool> {
979 let colon = line.rfind(':')?;
980 let clause = line[..colon].trim();
981 for (kw, is_ensures) in [("requires", false), ("ensures", true)] {
982 if let Some(rest) = clause.strip_prefix(kw) {
983 let rest = rest.trim();
984 if !rest.is_empty() && rest.chars().all(|c| c.is_alphanumeric() || c == '_') {
985 return Some(is_ensures);
986 }
987 }
988 }
989 None
990}
991
992fn record_field_names(
1002 name: &str,
1003 doc_text: &str,
1004 files: Option<&HashMap<PathBuf, String>>,
1005) -> Vec<Completion> {
1006 let mut out: Vec<Completion> = Vec::new();
1007 let mut found = false;
1008 for_each_unit(doc_text, files, |unit| {
1009 if found {
1010 return;
1011 }
1012 let items = match unit {
1013 SourceUnit::Commons(c) => &c.items,
1014 SourceUnit::Context(c) => &c.items,
1015 SourceUnit::Adapter(a) => &a.items,
1016 _ => return,
1017 };
1018 for item in items {
1019 if let CommonsItem::Type(t) = item
1020 && t.name.name == name
1021 && let TypeBody::Record(r) = &t.body
1022 {
1023 found = true;
1024 for f in &r.fields {
1025 out.push(
1026 Completion::item(
1027 f.name.name.clone(),
1028 CompletionKind::Field,
1029 Some(format!("field of `{name}`")),
1030 )
1031 .with_doc(f.documentation.as_deref()),
1032 );
1033 }
1034 return;
1035 }
1036 }
1037 });
1038 out
1039}
1040
1041pub fn sum_type_variants(
1049 name: &str,
1050 doc_text: &str,
1051 files: Option<&HashMap<PathBuf, String>>,
1052) -> Vec<Completion> {
1053 let mut out: Vec<Completion> = Vec::new();
1054 let mut found = false;
1055 for_each_unit(doc_text, files, |unit| {
1056 if found {
1057 return;
1058 }
1059 let items = match unit {
1060 SourceUnit::Commons(c) => &c.items,
1061 SourceUnit::Context(c) => &c.items,
1062 SourceUnit::Adapter(a) => &a.items,
1063 _ => return,
1064 };
1065 for item in items {
1066 if let CommonsItem::Type(t) = item
1067 && t.name.name == name
1068 && let TypeBody::Sum(s) = &t.body
1069 {
1070 found = true;
1071 for v in &s.variants {
1072 out.push(
1073 Completion::item(
1074 v.name.name.clone(),
1075 CompletionKind::Variant,
1076 Some(format!("variant of `{name}`")),
1077 )
1078 .with_doc(v.documentation.as_deref()),
1079 );
1080 }
1081 return;
1082 }
1083 }
1084 });
1085 out
1086}
1087
1088pub fn variants_for_ty(
1095 ty: TyId,
1096 tys: &Types,
1097 doc_text: &str,
1098 files: Option<&HashMap<PathBuf, String>>,
1099) -> Vec<Completion> {
1100 match &*tys.get(ty) {
1101 Ty::Named { name, .. } => sum_type_variants(name, doc_text, files),
1102 Ty::Result(..) => built_in_variants(&["Ok", "Err"], "Result"),
1103 Ty::Option(..) => built_in_variants(&["Some", "None"], "Option"),
1104 _ => Vec::new(),
1105 }
1106}
1107
1108pub fn nested_variant_completions(
1116 ty: TyId,
1117 tys: &Types,
1118 outer_variant: &str,
1119 doc_text: &str,
1120 files: Option<&HashMap<PathBuf, String>>,
1121) -> Vec<Completion> {
1122 match &*tys.get(ty) {
1123 Ty::Result(t, e) => match outer_variant {
1124 "Ok" => variants_for_ty(*t, tys, doc_text, files),
1125 "Err" => variants_for_ty(*e, tys, doc_text, files),
1126 _ => Vec::new(),
1127 },
1128 Ty::HttpResult(t) if outer_variant == "Ok" => variants_for_ty(*t, tys, doc_text, files),
1129 Ty::Option(t) if outer_variant == "Some" => variants_for_ty(*t, tys, doc_text, files),
1130 Ty::Named {
1131 kind: NamedKind::Sum,
1132 name,
1133 ..
1134 } => payload_type_ref_variants(name, outer_variant, doc_text, files),
1135 _ => Vec::new(),
1136 }
1137}
1138
1139fn built_in_variants(names: &[&str], of: &str) -> Vec<Completion> {
1141 names
1142 .iter()
1143 .map(|v| {
1144 Completion::item(
1145 (*v).to_string(),
1146 CompletionKind::Variant,
1147 Some(format!("variant of `{of}`")),
1148 )
1149 })
1150 .collect()
1151}
1152
1153fn payload_type_ref_variants(
1158 sum_name: &str,
1159 variant: &str,
1160 doc_text: &str,
1161 files: Option<&HashMap<PathBuf, String>>,
1162) -> Vec<Completion> {
1163 let mut field_ty: Option<TypeRef> = None;
1164 for_each_unit(doc_text, files, |unit| {
1165 let items = match unit {
1166 SourceUnit::Commons(c) => &c.items,
1167 SourceUnit::Context(c) => &c.items,
1168 SourceUnit::Adapter(a) => &a.items,
1169 _ => return,
1170 };
1171 for item in items {
1172 if let CommonsItem::Type(t) = item
1173 && t.name.name == sum_name
1174 && let TypeBody::Sum(s) = &t.body
1175 && let Some(v) = s.variants.iter().find(|v| v.name.name == variant)
1176 && let Some(f) = v.payload.first()
1177 {
1178 field_ty = Some(f.type_ref.clone());
1179 }
1180 }
1181 });
1182 match field_ty {
1183 Some(tr) => variants_for_type_ref(&tr, doc_text, files),
1184 None => Vec::new(),
1185 }
1186}
1187
1188fn variants_for_type_ref(
1191 tr: &TypeRef,
1192 doc_text: &str,
1193 files: Option<&HashMap<PathBuf, String>>,
1194) -> Vec<Completion> {
1195 match tr {
1196 TypeRef::Named(id) => sum_type_variants(&id.name, doc_text, files),
1197 TypeRef::Result(..) => built_in_variants(&["Ok", "Err"], "Result"),
1198 TypeRef::Option(..) => built_in_variants(&["Some", "None"], "Option"),
1199 _ => Vec::new(),
1200 }
1201}
1202
1203fn protocol_candidates() -> Vec<Completion> {
1205 ["http", "cron", "queue", "websocket"]
1206 .into_iter()
1207 .map(|p| Completion::item(p, CompletionKind::Keyword, Some("service protocol".into())))
1208 .collect()
1209}
1210
1211fn handler_kind_candidates() -> Vec<Completion> {
1213 [
1214 "call", "GET", "POST", "PUT", "PATCH", "DELETE", "schedule", "message", "open", "close",
1215 "rejected",
1216 ]
1217 .into_iter()
1218 .map(|k| Completion::item(k, CompletionKind::Keyword, Some("handler kind".into())))
1219 .collect()
1220}
1221
1222fn export_kind_candidates() -> Vec<Completion> {
1224 ["capability", "transparent", "opaque"]
1225 .into_iter()
1226 .map(|k| Completion::item(k, CompletionKind::Keyword, Some("export kind".into())))
1227 .collect()
1228}
1229
1230fn predicate_name_candidates() -> Vec<Completion> {
1234 [
1235 "Matches",
1236 "InRange",
1237 "MinLength",
1238 "MaxLength",
1239 "Length",
1240 "NonNegative",
1241 "Positive",
1242 "NonEmpty",
1243 ]
1244 .into_iter()
1245 .map(|k| {
1246 Completion::item(
1247 k,
1248 CompletionKind::Keyword,
1249 Some("refinement predicate".into()),
1250 )
1251 })
1252 .collect()
1253}
1254
1255fn actor_candidates(doc_text: &str, files: Option<&HashMap<PathBuf, String>>) -> Vec<Completion> {
1257 let mut out: Vec<Completion> = Vec::new();
1258 let in_suite = lexer::tokenize(doc_text).ok().is_some_and(|toks| {
1262 toks.iter()
1263 .find(|t| !matches!(t.kind, lexer::TokenKind::Comment))
1264 .is_some_and(|t| t.kind == lexer::TokenKind::Suite)
1265 });
1266 if in_suite {
1267 for (name, doc) in crate::symbols::RESERVED_PRINCIPALS {
1268 out.push(
1269 Completion::item(
1270 *name,
1271 CompletionKind::Type,
1272 Some("reserved test principal".into()),
1273 )
1274 .with_doc(Some(doc)),
1275 );
1276 }
1277 }
1278 let mut seen: BTreeSet<String> = BTreeSet::new();
1279 for_each_unit(doc_text, files, |unit| {
1280 let items = match unit {
1281 SourceUnit::Commons(c) => &c.items,
1282 SourceUnit::Context(c) => &c.items,
1283 SourceUnit::Adapter(a) => &a.items,
1284 _ => return,
1285 };
1286 for item in items {
1287 if let CommonsItem::Actor(a) = item
1288 && seen.insert(a.name.name.clone())
1289 {
1290 out.push(Completion::item(
1291 a.name.name.clone(),
1292 CompletionKind::Type,
1293 Some("actor".into()),
1294 ));
1295 }
1296 }
1297 });
1298 out
1299}
1300
1301pub const BUILTIN_STATICS: &[(&str, &[(&str, &str)])] = &[
1308 ("Int", &[("parse", "parse(s: String) -> Option[Int]")]),
1309 ("Float", &[("parse", "parse(s: String) -> Option[Float]")]),
1310 (
1311 "Json",
1312 &[
1313 ("encode", "encode(value) -> String"),
1314 ("decode", "decode[T](s: String) -> Result[T, JsonError]"),
1315 ],
1316 ),
1317 ("List", &[("empty", "empty() -> List[T]")]),
1318 ("Map", &[("empty", "empty() -> Map[K, V]")]),
1319 ("Effect", &[("pure", "pure(value) -> Effect[T]")]),
1320 (
1321 "Bytes",
1322 &[
1323 ("fromUtf8", "fromUtf8(s: String) -> Bytes"),
1324 ("fromBase64", "fromBase64(s: String) -> Option[Bytes]"),
1325 ("fromHex", "fromHex(s: String) -> Option[Bytes]"),
1326 ("fromBase64Url", "fromBase64Url(s: String) -> Option[Bytes]"),
1327 ("empty", "empty() -> Bytes"),
1328 ],
1329 ),
1330];
1331
1332fn builtin_sum_variants(receiver: &str) -> Vec<(String, String)> {
1336 match receiver {
1337 "HttpResult" => bynk_syntax::ast::HTTP_VARIANTS
1338 .iter()
1339 .map(|v| {
1340 (
1341 v.name.to_string(),
1342 if v.status == bynk_syntax::ast::AUTHOR_CHOSEN_STATUS {
1343 "variant of `HttpResult` (4xx/5xx, author-chosen)".to_string()
1344 } else {
1345 format!("variant of `HttpResult` ({})", v.status)
1346 },
1347 )
1348 })
1349 .collect(),
1350 "QueueResult" => bynk_syntax::ast::QUEUE_VARIANTS
1351 .iter()
1352 .map(|v| (v.name.to_string(), "variant of `QueueResult`".to_string()))
1353 .collect(),
1354 _ => Vec::new(),
1355 }
1356}
1357
1358fn member_candidates(
1364 receiver: &str,
1365 doc_text: &str,
1366 files: Option<&HashMap<PathBuf, String>>,
1367) -> Vec<Completion> {
1368 member_candidates_from(receiver, doc_text, files, &mut BTreeSet::new())
1369}
1370
1371fn member_candidates_from(
1376 receiver: &str,
1377 doc_text: &str,
1378 files: Option<&HashMap<PathBuf, String>>,
1379 followed: &mut BTreeSet<String>,
1380) -> Vec<Completion> {
1381 followed.insert(receiver.to_string());
1382 if let Some((_, statics)) = BUILTIN_STATICS.iter().find(|(name, _)| *name == receiver) {
1383 return statics
1384 .iter()
1385 .map(|(label, sig)| {
1386 Completion::item(*label, CompletionKind::Member, Some(sig.to_string()))
1387 })
1388 .collect();
1389 }
1390 let mut out: Vec<Completion> = Vec::new();
1391 let mut seen: BTreeSet<String> = BTreeSet::new();
1392 let mut derived_base: Option<String> = None;
1397 for (label, detail) in builtin_sum_variants(receiver) {
1398 if seen.insert(label.clone()) {
1399 out.push(Completion::item(
1400 label,
1401 CompletionKind::Variant,
1402 Some(detail),
1403 ));
1404 }
1405 }
1406 for_each_unit(doc_text, files, |unit| {
1407 let items = match unit {
1408 SourceUnit::Commons(c) => &c.items,
1409 SourceUnit::Context(c) => &c.items,
1410 SourceUnit::Adapter(a) => &a.items,
1411 _ => return,
1412 };
1413 for item in items {
1414 match item {
1415 CommonsItem::Type(t) if t.name.name == receiver => match &t.body {
1416 bynk_syntax::ast::TypeBody::Sum(s) => {
1417 for v in &s.variants {
1418 if seen.insert(v.name.name.clone()) {
1419 out.push(
1420 Completion::item(
1421 v.name.name.clone(),
1422 CompletionKind::Variant,
1423 Some(format!("variant of `{receiver}`")),
1424 )
1425 .with_doc(v.documentation.as_deref()),
1426 );
1427 }
1428 }
1429 }
1430 bynk_syntax::ast::TypeBody::Refined { .. }
1431 | bynk_syntax::ast::TypeBody::Opaque { .. } => {
1432 for (label, sig) in [
1433 (
1434 "of",
1435 format!("of(value) -> Result[{receiver}, ValidationError]"),
1436 ),
1437 ("unsafe", format!("unsafe(value) -> {receiver}")),
1438 ] {
1439 if seen.insert(label.to_string()) {
1440 out.push(Completion::item(
1441 label,
1442 CompletionKind::Member,
1443 Some(sig),
1444 ));
1445 }
1446 }
1447 }
1448 _ => {}
1452 },
1453 CommonsItem::Capability(c) if c.name.name == receiver && c.derived.is_some() => {
1454 derived_base = c.derived.as_ref().map(|d| d.base.name.clone());
1455 }
1456 CommonsItem::Capability(c) if c.name.name == receiver => {
1457 for op in &c.ops {
1458 if seen.insert(op.name.name.clone()) {
1459 let params = op
1463 .params
1464 .iter()
1465 .map(|p| format!("{}: {}", p.name.name, type_ref_str(&p.type_ref)))
1466 .collect::<Vec<_>>()
1467 .join(", ");
1468 let type_params = if op.type_params.is_empty() {
1470 String::new()
1471 } else {
1472 let names: Vec<&str> = op
1473 .type_params
1474 .iter()
1475 .map(|tp| tp.name.name.as_str())
1476 .collect();
1477 format!("[{}]", names.join(", "))
1478 };
1479 out.push(Completion::item(
1480 op.name.name.clone(),
1481 CompletionKind::Member,
1482 Some(format!(
1483 "{}{type_params}({params}) -> {} — operation of `{receiver}`",
1484 op.name.name,
1485 type_ref_str(&op.return_type)
1486 )),
1487 ));
1488 }
1489 }
1490 }
1491 _ => {}
1492 }
1493 }
1494 });
1495 if let Some(base) = derived_base.filter(|b| !followed.contains(b)) {
1496 for c in member_candidates_from(&base, doc_text, files, followed) {
1497 if seen.insert(c.label.clone()) {
1498 out.push(c);
1499 }
1500 }
1501 }
1502 out
1503}
1504
1505const BUILTIN_TYPES: &[&str] = &[
1511 bynk_check::builtin_names::types::INT,
1512 "Bool",
1513 bynk_check::builtin_names::types::FLOAT,
1514 "String",
1515 "Option",
1516 "Result",
1517 "Effect",
1518 bynk_check::builtin_names::types::LIST,
1519 bynk_check::builtin_names::types::MAP,
1520];
1521
1522pub const SNIPPETS: &[(&str, &str)] = &[
1525 ("context", "context ${1:name} {\n\t$0\n}"),
1527 ("commons", "commons ${1:my.lib}\n\n$0"),
1528 (
1529 "adapter",
1530 "adapter ${1:name} {\n\tbinding \"${2:./module}\"\n\t$0\n}",
1531 ),
1532 ("uses", "uses ${1:module}"),
1534 ("consumes", "consumes ${1:bynk} { ${2:Random} }"),
1535 (
1537 "type record",
1538 "type ${1:Name} = {\n\t${2:field}: ${3:Int},\n}",
1539 ),
1540 ("type enum", "type ${1:Name} = enum {\n\t${2:Variant},\n}"),
1541 (
1542 "type refined",
1543 "type ${1:Name} = ${2:String} where ${3:MinLength(1)}",
1544 ),
1545 (
1546 "type opaque",
1547 "type ${1:Name} = opaque ${2:Int} where ${3:NonNegative}",
1548 ),
1549 (
1551 "fn",
1552 "fn ${1:name}(${2:x}: ${3:Int}) -> ${4:Int} {\n\t$0\n}",
1553 ),
1554 (
1555 "fn contract",
1556 "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}",
1557 ),
1558 (
1560 "capability",
1561 "capability ${1:Name} {\n\tfn ${2:op}() -> Effect[${3:Unit}]\n}",
1562 ),
1563 (
1564 "provides",
1565 "provides ${1:Cap} = ${2:Impl} {\n\tfn ${3:op}(${4}) -> Effect[${5:()}] {\n\t\tEffect.pure(${6:()})\n\t}\n}",
1566 ),
1567 (
1569 "actor",
1570 "actor ${1:Name} { auth = ${2:Bearer(secret = \"AUTH_JWT_SECRET\")}, identity = ${3:UserId} }",
1571 ),
1572 (
1573 "agent",
1574 "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}",
1575 ),
1576 (
1578 "service",
1579 "service ${1:name} {\n\ton call(${2}) -> Effect[${3:Unit}] {\n\t\t$0\n\t}\n}",
1580 ),
1581 ("on call", "on call(${1}) -> Effect[${2:Unit}] {\n\t$0\n}"),
1582 (
1583 "on http",
1584 "on ${1|GET,POST,PUT,DELETE,PATCH|}(\"${2:/path}\") (${3:body}: ${4:Req}) -> Effect[HttpResult[${5:Res}]] given ${6:Cap} {\n\t$0\n}",
1585 ),
1586 (
1587 "on cron",
1588 "on schedule(\"${1:0 * * * *}\") () -> Effect[Result[(), String]] {\n\t$0\n\tOk(())\n}",
1589 ),
1590 (
1592 "suite",
1593 "suite ${1:target}\n\ncase \"${2:it works}\" {\n\tlet ${3:actual} = ${4:0}\n\texpect ${5:actual == 0}\n}",
1594 ),
1595 (
1596 "property",
1597 "property \"${1:invariant holds}\" {\n\tfor all ${2:x}: ${3:Int} {\n\t\texpect ${4:x == x}\n\t}\n}",
1598 ),
1599 ("advance Clock by", "advance Clock by ${1:15.minutes}"),
1601];
1602
1603const CONSTRUCTORS: &[&str] = &["Ok", "Err", "Some", "None", "true", "false"];
1608
1609fn expression_candidates(
1615 doc_text: &str,
1616 files: Option<&HashMap<PathBuf, String>>,
1617) -> Vec<Completion> {
1618 let mut out: Vec<Completion> = CONSTRUCTORS
1619 .iter()
1620 .map(|&name| {
1621 Completion::item(
1622 name,
1623 CompletionKind::Constructor,
1624 keyword_doc(name).map(str::to_string),
1625 )
1626 })
1627 .collect();
1628 out.extend(type_candidates(doc_text, files));
1631 out.extend(free_function_candidates(doc_text, files));
1635 out
1636}
1637
1638fn unit_items_and_uses(unit: &SourceUnit) -> (&[CommonsItem], &[UsesDecl]) {
1641 match unit {
1642 SourceUnit::Commons(c) => (&c.items, &c.uses),
1643 SourceUnit::Context(c) => (&c.items, &c.uses),
1644 SourceUnit::Adapter(a) => (&a.items, &a.uses),
1645 _ => (&[], &[]),
1646 }
1647}
1648
1649fn current_unit_name(doc_text: &str) -> Option<String> {
1653 let tokens = lexer::tokenize(doc_text).ok()?;
1654 let (unit, _errs) = parser::parse_unit_with_recovery(&tokens, doc_text);
1655 Some(unit?.name().joined())
1656}
1657
1658fn experimental(path: &str) -> bool {
1660 bynk_check::firstparty::stability_of(path)
1661 == Some(bynk_check::firstparty::Stability::Experimental)
1662}
1663
1664fn stability_suffix(experimental: bool) -> &'static str {
1667 if experimental {
1668 " — experimental"
1669 } else {
1670 ""
1671 }
1672}
1673
1674fn free_fn_signature(name: &str, f: &bynk_syntax::ast::FnDecl) -> String {
1678 let params = f
1679 .params
1680 .iter()
1681 .map(|p| format!("{}: {}", p.name.name, type_ref_str(&p.type_ref)))
1682 .collect::<Vec<_>>()
1683 .join(", ");
1684 format!("{name}({params}) -> {}", type_ref_str(&f.return_type))
1685}
1686
1687fn free_function_candidates(
1694 doc_text: &str,
1695 files: Option<&HashMap<PathBuf, String>>,
1696) -> Vec<Completion> {
1697 let Some(current) = current_unit_name(doc_text) else {
1698 return Vec::new();
1699 };
1700 struct UnitFns {
1703 name: String,
1704 fns: Vec<(String, String)>,
1705 assets: Vec<(String, String, Option<String>)>,
1706 embeds: Vec<(String, String, Option<String>)>,
1707 uses: Vec<String>,
1708 }
1709 let mut units: Vec<UnitFns> = Vec::new();
1710 for_each_unit(doc_text, files, |unit| {
1711 let (items, uses) = unit_items_and_uses(unit);
1712 let fns = items
1713 .iter()
1714 .filter_map(|it| match it {
1715 CommonsItem::Fn(f) => match &f.name {
1716 FnName::Free(id) => Some((id.name.clone(), free_fn_signature(&id.name, f))),
1717 FnName::Method { .. } => None,
1718 },
1719 _ => None,
1720 })
1721 .collect();
1722 let assets = items
1723 .iter()
1724 .filter_map(|it| match it {
1725 CommonsItem::Asset(a) => Some((
1726 a.name.name.clone(),
1727 format!(
1728 "asset {} = \"{}\"{}",
1729 a.name.name,
1730 bynk_fmt::escape_string(&a.path),
1731 a.at.as_ref()
1732 .map(|(p, _)| format!(" at \"{}\"", bynk_fmt::escape_string(p)))
1733 .unwrap_or_default()
1734 ),
1735 a.documentation.as_deref().map(str::to_string),
1736 )),
1737 _ => None,
1738 })
1739 .collect();
1740 let embeds = items
1742 .iter()
1743 .filter_map(|it| match it {
1744 CommonsItem::Embed(e) => Some((
1745 e.name.name.clone(),
1746 format!(
1747 "embed {} = \"{}\"",
1748 e.name.name,
1749 bynk_fmt::escape_string(&e.path)
1750 ),
1751 e.documentation.as_deref().map(str::to_string),
1752 )),
1753 _ => None,
1754 })
1755 .collect();
1756 units.push(UnitFns {
1757 name: unit.name().joined(),
1758 fns,
1759 assets,
1760 embeds,
1761 uses: uses.iter().map(|u| u.target.joined()).collect(),
1762 });
1763 });
1764 let mut imported: BTreeSet<String> = BTreeSet::new();
1767 for u in &units {
1768 if u.name == current {
1769 imported.extend(u.uses.iter().cloned());
1770 }
1771 }
1772 let mut out: Vec<Completion> = Vec::new();
1774 let mut seen: BTreeSet<String> = BTreeSet::new();
1775 for u in &units {
1776 let own = u.name == current;
1777 if !own && !imported.contains(&u.name) {
1778 continue;
1779 }
1780 let origin = if own { "this unit" } else { u.name.as_str() };
1781 for (name, sig) in &u.fns {
1782 if seen.insert(name.clone()) {
1783 let marked = !own && experimental(&format!("{}.{name}", u.name));
1784 out.push(Completion::item(
1785 name.clone(),
1786 CompletionKind::Function,
1787 Some(format!("{sig} — `{origin}`{}", stability_suffix(marked))),
1788 ));
1789 }
1790 }
1791 for (name, decl, doc) in u.assets.iter().chain(&u.embeds) {
1792 if seen.insert(name.clone()) {
1793 out.push(
1794 Completion::item(
1795 name.clone(),
1796 CompletionKind::Constant,
1797 Some(format!("{decl} — `{origin}`")),
1798 )
1799 .with_doc(doc.as_deref()),
1800 );
1801 }
1802 }
1803 }
1804 out
1805}
1806
1807pub fn keyword_doc(word: &str) -> Option<&'static str> {
1811 keywords::KEYWORDS
1812 .iter()
1813 .find(|k| k.word == word)
1814 .map(|k| k.meaning)
1815}
1816
1817fn type_candidates(doc_text: &str, files: Option<&HashMap<PathBuf, String>>) -> Vec<Completion> {
1821 let mut out: Vec<Completion> = Vec::new();
1822 let mut seen: BTreeSet<String> = BTreeSet::new();
1823 for &name in BUILTIN_TYPES {
1824 if seen.insert(name.to_string()) {
1825 let detail = keyword_doc(name)
1826 .map(str::to_string)
1827 .or_else(|| match name {
1828 "List" => Some("The built-in list type, `List[T]`.".to_string()),
1829 "Map" => Some("The built-in map type, `Map[K, V]`.".to_string()),
1830 _ => Some("built-in type".to_string()),
1831 });
1832 out.push(Completion::item(name, CompletionKind::Type, detail));
1833 }
1834 }
1835 for_each_unit(doc_text, files, |unit| {
1836 let items = match unit {
1837 SourceUnit::Commons(c) => &c.items,
1838 SourceUnit::Context(c) => &c.items,
1839 SourceUnit::Adapter(a) => &a.items,
1840 _ => return,
1841 };
1842 for item in items {
1843 if let CommonsItem::Type(t) = item
1844 && seen.insert(t.name.name.clone())
1845 {
1846 out.push(Completion::item(
1847 t.name.name.clone(),
1848 CompletionKind::Type,
1849 Some("type".to_string()),
1850 ));
1851 }
1852 }
1853 });
1854 out
1855}
1856
1857fn keyword_and_snippet_candidates() -> Vec<Completion> {
1862 let mut out: Vec<Completion> = keywords::KEYWORDS
1863 .iter()
1864 .filter(|k| k.word.chars().next().is_some_and(char::is_lowercase))
1865 .map(|k| Completion::item(k.word, CompletionKind::Keyword, Some(k.meaning.to_string())))
1866 .collect();
1867 for &(label, body) in SNIPPETS {
1868 out.push(Completion::snippet(label, body));
1869 }
1870 out
1871}
1872
1873fn parse_source_unit(src: &str) -> Option<SourceUnit> {
1878 let tokens = lexer::tokenize(src).ok()?;
1879 parser::parse_unit_with_recovery(&tokens, src).0
1880}
1881
1882static EMBEDDED_UNITS: LazyLock<Vec<Arc<SourceUnit>>> = LazyLock::new(|| {
1891 bynk_check::firstparty::FIRSTPARTY_SOURCES
1895 .iter()
1896 .filter_map(|(_, src)| parse_source_unit(src).map(Arc::new))
1897 .collect()
1898});
1899
1900pub(crate) fn embedded_capability(
1904 unit: &str,
1905 cap: &str,
1906) -> Option<bynk_syntax::ast::CapabilityDecl> {
1907 EMBEDDED_UNITS.iter().find_map(|u| {
1908 if u.name().joined() != unit {
1909 return None;
1910 }
1911 let SourceUnit::Adapter(a) = u.as_ref() else {
1912 return None;
1913 };
1914 a.items.iter().find_map(|item| match item {
1915 CommonsItem::Capability(c) if c.name.name == cap => Some(c.clone()),
1916 _ => None,
1917 })
1918 })
1919}
1920
1921fn cached_project_unit(path: &Path, content: &str) -> Option<Arc<SourceUnit>> {
1942 let (_file_id, result) = bynk_project::parse_cache::cached_parse(path, content);
1943 match result {
1944 Ok((units, _warnings)) => units.first().cloned().map(Arc::new),
1945 Err(_) => parse_source_unit(content).map(Arc::new),
1946 }
1947}
1948
1949pub fn for_each_unit(
1965 doc_text: &str,
1966 files: Option<&HashMap<PathBuf, String>>,
1967 mut f: impl FnMut(&SourceUnit),
1968) {
1969 for unit in EMBEDDED_UNITS.iter() {
1970 f(unit);
1971 }
1972 if let Some(unit) = parse_source_unit(doc_text) {
1973 f(&unit);
1974 }
1975 if let Some(content) = files {
1980 for (path, text) in content {
1981 if let Some(unit) = cached_project_unit(path, text) {
1982 f(&unit);
1983 }
1984 }
1985 }
1986}
1987
1988fn split_dependency_files<'f>(
1994 files: Option<&'f HashMap<PathBuf, String>>,
1995 project_root: Option<&Path>,
1996) -> SplitFiles<'f> {
1997 let Some(content) = files else {
1998 return SplitFiles::default();
1999 };
2000 let view = match project_root {
2001 Some(root) => crate::packages::DependencyView::for_root(root, content),
2002 None => crate::packages::DependencyView::from_files(content),
2003 };
2004 let Some(view) = view else {
2005 return SplitFiles {
2006 own: Some(Cow::Borrowed(content)),
2007 deps: Vec::new(),
2008 };
2009 };
2010 let mut own = HashMap::new();
2011 let mut deps = Vec::new();
2012 let mut keys: Vec<&PathBuf> = content.keys().collect();
2013 keys.sort();
2014 for path in keys {
2015 let text = &content[path];
2016 match view.package_of(path) {
2017 None => {
2018 own.insert(path.clone(), text.clone());
2019 }
2020 Some(id) => {
2021 if let Some(unit) = cached_project_unit(path, text) {
2022 for alias in view.aliases_of(id) {
2023 let declared = unit.name().joined();
2024 if view.exports(id, &declared) {
2025 deps.push((format!("{alias}.{declared}"), (*unit).clone()));
2026 }
2027 }
2028 }
2029 }
2030 }
2031 }
2032 SplitFiles {
2033 own: Some(Cow::Owned(own)),
2034 deps,
2035 }
2036}
2037
2038#[derive(Default)]
2042struct SplitFiles<'f> {
2043 own: Option<Cow<'f, HashMap<PathBuf, String>>>,
2044 deps: Vec<(String, SourceUnit)>,
2045}
2046
2047impl SplitFiles<'_> {
2048 fn own(&self) -> Option<&HashMap<PathBuf, String>> {
2049 self.own.as_deref()
2050 }
2051}
2052
2053fn usable_commons(doc_text: &str, split: &SplitFiles<'_>) -> Vec<Completion> {
2056 let mut seen: BTreeSet<String> = BTreeSet::new();
2057 let mut out: Vec<Completion> = Vec::new();
2058 for_each_unit(doc_text, split.own(), |unit| {
2059 if let SourceUnit::Commons(c) = unit {
2060 let name = c.name.joined();
2061 if seen.insert(name.clone()) {
2062 out.push(Completion::item(
2063 name,
2064 CompletionKind::Unit,
2065 Some("commons".to_string()),
2066 ));
2067 }
2068 }
2069 });
2070 for (label, unit) in &split.deps {
2071 if matches!(unit, SourceUnit::Commons(_)) && seen.insert(label.clone()) {
2072 out.push(Completion::item(
2073 label,
2074 CompletionKind::Unit,
2075 Some("commons from a dependency".to_string()),
2076 ));
2077 }
2078 }
2079 out
2080}
2081
2082fn consumable_units(doc_text: &str, split: &SplitFiles<'_>) -> Vec<Completion> {
2086 let files = split.own();
2087 let mut seen: BTreeSet<String> = BTreeSet::new();
2088 let mut out: Vec<Completion> = Vec::new();
2089 for (label, unit) in &split.deps {
2090 if matches!(unit, SourceUnit::Adapter(_)) && seen.insert(label.clone()) {
2091 out.push(Completion::item(
2092 label.clone(),
2093 CompletionKind::Unit,
2094 Some("adapter from a dependency".to_string()),
2095 ));
2096 }
2097 }
2098 for_each_unit(doc_text, files, |unit| {
2099 let (name, kind) = match unit {
2100 SourceUnit::Context(c) => (c.name.joined(), "context"),
2101 SourceUnit::Adapter(a) => (a.name.joined(), "adapter"),
2102 _ => return,
2103 };
2104 if seen.insert(name.clone()) {
2105 out.push(Completion::item(
2106 name,
2107 CompletionKind::Unit,
2108 Some(kind.to_string()),
2109 ));
2110 }
2111 });
2112 out
2113}
2114
2115fn capabilities_of_unit(unit: &str, doc_text: &str, split: &SplitFiles<'_>) -> Vec<String> {
2121 if let Some((_, dep)) = split.deps.iter().find(|(label, _)| label == unit) {
2124 let mut out: BTreeSet<String> = BTreeSet::new();
2125 if let SourceUnit::Adapter(a) = dep {
2126 for clause in &a.exports {
2127 if clause.kind == ExportKind::Capability {
2128 for n in clause.names.iter().map(|e| &e.name) {
2129 out.insert(n.name.clone());
2130 }
2131 }
2132 }
2133 }
2134 return out.into_iter().collect();
2135 }
2136 let files = split.own();
2137 let mut out: BTreeSet<String> = BTreeSet::new();
2138 let mut found = false;
2139 for_each_unit(doc_text, files, |u| {
2140 if found {
2141 return;
2142 }
2143 let (name, exports) = match u {
2144 SourceUnit::Context(c) => (c.name.joined(), &c.exports),
2145 SourceUnit::Adapter(a) => (a.name.joined(), &a.exports),
2146 _ => return,
2147 };
2148 if name != unit {
2149 return;
2150 }
2151 found = true;
2152 for clause in exports {
2153 if clause.kind == ExportKind::Capability {
2154 for n in clause.names.iter().map(|e| &e.name) {
2155 out.insert(n.name.clone());
2156 }
2157 }
2158 }
2159 });
2160 out.into_iter().collect()
2161}
2162
2163fn in_scope_capabilities(doc_text: &str, split: &SplitFiles<'_>) -> Vec<Completion> {
2167 let mut labels: BTreeSet<String> = BTreeSet::new();
2168 let Ok(tokens) = lexer::tokenize(doc_text) else {
2169 return Vec::new();
2170 };
2171 let (Some(unit), _errs) = parser::parse_unit_with_recovery(&tokens, doc_text) else {
2172 return Vec::new();
2173 };
2174 let (items, consumes) = match &unit {
2175 SourceUnit::Context(c) => (&c.items, &c.consumes),
2176 SourceUnit::Adapter(a) => (&a.items, &EMPTY_CONSUMES),
2177 _ => return Vec::new(),
2178 };
2179 for item in items {
2181 if let bynk_syntax::ast::CommonsItem::Capability(c) = item {
2182 labels.insert(c.name.name.clone());
2183 }
2184 }
2185 for c in consumes {
2187 let unit_name = c.target.joined();
2188 match &c.selected {
2189 Some(names) => {
2190 for n in names {
2191 labels.insert(n.name.clone());
2192 }
2193 }
2194 None => {
2195 let prefix = c
2196 .alias
2197 .as_ref()
2198 .map(|a| a.name.clone())
2199 .unwrap_or_else(|| unit_name.clone());
2200 for cap in capabilities_of_unit(&unit_name, doc_text, split) {
2201 labels.insert(format!("{prefix}.{cap}"));
2202 }
2203 }
2204 }
2205 }
2206 labels
2207 .into_iter()
2208 .map(|label| {
2209 Completion::item(
2210 label,
2211 CompletionKind::Capability,
2212 Some("capability in scope".to_string()),
2213 )
2214 })
2215 .collect()
2216}
2217
2218pub fn value_receiver_rewrite(text: &str, offset: usize) -> Option<(String, usize)> {
2230 let prefix = text.get(..offset)?;
2231 let head = prefix
2232 .trim_end_matches(|c: char| c.is_alphanumeric() || c == '_')
2233 .strip_suffix('.')?;
2234 let (recv, start) = ident_ending_at(head, head.len())?;
2235 let first = recv.chars().next()?;
2236 if !(first.is_ascii_lowercase() || first == '_') {
2237 return None; }
2239 if head[..start].ends_with('.') {
2240 return None; }
2242 let dot = head.len(); let rewritten = format!("{}{}", &text[..dot], &text[offset..]);
2244 Some((rewritten, dot.saturating_sub(1)))
2245}
2246
2247pub fn ident_ending_at(text: &str, end: usize) -> Option<(&str, usize)> {
2258 let before = text.get(..end)?;
2259 let start = before
2260 .char_indices()
2261 .rfind(|&(_, c)| !(c.is_alphanumeric() || c == '_'))
2262 .map_or(0, |(i, c)| i + c.len_utf8());
2263 let name = &before[start..];
2264 (!name.is_empty()).then_some((name, start))
2265}
2266
2267pub fn value_member_candidates(
2270 ty: TyId,
2271 tys: &Types,
2272 doc_text: &str,
2273 files: Option<&HashMap<PathBuf, String>>,
2274) -> Vec<Completion> {
2275 let mut out: Vec<Completion> = kernel_methods::methods_for(ty, tys)
2276 .iter()
2277 .map(|km| {
2278 Completion::item(
2279 km.name,
2280 CompletionKind::Member,
2281 Some(km.signature.to_string()),
2282 )
2283 })
2284 .collect();
2285 if matches!(&*tys.get(ty), Ty::Asset) {
2287 out.push(Completion::item(
2288 "url",
2289 CompletionKind::Field,
2290 Some("String — the URL the platform serves the asset at".to_string()),
2291 ));
2292 }
2293 if let Ty::Named { name, .. } = &*tys.get(ty) {
2295 let mut seen: BTreeSet<String> = BTreeSet::new();
2296 for_each_unit(doc_text, files, |unit| {
2297 let items = match unit {
2298 SourceUnit::Commons(c) => &c.items,
2299 SourceUnit::Context(c) => &c.items,
2300 SourceUnit::Adapter(a) => &a.items,
2301 _ => return,
2302 };
2303 for item in items {
2304 if let CommonsItem::Type(t) = item
2305 && &t.name.name == name
2306 && let TypeBody::Record(r) = &t.body
2307 {
2308 for f in &r.fields {
2309 if seen.insert(f.name.name.clone()) {
2310 out.push(
2311 Completion::item(
2312 f.name.name.clone(),
2313 CompletionKind::Field,
2314 Some(format!("field of `{name}`")),
2315 )
2316 .with_doc(f.documentation.as_deref()),
2317 );
2318 }
2319 }
2320 }
2321 }
2322 });
2323 }
2324 out
2325}
2326
2327pub fn run_id_member_candidates(rewritten: &str, recv_offset: usize) -> Vec<Completion> {
2332 let Some((name, start)) = ident_ending_at(rewritten, recv_offset + 1) else {
2333 return Vec::new();
2334 };
2335 if name != "run" || !crate::symbols::is_test_run_receiver(rewritten, start) {
2336 return Vec::new();
2337 }
2338 vec![
2339 Completion::item("id", CompletionKind::Field, Some("String".to_string()))
2340 .with_doc(Some(crate::symbols::RUN_ID_DOC)),
2341 ]
2342}
2343
2344pub fn store_field_member_candidates(
2359 rewritten: &str,
2360 recv_offset: usize,
2361 locals: &[LocalBinding],
2362) -> Vec<Completion> {
2363 let Some((kind_head, held)) =
2364 crate::symbols::store_field_kind_at(rewritten, recv_offset + 1, locals)
2365 else {
2366 return Vec::new();
2367 };
2368 let mut out: Vec<Completion> = store_ops::ops_for(&kind_head)
2369 .iter()
2370 .map(|o| {
2371 Completion::item(
2372 o.name,
2373 CompletionKind::Member,
2374 Some(o.signature.to_string()),
2375 )
2376 })
2377 .collect();
2378 if kind_head == "Map" && !held {
2379 out.extend(store_ops::MAP_QUERY_ACCESSORS.iter().map(|a| {
2380 Completion::item(a.name, CompletionKind::Field, Some(a.signature.to_string()))
2381 }));
2382 }
2383 out
2384}
2385
2386static EMPTY_CONSUMES: Vec<bynk_syntax::ast::ConsumesDecl> = Vec::new();
2387
2388#[cfg(test)]
2389mod tests {
2390 use super::*;
2391 use bynk_check::firstparty::BYNK_LIST_SRC;
2392
2393 #[test]
2396 fn completion_marks_experimental_items() {
2397 assert_eq!(stability_suffix(true), " — experimental");
2398 assert_eq!(stability_suffix(false), "");
2399 assert!(!experimental("bynk.cloudflare.Kv"));
2400 let caps = complete("consumes bynk.cloudflare { ", "context app.probe\n", None);
2401 let kv = caps
2402 .iter()
2403 .find(|c| c.label == "Kv")
2404 .expect("Kv is offered");
2405 assert_eq!(
2406 kv.detail.as_deref(),
2407 Some("capability exported by `bynk.cloudflare`")
2408 );
2409 }
2410
2411 fn labels(line: &str, doc: &str) -> Vec<String> {
2412 complete(line, doc, None)
2413 .into_iter()
2414 .map(|c| c.label)
2415 .collect()
2416 }
2417
2418 #[test]
2419 fn consumes_target_suggests_units_including_bynk() {
2420 let doc = "adapter tokens {\n binding \"./b.ts\"\n capability Jwt { fn f() -> Effect[Int] }\n provides Jwt = X\n}\n";
2422 let got = labels(" consumes ", doc);
2423 assert!(got.contains(&"bynk".to_string()), "{got:?}");
2424 assert!(got.contains(&"tokens".to_string()), "{got:?}");
2425 }
2426
2427 #[test]
2428 fn consumes_brace_suggests_that_units_capabilities() {
2429 let got = labels(" consumes bynk { ", "context a.b\n");
2430 assert!(got.contains(&"Clock".to_string()), "{got:?}");
2432 assert!(got.contains(&"Random".to_string()), "{got:?}");
2433 assert!(got.contains(&"Logger".to_string()), "{got:?}");
2434 }
2435
2436 #[test]
2437 fn given_suggests_local_and_flattened_capabilities() {
2438 let doc = "context a.b\n\
2439 consumes bynk { Clock }\n\
2440 capability Local { fn f() -> Effect[Int] }\n\
2441 service s {\n\
2442 on call() -> Effect[Int] given Clock {\n\
2443 1\n\
2444 }\n\
2445 }\n";
2446 let got = labels(" on call() -> Effect[Int] given ", doc);
2447 assert!(got.contains(&"Clock".to_string()), "flattened: {got:?}");
2448 assert!(got.contains(&"Local".to_string()), "local: {got:?}");
2449 }
2450
2451 #[test]
2452 fn expression_position_offers_constructors_and_types() {
2453 let doc = "commons m {\n type Order = { id: Int }\n}\n";
2458 let items = complete(" let x = ", doc, None);
2459 for &c in CONSTRUCTORS {
2460 assert!(
2461 find(&items, c, CompletionKind::Constructor).is_some(),
2462 "constructor {c}: {:?}",
2463 items.iter().map(|i| &i.label).collect::<Vec<_>>()
2464 );
2465 }
2466 assert!(
2467 find(&items, "Int", CompletionKind::Type).is_some(),
2468 "builtin type"
2469 );
2470 assert!(
2471 find(&items, "Order", CompletionKind::Type).is_some(),
2472 "project type"
2473 );
2474 }
2475
2476 #[test]
2477 fn value_receiver_and_decimal_are_not_expression_positions() {
2478 assert!(complete(" let p = q.", "context a.b\n", None).is_empty());
2482 assert!(complete(" let n = 1.", "context a.b\n", None).is_empty());
2483 }
2484
2485 fn free_fn_names(src: &str) -> Vec<String> {
2487 let tokens = lexer::tokenize(src).unwrap();
2488 let (unit, _) = parser::parse_unit_with_recovery(&tokens, src);
2489 let unit = unit.unwrap();
2490 let (items, _) = unit_items_and_uses(&unit);
2491 items
2492 .iter()
2493 .filter_map(|it| match it {
2494 CommonsItem::Fn(f) => match &f.name {
2495 FnName::Free(id) => Some(id.name.clone()),
2496 FnName::Method { .. } => None,
2497 },
2498 _ => None,
2499 })
2500 .collect()
2501 }
2502
2503 #[test]
2504 fn free_functions_offered_for_own_unit_and_used_modules() {
2505 let doc = "commons app {\n uses bynk.list\n fn helper(x: Int) -> Int { x }\n}\n";
2508 let items = complete(" let y = ", doc, None);
2509 assert!(
2511 find(&items, "helper", CompletionKind::Function).is_some(),
2512 "own fn: {:?}",
2513 items.iter().map(|i| &i.label).collect::<Vec<_>>()
2514 );
2515 for name in free_fn_names(BYNK_LIST_SRC) {
2518 assert!(
2519 find(&items, &name, CompletionKind::Function).is_some(),
2520 "bynk.list.{name}: {:?}",
2521 items.iter().map(|i| &i.label).collect::<Vec<_>>()
2522 );
2523 }
2524 assert!(
2526 find(&items, "values", CompletionKind::Function).is_none(),
2527 "bynk.map.values leaked without `uses bynk.map`"
2528 );
2529 }
2530
2531 #[test]
2532 fn locale_functions_offered_when_bynk_locale_is_used() {
2533 let doc = "commons app {\n uses bynk.locale\n}\n";
2538 let items = complete(" let y = ", doc, None);
2539 for name in [
2540 "render",
2541 "message",
2542 "withText",
2543 "withWhole",
2544 "withNum",
2545 "withMoment",
2546 ] {
2547 assert!(
2548 find(&items, name, CompletionKind::Function).is_some(),
2549 "bynk.locale.{name} missing from completion: {:?}",
2550 items.iter().map(|i| &i.label).collect::<Vec<_>>()
2551 );
2552 }
2553 }
2554
2555 #[test]
2556 fn free_functions_require_a_uses_import() {
2557 let doc = "commons app {\n fn helper(x: Int) -> Int { x }\n}\n";
2559 let items = complete(" let y = ", doc, None);
2560 assert!(find(&items, "helper", CompletionKind::Function).is_some());
2561 for name in ["map", "filter", "reverse"] {
2562 assert!(
2563 find(&items, name, CompletionKind::Function).is_none(),
2564 "bynk.list.{name} offered without `uses bynk.list`"
2565 );
2566 }
2567 }
2568
2569 #[test]
2570 fn assets_offered_by_the_free_function_scope_rule() {
2571 let doc = "context app.page {\n uses app.shared\n asset logo = \"logo.svg\"\n asset icon = \"favicon.ico\" at \"/favicon.ico\"\n}\n";
2575 let mut files = HashMap::new();
2576 files.insert(
2577 PathBuf::from("/p/shared.bynk"),
2578 "context app.shared {\n --- The site stylesheet.\n asset style = \"site.css\"\n}\n"
2579 .to_string(),
2580 );
2581 files.insert(
2582 PathBuf::from("/p/other.bynk"),
2583 "context app.other {\n asset banner = \"banner.png\"\n}\n".to_string(),
2584 );
2585 let items = complete(" let y = ", doc, Some(&files));
2586 let labels = || items.iter().map(|i| &i.label).collect::<Vec<_>>();
2587 let logo = find(&items, "logo", CompletionKind::Constant);
2588 assert!(logo.is_some(), "own asset: {:?}", labels());
2589 assert_eq!(
2590 logo.unwrap().detail.as_deref(),
2591 Some("asset logo = \"logo.svg\" — `this unit`")
2592 );
2593 assert_eq!(
2594 find(&items, "icon", CompletionKind::Constant).and_then(|c| c.detail.as_deref()),
2595 Some("asset icon = \"favicon.ico\" at \"/favicon.ico\" — `this unit`")
2596 );
2597 let style = find(&items, "style", CompletionKind::Constant);
2598 assert!(style.is_some(), "used unit's asset: {:?}", labels());
2599 assert_eq!(
2600 style.unwrap().documentation.as_deref(),
2601 Some("The site stylesheet.")
2602 );
2603 assert!(
2604 find(&items, "banner", CompletionKind::Constant).is_none(),
2605 "an asset of a unit not in `uses` leaked: {:?}",
2606 labels()
2607 );
2608 }
2609
2610 #[test]
2611 fn embeds_offered_by_the_free_function_scope_rule() {
2612 let doc =
2616 "context app.page {\n uses app.shared\n embed shell = \"embeds/shell.html\"\n}\n";
2617 let mut files = HashMap::new();
2618 files.insert(
2619 PathBuf::from("/p/shared.bynk"),
2620 "commons app.shared {\n --| The mail body.\n embed mail = \"embeds/mail.txt\"\n}\n"
2621 .to_string(),
2622 );
2623 files.insert(
2624 PathBuf::from("/p/other.bynk"),
2625 "context app.other {\n embed hidden = \"embeds/hidden.txt\"\n}\n".to_string(),
2626 );
2627 let items = complete(" let y = ", doc, Some(&files));
2628 let labels = || items.iter().map(|i| &i.label).collect::<Vec<_>>();
2629 assert_eq!(
2630 find(&items, "shell", CompletionKind::Constant).and_then(|c| c.detail.as_deref()),
2631 Some("embed shell = \"embeds/shell.html\" — `this unit`"),
2632 "own embed: {:?}",
2633 labels()
2634 );
2635 let mail = find(&items, "mail", CompletionKind::Constant);
2636 assert!(mail.is_some(), "used unit's embed: {:?}", labels());
2637 assert_eq!(
2638 mail.unwrap().documentation.as_deref(),
2639 Some("The mail body.")
2640 );
2641 assert!(
2642 find(&items, "hidden", CompletionKind::Constant).is_none(),
2643 "an embed of a unit not in `uses` leaked: {:?}",
2644 labels()
2645 );
2646 }
2647
2648 #[test]
2649 fn asset_member_completion_offers_url() {
2650 let asset = TYS.intern(Ty::Asset);
2652 let items = value_member_candidates(asset, &TYS, "context a.b\n", None);
2653 assert!(find(&items, "url", CompletionKind::Field).is_some());
2654 }
2655
2656 #[test]
2657 fn member_completion_reaches_inside_an_interpolation_hole() {
2658 let doc = "context a.b\n capability Timer { fn now() -> Effect[Int] }\n";
2662 let in_hole = complete(" \"the time is \\(Timer.", doc, None);
2663 assert!(
2664 find(&in_hole, "now", CompletionKind::Member).is_some(),
2665 "capability op not offered inside a hole: {:?}",
2666 in_hole.iter().map(|c| &c.label).collect::<Vec<_>>()
2667 );
2668 let statics = complete(" \"n=\\(Int.", "context a.b\n", None);
2670 assert!(find(&statics, "parse", CompletionKind::Member).is_some());
2671 }
2672
2673 #[test]
2674 fn consumes_with_as_is_not_a_target_completion() {
2675 assert!(!is_consumes_target("consumes platform.time as "));
2677 assert!(is_consumes_target("consumes platform"));
2678 }
2679
2680 fn find<'a>(
2681 items: &'a [Completion],
2682 label: &str,
2683 kind: CompletionKind,
2684 ) -> Option<&'a Completion> {
2685 items.iter().find(|c| c.label == label && c.kind == kind)
2686 }
2687
2688 #[test]
2689 fn type_annotation_suggests_builtins_surface_and_project_types() {
2690 let doc = "commons m {\n type Order = { id: Int }\n}\n";
2691 let got = labels(" let x: ", doc);
2692 for want in ["Int", "Option", "Result", "Effect", "List", "Map"] {
2695 assert!(got.contains(&want.to_string()), "built-in {want}: {got:?}");
2696 }
2697 assert!(got.contains(&"Uuid".to_string()), "surface: {got:?}");
2698 assert!(got.contains(&"Order".to_string()), "project: {got:?}");
2699 }
2700
2701 #[test]
2702 fn return_type_and_type_args_are_type_positions() {
2703 assert!(is_type_position(" on call() -> "));
2704 assert!(is_type_position(" let x: Option["));
2705 assert!(is_type_position(" let x: Result[Int, "));
2706 assert!(is_type_position(" -> Eff"));
2708 }
2709
2710 #[test]
2711 fn list_literal_is_not_a_type_position() {
2712 assert!(!is_type_position(" let xs = ["));
2714 let items = complete(" let xs = [", "context a.b\n", None);
2718 assert!(
2719 find(&items, "Some", CompletionKind::Constructor).is_some(),
2720 "{:?}",
2721 items.iter().map(|c| &c.label).collect::<Vec<_>>()
2722 );
2723 }
2724
2725 #[test]
2726 fn builtin_type_carries_its_registry_doc() {
2727 let items = complete(" let x: ", "context a.b\n", None);
2728 let int = find(&items, "Int", CompletionKind::Type).expect("Int present");
2729 assert_eq!(int.detail.as_deref(), keyword_doc("Int"));
2730 assert!(int.detail.is_some(), "Int should have a doc");
2731 }
2732
2733 #[test]
2734 fn keyword_position_suggests_keywords_and_snippets() {
2735 let items = complete(" ", "context a.b\n", None);
2736 assert!(find(&items, "capability", CompletionKind::Keyword).is_some());
2738 assert!(find(&items, "fn", CompletionKind::Keyword).is_some());
2739 assert!(find(&items, "let", CompletionKind::Keyword).is_some());
2740 assert!(find(&items, "Int", CompletionKind::Keyword).is_none());
2742 assert!(find(&items, "Some", CompletionKind::Keyword).is_none());
2743 let snip = find(&items, "service", CompletionKind::Snippet).expect("service snippet");
2745 let body = snip.insert_text.as_deref().unwrap_or("");
2746 assert!(body.contains("on call"), "snippet body: {body:?}");
2747 assert!(body.contains("${1"), "snippet tab stop: {body:?}");
2748 }
2749
2750 #[test]
2751 fn keyword_position_fires_on_an_empty_line() {
2752 assert!(is_keyword_position(""));
2753 assert!(is_keyword_position(" cap"));
2754 assert!(!is_keyword_position(" let x ="));
2755 assert!(!is_keyword_position(" x: "));
2756 assert!(!complete("", "context a.b\n", None).is_empty());
2757 }
2758
2759 #[test]
2760 fn member_receiver_is_a_single_upper_ident_before_a_dot() {
2761 assert_eq!(member_receiver(" Color."), Some("Color".to_string()));
2762 assert_eq!(
2763 member_receiver(" let e = Email.o"),
2764 Some("Email".to_string())
2765 );
2766 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); }
2771
2772 #[test]
2773 fn receiver_extraction_survives_a_multibyte_char_before_the_receiver() {
2774 assert_eq!(member_receiver("\"Foo."), Some("Foo".to_string()));
2778 assert_eq!(member_receiver("€Color."), Some("Color".to_string()));
2779 assert_eq!(member_receiver("—Bar."), Some("Bar".to_string()));
2780 assert_eq!(word_before_brace("\"cors {", 6), "cors");
2781 assert_eq!(
2782 record_construction_receiver("\"€Order {"),
2783 Some("Order".to_string())
2784 );
2785 let _ = complete("let x = \"Foo.", "commons m {}\n", None);
2788 let _ = complete(" \"€42.", "commons m {}\n", None);
2789 assert_eq!(
2790 value_receiver_rewrite("\"email.", 7).map(|(_, r)| r),
2791 Some(5),
2792 );
2793 }
2794
2795 #[test]
2796 fn sum_member_suggests_variants() {
2797 let doc = "commons m {\n type Color = enum { Red, Green, Blue }\n}\n";
2798 let items = complete(" let c = Color.", doc, None);
2799 for v in ["Red", "Green", "Blue"] {
2800 assert!(
2801 find(&items, v, CompletionKind::Variant).is_some(),
2802 "variant {v}: {:?}",
2803 items.iter().map(|c| &c.label).collect::<Vec<_>>()
2804 );
2805 }
2806 }
2807
2808 #[test]
2809 fn refined_and_plain_alias_members_are_of_and_unsafe() {
2810 let doc = "commons m {\n type Email = String where NonEmpty\n}\n";
2812 let items = complete(" Email.", doc, None);
2813 assert!(find(&items, "of", CompletionKind::Member).is_some());
2814 assert!(find(&items, "unsafe", CompletionKind::Member).is_some());
2815 let doc = "commons m {\n type Id = Int\n}\n";
2818 assert!(find(&complete(" Id.", doc, None), "of", CompletionKind::Member).is_some());
2819 }
2820
2821 #[test]
2822 fn capability_member_suggests_ops() {
2823 let doc = "context a.b\n capability Timer { fn now() -> Effect[Int]\n fn at(t: Int) -> Effect[()] }\n";
2824 let items = complete(" Timer.", doc, None);
2825 let now = find(&items, "now", CompletionKind::Member).expect("`now` op offered");
2826 assert_eq!(
2829 now.detail.as_deref(),
2830 Some("now() -> Effect[Int] — operation of `Timer`")
2831 );
2832 let at = find(&items, "at", CompletionKind::Member).expect("`at` op offered");
2833 assert_eq!(
2834 at.detail.as_deref(),
2835 Some("at(t: Int) -> Effect[()] — operation of `Timer`")
2836 );
2837 }
2838
2839 #[test]
2840 fn builtin_type_statics_are_offered() {
2841 assert!(
2842 find(
2843 &complete(" Int.", "context a.b\n", None),
2844 "parse",
2845 CompletionKind::Member
2846 )
2847 .is_some()
2848 );
2849 let j = complete(" Json.", "context a.b\n", None);
2850 assert!(find(&j, "encode", CompletionKind::Member).is_some());
2851 assert!(find(&j, "decode", CompletionKind::Member).is_some());
2852 }
2853
2854 #[test]
2855 fn builtin_sum_variants_are_complete() {
2856 let http: Vec<&str> = bynk_syntax::ast::HTTP_VARIANTS
2861 .iter()
2862 .map(|v| v.name)
2863 .collect();
2864 let queue: Vec<&str> = bynk_syntax::ast::QUEUE_VARIANTS
2865 .iter()
2866 .map(|v| v.name)
2867 .collect();
2868 for (recv, names) in [("HttpResult", http), ("QueueResult", queue)] {
2869 let items = complete(&format!(" {recv}."), "context a.b\n", None);
2870 for name in names {
2871 assert!(
2872 find(&items, name, CompletionKind::Variant).is_some(),
2873 "{recv}.{name} missing: {:?}",
2874 items.iter().map(|c| &c.label).collect::<Vec<_>>()
2875 );
2876 }
2877 }
2878 }
2879
2880 #[test]
2881 fn builtin_statics_are_reachable() {
2882 for &(recv, members) in BUILTIN_STATICS {
2886 let items = complete(&format!(" {recv}."), "context a.b\n", None);
2887 for &(member, _) in members {
2888 assert!(
2889 find(&items, member, CompletionKind::Member).is_some(),
2890 "{recv}.{member} unreachable: {:?}",
2891 items.iter().map(|c| &c.label).collect::<Vec<_>>()
2892 );
2893 }
2894 }
2895 for (recv, member) in [("List", "empty"), ("Map", "empty"), ("Effect", "pure")] {
2898 let items = complete(&format!(" {recv}."), "context a.b\n", None);
2899 assert!(
2900 find(&items, member, CompletionKind::Member).is_some(),
2901 "{recv}.{member} missing from the statics table"
2902 );
2903 }
2904 }
2905
2906 #[test]
2907 fn record_value_and_decimal_receivers_yield_nothing() {
2908 let doc = "commons m {\n type Point = { x: Int }\n}\n";
2910 assert!(complete(" Point.", doc, None).is_empty(), "record");
2911 assert!(complete(" let p = q.", doc, None).is_empty(), "value");
2913 assert!(complete(" let n = 1.", doc, None).is_empty(), "decimal");
2915 }
2916
2917 #[test]
2918 fn value_receiver_rewrite_drops_the_dot_for_lowercase_receivers() {
2919 let text = " let x = email.\n";
2920 let offset = text.find('.').unwrap() + 1; let (rewritten, recv) = value_receiver_rewrite(text, offset).expect("value receiver");
2922 assert_eq!(
2923 rewritten, " let x = email\n",
2924 "the trailing dot is dropped"
2925 );
2926 assert!(
2927 text.get(recv..=recv).is_some_and(|c| c == "l"),
2928 "the receiver offset lands inside `email`"
2929 );
2930 let text2 = " let x = email.ma\n";
2932 let off2 = text2.find(".ma").unwrap() + 3;
2933 assert_eq!(
2934 value_receiver_rewrite(text2, off2).map(|(r, _)| r),
2935 Some(" let x = email\n".to_string())
2936 );
2937 assert!(value_receiver_rewrite(" Email.", 8).is_none());
2939 assert!(value_receiver_rewrite(" let n = 1.", 12).is_none());
2940 assert!(value_receiver_rewrite(" email", 7).is_none());
2941 }
2942
2943 static TYS: std::sync::LazyLock<Types> = std::sync::LazyLock::new(Types::new);
2947
2948 #[test]
2951 fn run_member_offers_id_in_a_case_body() {
2952 let labels = |text: &str| {
2953 let offset = text.find("run.").expect("run.") + "run.".len();
2954 let (rewritten, recv) = value_receiver_rewrite(text, offset).expect("a value receiver");
2955 run_id_member_candidates(&rewritten, recv)
2956 .into_iter()
2957 .map(|c| c.label)
2958 .collect::<Vec<_>>()
2959 };
2960 assert_eq!(
2961 labels("suite rid.orders {\n case \"a\" {\n expect run. == \"local\"\n }\n}\n"),
2962 ["id"]
2963 );
2964 assert!(
2965 labels("suite rid.orders {\n case \"a\" {\n let run = 1\n expect run. == 1\n }\n}\n")
2966 .is_empty()
2967 );
2968 }
2969
2970 #[test]
2971 fn value_member_candidates_lists_kernel_methods() {
2972 use bynk_syntax::ast::BaseType;
2973 let list = TYS.intern(Ty::List(TYS.intern(Ty::Base(BaseType::Int))));
2974 let items = value_member_candidates(list, &TYS, "context a.b\n", None);
2975 assert!(find(&items, "fold", CompletionKind::Member).is_some());
2976 assert!(find(&items, "get", CompletionKind::Member).is_some());
2977
2978 let string = TYS.intern(Ty::Base(BaseType::String));
2979 let items = value_member_candidates(string, &TYS, "context a.b\n", None);
2980 assert!(find(&items, "split", CompletionKind::Member).is_some());
2981 assert!(find(&items, "trim", CompletionKind::Member).is_some());
2982 }
2983
2984 #[test]
2985 fn value_member_candidates_lists_refined_inherited_kernel_methods() {
2986 use bynk_check::checker::NamedKind;
2989 use bynk_syntax::ast::BaseType;
2990 let name = TYS.intern(Ty::Named {
2991 name: "Name".to_string(),
2992 kind: NamedKind::Refined(BaseType::String),
2993 args: Vec::new(),
2994 });
2995 let items = value_member_candidates(
2996 name,
2997 &TYS,
2998 "commons m {\n type Name = String where NonEmpty\n}\n",
2999 None,
3000 );
3001 assert!(find(&items, "toUpper", CompletionKind::Member).is_some());
3002 assert!(find(&items, "length", CompletionKind::Member).is_some());
3003 }
3004
3005 #[test]
3006 fn expression_position_offers_locals() {
3007 assert!(is_expression_position(" let y = "));
3009 assert!(is_expression_position(" let y = a + lo")); assert!(is_expression_position(" f("));
3011 assert!(is_expression_position(" g(a, "));
3012 assert!(is_expression_position(" xs.fold(0, (acc, x) => ac")); assert!(is_expression_position(" let y = foo"));
3015 assert!(!is_expression_position(" let y: ")); assert!(!is_expression_position(" on call() -> ")); assert!(!is_expression_position(" tot")); }
3020
3021 #[test]
3022 fn value_member_candidates_lists_record_fields() {
3023 use bynk_check::checker::NamedKind;
3024 let order = TYS.intern(Ty::Named {
3025 name: "Order".to_string(),
3026 kind: NamedKind::Record,
3027 args: Vec::new(),
3028 });
3029 let doc = "commons m {\n type Order = { id: Int, total: Int }\n}\n";
3030 let items = value_member_candidates(order, &TYS, doc, None);
3031 assert!(
3032 find(&items, "id", CompletionKind::Field).is_some(),
3033 "{items:?}",
3034 items = items.iter().map(|c| &c.label).collect::<Vec<_>>()
3035 );
3036 assert!(find(&items, "total", CompletionKind::Field).is_some());
3037 }
3038
3039 #[test]
3040 fn store_field_member_candidates_offers_entry_ops_and_query_accessors() {
3041 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";
3046 let offset = doc.find("items.").unwrap() + "items.".len();
3047 let (rewritten, recv_offset) = value_receiver_rewrite(doc, offset).expect("bare receiver");
3048 let items = store_field_member_candidates(&rewritten, recv_offset, &[]);
3049 assert!(
3050 find(&items, "put", CompletionKind::Member).is_some(),
3051 "{items:?}",
3052 items = items.iter().map(|c| &c.label).collect::<Vec<_>>()
3053 );
3054 assert!(find(&items, "get", CompletionKind::Member).is_some());
3055 assert!(find(&items, "update", CompletionKind::Member).is_some());
3056 assert!(find(&items, "entries", CompletionKind::Field).is_some());
3057 assert!(find(&items, "keys", CompletionKind::Field).is_some());
3058 assert!(find(&items, "values", CompletionKind::Field).is_some());
3059 }
3060
3061 #[test]
3062 fn store_field_member_candidates_empty_for_ordinary_local() {
3063 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";
3066 let offset = doc.rfind("items.").unwrap() + "items.".len();
3067 let (rewritten, recv_offset) = value_receiver_rewrite(doc, offset).expect("bare receiver");
3068 let items = store_field_member_candidates(&rewritten, recv_offset, &[]);
3069 assert!(items.is_empty());
3070 }
3071
3072 #[test]
3073 fn store_field_member_candidates_skips_query_accessors_on_held_map() {
3074 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";
3077 let offset = doc.find("conns.").unwrap() + "conns.".len();
3078 let (rewritten, recv_offset) = value_receiver_rewrite(doc, offset).expect("bare receiver");
3079 let items = store_field_member_candidates(&rewritten, recv_offset, &[]);
3080 assert!(find(&items, "put", CompletionKind::Member).is_some());
3081 assert!(find(&items, "entries", CompletionKind::Field).is_none());
3082 assert!(find(&items, "keys", CompletionKind::Field).is_none());
3083 assert!(find(&items, "values", CompletionKind::Field).is_none());
3084 }
3085
3086 #[test]
3087 fn store_field_member_candidates_offers_set_and_cache_vocabularies() {
3088 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";
3091 let offset = doc.find("tags.").unwrap() + "tags.".len();
3092 let (rewritten, recv_offset) = value_receiver_rewrite(doc, offset).expect("bare receiver");
3093 let items = store_field_member_candidates(&rewritten, recv_offset, &[]);
3094 assert!(find(&items, "add", CompletionKind::Member).is_some());
3095 assert!(find(&items, "entries", CompletionKind::Field).is_none());
3096 }
3097
3098 #[test]
3101 fn record_construction_offers_field_names() {
3102 let doc = "commons m {\n type Order = { id: Int, total: Int }\n}\n";
3103 let got = labels(" let o = Order { ", doc);
3104 assert!(got.contains(&"id".to_string()), "{got:?}");
3105 assert!(got.contains(&"total".to_string()), "{got:?}");
3106 let got2 = labels(" let o = Order { id: 1, ", doc);
3108 assert!(got2.contains(&"total".to_string()), "{got2:?}");
3109 assert!(record_construction_receiver(" let o = Order { id: ").is_none());
3111 assert!(record_construction_receiver(" if x { ").is_none());
3113 }
3114
3115 #[test]
3116 fn from_offers_protocols() {
3117 let got = labels(" service s from ", "context a.b\n");
3118 assert!(got.contains(&"http".to_string()), "{got:?}");
3119 assert!(got.contains(&"cron".to_string()) && got.contains(&"queue".to_string()));
3120 }
3121
3122 #[test]
3123 fn on_offers_handler_kinds() {
3124 let got = labels(" on ", "context a.b\n");
3125 assert!(got.contains(&"call".to_string()), "{got:?}");
3126 assert!(got.contains(&"GET".to_string()) && got.contains(&"schedule".to_string()));
3127 }
3128
3129 #[test]
3130 fn by_offers_project_actors() {
3131 let doc = "context a.b\n\nactor Caller { auth = Bearer }\n";
3132 let got = labels(" by ", doc);
3133 assert!(got.contains(&"Caller".to_string()), "{got:?}");
3134 }
3135
3136 #[test]
3137 fn exports_offers_export_kinds() {
3138 let got = labels(" exports ", "adapter t {\n binding \"./b.ts\"\n}\n");
3139 assert!(got.contains(&"capability".to_string()), "{got:?}");
3140 assert!(got.contains(&"transparent".to_string()));
3141 }
3142
3143 #[test]
3144 fn provides_offers_in_scope_capabilities() {
3145 let doc = "context a.b\n\ncapability Store { fn get() -> Effect[Int] }\n";
3146 let got = labels(" provides ", doc);
3147 assert!(got.contains(&"Store".to_string()), "{got:?}");
3148 }
3149
3150 #[test]
3151 fn where_offers_predicate_names() {
3152 let got = labels(" type Code = Int where ", "context a.b\n");
3154 assert!(got.contains(&"InRange".to_string()), "{got:?}");
3155 assert!(got.contains(&"NonNegative".to_string()));
3156 let got = labels(" _ where ", "context a.b\n");
3158 assert!(got.contains(&"Matches".to_string()), "{got:?}");
3159 assert!(got.contains(&"NonEmpty".to_string()));
3160 }
3161
3162 #[test]
3163 fn is_for_all_where_matches_only_the_for_all_binder() {
3164 assert!(is_for_all_where("\tfor all x: Int, y: Int where "));
3165 assert!(is_for_all_where("for all x: Int where "));
3166 assert!(!is_for_all_where("format(x) where "));
3169 assert!(!is_for_all_where("for allocate x where "));
3170 assert!(!is_for_all_where(" type Code = Int where "));
3172 assert!(!is_for_all_where(" _ where "));
3173 assert!(!is_for_all_where("no where here"));
3174 }
3175
3176 #[test]
3177 fn for_all_where_falls_through_to_expression_position_not_predicates() {
3178 let got = labels("\tfor all x: Int where ", "context a.b\n");
3182 assert!(!got.contains(&"InRange".to_string()), "{got:?}");
3183 assert!(!got.contains(&"NonNegative".to_string()), "{got:?}");
3184 }
3185
3186 #[test]
3187 fn clause_detectors_do_not_over_fire() {
3188 assert!(!after_clause_keyword(" session ", "on"));
3190 assert!(!after_clause_keyword(" let from = ", "from"));
3191 assert!(after_clause_keyword(" service s from ", "from"));
3193 assert!(after_clause_keyword(" by ", "by"));
3194 }
3195
3196 #[test]
3197 fn contract_clause_kind_detects_requires_and_ensures() {
3198 assert_eq!(contract_clause_kind(" requires positive: "), Some(false));
3199 assert_eq!(contract_clause_kind(" ensures never_neg: "), Some(true));
3200 assert_eq!(contract_clause_kind(" id: Int"), None);
3202 assert_eq!(contract_clause_kind(" let x = 1"), None);
3203 }
3204
3205 #[test]
3206 fn cors_field_position_inside_cors_block() {
3207 let doc = "service api from http {\n cors {\n ";
3209 assert!(in_cors_field_position(doc, doc.len()));
3210 let doc2 = "service api from http {\n cors {\n origins: ";
3212 assert!(!in_cors_field_position(doc2, doc2.len()));
3213 let doc3 = "let x = Order {\n ";
3215 assert!(!in_cors_field_position(doc3, doc3.len()));
3216 }
3217
3218 #[test]
3219 fn security_field_position_inside_security_block() {
3220 let doc = "service api from http {\n security {\n ";
3222 assert!(in_security_field_position(doc, doc.len()));
3223 let doc2 = "service api from http {\n security {\n hsts: ";
3225 assert!(!in_security_field_position(doc2, doc2.len()));
3226 let doc3 = "service api from http {\n cors {\n ";
3228 assert!(!in_security_field_position(doc3, doc3.len()));
3229 }
3230
3231 #[test]
3235 fn field_position_probes_survive_non_ascii() {
3236 let doc = "service api from http {\n cors { // café\n ";
3237 assert!(in_cors_field_position(doc, doc.len()));
3238 let mid = doc.find('é').unwrap() + 1;
3240 assert!(!doc.is_char_boundary(mid));
3241 let _ = in_cors_field_position(doc, mid);
3242 let _ = in_security_field_position(doc, mid);
3243 let _ = in_limits_field_position(doc, mid);
3244 }
3245
3246 #[test]
3247 fn by_in_a_suite_offers_the_reserved_principals() {
3248 let suite = "suite pay.hooks as system {\n case \"c\" {\n let r <- webhooks.POST(\"/h\", \"x\") by ";
3250 let labels: Vec<String> =
3251 complete(" let r <- webhooks.POST(\"/h\", \"x\") by ", suite, None)
3252 .into_iter()
3253 .map(|c| c.label)
3254 .collect();
3255 for name in ["Nobody", "Forged", "Stale"] {
3256 assert!(labels.iter().any(|l| l == name), "{name}: {labels:?}");
3257 }
3258 let context = "context pay.hooks\n\nservice api from http {\n on GET(\"/a\") () -> Effect[HttpResult[String]] by ";
3259 let labels: Vec<String> = complete(
3260 " on GET(\"/a\") () -> Effect[HttpResult[String]] by ",
3261 context,
3262 None,
3263 )
3264 .into_iter()
3265 .map(|c| c.label)
3266 .collect();
3267 assert!(!labels.iter().any(|l| l == "Forged"), "{labels:?}");
3268 }
3269
3270 #[test]
3271 fn env_name_position_and_candidates() {
3272 let doc = "suite shop.pay {\n env {\n ";
3274 assert!(in_env_name_position(doc, doc.len()));
3275 let doc2 = "suite shop.pay {\n env {\n A = ";
3277 assert!(!in_env_name_position(doc2, doc2.len()));
3278 let doc3 = "suite shop.pay {\n case \"c\" {\n ";
3280 assert!(!in_env_name_position(doc3, doc3.len()));
3281 let mut files = HashMap::new();
3282 files.insert(
3283 PathBuf::from("src/pay.bynk"),
3284 "actor User { auth = Bearer(secret = \"AUTH_JWT_SECRET\"), identity = String }\n\
3285 let p <- Secrets.get(\"PAYMENTS_PROCESSOR\")\n"
3286 .to_string(),
3287 );
3288 let labels: Vec<String> = env_name_candidates(doc, Some(&files))
3289 .into_iter()
3290 .map(|c| c.label)
3291 .collect();
3292 assert_eq!(labels, ["AUTH_JWT_SECRET", "PAYMENTS_PROCESSOR"]);
3293 }
3294
3295 #[test]
3296 fn limits_field_position_inside_limits_block() {
3297 let doc = "service api from http {\n limits {\n ";
3299 assert!(in_limits_field_position(doc, doc.len()));
3300 let doc2 = "service api from http {\n limits {\n maxBody: ";
3302 assert!(!in_limits_field_position(doc2, doc2.len()));
3303 let doc3 = "service api from http {\n security {\n ";
3305 assert!(!in_limits_field_position(doc3, doc3.len()));
3306 }
3307
3308 #[test]
3309 fn service_body_item_position_offers_cors() {
3310 let doc = "service api from http {\n ";
3312 assert!(in_service_body_item_position(doc, doc.len(), " "));
3313 let doc2 = "service api from http {\n cors {\n ";
3316 assert!(!in_service_body_item_position(doc2, doc2.len(), " "));
3317 }
3318
3319 #[test]
3320 fn cache_arg_position_inside_cache_annotation() {
3321 let doc = "service api from http {\n @cache(";
3323 assert!(in_cache_arg_position(doc, doc.len()));
3324 let doc2 = "service api from http {\n @cache(maxAge: 5.minutes, ";
3326 assert!(in_cache_arg_position(doc2, doc2.len()));
3327 let doc3 = "service api from http {\n @cache(maxAge: ";
3329 assert!(!in_cache_arg_position(doc3, doc3.len()));
3330 let doc4 = "let x = cache(";
3332 assert!(!in_cache_arg_position(doc4, doc4.len()));
3333 let doc5 = "on GET(\"/x\") by v: Visitor (";
3335 assert!(!in_cache_arg_position(doc5, doc5.len()));
3336 }
3337
3338 #[test]
3339 fn a_capability_declaration_completes_its_base_then_its_options() {
3340 let bases: Vec<String> = instance_declaration_completions("capability LoginLimit = ")
3342 .into_iter()
3343 .map(|(label, ..)| label)
3344 .collect();
3345 assert_eq!(bases, ["Kv", "Service", "R2", "RateLimit", "EdgeCache"]);
3346 let narrowed: Vec<String> = instance_declaration_completions("capability L = Ra")
3347 .into_iter()
3348 .map(|(label, ..)| label)
3349 .collect();
3350 assert_eq!(narrowed, ["RateLimit"]);
3351 let options = instance_declaration_completions("capability LoginLimit = RateLimit ");
3353 assert_eq!(options.len(), 1);
3354 assert_eq!(options[0].0, "@limit(…) @period(…)");
3355 assert_eq!(options[0].2, "@limit(${1:100}) @period(${2:60.seconds})");
3356 assert!(instance_declaration_completions("capability A = R2 ").is_empty());
3359 assert!(instance_declaration_completions("capability Edge = EdgeCache ").is_empty());
3360 assert!(instance_declaration_completions("capability L = RateLimit @limit(1) ").is_empty());
3361 assert!(instance_declaration_completions("let x = R").is_empty());
3362 }
3363
3364 #[test]
3365 fn a_rate_limit_instance_option_is_not_the_limit_annotation() {
3366 let doc = "context a\ncapability LoginLimit = RateLimit @limit(";
3368 assert!(!in_limit_arg_position(doc, doc.len()));
3369 let doc = "context a\ncapability LoginLimit = RateLimit @limit(100) @period(";
3371 let offered: Vec<&str> = instance_option_values(doc, doc.len())
3372 .iter()
3373 .map(|(v, _)| *v)
3374 .collect();
3375 assert_eq!(offered, ["10.seconds", "60.seconds"]);
3376 assert!(instance_option_values("let x = period(", 15).is_empty());
3377 let doc = "service api from http {\n @period(";
3378 assert!(instance_option_values(doc, doc.len()).is_empty());
3379 }
3380
3381 #[test]
3382 fn limit_arg_position_inside_limit_annotation() {
3383 let doc = "service api from http {\n @limit(";
3385 assert!(in_limit_arg_position(doc, doc.len()));
3386 let doc2 = "service api from http {\n @limit(maxBody: 1048576, ";
3388 assert!(in_limit_arg_position(doc2, doc2.len()));
3389 let doc3 = "service api from http {\n @limit(maxBody: ";
3391 assert!(!in_limit_arg_position(doc3, doc3.len()));
3392 let doc4 = "let x = limit(";
3394 assert!(!in_limit_arg_position(doc4, doc4.len()));
3395 let doc5 = "on GET(\"/x\") by v: Visitor (";
3397 assert!(!in_limit_arg_position(doc5, doc5.len()));
3398 }
3399
3400 #[test]
3401 fn raw_arg_position_inside_raw_annotation() {
3402 let doc = "service api from http {\n @raw(";
3403 assert!(in_raw_arg_position(doc, doc.len()));
3404 let doc2 = "service api from http {\n @raw(contentType: ";
3405 assert!(!in_raw_arg_position(doc2, doc2.len()));
3406 let doc3 = "let x = raw(";
3407 assert!(!in_raw_arg_position(doc3, doc3.len()));
3408 let doc4 = "service api from http {\n @limit(";
3409 assert!(!in_raw_arg_position(doc4, doc4.len()));
3410 }
3411
3412 #[test]
3413 fn sum_type_variants_lists_variants() {
3414 let doc = "commons m {\n type Status = enum { Pending, Shipped }\n}\n";
3415 let got: Vec<String> = sum_type_variants("Status", doc, None)
3416 .into_iter()
3417 .map(|c| c.label)
3418 .collect();
3419 assert!(got.contains(&"Pending".to_string()), "{got:?}");
3420 assert!(got.contains(&"Shipped".to_string()), "{got:?}");
3421 }
3422
3423 #[test]
3428 fn a_derivation_cycle_completes_nothing() {
3429 let doc = "context a\n\ncapability A = B\ncapability B = C\ncapability C = A\n";
3430 assert!(member_candidates("A", doc, None).is_empty());
3431 }
3432
3433 #[test]
3435 fn a_derived_capability_completes_its_bases_operations() {
3436 const DERIVED_DOC: &str =
3437 "context a\n\nconsumes bynk.cloudflare { Kv }\n\ncapability Sessions = Kv\n";
3438 let labels: Vec<String> = member_candidates("Sessions", DERIVED_DOC, None)
3439 .into_iter()
3440 .map(|c| c.label)
3441 .collect();
3442 for op in ["get", "put", "putTtl", "delete", "list"] {
3443 assert!(labels.iter().any(|l| l == op), "{op} missing: {labels:?}");
3444 }
3445 }
3446
3447 #[test]
3448 fn member_completions_carry_their_docs() {
3449 let doc = "commons m\n\ntype Rec = {\n --- The id.\n id: Int,\n n: Int,\n}\n\ntype Status = enum {\n ---\n Waiting.\n ---\n Pending,\n Shipped,\n}\n";
3450 let docs = |items: Vec<Completion>| -> Vec<(String, Option<String>)> {
3451 items
3452 .into_iter()
3453 .map(|c| (c.label, c.documentation.map(|d| d.trim_end().to_string())))
3454 .collect()
3455 };
3456 assert_eq!(
3457 docs(record_field_names("Rec", doc, None)),
3458 [("id".into(), Some("The id.".into())), ("n".into(), None)]
3459 );
3460 assert_eq!(
3461 docs(sum_type_variants("Status", doc, None)),
3462 [
3463 ("Pending".into(), Some("Waiting.".into())),
3464 ("Shipped".into(), None)
3465 ]
3466 );
3467 assert_eq!(
3468 docs(member_candidates("Status", doc, None))
3469 .into_iter()
3470 .filter(|(l, _)| l == "Pending")
3471 .collect::<Vec<_>>(),
3472 [("Pending".into(), Some("Waiting.".into()))]
3473 );
3474 }
3475
3476 #[test]
3477 fn variants_for_ty_offers_built_in_result_and_option() {
3478 use bynk_syntax::ast::BaseType;
3483 let result = TYS.intern(Ty::Result(
3484 TYS.intern(Ty::Base(BaseType::Int)),
3485 TYS.intern(Ty::Base(BaseType::String)),
3486 ));
3487 let got: Vec<String> = variants_for_ty(result, &TYS, "", None)
3488 .into_iter()
3489 .map(|c| c.label)
3490 .collect();
3491 assert_eq!(got, vec!["Ok".to_string(), "Err".to_string()]);
3492
3493 let option = TYS.intern(Ty::Option(TYS.intern(Ty::Base(BaseType::Int))));
3494 let got: Vec<String> = variants_for_ty(option, &TYS, "", None)
3495 .into_iter()
3496 .map(|c| c.label)
3497 .collect();
3498 assert_eq!(got, vec!["Some".to_string(), "None".to_string()]);
3499 }
3500
3501 #[test]
3502 fn nested_variant_completions_resolves_the_payload_type() {
3503 use bynk_check::checker::NamedKind;
3504 use bynk_syntax::ast::BaseType;
3505 let payload = TYS.intern(Ty::Result(
3508 TYS.intern(Ty::Base(BaseType::Int)),
3509 TYS.intern(Ty::Named {
3510 name: "E".to_string(),
3511 kind: NamedKind::Sum,
3512 args: Vec::new(),
3513 }),
3514 ));
3515 let scrut = TYS.intern(Ty::Option(payload));
3516 let got: Vec<String> = nested_variant_completions(scrut, &TYS, "Some", "", None)
3517 .into_iter()
3518 .map(|c| c.label)
3519 .collect();
3520 assert_eq!(got, vec!["Ok".to_string(), "Err".to_string()]);
3521
3522 let opt_int = TYS.intern(Ty::Option(TYS.intern(Ty::Base(BaseType::Int))));
3524 let inner = TYS.intern(Ty::Result(TYS.intern(Ty::Base(BaseType::Int)), opt_int));
3525 let got: Vec<String> = nested_variant_completions(inner, &TYS, "Err", "", None)
3526 .into_iter()
3527 .map(|c| c.label)
3528 .collect();
3529 assert_eq!(got, vec!["Some".to_string(), "None".to_string()]);
3530
3531 let doc = "commons m {\n type Inner = enum { A, B }\n \
3534 type Outer = | Wrap(inner: Inner) | Bare\n}\n";
3535 let outer = TYS.intern(Ty::Named {
3536 name: "Outer".to_string(),
3537 kind: NamedKind::Sum,
3538 args: Vec::new(),
3539 });
3540 let got: Vec<String> = nested_variant_completions(outer, &TYS, "Wrap", doc, None)
3541 .into_iter()
3542 .map(|c| c.label)
3543 .collect();
3544 assert!(got.contains(&"A".to_string()), "{got:?}");
3545 assert!(got.contains(&"B".to_string()), "{got:?}");
3546 }
3547
3548 #[test]
3549 fn literal_kind_scrutinee_suggests_no_variants() {
3550 let doc = "commons m {\n type Quantity = Int where InRange(1, 99)\n}\n";
3556 assert!(sum_type_variants("Quantity", doc, None).is_empty());
3557 assert!(sum_type_variants("Int", doc, None).is_empty());
3558 }
3559
3560 fn enumerated_units(doc_text: &str, files: Option<&HashMap<PathBuf, String>>) -> Vec<String> {
3564 let mut names = Vec::new();
3565 for_each_unit(doc_text, files, |u| names.push(u.name().joined()));
3566 names
3567 }
3568
3569 fn synthetic_path(name: &str) -> PathBuf {
3575 PathBuf::from(format!("/synthetic/{name}.bynk"))
3576 }
3577
3578 #[test]
3579 fn for_each_unit_yields_embedded_buffer_and_project_files() {
3580 let sibling = synthetic_path("sibling");
3581 let files = HashMap::from([(
3582 sibling,
3583 "commons proj.sibling {\n fn s() -> Int { 1 }\n}\n".to_string(),
3584 )]);
3585
3586 let names = enumerated_units(
3587 "commons proj.buffer {\n fn b() -> Int { 1 }\n}\n",
3588 Some(&files),
3589 );
3590 assert!(names.iter().any(|n| n == "bynk"), "embedded: {names:?}");
3592 assert!(
3593 names.iter().any(|n| n == "proj.buffer"),
3594 "buffer: {names:?}"
3595 );
3596 assert!(
3597 names.iter().any(|n| n == "proj.sibling"),
3598 "project file: {names:?}"
3599 );
3600 }
3601
3602 #[test]
3609 fn same_named_declarations_do_not_union_across_units() {
3610 let stale = HashMap::from([(
3611 synthetic_path("stale"),
3612 "commons m {\n \
3613 type Rec = { total: Int, old_field: Int }\n \
3614 type Status = enum { Pending, Retired }\n\
3615 }\n"
3616 .to_string(),
3617 )]);
3618
3619 let buffer = "commons m {\n \
3620 type Rec = { total: Int }\n \
3621 type Status = enum { Pending, Shipped }\n\
3622 }\n";
3623 let files = Some(&stale);
3624
3625 let fields: Vec<String> = record_field_names("Rec", buffer, files)
3626 .into_iter()
3627 .map(|c| c.label)
3628 .collect();
3629 assert_eq!(fields, vec!["total".to_string()], "{fields:?}");
3630
3631 let variants: Vec<String> = sum_type_variants("Status", buffer, files)
3632 .into_iter()
3633 .map(|c| c.label)
3634 .collect();
3635 assert_eq!(
3636 variants,
3637 vec!["Pending".to_string(), "Shipped".to_string()],
3638 "{variants:?}"
3639 );
3640 }
3641
3642 #[test]
3647 fn capabilities_of_unit_does_not_union_across_units() {
3648 let stale = HashMap::from([(
3649 synthetic_path("stale"),
3650 "adapter tokens {\n \
3651 exports capability { Jwt, Retired }\n\
3652 }\n"
3653 .to_string(),
3654 )]);
3655
3656 let buffer = "adapter tokens {\n exports capability { Jwt }\n}\n";
3657 let files = Some(&stale);
3658
3659 let caps = capabilities_of_unit("tokens", buffer, &split_dependency_files(files, None));
3660 assert_eq!(caps, vec!["Jwt".to_string()], "{caps:?}");
3661 }
3662
3663 #[test]
3664 fn project_unit_cache_invalidates_on_change() {
3665 let path = synthetic_path("unit");
3666
3667 let first_files = HashMap::from([(
3668 path.clone(),
3669 "commons proj.first {\n fn a() -> Int { 1 }\n}\n".to_string(),
3670 )]);
3671 let first = enumerated_units("context a.b\n", Some(&first_files));
3672 assert!(
3673 first.iter().any(|n| n == "proj.first"),
3674 "first read: {first:?}"
3675 );
3676
3677 let second_files = HashMap::from([(
3682 path,
3683 "commons proj.second.longer {\n fn a() -> Int { 1 }\n fn c() -> Int { 3 }\n}\n"
3684 .to_string(),
3685 )]);
3686 let second = enumerated_units("context a.b\n", Some(&second_files));
3687 assert!(
3688 second.iter().any(|n| n == "proj.second.longer"),
3689 "after change: {second:?}"
3690 );
3691 assert!(
3692 !second.iter().any(|n| n == "proj.first"),
3693 "stale served: {second:?}"
3694 );
3695 }
3696
3697 #[test]
3698 fn unparseable_project_file_is_skipped() {
3699 let files = HashMap::from([(synthetic_path("broken"), "not bynk source {{{".to_string())]);
3706 let names = enumerated_units("context a.b\n", Some(&files));
3707 let baseline = enumerated_units("context a.b\n", None);
3708 assert_eq!(names, baseline, "{names:?}");
3714 }
3715}