use std::collections::BTreeSet;
use std::path::Path;
use bynk_check::checker::Ty;
use bynk_check::firstparty::{
BYNK_ADAPTER_SRC, BYNK_LIST_SRC, BYNK_MAP_SRC, BYNK_STRING_SRC, CLOUDFLARE_ADAPTER_SRC,
};
use bynk_check::kernel_methods;
use bynk_syntax::ast::{CommonsItem, ExportKind, FnName, SourceUnit, TypeBody, UsesDecl};
use bynk_syntax::{keywords, lexer, parser};
use crate::symbols::{type_ref_str, walk_bynk_files};
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum CompletionKind {
Unit,
Capability,
Type,
Keyword,
Snippet,
Variant,
Member,
Field,
Constructor,
Function,
}
pub struct Completion {
pub label: String,
pub kind: CompletionKind,
pub detail: Option<String>,
pub insert_text: Option<String>,
}
impl Completion {
fn item(label: impl Into<String>, kind: CompletionKind, detail: Option<String>) -> Self {
Completion {
label: label.into(),
kind,
detail,
insert_text: None,
}
}
fn snippet(label: &str, body: &str) -> Self {
Completion {
label: label.to_string(),
kind: CompletionKind::Snippet,
detail: Some(format!("{label} scaffold")),
insert_text: Some(body.to_string()),
}
}
}
pub fn complete(line_prefix: &str, doc_text: &str, src_root: Option<&Path>) -> Vec<Completion> {
if let Some(unit) = consumes_brace_unit(line_prefix) {
return capabilities_of_unit(&unit, doc_text, src_root)
.into_iter()
.map(|c| {
Completion::item(
c,
CompletionKind::Capability,
Some(format!("capability exported by `{unit}`")),
)
})
.collect();
}
if is_consumes_target(line_prefix) {
return consumable_units(doc_text, src_root);
}
if is_given_position(line_prefix) {
return in_scope_capabilities(doc_text, src_root);
}
if let Some(receiver) = member_receiver(line_prefix) {
return member_candidates(&receiver, doc_text, src_root);
}
if is_type_position(line_prefix) {
return type_candidates(doc_text, src_root);
}
if is_keyword_position(line_prefix) {
return keyword_and_snippet_candidates();
}
if is_expression_position(line_prefix) {
return expression_candidates(doc_text, src_root);
}
Vec::new()
}
fn consumes_brace_unit(line: &str) -> Option<String> {
let idx = line.rfind("consumes")?;
let after = &line[idx + "consumes".len()..];
let open = after.find('{')?;
if after[open + 1..].contains('}') {
return None;
}
let unit = after[..open].trim();
if unit.is_empty() || !is_qualified_name(unit) {
return None;
}
Some(unit.to_string())
}
fn is_consumes_target(line: &str) -> bool {
let Some(idx) = line.rfind("consumes") else {
return false;
};
if !line[..idx]
.chars()
.last()
.map(|c| c.is_whitespace())
.unwrap_or(true)
{
return false;
}
let after = &line[idx + "consumes".len()..];
after.starts_with(char::is_whitespace)
&& !after.contains('{')
&& !after.contains('}')
&& !after.split_whitespace().any(|w| w == "as")
}
fn is_given_position(line: &str) -> bool {
let Some(idx) = line.rfind("given") else {
return false;
};
if !line[..idx]
.chars()
.last()
.map(|c| c.is_whitespace())
.unwrap_or(true)
{
return false;
}
let after = &line[idx + "given".len()..];
if !after.starts_with(char::is_whitespace) {
return false;
}
after
.chars()
.all(|c| c.is_alphanumeric() || matches!(c, '_' | '.' | ',' | ' ' | '\t'))
}
fn is_qualified_name(s: &str) -> bool {
!s.is_empty()
&& s.split('.').all(|seg| {
!seg.is_empty()
&& seg.chars().all(|c| c.is_alphanumeric() || c == '_')
&& !seg.chars().next().unwrap().is_ascii_digit()
})
}
fn is_type_position(line: &str) -> bool {
let head = line
.trim_end_matches(|c: char| c.is_alphanumeric() || c == '_')
.trim_end();
head.ends_with("->") || (head.ends_with(':') && !head.ends_with("::")) || in_type_arg_list(head)
}
fn in_type_arg_list(head: &str) -> bool {
let chars: Vec<char> = head.chars().collect();
let mut depth = 0i32;
let mut opener_after_ident = false;
for (i, &c) in chars.iter().enumerate() {
match c {
'[' => {
depth += 1;
if depth == 1 {
opener_after_ident =
i > 0 && (chars[i - 1].is_alphanumeric() || chars[i - 1] == '_');
}
}
']' => depth -= 1,
_ => {}
}
}
depth > 0 && opener_after_ident
}
pub fn is_keyword_position(line: &str) -> bool {
line.trim().chars().all(|c| c.is_alphanumeric() || c == '_')
}
pub fn is_expression_position(line: &str) -> bool {
let head = line
.trim_end_matches(|c: char| c.is_alphanumeric() || c == '_')
.trim_end();
if head.ends_with("->") {
return false; }
if head.ends_with("=>") {
return true; }
matches!(
head.chars().last(),
Some('=' | '(' | ',' | '[' | '+' | '-' | '*' | '/' | '<' | '>' | '&' | '|')
)
}
fn member_receiver(line: &str) -> Option<String> {
let head = line
.trim_end_matches(|c: char| c.is_alphanumeric() || c == '_')
.strip_suffix('.')?;
let start = head
.rfind(|c: char| !(c.is_alphanumeric() || c == '_'))
.map_or(0, |i| i + 1);
let recv = &head[start..];
let first = recv.chars().next()?;
if !first.is_ascii_uppercase() {
return None;
}
if head[..start].ends_with('.') {
return None;
}
Some(recv.to_string())
}
pub(crate) const BUILTIN_STATICS: &[(&str, &[(&str, &str)])] = &[
("Int", &[("parse", "parse(s: String) -> Option[Int]")]),
("Float", &[("parse", "parse(s: String) -> Option[Float]")]),
(
"Json",
&[
("encode", "encode(value) -> String"),
("decode", "decode[T](s: String) -> Result[T, JsonError]"),
],
),
("List", &[("empty", "empty() -> List[T]")]),
("Map", &[("empty", "empty() -> Map[K, V]")]),
("Effect", &[("pure", "pure(value) -> Effect[T]")]),
(
"Bytes",
&[
("fromUtf8", "fromUtf8(s: String) -> Bytes"),
("fromBase64", "fromBase64(s: String) -> Option[Bytes]"),
("empty", "empty() -> Bytes"),
],
),
];
fn builtin_sum_variants(receiver: &str) -> Vec<(String, String)> {
match receiver {
"HttpResult" => bynk_syntax::ast::HTTP_VARIANTS
.iter()
.map(|v| {
(
v.name.to_string(),
format!("variant of `HttpResult` ({})", v.status),
)
})
.collect(),
"QueueResult" => bynk_syntax::ast::QUEUE_VARIANTS
.iter()
.map(|v| (v.name.to_string(), "variant of `QueueResult`".to_string()))
.collect(),
_ => Vec::new(),
}
}
fn member_candidates(receiver: &str, doc_text: &str, src_root: Option<&Path>) -> Vec<Completion> {
if let Some((_, statics)) = BUILTIN_STATICS.iter().find(|(name, _)| *name == receiver) {
return statics
.iter()
.map(|(label, sig)| {
Completion::item(*label, CompletionKind::Member, Some(sig.to_string()))
})
.collect();
}
let mut out: Vec<Completion> = Vec::new();
let mut seen: BTreeSet<String> = BTreeSet::new();
for (label, detail) in builtin_sum_variants(receiver) {
if seen.insert(label.clone()) {
out.push(Completion::item(
label,
CompletionKind::Variant,
Some(detail),
));
}
}
for_each_unit(doc_text, src_root, |unit| {
let items = match unit {
SourceUnit::Commons(c) => &c.items,
SourceUnit::Context(c) => &c.items,
SourceUnit::Adapter(a) => &a.items,
_ => return,
};
for item in items {
match item {
CommonsItem::Type(t) if t.name.name == receiver => match &t.body {
bynk_syntax::ast::TypeBody::Sum(s) => {
for v in &s.variants {
if seen.insert(v.name.name.clone()) {
out.push(Completion::item(
v.name.name.clone(),
CompletionKind::Variant,
Some(format!("variant of `{receiver}`")),
));
}
}
}
bynk_syntax::ast::TypeBody::Refined { .. }
| bynk_syntax::ast::TypeBody::Opaque { .. } => {
for (label, sig) in [
(
"of",
format!("of(value) -> Result[{receiver}, ValidationError]"),
),
("unsafe", format!("unsafe(value) -> {receiver}")),
] {
if seen.insert(label.to_string()) {
out.push(Completion::item(
label,
CompletionKind::Member,
Some(sig),
));
}
}
}
_ => {}
},
CommonsItem::Capability(c) if c.name.name == receiver => {
for op in &c.ops {
if seen.insert(op.name.name.clone()) {
let params = op
.params
.iter()
.map(|p| format!("{}: {}", p.name.name, type_ref_str(&p.type_ref)))
.collect::<Vec<_>>()
.join(", ");
out.push(Completion::item(
op.name.name.clone(),
CompletionKind::Member,
Some(format!(
"{}({params}) -> {} — operation of `{receiver}`",
op.name.name,
type_ref_str(&op.return_type)
)),
));
}
}
}
_ => {}
}
}
});
out
}
const BUILTIN_TYPES: &[&str] = &[
bynk_check::builtin_names::types::INT,
"Bool",
bynk_check::builtin_names::types::FLOAT,
"String",
"Option",
"Result",
"Effect",
bynk_check::builtin_names::types::LIST,
bynk_check::builtin_names::types::MAP,
];
const SNIPPETS: &[(&str, &str)] = &[
("context", "context ${1:name} {\n\t$0\n}"),
(
"adapter",
"adapter ${1:name} {\n\tbinding \"${2:./module}\"\n\t$0\n}",
),
(
"capability",
"capability ${1:Name} {\n\tfn ${2:op}() -> Effect[${3:Unit}]\n}",
),
(
"service",
"service ${1:name} {\n\ton call(${2}) -> Effect[${3:Unit}] {\n\t\t$0\n\t}\n}",
),
("on call", "on call(${1}) -> Effect[${2:Unit}] {\n\t$0\n}"),
("test", "test \"${1:description}\" {\n\t$0\n}"),
];
const CONSTRUCTORS: &[&str] = &["Ok", "Err", "Some", "None", "true", "false"];
fn expression_candidates(doc_text: &str, src_root: Option<&Path>) -> Vec<Completion> {
let mut out: Vec<Completion> = CONSTRUCTORS
.iter()
.map(|&name| {
Completion::item(
name,
CompletionKind::Constructor,
keyword_doc(name).map(str::to_string),
)
})
.collect();
out.extend(type_candidates(doc_text, src_root));
out.extend(free_function_candidates(doc_text, src_root));
out
}
fn unit_items_and_uses(unit: &SourceUnit) -> (&[CommonsItem], &[UsesDecl]) {
match unit {
SourceUnit::Commons(c) => (&c.items, &c.uses),
SourceUnit::Context(c) => (&c.items, &c.uses),
SourceUnit::Adapter(a) => (&a.items, &a.uses),
_ => (&[], &[]),
}
}
fn current_unit_name(doc_text: &str) -> Option<String> {
let tokens = lexer::tokenize(doc_text).ok()?;
let (unit, _errs) = parser::parse_unit_with_recovery(&tokens, doc_text);
Some(unit?.name().joined())
}
fn free_fn_signature(name: &str, f: &bynk_syntax::ast::FnDecl) -> String {
let params = f
.params
.iter()
.map(|p| format!("{}: {}", p.name.name, type_ref_str(&p.type_ref)))
.collect::<Vec<_>>()
.join(", ");
format!("{name}({params}) -> {}", type_ref_str(&f.return_type))
}
fn free_function_candidates(doc_text: &str, src_root: Option<&Path>) -> Vec<Completion> {
let Some(current) = current_unit_name(doc_text) else {
return Vec::new();
};
struct UnitFns {
name: String,
fns: Vec<(String, String)>,
uses: Vec<String>,
}
let mut units: Vec<UnitFns> = Vec::new();
for_each_unit(doc_text, src_root, |unit| {
let (items, uses) = unit_items_and_uses(unit);
let fns = items
.iter()
.filter_map(|it| match it {
CommonsItem::Fn(f) => match &f.name {
FnName::Free(id) => Some((id.name.clone(), free_fn_signature(&id.name, f))),
FnName::Method { .. } => None,
},
_ => None,
})
.collect();
units.push(UnitFns {
name: unit.name().joined(),
fns,
uses: uses.iter().map(|u| u.target.joined()).collect(),
});
});
let mut imported: BTreeSet<String> = BTreeSet::new();
for u in &units {
if u.name == current {
imported.extend(u.uses.iter().cloned());
}
}
let mut out: Vec<Completion> = Vec::new();
let mut seen: BTreeSet<String> = BTreeSet::new();
for u in &units {
let own = u.name == current;
if !own && !imported.contains(&u.name) {
continue;
}
let origin = if own { "this unit" } else { u.name.as_str() };
for (name, sig) in &u.fns {
if seen.insert(name.clone()) {
out.push(Completion::item(
name.clone(),
CompletionKind::Function,
Some(format!("{sig} — `{origin}`")),
));
}
}
}
out
}
fn keyword_doc(word: &str) -> Option<&'static str> {
keywords::KEYWORDS
.iter()
.find(|k| k.word == word)
.map(|k| k.meaning)
}
fn type_candidates(doc_text: &str, src_root: Option<&Path>) -> Vec<Completion> {
let mut out: Vec<Completion> = Vec::new();
let mut seen: BTreeSet<String> = BTreeSet::new();
for &name in BUILTIN_TYPES {
if seen.insert(name.to_string()) {
let detail = keyword_doc(name)
.map(str::to_string)
.or_else(|| match name {
"List" => Some("The built-in list type, `List[T]`.".to_string()),
"Map" => Some("The built-in map type, `Map[K, V]`.".to_string()),
_ => Some("built-in type".to_string()),
});
out.push(Completion::item(name, CompletionKind::Type, detail));
}
}
for_each_unit(doc_text, src_root, |unit| {
let items = match unit {
SourceUnit::Commons(c) => &c.items,
SourceUnit::Context(c) => &c.items,
SourceUnit::Adapter(a) => &a.items,
_ => return,
};
for item in items {
if let CommonsItem::Type(t) = item
&& seen.insert(t.name.name.clone())
{
out.push(Completion::item(
t.name.name.clone(),
CompletionKind::Type,
Some("type".to_string()),
));
}
}
});
out
}
fn keyword_and_snippet_candidates() -> Vec<Completion> {
let mut out: Vec<Completion> = keywords::KEYWORDS
.iter()
.filter(|k| k.word.chars().next().is_some_and(char::is_lowercase))
.map(|k| Completion::item(k.word, CompletionKind::Keyword, Some(k.meaning.to_string())))
.collect();
for &(label, body) in SNIPPETS {
out.push(Completion::snippet(label, body));
}
out
}
pub(crate) fn for_each_unit(
doc_text: &str,
src_root: Option<&Path>,
mut f: impl FnMut(&SourceUnit),
) {
let mut sources: Vec<String> = vec![
BYNK_ADAPTER_SRC.to_string(),
CLOUDFLARE_ADAPTER_SRC.to_string(),
BYNK_LIST_SRC.to_string(),
BYNK_MAP_SRC.to_string(),
BYNK_STRING_SRC.to_string(),
doc_text.to_string(),
];
if let Some(root) = src_root {
for path in walk_bynk_files(root) {
if let Ok(s) = std::fs::read_to_string(&path) {
sources.push(s);
}
}
}
for src in &sources {
let Ok(tokens) = lexer::tokenize(src) else {
continue;
};
let (unit, _errs) = parser::parse_unit_with_recovery(&tokens, src);
if let Some(unit) = unit {
f(&unit);
}
}
}
fn consumable_units(doc_text: &str, src_root: Option<&Path>) -> Vec<Completion> {
let mut seen: BTreeSet<String> = BTreeSet::new();
let mut out: Vec<Completion> = Vec::new();
for_each_unit(doc_text, src_root, |unit| {
let (name, kind) = match unit {
SourceUnit::Context(c) => (c.name.joined(), "context"),
SourceUnit::Adapter(a) => (a.name.joined(), "adapter"),
_ => return,
};
if seen.insert(name.clone()) {
out.push(Completion::item(
name,
CompletionKind::Unit,
Some(kind.to_string()),
));
}
});
out
}
fn capabilities_of_unit(unit: &str, doc_text: &str, src_root: Option<&Path>) -> Vec<String> {
let mut out: BTreeSet<String> = BTreeSet::new();
for_each_unit(doc_text, src_root, |u| {
let (name, exports) = match u {
SourceUnit::Context(c) => (c.name.joined(), &c.exports),
SourceUnit::Adapter(a) => (a.name.joined(), &a.exports),
_ => return,
};
if name != unit {
return;
}
for clause in exports {
if clause.kind == ExportKind::Capability {
for n in &clause.names {
out.insert(n.name.clone());
}
}
}
});
out.into_iter().collect()
}
fn in_scope_capabilities(doc_text: &str, src_root: Option<&Path>) -> Vec<Completion> {
let mut labels: BTreeSet<String> = BTreeSet::new();
let Ok(tokens) = lexer::tokenize(doc_text) else {
return Vec::new();
};
let (Some(unit), _errs) = parser::parse_unit_with_recovery(&tokens, doc_text) else {
return Vec::new();
};
let (items, consumes) = match &unit {
SourceUnit::Context(c) => (&c.items, &c.consumes),
SourceUnit::Adapter(a) => (&a.items, &EMPTY_CONSUMES),
_ => return Vec::new(),
};
for item in items {
if let bynk_syntax::ast::CommonsItem::Capability(c) = item {
labels.insert(c.name.name.clone());
}
}
for c in consumes {
let unit_name = c.target.joined();
match &c.selected {
Some(names) => {
for n in names {
labels.insert(n.name.clone());
}
}
None => {
let prefix = c
.alias
.as_ref()
.map(|a| a.name.clone())
.unwrap_or_else(|| unit_name.clone());
for cap in capabilities_of_unit(&unit_name, doc_text, src_root) {
labels.insert(format!("{prefix}.{cap}"));
}
}
}
}
labels
.into_iter()
.map(|label| {
Completion::item(
label,
CompletionKind::Capability,
Some("capability in scope".to_string()),
)
})
.collect()
}
pub fn value_receiver_rewrite(text: &str, offset: usize) -> Option<(String, usize)> {
let prefix = text.get(..offset)?;
let head = prefix
.trim_end_matches(|c: char| c.is_alphanumeric() || c == '_')
.strip_suffix('.')?;
let start = head
.rfind(|c: char| !(c.is_alphanumeric() || c == '_'))
.map_or(0, |i| i + 1);
let recv = &head[start..];
let first = recv.chars().next()?;
if !(first.is_ascii_lowercase() || first == '_') {
return None; }
if head[..start].ends_with('.') {
return None; }
let dot = head.len(); let rewritten = format!("{}{}", &text[..dot], &text[offset..]);
Some((rewritten, dot.saturating_sub(1)))
}
pub fn value_member_candidates(
ty: &Ty,
doc_text: &str,
src_root: Option<&Path>,
) -> Vec<Completion> {
let mut out: Vec<Completion> = kernel_methods::methods_for(ty)
.iter()
.map(|km| {
Completion::item(
km.name,
CompletionKind::Member,
Some(km.signature.to_string()),
)
})
.collect();
if let Ty::Named { name, .. } = ty {
let mut seen: BTreeSet<String> = BTreeSet::new();
for_each_unit(doc_text, src_root, |unit| {
let items = match unit {
SourceUnit::Commons(c) => &c.items,
SourceUnit::Context(c) => &c.items,
SourceUnit::Adapter(a) => &a.items,
_ => return,
};
for item in items {
if let CommonsItem::Type(t) = item
&& &t.name.name == name
&& let TypeBody::Record(r) = &t.body
{
for f in &r.fields {
if seen.insert(f.name.name.clone()) {
out.push(Completion::item(
f.name.name.clone(),
CompletionKind::Field,
Some(format!("field of `{name}`")),
));
}
}
}
}
});
}
out
}
static EMPTY_CONSUMES: Vec<bynk_syntax::ast::ConsumesDecl> = Vec::new();
#[cfg(test)]
mod tests {
use super::*;
fn labels(line: &str, doc: &str) -> Vec<String> {
complete(line, doc, None)
.into_iter()
.map(|c| c.label)
.collect()
}
#[test]
fn consumes_target_suggests_units_including_bynk() {
let doc = "adapter tokens {\n binding \"./b.ts\"\n capability Jwt { fn f() -> Effect[Int] }\n provides Jwt = X\n}\n";
let got = labels(" consumes ", doc);
assert!(got.contains(&"bynk".to_string()), "{got:?}");
assert!(got.contains(&"tokens".to_string()), "{got:?}");
}
#[test]
fn consumes_brace_suggests_that_units_capabilities() {
let got = labels(" consumes bynk { ", "context a.b\n");
assert!(got.contains(&"Clock".to_string()), "{got:?}");
assert!(got.contains(&"Random".to_string()), "{got:?}");
assert!(got.contains(&"Logger".to_string()), "{got:?}");
}
#[test]
fn given_suggests_local_and_flattened_capabilities() {
let doc = "context a.b\n\
consumes bynk { Clock }\n\
capability Local { fn f() -> Effect[Int] }\n\
service s {\n\
on call() -> Effect[Int] given Clock {\n\
1\n\
}\n\
}\n";
let got = labels(" on call() -> Effect[Int] given ", doc);
assert!(got.contains(&"Clock".to_string()), "flattened: {got:?}");
assert!(got.contains(&"Local".to_string()), "local: {got:?}");
}
#[test]
fn expression_position_offers_constructors_and_types() {
let doc = "commons m {\n type Order = { id: Int }\n}\n";
let items = complete(" let x = ", doc, None);
for &c in CONSTRUCTORS {
assert!(
find(&items, c, CompletionKind::Constructor).is_some(),
"constructor {c}: {:?}",
items.iter().map(|i| &i.label).collect::<Vec<_>>()
);
}
assert!(
find(&items, "Int", CompletionKind::Type).is_some(),
"builtin type"
);
assert!(
find(&items, "Order", CompletionKind::Type).is_some(),
"project type"
);
}
#[test]
fn value_receiver_and_decimal_are_not_expression_positions() {
assert!(complete(" let p = q.", "context a.b\n", None).is_empty());
assert!(complete(" let n = 1.", "context a.b\n", None).is_empty());
}
fn free_fn_names(src: &str) -> Vec<String> {
let tokens = lexer::tokenize(src).unwrap();
let (unit, _) = parser::parse_unit_with_recovery(&tokens, src);
let unit = unit.unwrap();
let (items, _) = unit_items_and_uses(&unit);
items
.iter()
.filter_map(|it| match it {
CommonsItem::Fn(f) => match &f.name {
FnName::Free(id) => Some(id.name.clone()),
FnName::Method { .. } => None,
},
_ => None,
})
.collect()
}
#[test]
fn free_functions_offered_for_own_unit_and_used_modules() {
let doc = "commons app {\n uses bynk.list\n fn helper(x: Int) -> Int { x }\n}\n";
let items = complete(" let y = ", doc, None);
assert!(
find(&items, "helper", CompletionKind::Function).is_some(),
"own fn: {:?}",
items.iter().map(|i| &i.label).collect::<Vec<_>>()
);
for name in free_fn_names(BYNK_LIST_SRC) {
assert!(
find(&items, &name, CompletionKind::Function).is_some(),
"bynk.list.{name}: {:?}",
items.iter().map(|i| &i.label).collect::<Vec<_>>()
);
}
assert!(
find(&items, "values", CompletionKind::Function).is_none(),
"bynk.map.values leaked without `uses bynk.map`"
);
}
#[test]
fn free_functions_require_a_uses_import() {
let doc = "commons app {\n fn helper(x: Int) -> Int { x }\n}\n";
let items = complete(" let y = ", doc, None);
assert!(find(&items, "helper", CompletionKind::Function).is_some());
for name in ["map", "filter", "reverse"] {
assert!(
find(&items, name, CompletionKind::Function).is_none(),
"bynk.list.{name} offered without `uses bynk.list`"
);
}
}
#[test]
fn member_completion_reaches_inside_an_interpolation_hole() {
let doc = "context a.b\n capability Timer { fn now() -> Effect[Int] }\n";
let in_hole = complete(" \"the time is \\(Timer.", doc, None);
assert!(
find(&in_hole, "now", CompletionKind::Member).is_some(),
"capability op not offered inside a hole: {:?}",
in_hole.iter().map(|c| &c.label).collect::<Vec<_>>()
);
let statics = complete(" \"n=\\(Int.", "context a.b\n", None);
assert!(find(&statics, "parse", CompletionKind::Member).is_some());
}
#[test]
fn consumes_with_as_is_not_a_target_completion() {
assert!(!is_consumes_target("consumes platform.time as "));
assert!(is_consumes_target("consumes platform"));
}
fn find<'a>(
items: &'a [Completion],
label: &str,
kind: CompletionKind,
) -> Option<&'a Completion> {
items.iter().find(|c| c.label == label && c.kind == kind)
}
#[test]
fn type_annotation_suggests_builtins_surface_and_project_types() {
let doc = "commons m {\n type Order = { id: Int }\n}\n";
let got = labels(" let x: ", doc);
for want in ["Int", "Option", "Result", "Effect", "List", "Map"] {
assert!(got.contains(&want.to_string()), "built-in {want}: {got:?}");
}
assert!(got.contains(&"Uuid".to_string()), "surface: {got:?}");
assert!(got.contains(&"Order".to_string()), "project: {got:?}");
}
#[test]
fn return_type_and_type_args_are_type_positions() {
assert!(is_type_position(" on call() -> "));
assert!(is_type_position(" let x: Option["));
assert!(is_type_position(" let x: Result[Int, "));
assert!(is_type_position(" -> Eff"));
}
#[test]
fn list_literal_is_not_a_type_position() {
assert!(!is_type_position(" let xs = ["));
let items = complete(" let xs = [", "context a.b\n", None);
assert!(
find(&items, "Some", CompletionKind::Constructor).is_some(),
"{:?}",
items.iter().map(|c| &c.label).collect::<Vec<_>>()
);
}
#[test]
fn builtin_type_carries_its_registry_doc() {
let items = complete(" let x: ", "context a.b\n", None);
let int = find(&items, "Int", CompletionKind::Type).expect("Int present");
assert_eq!(int.detail.as_deref(), keyword_doc("Int"));
assert!(int.detail.is_some(), "Int should have a doc");
}
#[test]
fn keyword_position_suggests_keywords_and_snippets() {
let items = complete(" ", "context a.b\n", None);
assert!(find(&items, "capability", CompletionKind::Keyword).is_some());
assert!(find(&items, "fn", CompletionKind::Keyword).is_some());
assert!(find(&items, "let", CompletionKind::Keyword).is_some());
assert!(find(&items, "Int", CompletionKind::Keyword).is_none());
assert!(find(&items, "Some", CompletionKind::Keyword).is_none());
let snip = find(&items, "service", CompletionKind::Snippet).expect("service snippet");
let body = snip.insert_text.as_deref().unwrap_or("");
assert!(body.contains("on call"), "snippet body: {body:?}");
assert!(body.contains("${1"), "snippet tab stop: {body:?}");
}
#[test]
fn keyword_position_fires_on_an_empty_line() {
assert!(is_keyword_position(""));
assert!(is_keyword_position(" cap"));
assert!(!is_keyword_position(" let x ="));
assert!(!is_keyword_position(" x: "));
assert!(!complete("", "context a.b\n", None).is_empty());
}
#[test]
fn member_receiver_is_a_single_upper_ident_before_a_dot() {
assert_eq!(member_receiver(" Color."), Some("Color".to_string()));
assert_eq!(
member_receiver(" let e = Email.o"),
Some("Email".to_string())
);
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); }
#[test]
fn sum_member_suggests_variants() {
let doc = "commons m {\n type Color = enum { Red, Green, Blue }\n}\n";
let items = complete(" let c = Color.", doc, None);
for v in ["Red", "Green", "Blue"] {
assert!(
find(&items, v, CompletionKind::Variant).is_some(),
"variant {v}: {:?}",
items.iter().map(|c| &c.label).collect::<Vec<_>>()
);
}
}
#[test]
fn refined_and_plain_alias_members_are_of_and_unsafe() {
let doc = "commons m {\n type Email = String where NonEmpty\n}\n";
let items = complete(" Email.", doc, None);
assert!(find(&items, "of", CompletionKind::Member).is_some());
assert!(find(&items, "unsafe", CompletionKind::Member).is_some());
let doc = "commons m {\n type Id = Int\n}\n";
assert!(find(&complete(" Id.", doc, None), "of", CompletionKind::Member).is_some());
}
#[test]
fn capability_member_suggests_ops() {
let doc = "context a.b\n capability Timer { fn now() -> Effect[Int]\n fn at(t: Int) -> Effect[()] }\n";
let items = complete(" Timer.", doc, None);
let now = find(&items, "now", CompletionKind::Member).expect("`now` op offered");
assert_eq!(
now.detail.as_deref(),
Some("now() -> Effect[Int] — operation of `Timer`")
);
let at = find(&items, "at", CompletionKind::Member).expect("`at` op offered");
assert_eq!(
at.detail.as_deref(),
Some("at(t: Int) -> Effect[()] — operation of `Timer`")
);
}
#[test]
fn builtin_type_statics_are_offered() {
assert!(
find(
&complete(" Int.", "context a.b\n", None),
"parse",
CompletionKind::Member
)
.is_some()
);
let j = complete(" Json.", "context a.b\n", None);
assert!(find(&j, "encode", CompletionKind::Member).is_some());
assert!(find(&j, "decode", CompletionKind::Member).is_some());
}
#[test]
fn builtin_sum_variants_are_complete() {
let http: Vec<&str> = bynk_syntax::ast::HTTP_VARIANTS
.iter()
.map(|v| v.name)
.collect();
let queue: Vec<&str> = bynk_syntax::ast::QUEUE_VARIANTS
.iter()
.map(|v| v.name)
.collect();
for (recv, names) in [("HttpResult", http), ("QueueResult", queue)] {
let items = complete(&format!(" {recv}."), "context a.b\n", None);
for name in names {
assert!(
find(&items, name, CompletionKind::Variant).is_some(),
"{recv}.{name} missing: {:?}",
items.iter().map(|c| &c.label).collect::<Vec<_>>()
);
}
}
}
#[test]
fn builtin_statics_are_reachable() {
for &(recv, members) in BUILTIN_STATICS {
let items = complete(&format!(" {recv}."), "context a.b\n", None);
for &(member, _) in members {
assert!(
find(&items, member, CompletionKind::Member).is_some(),
"{recv}.{member} unreachable: {:?}",
items.iter().map(|c| &c.label).collect::<Vec<_>>()
);
}
}
for (recv, member) in [("List", "empty"), ("Map", "empty"), ("Effect", "pure")] {
let items = complete(&format!(" {recv}."), "context a.b\n", None);
assert!(
find(&items, member, CompletionKind::Member).is_some(),
"{recv}.{member} missing from the statics table"
);
}
}
#[test]
fn record_value_and_decimal_receivers_yield_nothing() {
let doc = "commons m {\n type Point = { x: Int }\n}\n";
assert!(complete(" Point.", doc, None).is_empty(), "record");
assert!(complete(" let p = q.", doc, None).is_empty(), "value");
assert!(complete(" let n = 1.", doc, None).is_empty(), "decimal");
}
#[test]
fn value_receiver_rewrite_drops_the_dot_for_lowercase_receivers() {
let text = " let x = email.\n";
let offset = text.find('.').unwrap() + 1; let (rewritten, recv) = value_receiver_rewrite(text, offset).expect("value receiver");
assert_eq!(
rewritten, " let x = email\n",
"the trailing dot is dropped"
);
assert!(
text.get(recv..=recv).is_some_and(|c| c == "l"),
"the receiver offset lands inside `email`"
);
let text2 = " let x = email.ma\n";
let off2 = text2.find(".ma").unwrap() + 3;
assert_eq!(
value_receiver_rewrite(text2, off2).map(|(r, _)| r),
Some(" let x = email\n".to_string())
);
assert!(value_receiver_rewrite(" Email.", 8).is_none());
assert!(value_receiver_rewrite(" let n = 1.", 12).is_none());
assert!(value_receiver_rewrite(" email", 7).is_none());
}
#[test]
fn value_member_candidates_lists_kernel_methods() {
use bynk_syntax::ast::BaseType;
let list = Ty::List(Box::new(Ty::Base(BaseType::Int)));
let items = value_member_candidates(&list, "context a.b\n", None);
assert!(find(&items, "fold", CompletionKind::Member).is_some());
assert!(find(&items, "get", CompletionKind::Member).is_some());
let string = Ty::Base(BaseType::String);
let items = value_member_candidates(&string, "context a.b\n", None);
assert!(find(&items, "split", CompletionKind::Member).is_some());
assert!(find(&items, "trim", CompletionKind::Member).is_some());
}
#[test]
fn expression_position_offers_locals() {
assert!(is_expression_position(" let y = "));
assert!(is_expression_position(" let y = a + lo")); assert!(is_expression_position(" f("));
assert!(is_expression_position(" g(a, "));
assert!(is_expression_position(" xs.fold(0, (acc, x) => ac")); assert!(is_expression_position(" let y = foo"));
assert!(!is_expression_position(" let y: ")); assert!(!is_expression_position(" on call() -> ")); assert!(!is_expression_position(" tot")); }
#[test]
fn value_member_candidates_lists_record_fields() {
use bynk_check::checker::NamedKind;
let order = Ty::Named {
name: "Order".to_string(),
kind: NamedKind::Record,
};
let doc = "commons m {\n type Order = { id: Int, total: Int }\n}\n";
let items = value_member_candidates(&order, doc, None);
assert!(
find(&items, "id", CompletionKind::Field).is_some(),
"{items:?}",
items = items.iter().map(|c| &c.label).collect::<Vec<_>>()
);
assert!(find(&items, "total", CompletionKind::Field).is_some());
}
}