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 {
pub(crate) 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 let Some(recv) = record_construction_receiver(line_prefix) {
let fields = record_field_names(&recv, doc_text, src_root);
if !fields.is_empty() {
return fields;
}
}
if after_clause_keyword(line_prefix, "from") {
return protocol_candidates();
}
if after_clause_keyword(line_prefix, "on") {
return handler_kind_candidates();
}
if after_clause_keyword(line_prefix, "by") {
return actor_candidates(doc_text, src_root);
}
if after_clause_keyword(line_prefix, "exports") {
return export_kind_candidates();
}
if after_clause_keyword(line_prefix, "provides") {
return in_scope_capabilities(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())
}
fn record_construction_receiver(line: &str) -> Option<String> {
let bytes = line.as_bytes();
let mut depth = 0i32;
let mut open = None;
for i in (0..bytes.len()).rev() {
match bytes[i] {
b'}' => depth += 1,
b'{' => {
if depth == 0 {
open = Some(i);
break;
}
depth -= 1;
}
_ => {}
}
}
let open = open?;
let current = line[open + 1..].rsplit(',').next().unwrap_or("");
if current.contains(':') {
return None;
}
let head = line[..open].trim_end();
let start = head
.rfind(|c: char| !(c.is_alphanumeric() || c == '_'))
.map_or(0, |i| i + 1);
let recv = &head[start..];
if recv.chars().next()?.is_ascii_uppercase() {
Some(recv.to_string())
} else {
None
}
}
fn after_clause_keyword(line: &str, kw: &str) -> bool {
let Some(idx) = line.rfind(kw) else {
return false;
};
if !line[..idx]
.chars()
.last()
.map(char::is_whitespace)
.unwrap_or(true)
{
return false;
}
let after = &line[idx + kw.len()..];
after.starts_with(char::is_whitespace)
&& after
.trim_start()
.chars()
.all(|c| c.is_alphanumeric() || c == '_')
}
pub(crate) fn contract_clause_kind(line: &str) -> Option<bool> {
let colon = line.rfind(':')?;
let clause = line[..colon].trim();
for (kw, is_ensures) in [("requires", false), ("ensures", true)] {
if let Some(rest) = clause.strip_prefix(kw) {
let rest = rest.trim();
if !rest.is_empty() && rest.chars().all(|c| c.is_alphanumeric() || c == '_') {
return Some(is_ensures);
}
}
}
None
}
fn record_field_names(name: &str, doc_text: &str, src_root: Option<&Path>) -> Vec<Completion> {
let mut out: Vec<Completion> = Vec::new();
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
}
pub(crate) fn sum_type_variants(
name: &str,
doc_text: &str,
src_root: Option<&Path>,
) -> Vec<Completion> {
let mut out: Vec<Completion> = Vec::new();
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::Sum(s) = &t.body
{
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 `{name}`")),
));
}
}
}
}
});
out
}
fn protocol_candidates() -> Vec<Completion> {
["http", "cron", "queue", "WebSocket"]
.into_iter()
.map(|p| Completion::item(p, CompletionKind::Keyword, Some("service protocol".into())))
.collect()
}
fn handler_kind_candidates() -> Vec<Completion> {
[
"call", "GET", "POST", "PUT", "PATCH", "DELETE", "schedule", "message", "open", "close",
]
.into_iter()
.map(|k| Completion::item(k, CompletionKind::Keyword, Some("handler kind".into())))
.collect()
}
fn export_kind_candidates() -> Vec<Completion> {
["capability", "transparent", "opaque"]
.into_iter()
.map(|k| Completion::item(k, CompletionKind::Keyword, Some("export kind".into())))
.collect()
}
fn actor_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_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::Actor(a) = item
&& seen.insert(a.name.name.clone())
{
out.push(Completion::item(
a.name.name.clone(),
CompletionKind::Type,
Some("actor".into()),
));
}
}
});
out
}
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,
];
pub(crate) const SNIPPETS: &[(&str, &str)] = &[
("context", "context ${1:name} {\n\t$0\n}"),
("commons", "commons ${1:my.lib}\n\n$0"),
(
"adapter",
"adapter ${1:name} {\n\tbinding \"${2:./module}\"\n\t$0\n}",
),
("uses", "uses ${1:module}"),
("consumes", "consumes ${1:bynk} { ${2:Random} }"),
(
"type record",
"type ${1:Name} = {\n\t${2:field}: ${3:Int},\n}",
),
("type enum", "type ${1:Name} = enum {\n\t${2:Variant},\n}"),
(
"type refined",
"type ${1:Name} = ${2:String} where ${3:MinLength(1)}",
),
(
"type opaque",
"type ${1:Name} = opaque ${2:Int} where ${3:NonNegative}",
),
(
"fn",
"fn ${1:name}(${2:x}: ${3:Int}) -> ${4:Int} {\n\t$0\n}",
),
(
"fn contract",
"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}",
),
(
"capability",
"capability ${1:Name} {\n\tfn ${2:op}() -> Effect[${3:Unit}]\n}",
),
(
"provides",
"provides ${1:Cap} = ${2:Impl} {\n\tfn ${3:op}(${4}) -> Effect[${5:()}] {\n\t\tEffect.pure(${6:()})\n\t}\n}",
),
(
"actor",
"actor ${1:Name} { auth = ${2:Bearer(secret = \"AUTH_JWT_SECRET\")}, identity = ${3:UserId} }",
),
(
"agent",
"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}",
),
(
"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}"),
(
"on http",
"on ${1|GET,POST,PUT,DELETE,PATCH|}(\"${2:/path}\") (${3:body}: ${4:Req}) -> Effect[HttpResult[${5:Res}]] given ${6:Cap} {\n\t$0\n}",
),
(
"on cron",
"on schedule(\"${1:0 * * * *}\") () -> Effect[Result[(), String]] {\n\t$0\n\tOk(())\n}",
),
(
"suite",
"suite ${1:target}\n\ncase \"${2:it works}\" {\n\tlet ${3:actual} = ${4:0}\n\texpect ${5:actual == 0}\n}",
),
(
"property",
"property \"${1:invariant holds}\" {\n\tfor all ${2:x}: ${3:Int} {\n\t\texpect ${4:x == x}\n\t}\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
}
pub(crate) 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());
}
#[test]
fn record_construction_offers_field_names() {
let doc = "commons m {\n type Order = { id: Int, total: Int }\n}\n";
let got = labels(" let o = Order { ", doc);
assert!(got.contains(&"id".to_string()), "{got:?}");
assert!(got.contains(&"total".to_string()), "{got:?}");
let got2 = labels(" let o = Order { id: 1, ", doc);
assert!(got2.contains(&"total".to_string()), "{got2:?}");
assert!(record_construction_receiver(" let o = Order { id: ").is_none());
assert!(record_construction_receiver(" if x { ").is_none());
}
#[test]
fn from_offers_protocols() {
let got = labels(" service s from ", "context a.b\n");
assert!(got.contains(&"http".to_string()), "{got:?}");
assert!(got.contains(&"cron".to_string()) && got.contains(&"queue".to_string()));
}
#[test]
fn on_offers_handler_kinds() {
let got = labels(" on ", "context a.b\n");
assert!(got.contains(&"call".to_string()), "{got:?}");
assert!(got.contains(&"GET".to_string()) && got.contains(&"schedule".to_string()));
}
#[test]
fn by_offers_project_actors() {
let doc = "context a.b\n\nactor Caller { auth = Bearer }\n";
let got = labels(" by ", doc);
assert!(got.contains(&"Caller".to_string()), "{got:?}");
}
#[test]
fn exports_offers_export_kinds() {
let got = labels(" exports ", "adapter t {\n binding \"./b.ts\"\n}\n");
assert!(got.contains(&"capability".to_string()), "{got:?}");
assert!(got.contains(&"transparent".to_string()));
}
#[test]
fn provides_offers_in_scope_capabilities() {
let doc = "context a.b\n\ncapability Store { fn get() -> Effect[Int] }\n";
let got = labels(" provides ", doc);
assert!(got.contains(&"Store".to_string()), "{got:?}");
}
#[test]
fn clause_detectors_do_not_over_fire() {
assert!(!after_clause_keyword(" session ", "on"));
assert!(!after_clause_keyword(" let from = ", "from"));
assert!(after_clause_keyword(" service s from ", "from"));
assert!(after_clause_keyword(" by ", "by"));
}
#[test]
fn contract_clause_kind_detects_requires_and_ensures() {
assert_eq!(contract_clause_kind(" requires positive: "), Some(false));
assert_eq!(contract_clause_kind(" ensures never_neg: "), Some(true));
assert_eq!(contract_clause_kind(" id: Int"), None);
assert_eq!(contract_clause_kind(" let x = 1"), None);
}
#[test]
fn sum_type_variants_lists_variants() {
let doc = "commons m {\n type Status = enum { Pending, Shipped }\n}\n";
let got: Vec<String> = sum_type_variants("Status", doc, None)
.into_iter()
.map(|c| c.label)
.collect();
assert!(got.contains(&"Pending".to_string()), "{got:?}");
assert!(got.contains(&"Shipped".to_string()), "{got:?}");
}
}