Skip to main content

bynkc_lsp/
completion.rs

1//! Completion for the cursor, keyed off the line up to it.
2//!
3//! The surface is the canonical *cursor context × candidate-kind* matrix fixed
4//! by ADR 0093 (`design/decisions/0093-completion-surface-contract.md`), spec'd
5//! at `design/bynk-lsp-spec.md` §3.15. [`complete`] dispatches the six contexts
6//! it can serve purely (no analysis cache):
7//!
8//! - `consumes <prefix>` / `consumes U { … }` / `given …` — consumable units and
9//!   in-scope capabilities (v0.17);
10//! - **type position** (`: T`, `-> T`, inside `[ … ]` type args) — built-in
11//!   types, the `bynk`-surface transparent types, and project `type` decls;
12//! - **keyword position** (a bare word at a declaration/statement start) — the
13//!   reserved keywords (with registry docs) and declaration snippets;
14//! - **name-receiver `UpperIdent.`** — sum variants (project + built-in
15//!   `HttpResult`/`QueueResult`), refined/opaque `of`/`unsafe`, capability ops,
16//!   and built-in type statics (`Int.parse`/`List.empty`/`Effect.pure`/…);
17//! - **expression position** (after `=`/`(`/`,`/`=>`/an operator) — the value
18//!   constructors (`Ok`/`Some`/`true`/…), in-scope type names, and in-scope free
19//!   functions (the current unit's own `fn`s + `uses`-imported stdlib/project
20//!   combinators, gated on the `uses` set) (ADR 0093 D3).
21//!
22//! Two further contexts need the analysis overlay and so live handler-side
23//! (`main.rs`): **value-receiver `lower.`** members (kernel methods + record
24//! fields) and **in-scope locals/params**. They depend on the analysis overlay
25//! (the boundary is ADR 0093 D4), but since slice 4 (ADR 0094) it is
26//! error-tolerant: best-effort partial types are recorded even on a broken
27//! buffer, so they no longer go silent on an unrelated error. Items also carry a
28//! one-line `detail` eagerly; the richer `documentation` is filled in lazily by
29//! `completionItem/resolve`, handler-side (slice 5).
30//!
31//! Context detection is lexical (it must work mid-edit, when the buffer rarely
32//! parses); candidates are semantic. Unit/type/capability/member enumeration
33//! parses the project's `.bynk` files (and the embedded `bynk` surface) with
34//! recovery, so it works even while the file the cursor sits in is mid-edit.
35//! Built-ins, keywords, and constructors come from the static `bynkc` registries
36//! (`keywords`/`builtin_names`/`firstparty`/`ast`), never the index — first-party
37//! symbols aren't indexed (the v0.28 finding); the project parse supplies only
38//! *project* symbols.
39
40use std::collections::BTreeSet;
41use std::path::Path;
42
43use bynk_check::checker::Ty;
44use bynk_check::firstparty::{
45    BYNK_ADAPTER_SRC, BYNK_LIST_SRC, BYNK_MAP_SRC, BYNK_STRING_SRC, CLOUDFLARE_ADAPTER_SRC,
46};
47use bynk_check::kernel_methods;
48use bynk_syntax::ast::{CommonsItem, ExportKind, FnName, SourceUnit, TypeBody, UsesDecl};
49use bynk_syntax::{keywords, lexer, parser};
50
51use crate::symbols::{type_ref_str, walk_bynk_files};
52
53/// What a candidate refers to — maps to an LSP `CompletionItemKind`.
54#[derive(Clone, Copy, PartialEq, Eq)]
55pub enum CompletionKind {
56    Unit,
57    Capability,
58    Type,
59    Keyword,
60    Snippet,
61    /// A sum-type variant (`Color.Red`).
62    Variant,
63    /// A name-receiver member: a refined/opaque `of`/`unsafe` constructor, a
64    /// capability operation, or a built-in type static (`Int.parse`).
65    Member,
66    /// A record field on a value receiver (`order.total`).
67    Field,
68    /// A value constructor at expression position (`Ok`/`Some`/`true`).
69    Constructor,
70    /// A free function in scope at expression position — the current unit's own
71    /// top-level `fn`s and the `uses`-imported stdlib/project combinators.
72    Function,
73}
74
75pub struct Completion {
76    pub label: String,
77    pub kind: CompletionKind,
78    pub detail: Option<String>,
79    /// LSP snippet text (with `${n:…}`/`$0` tab stops) for `Snippet` items;
80    /// `None` means insert the label verbatim.
81    pub insert_text: Option<String>,
82}
83
84impl Completion {
85    fn item(label: impl Into<String>, kind: CompletionKind, detail: Option<String>) -> Self {
86        Completion {
87            label: label.into(),
88            kind,
89            detail,
90            insert_text: None,
91        }
92    }
93
94    fn snippet(label: &str, body: &str) -> Self {
95        Completion {
96            label: label.to_string(),
97            kind: CompletionKind::Snippet,
98            detail: Some(format!("{label} scaffold")),
99            insert_text: Some(body.to_string()),
100        }
101    }
102}
103
104/// Produce completions for the cursor, given the text of the line up to the
105/// cursor, the current document text, and the project source root (if any).
106pub fn complete(line_prefix: &str, doc_text: &str, src_root: Option<&Path>) -> Vec<Completion> {
107    // 1. Inside `consumes U { … <cursor>` — the capabilities U exports.
108    if let Some(unit) = consumes_brace_unit(line_prefix) {
109        return capabilities_of_unit(&unit, doc_text, src_root)
110            .into_iter()
111            .map(|c| {
112                Completion::item(
113                    c,
114                    CompletionKind::Capability,
115                    Some(format!("capability exported by `{unit}`")),
116                )
117            })
118            .collect();
119    }
120    // 2. After `consumes <prefix>` — consumable unit names.
121    if is_consumes_target(line_prefix) {
122        return consumable_units(doc_text, src_root);
123    }
124    // 3. After `given …` — in-scope capabilities.
125    if is_given_position(line_prefix) {
126        return in_scope_capabilities(doc_text, src_root);
127    }
128    // 4. `UpperIdent.<cursor>` — name-receiver members: sum variants, refined/
129    //    opaque `of`/`unsafe`, capability ops, or built-in type statics.
130    if let Some(receiver) = member_receiver(line_prefix) {
131        return member_candidates(&receiver, doc_text, src_root);
132    }
133    // 5. Type position (`: T`, `-> T`, `[ … ]` type args) — built-ins, the
134    //    `bynk`-surface transparent types, and project type declarations.
135    if is_type_position(line_prefix) {
136        return type_candidates(doc_text, src_root);
137    }
138    // 6. Keyword position (a bare word at a declaration/statement start) — the
139    //    reserved keywords plus declaration snippets.
140    if is_keyword_position(line_prefix) {
141        return keyword_and_snippet_candidates();
142    }
143    // 7. Expression position (after `=`/`(`/`,`/`=>`/a binary operator) — a value
144    //    starts here: the constructor keywords + in-scope type names. In-scope
145    //    locals/params (and, from slice 3, free functions) are appended
146    //    handler-side, where the analysis cache lives (ADR 0093 D3).
147    if is_expression_position(line_prefix) {
148        return expression_candidates(doc_text, src_root);
149    }
150    Vec::new()
151}
152
153// -- Cursor-context detection (line-prefix scanning) --
154
155/// `consumes U { … ` with the brace still open at the cursor → `Some(U)`.
156fn consumes_brace_unit(line: &str) -> Option<String> {
157    let idx = line.rfind("consumes")?;
158    let after = &line[idx + "consumes".len()..];
159    let open = after.find('{')?;
160    // The brace must still be open up to the cursor (no closing brace after it).
161    if after[open + 1..].contains('}') {
162        return None;
163    }
164    let unit = after[..open].trim();
165    if unit.is_empty() || !is_qualified_name(unit) {
166        return None;
167    }
168    Some(unit.to_string())
169}
170
171/// `consumes <partial>` with no brace or `as` yet → completing the target name.
172fn is_consumes_target(line: &str) -> bool {
173    let Some(idx) = line.rfind("consumes") else {
174        return false;
175    };
176    // `consumes` must be a standalone keyword (preceded by start/whitespace).
177    if !line[..idx]
178        .chars()
179        .last()
180        .map(|c| c.is_whitespace())
181        .unwrap_or(true)
182    {
183        return false;
184    }
185    let after = &line[idx + "consumes".len()..];
186    // Need at least one separating space, and no `{`, `}`, or `as` yet.
187    after.starts_with(char::is_whitespace)
188        && !after.contains('{')
189        && !after.contains('}')
190        && !after.split_whitespace().any(|w| w == "as")
191}
192
193/// The cursor is inside a `given` list (after `given`, before the `{` body).
194fn is_given_position(line: &str) -> bool {
195    let Some(idx) = line.rfind("given") else {
196        return false;
197    };
198    if !line[..idx]
199        .chars()
200        .last()
201        .map(|c| c.is_whitespace())
202        .unwrap_or(true)
203    {
204        return false;
205    }
206    let after = &line[idx + "given".len()..];
207    if !after.starts_with(char::is_whitespace) {
208        return false;
209    }
210    // Still in the given list while only capability names, dots, commas and
211    // whitespace follow — a `{` opens the handler body.
212    after
213        .chars()
214        .all(|c| c.is_alphanumeric() || matches!(c, '_' | '.' | ',' | ' ' | '\t'))
215}
216
217fn is_qualified_name(s: &str) -> bool {
218    !s.is_empty()
219        && s.split('.').all(|seg| {
220            !seg.is_empty()
221                && seg.chars().all(|c| c.is_alphanumeric() || c == '_')
222                && !seg.chars().next().unwrap().is_ascii_digit()
223        })
224}
225
226/// The cursor sits in a type position: a return type (`-> T`), a type
227/// annotation/field type (`: T`), or inside a `[ … ]` type-argument list. The
228/// partial type name being typed is stripped before inspecting the preceding
229/// token, so `: Optio` and `-> Eff` both qualify.
230///
231/// Conservative by construction: a list literal `[1, 2` is excluded (its `[` is
232/// not preceded by a type constructor). The one accepted false positive is a
233/// record *construction* value (`Order { id: <cursor>`), lexically identical to
234/// a record field-type declaration — offering type names there is mild noise.
235fn is_type_position(line: &str) -> bool {
236    let head = line
237        .trim_end_matches(|c: char| c.is_alphanumeric() || c == '_')
238        .trim_end();
239    head.ends_with("->") || (head.ends_with(':') && !head.ends_with("::")) || in_type_arg_list(head)
240}
241
242/// `head` ends inside an unclosed `[ … ` whose opening bracket immediately
243/// follows an identifier (a type constructor, e.g. `Option[`, `Result[Int, `) —
244/// as opposed to a bare list-literal `[`.
245fn in_type_arg_list(head: &str) -> bool {
246    let chars: Vec<char> = head.chars().collect();
247    let mut depth = 0i32;
248    let mut opener_after_ident = false;
249    for (i, &c) in chars.iter().enumerate() {
250        match c {
251            '[' => {
252                depth += 1;
253                if depth == 1 {
254                    opener_after_ident =
255                        i > 0 && (chars[i - 1].is_alphanumeric() || chars[i - 1] == '_');
256                }
257            }
258            ']' => depth -= 1,
259            _ => {}
260        }
261    }
262    depth > 0 && opener_after_ident
263}
264
265/// A bare word at a declaration/statement start: the line up to the cursor is
266/// only leading whitespace plus an optional partial identifier (no operators,
267/// colons, or brackets). Fires on an empty line too. Disjoint from
268/// [`is_type_position`], whose triggers (`:`/`->`/`[`) make this false.
269pub fn is_keyword_position(line: &str) -> bool {
270    line.trim().chars().all(|c| c.is_alphanumeric() || c == '_')
271}
272
273/// The cursor sits where a **value** expression is expected — after `=`/`(`/`,`,
274/// a `=>` lambda arrow, or a binary operator — so in-scope locals are offered
275/// (v0.31, ADR 0064). Conservative: covers the common positions, excludes the
276/// type arrow `->`. (The handler also offers locals at keyword position.)
277pub fn is_expression_position(line: &str) -> bool {
278    let head = line
279        .trim_end_matches(|c: char| c.is_alphanumeric() || c == '_')
280        .trim_end();
281    if head.ends_with("->") {
282        return false; // a return/param type, not a value
283    }
284    if head.ends_with("=>") {
285        return true; // a lambda body
286    }
287    matches!(
288        head.chars().last(),
289        Some('=' | '(' | ',' | '[' | '+' | '-' | '*' | '/' | '<' | '>' | '&' | '|')
290    )
291}
292
293/// `UpperIdent.<partial>` at the cursor → `Some("UpperIdent")` — a name
294/// receiver whose members are statically enumerable (a sum/refined/opaque
295/// type or a capability). Conservative: the receiver is a **single**
296/// uppercase-initial identifier, not itself a `.`-qualified segment (so
297/// `bynk.cloudflare.` and `a.B.` are excluded) and not a number (so the
298/// decimal `1.` is excluded). A lowercase `x.` is a *value* receiver — deferred
299/// to slice 3 — and yields `None`.
300fn member_receiver(line: &str) -> Option<String> {
301    // Drop the partial member name being typed, then require a trailing dot.
302    let head = line
303        .trim_end_matches(|c: char| c.is_alphanumeric() || c == '_')
304        .strip_suffix('.')?;
305    // The receiver is the identifier immediately before that dot.
306    let start = head
307        .rfind(|c: char| !(c.is_alphanumeric() || c == '_'))
308        .map_or(0, |i| i + 1);
309    let recv = &head[start..];
310    let first = recv.chars().next()?;
311    if !first.is_ascii_uppercase() {
312        return None;
313    }
314    // Reject a `.`-qualified receiver (`a.B.`): the char before it is a dot.
315    if head[..start].ends_with('.') {
316        return None;
317    }
318    Some(recv.to_string())
319}
320
321/// Built-in type statics — real language statics that are not user-declared, so
322/// they come from this small table rather than the project parse. Covers the
323/// numeric parse statics and the JSON codec (v0.22, ADRs 0048/0049), the
324/// collection `empty` constructors (v0.20b), and `Effect.pure` (v0.5). The full
325/// real set per ADR 0093 D2 — kept complete and drift-tested
326/// (`builtin_statics_are_reachable`).
327pub(crate) const BUILTIN_STATICS: &[(&str, &[(&str, &str)])] = &[
328    ("Int", &[("parse", "parse(s: String) -> Option[Int]")]),
329    ("Float", &[("parse", "parse(s: String) -> Option[Float]")]),
330    (
331        "Json",
332        &[
333            ("encode", "encode(value) -> String"),
334            ("decode", "decode[T](s: String) -> Result[T, JsonError]"),
335        ],
336    ),
337    ("List", &[("empty", "empty() -> List[T]")]),
338    ("Map", &[("empty", "empty() -> Map[K, V]")]),
339    ("Effect", &[("pure", "pure(value) -> Effect[T]")]),
340    (
341        "Bytes",
342        &[
343            ("fromUtf8", "fromUtf8(s: String) -> Bytes"),
344            ("fromBase64", "fromBase64(s: String) -> Option[Bytes]"),
345            ("empty", "empty() -> Bytes"),
346        ],
347    ),
348];
349
350/// Variants of a built-in sum type (`HttpResult`/`QueueResult`), sourced from
351/// the AST variant registries so a new variant surfaces in completion for free
352/// (ADR 0093 D2/G3). Empty for any other receiver.
353fn builtin_sum_variants(receiver: &str) -> Vec<(String, String)> {
354    match receiver {
355        "HttpResult" => bynk_syntax::ast::HTTP_VARIANTS
356            .iter()
357            .map(|v| {
358                (
359                    v.name.to_string(),
360                    format!("variant of `HttpResult` ({})", v.status),
361                )
362            })
363            .collect(),
364        "QueueResult" => bynk_syntax::ast::QUEUE_VARIANTS
365            .iter()
366            .map(|v| (v.name.to_string(), "variant of `QueueResult`".to_string()))
367            .collect(),
368        _ => Vec::new(),
369    }
370}
371
372/// Members of a name receiver: built-in type statics, then built-in sum-type
373/// variants, then — from the project and embedded-surface parse — project sum
374/// variants, refined/opaque `of`/`unsafe`, or capability operations. Yields `[]`
375/// when the receiver resolves to none of these (e.g. a plain `type X = Int`
376/// alias or a record).
377fn member_candidates(receiver: &str, doc_text: &str, src_root: Option<&Path>) -> Vec<Completion> {
378    if let Some((_, statics)) = BUILTIN_STATICS.iter().find(|(name, _)| *name == receiver) {
379        return statics
380            .iter()
381            .map(|(label, sig)| {
382                Completion::item(*label, CompletionKind::Member, Some(sig.to_string()))
383            })
384            .collect();
385    }
386    let mut out: Vec<Completion> = Vec::new();
387    let mut seen: BTreeSet<String> = BTreeSet::new();
388    // Built-in sum types (`HttpResult`/`QueueResult`) — variants from the AST
389    // registry, on the same name-receiver path as project sums (ADR 0093 G3).
390    for (label, detail) in builtin_sum_variants(receiver) {
391        if seen.insert(label.clone()) {
392            out.push(Completion::item(
393                label,
394                CompletionKind::Variant,
395                Some(detail),
396            ));
397        }
398    }
399    for_each_unit(doc_text, src_root, |unit| {
400        let items = match unit {
401            SourceUnit::Commons(c) => &c.items,
402            SourceUnit::Context(c) => &c.items,
403            SourceUnit::Adapter(a) => &a.items,
404            _ => return,
405        };
406        for item in items {
407            match item {
408                CommonsItem::Type(t) if t.name.name == receiver => match &t.body {
409                    bynk_syntax::ast::TypeBody::Sum(s) => {
410                        for v in &s.variants {
411                            if seen.insert(v.name.name.clone()) {
412                                out.push(Completion::item(
413                                    v.name.name.clone(),
414                                    CompletionKind::Variant,
415                                    Some(format!("variant of `{receiver}`")),
416                                ));
417                            }
418                        }
419                    }
420                    bynk_syntax::ast::TypeBody::Refined { .. }
421                    | bynk_syntax::ast::TypeBody::Opaque { .. } => {
422                        for (label, sig) in [
423                            (
424                                "of",
425                                format!("of(value) -> Result[{receiver}, ValidationError]"),
426                            ),
427                            ("unsafe", format!("unsafe(value) -> {receiver}")),
428                        ] {
429                            if seen.insert(label.to_string()) {
430                                out.push(Completion::item(
431                                    label,
432                                    CompletionKind::Member,
433                                    Some(sig),
434                                ));
435                            }
436                        }
437                    }
438                    // A plain alias (`type X = Int`) or a record has no
439                    // name-receiver members — record fields are value-receiver
440                    // (slice 3).
441                    _ => {}
442                },
443                CommonsItem::Capability(c) if c.name.name == receiver => {
444                    for op in &c.ops {
445                        if seen.insert(op.name.name.clone()) {
446                            // Typed signature (params + return), the same
447                            // `type_ref_str` rendering hover/signature help use —
448                            // not bare param names (slice 5 detail polish).
449                            let params = op
450                                .params
451                                .iter()
452                                .map(|p| format!("{}: {}", p.name.name, type_ref_str(&p.type_ref)))
453                                .collect::<Vec<_>>()
454                                .join(", ");
455                            out.push(Completion::item(
456                                op.name.name.clone(),
457                                CompletionKind::Member,
458                                Some(format!(
459                                    "{}({params}) -> {} — operation of `{receiver}`",
460                                    op.name.name,
461                                    type_ref_str(&op.return_type)
462                                )),
463                            ));
464                        }
465                    }
466                }
467                _ => {}
468            }
469        }
470    });
471    out
472}
473
474// -- Positional candidate sources (static registries + project parse) --
475
476/// Built-in type names not declared in any parseable source. Base and generic
477/// types from the language core; collection types from `builtin_names`. Docs
478/// are drawn from the `keywords` registry where present (one source of truth).
479const BUILTIN_TYPES: &[&str] = &[
480    bynk_check::builtin_names::types::INT,
481    "Bool",
482    bynk_check::builtin_names::types::FLOAT,
483    "String",
484    "Option",
485    "Result",
486    "Effect",
487    bynk_check::builtin_names::types::LIST,
488    bynk_check::builtin_names::types::MAP,
489];
490
491/// Declaration snippets (`CompletionItemKind::SNIPPET`), as LSP snippet bodies.
492const SNIPPETS: &[(&str, &str)] = &[
493    ("context", "context ${1:name} {\n\t$0\n}"),
494    (
495        "adapter",
496        "adapter ${1:name} {\n\tbinding \"${2:./module}\"\n\t$0\n}",
497    ),
498    (
499        "capability",
500        "capability ${1:Name} {\n\tfn ${2:op}() -> Effect[${3:Unit}]\n}",
501    ),
502    (
503        "service",
504        "service ${1:name} {\n\ton call(${2}) -> Effect[${3:Unit}] {\n\t\t$0\n\t}\n}",
505    ),
506    ("on call", "on call(${1}) -> Effect[${2:Unit}] {\n\t$0\n}"),
507    ("test", "test \"${1:description}\" {\n\t$0\n}"),
508];
509
510/// The value constructors offered at expression position (ADR 0093 D3) — the
511/// closed set of `Result`/`Option` variant constructors and the boolean
512/// literals. A value expression can begin with any of these; their docs reuse
513/// the `keywords` registry (one source of truth).
514const CONSTRUCTORS: &[&str] = &["Ok", "Err", "Some", "None", "true", "false"];
515
516/// Expression-position candidates: the value constructors plus in-scope type
517/// names (the entry to a static call like `Int.parse` or a record construction
518/// like `Order { … }`). In-scope values — locals/params, and from slice 3 free
519/// functions — are appended by the handler, which owns the analysis cache, so
520/// they are not produced here (ADR 0093 D3).
521fn expression_candidates(doc_text: &str, src_root: Option<&Path>) -> Vec<Completion> {
522    let mut out: Vec<Completion> = CONSTRUCTORS
523        .iter()
524        .map(|&name| {
525            Completion::item(
526                name,
527                CompletionKind::Constructor,
528                keyword_doc(name).map(str::to_string),
529            )
530        })
531        .collect();
532    // Type names are valid here too (static receiver / record construction); the
533    // `Type.` member context (slice 1) takes over once the user types the dot.
534    out.extend(type_candidates(doc_text, src_root));
535    // In-scope free functions — the current unit's own `fn`s and the combinators
536    // of every `uses`-imported module (project + stdlib) — ADR 0093 D3 / G5.
537    out.extend(free_function_candidates(doc_text, src_root));
538    out
539}
540
541/// A unit's top-level items and its `uses` clauses, for the kinds that carry
542/// free functions. Service/other units contribute neither.
543fn unit_items_and_uses(unit: &SourceUnit) -> (&[CommonsItem], &[UsesDecl]) {
544    match unit {
545        SourceUnit::Commons(c) => (&c.items, &c.uses),
546        SourceUnit::Context(c) => (&c.items, &c.uses),
547        SourceUnit::Adapter(a) => (&a.items, &a.uses),
548        _ => (&[], &[]),
549    }
550}
551
552/// The qualified name of the unit the cursor's document declares, via a recovery
553/// parse (the header survives a mid-edit body). `None` for a headerless fragment
554/// that names no unit.
555fn current_unit_name(doc_text: &str) -> Option<String> {
556    let tokens = lexer::tokenize(doc_text).ok()?;
557    let (unit, _errs) = parser::parse_unit_with_recovery(&tokens, doc_text);
558    Some(unit?.name().joined())
559}
560
561/// Render a free function's signature for the completion detail, the same way
562/// hover and signature help do (`symbols::type_ref_str`) — one format, never
563/// divergent. Mirrors signature help: no generic-parameter list.
564fn free_fn_signature(name: &str, f: &bynk_syntax::ast::FnDecl) -> String {
565    let params = f
566        .params
567        .iter()
568        .map(|p| format!("{}: {}", p.name.name, type_ref_str(&p.type_ref)))
569        .collect::<Vec<_>>()
570        .join(", ");
571    format!("{name}({params}) -> {}", type_ref_str(&f.return_type))
572}
573
574/// Free-function candidates at expression position: the current unit's own
575/// top-level `fn`s plus the free `fn`s of every `uses`-imported module (project
576/// commons and the embedded stdlib). Gated on the `uses` set so a combinator is
577/// offered only where it is actually in scope (ADR 0093 D3 / G5).
578fn free_function_candidates(doc_text: &str, src_root: Option<&Path>) -> Vec<Completion> {
579    let Some(current) = current_unit_name(doc_text) else {
580        return Vec::new();
581    };
582    // One parse pass: collect each unit's name, its free `fn`s (name + signature),
583    // and its `uses` targets.
584    struct UnitFns {
585        name: String,
586        fns: Vec<(String, String)>,
587        uses: Vec<String>,
588    }
589    let mut units: Vec<UnitFns> = Vec::new();
590    for_each_unit(doc_text, src_root, |unit| {
591        let (items, uses) = unit_items_and_uses(unit);
592        let fns = items
593            .iter()
594            .filter_map(|it| match it {
595                CommonsItem::Fn(f) => match &f.name {
596                    FnName::Free(id) => Some((id.name.clone(), free_fn_signature(&id.name, f))),
597                    FnName::Method { .. } => None,
598                },
599                _ => None,
600            })
601            .collect();
602        units.push(UnitFns {
603            name: unit.name().joined(),
604            fns,
605            uses: uses.iter().map(|u| u.target.joined()).collect(),
606        });
607    });
608    // The import scope: the `uses` targets of every unit sharing the current name
609    // (a unit may span files, so union them).
610    let mut imported: BTreeSet<String> = BTreeSet::new();
611    for u in &units {
612        if u.name == current {
613            imported.extend(u.uses.iter().cloned());
614        }
615    }
616    // Offer the current unit's own fns and the fns of each imported module.
617    let mut out: Vec<Completion> = Vec::new();
618    let mut seen: BTreeSet<String> = BTreeSet::new();
619    for u in &units {
620        let own = u.name == current;
621        if !own && !imported.contains(&u.name) {
622            continue;
623        }
624        let origin = if own { "this unit" } else { u.name.as_str() };
625        for (name, sig) in &u.fns {
626            if seen.insert(name.clone()) {
627                out.push(Completion::item(
628                    name.clone(),
629                    CompletionKind::Function,
630                    Some(format!("{sig} — `{origin}`")),
631                ));
632            }
633        }
634    }
635    out
636}
637
638/// The one-line doc for a name in the `keywords` registry, if present.
639fn keyword_doc(word: &str) -> Option<&'static str> {
640    keywords::KEYWORDS
641        .iter()
642        .find(|k| k.word == word)
643        .map(|k| k.meaning)
644}
645
646/// Type-position candidates: built-in types (with registry docs), then every
647/// `type` declaration found in the project sources and the embedded `bynk`
648/// surface (so the transparent surface types `Uuid`/`Method`/… come for free).
649fn type_candidates(doc_text: &str, src_root: Option<&Path>) -> Vec<Completion> {
650    let mut out: Vec<Completion> = Vec::new();
651    let mut seen: BTreeSet<String> = BTreeSet::new();
652    for &name in BUILTIN_TYPES {
653        if seen.insert(name.to_string()) {
654            let detail = keyword_doc(name)
655                .map(str::to_string)
656                .or_else(|| match name {
657                    "List" => Some("The built-in list type, `List[T]`.".to_string()),
658                    "Map" => Some("The built-in map type, `Map[K, V]`.".to_string()),
659                    _ => Some("built-in type".to_string()),
660                });
661            out.push(Completion::item(name, CompletionKind::Type, detail));
662        }
663    }
664    for_each_unit(doc_text, src_root, |unit| {
665        let items = match unit {
666            SourceUnit::Commons(c) => &c.items,
667            SourceUnit::Context(c) => &c.items,
668            SourceUnit::Adapter(a) => &a.items,
669            _ => return,
670        };
671        for item in items {
672            if let CommonsItem::Type(t) = item
673                && seen.insert(t.name.name.clone())
674            {
675                out.push(Completion::item(
676                    t.name.name.clone(),
677                    CompletionKind::Type,
678                    Some("type".to_string()),
679                ));
680            }
681        }
682    });
683    out
684}
685
686/// Keyword-position candidates: the lowercase-initial reserved keywords (the
687/// declaration/statement words — uppercase type/value names like `Int`/`Some`
688/// belong to type/expression position) with their registry docs, plus the
689/// declaration snippets.
690fn keyword_and_snippet_candidates() -> Vec<Completion> {
691    let mut out: Vec<Completion> = keywords::KEYWORDS
692        .iter()
693        .filter(|k| k.word.chars().next().is_some_and(char::is_lowercase))
694        .map(|k| Completion::item(k.word, CompletionKind::Keyword, Some(k.meaning.to_string())))
695        .collect();
696    for &(label, body) in SNIPPETS {
697        out.push(Completion::snippet(label, body));
698    }
699    out
700}
701
702// -- Enumeration (parse project sources + the embedded `bynk` surface) --
703
704/// Parse every project unit, plus the embedded first-party adapters (the
705/// `bynk` surface and the `bynk.cloudflare` platform adapter), and call `f`
706/// for each. Recovery parsing tolerates the in-progress edit at the cursor.
707pub(crate) fn for_each_unit(
708    doc_text: &str,
709    src_root: Option<&Path>,
710    mut f: impl FnMut(&SourceUnit),
711) {
712    let mut sources: Vec<String> = vec![
713        BYNK_ADAPTER_SRC.to_string(),
714        CLOUDFLARE_ADAPTER_SRC.to_string(),
715        // The embedded stdlib commons (`bynk.list`/`bynk.map`/`bynk.string`) so
716        // their free fns are enumerable for `uses`-imported completion (G5) and
717        // signature help. Harmless to the other contexts — these units declare
718        // only `fn`s (no types/capabilities), and they are `commons`, never a
719        // `consumes` target.
720        BYNK_LIST_SRC.to_string(),
721        BYNK_MAP_SRC.to_string(),
722        BYNK_STRING_SRC.to_string(),
723        doc_text.to_string(),
724    ];
725    if let Some(root) = src_root {
726        for path in walk_bynk_files(root) {
727            if let Ok(s) = std::fs::read_to_string(&path) {
728                sources.push(s);
729            }
730        }
731    }
732    for src in &sources {
733        let Ok(tokens) = lexer::tokenize(src) else {
734            continue;
735        };
736        let (unit, _errs) = parser::parse_unit_with_recovery(&tokens, src);
737        if let Some(unit) = unit {
738            f(&unit);
739        }
740    }
741}
742
743/// Consumable unit names: contexts and adapters (plus `bynk`), deduplicated.
744fn consumable_units(doc_text: &str, src_root: Option<&Path>) -> Vec<Completion> {
745    let mut seen: BTreeSet<String> = BTreeSet::new();
746    let mut out: Vec<Completion> = Vec::new();
747    for_each_unit(doc_text, src_root, |unit| {
748        let (name, kind) = match unit {
749            SourceUnit::Context(c) => (c.name.joined(), "context"),
750            SourceUnit::Adapter(a) => (a.name.joined(), "adapter"),
751            _ => return,
752        };
753        if seen.insert(name.clone()) {
754            out.push(Completion::item(
755                name,
756                CompletionKind::Unit,
757                Some(kind.to_string()),
758            ));
759        }
760    });
761    out
762}
763
764/// The capability names a unit `exports capability`.
765fn capabilities_of_unit(unit: &str, doc_text: &str, src_root: Option<&Path>) -> Vec<String> {
766    let mut out: BTreeSet<String> = BTreeSet::new();
767    for_each_unit(doc_text, src_root, |u| {
768        let (name, exports) = match u {
769            SourceUnit::Context(c) => (c.name.joined(), &c.exports),
770            SourceUnit::Adapter(a) => (a.name.joined(), &a.exports),
771            _ => return,
772        };
773        if name != unit {
774            return;
775        }
776        for clause in exports {
777            if clause.kind == ExportKind::Capability {
778                for n in &clause.names {
779                    out.insert(n.name.clone());
780                }
781            }
782        }
783    });
784    out.into_iter().collect()
785}
786
787/// Capabilities in scope for a `given` clause in the current document: locally
788/// declared capabilities, bare names flattened by a braced `consumes`, and
789/// `U.Cap` for each whole-unit `consumes U`.
790fn in_scope_capabilities(doc_text: &str, src_root: Option<&Path>) -> Vec<Completion> {
791    let mut labels: BTreeSet<String> = BTreeSet::new();
792    let Ok(tokens) = lexer::tokenize(doc_text) else {
793        return Vec::new();
794    };
795    let (Some(unit), _errs) = parser::parse_unit_with_recovery(&tokens, doc_text) else {
796        return Vec::new();
797    };
798    let (items, consumes) = match &unit {
799        SourceUnit::Context(c) => (&c.items, &c.consumes),
800        SourceUnit::Adapter(a) => (&a.items, &EMPTY_CONSUMES),
801        _ => return Vec::new(),
802    };
803    // Locally declared capabilities.
804    for item in items {
805        if let bynk_syntax::ast::CommonsItem::Capability(c) = item {
806            labels.insert(c.name.name.clone());
807        }
808    }
809    // Consumed capabilities: flattened bare names, or qualified `U.Cap`.
810    for c in consumes {
811        let unit_name = c.target.joined();
812        match &c.selected {
813            Some(names) => {
814                for n in names {
815                    labels.insert(n.name.clone());
816                }
817            }
818            None => {
819                let prefix = c
820                    .alias
821                    .as_ref()
822                    .map(|a| a.name.clone())
823                    .unwrap_or_else(|| unit_name.clone());
824                for cap in capabilities_of_unit(&unit_name, doc_text, src_root) {
825                    labels.insert(format!("{prefix}.{cap}"));
826                }
827            }
828        }
829    }
830    labels
831        .into_iter()
832        .map(|label| {
833            Completion::item(
834                label,
835                CompletionKind::Capability,
836                Some("capability in scope".to_string()),
837            )
838        })
839        .collect()
840}
841
842// -- Value-receiver `.method`/`.field` (slice 3, ADR 0063) --
843
844/// If the cursor (byte `offset` into `text`) sits just after a **lowercase**
845/// `receiver.`(`partial`) — a *value* receiver — return the buffer **rewritten**
846/// so the receiver is a complete expression (the trailing `.partial` dropped,
847/// so the file parses) and the byte offset of the receiver to type. Returns
848/// `None` for an uppercase name receiver (slice 2), a decimal `1.`, or a
849/// `.`-qualified segment.
850///
851/// The rewrite is the spike's fix for the mid-edit parse: a bare `email.`
852/// cascades and loses the receiver, but `email` (dot dropped) types cleanly.
853pub fn value_receiver_rewrite(text: &str, offset: usize) -> Option<(String, usize)> {
854    let prefix = text.get(..offset)?;
855    let head = prefix
856        .trim_end_matches(|c: char| c.is_alphanumeric() || c == '_')
857        .strip_suffix('.')?;
858    let start = head
859        .rfind(|c: char| !(c.is_alphanumeric() || c == '_'))
860        .map_or(0, |i| i + 1);
861    let recv = &head[start..];
862    let first = recv.chars().next()?;
863    if !(first.is_ascii_lowercase() || first == '_') {
864        return None; // uppercase = name receiver (slice 2); a digit = a decimal
865    }
866    if head[..start].ends_with('.') {
867        return None; // a `.`-qualified segment, not a bare value receiver
868    }
869    let dot = head.len(); // the receiver ends here; the dot was the next byte
870    let rewritten = format!("{}{}", &text[..dot], &text[offset..]);
871    Some((rewritten, dot.saturating_sub(1)))
872}
873
874/// The members of a typed value receiver: the built-in kernel methods of its
875/// type (from the enumerable registry) plus, for a record, its fields.
876pub fn value_member_candidates(
877    ty: &Ty,
878    doc_text: &str,
879    src_root: Option<&Path>,
880) -> Vec<Completion> {
881    let mut out: Vec<Completion> = kernel_methods::methods_for(ty)
882        .iter()
883        .map(|km| {
884            Completion::item(
885                km.name,
886                CompletionKind::Member,
887                Some(km.signature.to_string()),
888            )
889        })
890        .collect();
891    // Record fields — resolve the receiver's named type to its declaration.
892    if let Ty::Named { name, .. } = ty {
893        let mut seen: BTreeSet<String> = BTreeSet::new();
894        for_each_unit(doc_text, src_root, |unit| {
895            let items = match unit {
896                SourceUnit::Commons(c) => &c.items,
897                SourceUnit::Context(c) => &c.items,
898                SourceUnit::Adapter(a) => &a.items,
899                _ => return,
900            };
901            for item in items {
902                if let CommonsItem::Type(t) = item
903                    && &t.name.name == name
904                    && let TypeBody::Record(r) = &t.body
905                {
906                    for f in &r.fields {
907                        if seen.insert(f.name.name.clone()) {
908                            out.push(Completion::item(
909                                f.name.name.clone(),
910                                CompletionKind::Field,
911                                Some(format!("field of `{name}`")),
912                            ));
913                        }
914                    }
915                }
916            }
917        });
918    }
919    out
920}
921
922static EMPTY_CONSUMES: Vec<bynk_syntax::ast::ConsumesDecl> = Vec::new();
923
924#[cfg(test)]
925mod tests {
926    use super::*;
927
928    fn labels(line: &str, doc: &str) -> Vec<String> {
929        complete(line, doc, None)
930            .into_iter()
931            .map(|c| c.label)
932            .collect()
933    }
934
935    #[test]
936    fn consumes_target_suggests_units_including_bynk() {
937        // An adapter in the open doc plus the always-available `bynk` surface.
938        let doc = "adapter tokens {\n  binding \"./b.ts\"\n  capability Jwt { fn f() -> Effect[Int] }\n  provides Jwt = X\n}\n";
939        let got = labels("  consumes ", doc);
940        assert!(got.contains(&"bynk".to_string()), "{got:?}");
941        assert!(got.contains(&"tokens".to_string()), "{got:?}");
942    }
943
944    #[test]
945    fn consumes_brace_suggests_that_units_capabilities() {
946        let got = labels("  consumes bynk { ", "context a.b\n");
947        // The embedded `bynk` surface exports these.
948        assert!(got.contains(&"Clock".to_string()), "{got:?}");
949        assert!(got.contains(&"Random".to_string()), "{got:?}");
950        assert!(got.contains(&"Logger".to_string()), "{got:?}");
951    }
952
953    #[test]
954    fn given_suggests_local_and_flattened_capabilities() {
955        let doc = "context a.b\n\
956                   consumes bynk { Clock }\n\
957                   capability Local { fn f() -> Effect[Int] }\n\
958                   service s {\n\
959                   on call() -> Effect[Int] given Clock {\n\
960                   1\n\
961                   }\n\
962                   }\n";
963        let got = labels("    on call() -> Effect[Int] given ", doc);
964        assert!(got.contains(&"Clock".to_string()), "flattened: {got:?}");
965        assert!(got.contains(&"Local".to_string()), "local: {got:?}");
966    }
967
968    #[test]
969    fn expression_position_offers_constructors_and_types() {
970        // ADR 0093 D3/D5: a value position (after `=`) yields every constructor
971        // keyword and in-scope type names — the entry to a static call or a
972        // record construction. (Locals/params are appended handler-side, not by
973        // `complete()`.) Registry-driven over CONSTRUCTORS.
974        let doc = "commons m {\n  type Order = { id: Int }\n}\n";
975        let items = complete("  let x = ", doc, None);
976        for &c in CONSTRUCTORS {
977            assert!(
978                find(&items, c, CompletionKind::Constructor).is_some(),
979                "constructor {c}: {:?}",
980                items.iter().map(|i| &i.label).collect::<Vec<_>>()
981            );
982        }
983        assert!(
984            find(&items, "Int", CompletionKind::Type).is_some(),
985            "builtin type"
986        );
987        assert!(
988            find(&items, "Order", CompletionKind::Type).is_some(),
989            "project type"
990        );
991    }
992
993    #[test]
994    fn value_receiver_and_decimal_are_not_expression_positions() {
995        // A trailing `x.`/`1.` is a member/decimal context, not an expression
996        // start — `complete()` yields nothing (the value-receiver path is
997        // handler-side; see `record_value_and_decimal_receivers_yield_nothing`).
998        assert!(complete("  let p = q.", "context a.b\n", None).is_empty());
999        assert!(complete("  let n = 1.", "context a.b\n", None).is_empty());
1000    }
1001
1002    /// Free `fn` names declared in a unit source (registry-driven test helper).
1003    fn free_fn_names(src: &str) -> Vec<String> {
1004        let tokens = lexer::tokenize(src).unwrap();
1005        let (unit, _) = parser::parse_unit_with_recovery(&tokens, src);
1006        let unit = unit.unwrap();
1007        let (items, _) = unit_items_and_uses(&unit);
1008        items
1009            .iter()
1010            .filter_map(|it| match it {
1011                CommonsItem::Fn(f) => match &f.name {
1012                    FnName::Free(id) => Some(id.name.clone()),
1013                    FnName::Method { .. } => None,
1014                },
1015                _ => None,
1016            })
1017            .collect()
1018    }
1019
1020    #[test]
1021    fn free_functions_offered_for_own_unit_and_used_modules() {
1022        // ADR 0093 D3/G5: expression position offers the current unit's own
1023        // free `fn`s and the combinators of every `uses`-imported module.
1024        let doc = "commons app {\n  uses bynk.list\n  fn helper(x: Int) -> Int { x }\n}\n";
1025        let items = complete("  let y = ", doc, None);
1026        // The current unit's own function.
1027        assert!(
1028            find(&items, "helper", CompletionKind::Function).is_some(),
1029            "own fn: {:?}",
1030            items.iter().map(|i| &i.label).collect::<Vec<_>>()
1031        );
1032        // Every combinator of the imported `bynk.list` — registry-driven over the
1033        // embedded source, so a new stdlib combinator must surface or this fails.
1034        for name in free_fn_names(BYNK_LIST_SRC) {
1035            assert!(
1036                find(&items, &name, CompletionKind::Function).is_some(),
1037                "bynk.list.{name}: {:?}",
1038                items.iter().map(|i| &i.label).collect::<Vec<_>>()
1039            );
1040        }
1041        // A module that is not imported does not leak its fns.
1042        assert!(
1043            find(&items, "values", CompletionKind::Function).is_none(),
1044            "bynk.map.values leaked without `uses bynk.map`"
1045        );
1046    }
1047
1048    #[test]
1049    fn free_functions_require_a_uses_import() {
1050        // Own fns are always in scope; stdlib combinators only with their `uses`.
1051        let doc = "commons app {\n  fn helper(x: Int) -> Int { x }\n}\n";
1052        let items = complete("  let y = ", doc, None);
1053        assert!(find(&items, "helper", CompletionKind::Function).is_some());
1054        for name in ["map", "filter", "reverse"] {
1055            assert!(
1056                find(&items, name, CompletionKind::Function).is_none(),
1057                "bynk.list.{name} offered without `uses bynk.list`"
1058            );
1059        }
1060    }
1061
1062    #[test]
1063    fn member_completion_reaches_inside_an_interpolation_hole() {
1064        // v0.43: a `Type.`/`Cap.` receiver inside a `\(…)` hole completes just
1065        // as it does in bare expression position — context detection is purely
1066        // lexical, so the surrounding string and `\(` do not interfere.
1067        let doc = "context a.b\n  capability Timer { fn now() -> Effect[Int] }\n";
1068        let in_hole = complete("    \"the time is \\(Timer.", doc, None);
1069        assert!(
1070            find(&in_hole, "now", CompletionKind::Member).is_some(),
1071            "capability op not offered inside a hole: {:?}",
1072            in_hole.iter().map(|c| &c.label).collect::<Vec<_>>()
1073        );
1074        // A built-in static receiver works inside a hole too.
1075        let statics = complete("  \"n=\\(Int.", "context a.b\n", None);
1076        assert!(find(&statics, "parse", CompletionKind::Member).is_some());
1077    }
1078
1079    #[test]
1080    fn consumes_with_as_is_not_a_target_completion() {
1081        // `consumes X as ` is aliasing, not target-name completion.
1082        assert!(!is_consumes_target("consumes platform.time as "));
1083        assert!(is_consumes_target("consumes platform"));
1084    }
1085
1086    fn find<'a>(
1087        items: &'a [Completion],
1088        label: &str,
1089        kind: CompletionKind,
1090    ) -> Option<&'a Completion> {
1091        items.iter().find(|c| c.label == label && c.kind == kind)
1092    }
1093
1094    #[test]
1095    fn type_annotation_suggests_builtins_surface_and_project_types() {
1096        let doc = "commons m {\n  type Order = { id: Int }\n}\n";
1097        let got = labels("  let x: ", doc);
1098        // Built-ins (with registry docs), the `bynk`-surface transparent types,
1099        // and the project's own type declaration.
1100        for want in ["Int", "Option", "Result", "Effect", "List", "Map"] {
1101            assert!(got.contains(&want.to_string()), "built-in {want}: {got:?}");
1102        }
1103        assert!(got.contains(&"Uuid".to_string()), "surface: {got:?}");
1104        assert!(got.contains(&"Order".to_string()), "project: {got:?}");
1105    }
1106
1107    #[test]
1108    fn return_type_and_type_args_are_type_positions() {
1109        assert!(is_type_position("  on call() -> "));
1110        assert!(is_type_position("  let x: Option["));
1111        assert!(is_type_position("  let x: Result[Int, "));
1112        // A partial type name being typed still counts.
1113        assert!(is_type_position("  -> Eff"));
1114    }
1115
1116    #[test]
1117    fn list_literal_is_not_a_type_position() {
1118        // A bare `[` opening a list literal is an expression, not type args…
1119        assert!(!is_type_position("  let xs = ["));
1120        // …so it is an expression position: a list element is a value, and the
1121        // constructor keywords are offered there (ADR 0093 D3) — not a
1122        // type-argument completion.
1123        let items = complete("  let xs = [", "context a.b\n", None);
1124        assert!(
1125            find(&items, "Some", CompletionKind::Constructor).is_some(),
1126            "{:?}",
1127            items.iter().map(|c| &c.label).collect::<Vec<_>>()
1128        );
1129    }
1130
1131    #[test]
1132    fn builtin_type_carries_its_registry_doc() {
1133        let items = complete("  let x: ", "context a.b\n", None);
1134        let int = find(&items, "Int", CompletionKind::Type).expect("Int present");
1135        assert_eq!(int.detail.as_deref(), keyword_doc("Int"));
1136        assert!(int.detail.is_some(), "Int should have a doc");
1137    }
1138
1139    #[test]
1140    fn keyword_position_suggests_keywords_and_snippets() {
1141        let items = complete("  ", "context a.b\n", None);
1142        // Declaration/statement keywords, with docs.
1143        assert!(find(&items, "capability", CompletionKind::Keyword).is_some());
1144        assert!(find(&items, "fn", CompletionKind::Keyword).is_some());
1145        assert!(find(&items, "let", CompletionKind::Keyword).is_some());
1146        // Uppercase type/value names are *not* keyword-position candidates.
1147        assert!(find(&items, "Int", CompletionKind::Keyword).is_none());
1148        assert!(find(&items, "Some", CompletionKind::Keyword).is_none());
1149        // Snippets are offered alongside.
1150        let snip = find(&items, "service", CompletionKind::Snippet).expect("service snippet");
1151        let body = snip.insert_text.as_deref().unwrap_or("");
1152        assert!(body.contains("on call"), "snippet body: {body:?}");
1153        assert!(body.contains("${1"), "snippet tab stop: {body:?}");
1154    }
1155
1156    #[test]
1157    fn keyword_position_fires_on_an_empty_line() {
1158        assert!(is_keyword_position(""));
1159        assert!(is_keyword_position("  cap"));
1160        assert!(!is_keyword_position("  let x ="));
1161        assert!(!is_keyword_position("  x: "));
1162        assert!(!complete("", "context a.b\n", None).is_empty());
1163    }
1164
1165    #[test]
1166    fn member_receiver_is_a_single_upper_ident_before_a_dot() {
1167        assert_eq!(member_receiver("  Color."), Some("Color".to_string()));
1168        assert_eq!(
1169            member_receiver("  let e = Email.o"),
1170            Some("Email".to_string())
1171        );
1172        assert_eq!(member_receiver("  x."), None); // lowercase = value receiver (slice 3)
1173        assert_eq!(member_receiver("  1."), None); // decimal literal, not a member access
1174        assert_eq!(member_receiver("  a.B."), None); // `.`-qualified segment
1175        assert_eq!(member_receiver("  Color"), None); // no dot yet
1176    }
1177
1178    #[test]
1179    fn sum_member_suggests_variants() {
1180        let doc = "commons m {\n  type Color = enum { Red, Green, Blue }\n}\n";
1181        let items = complete("  let c = Color.", doc, None);
1182        for v in ["Red", "Green", "Blue"] {
1183            assert!(
1184                find(&items, v, CompletionKind::Variant).is_some(),
1185                "variant {v}: {:?}",
1186                items.iter().map(|c| &c.label).collect::<Vec<_>>()
1187            );
1188        }
1189    }
1190
1191    #[test]
1192    fn refined_and_plain_alias_members_are_of_and_unsafe() {
1193        // A refinement-bearing type…
1194        let doc = "commons m {\n  type Email = String where NonEmpty\n}\n";
1195        let items = complete("  Email.", doc, None);
1196        assert!(find(&items, "of", CompletionKind::Member).is_some());
1197        assert!(find(&items, "unsafe", CompletionKind::Member).is_some());
1198        // …and a plain alias `type Id = Int` is *also* branded (the emitter
1199        // emits Id.of/Id.unsafe for every Refined body, refinement or not).
1200        let doc = "commons m {\n  type Id = Int\n}\n";
1201        assert!(find(&complete("  Id.", doc, None), "of", CompletionKind::Member).is_some());
1202    }
1203
1204    #[test]
1205    fn capability_member_suggests_ops() {
1206        let doc = "context a.b\n  capability Timer { fn now() -> Effect[Int]\n  fn at(t: Int) -> Effect[()] }\n";
1207        let items = complete("    Timer.", doc, None);
1208        let now = find(&items, "now", CompletionKind::Member).expect("`now` op offered");
1209        // Slice 5 detail polish: a typed signature (params + return), not bare
1210        // param names.
1211        assert_eq!(
1212            now.detail.as_deref(),
1213            Some("now() -> Effect[Int] — operation of `Timer`")
1214        );
1215        let at = find(&items, "at", CompletionKind::Member).expect("`at` op offered");
1216        assert_eq!(
1217            at.detail.as_deref(),
1218            Some("at(t: Int) -> Effect[()] — operation of `Timer`")
1219        );
1220    }
1221
1222    #[test]
1223    fn builtin_type_statics_are_offered() {
1224        assert!(
1225            find(
1226                &complete("  Int.", "context a.b\n", None),
1227                "parse",
1228                CompletionKind::Member
1229            )
1230            .is_some()
1231        );
1232        let j = complete("  Json.", "context a.b\n", None);
1233        assert!(find(&j, "encode", CompletionKind::Member).is_some());
1234        assert!(find(&j, "decode", CompletionKind::Member).is_some());
1235    }
1236
1237    #[test]
1238    fn builtin_sum_variants_are_complete() {
1239        // ADR 0093 D5/G3: every built-in sum variant in the AST registry must
1240        // surface on its name receiver. Registry-driven — adding an
1241        // `HttpResult`/`QueueResult` variant must appear in completion or this
1242        // fails (the standing drift guard, mirroring `kernel_registry`).
1243        let http: Vec<&str> = bynk_syntax::ast::HTTP_VARIANTS
1244            .iter()
1245            .map(|v| v.name)
1246            .collect();
1247        let queue: Vec<&str> = bynk_syntax::ast::QUEUE_VARIANTS
1248            .iter()
1249            .map(|v| v.name)
1250            .collect();
1251        for (recv, names) in [("HttpResult", http), ("QueueResult", queue)] {
1252            let items = complete(&format!("  {recv}."), "context a.b\n", None);
1253            for name in names {
1254                assert!(
1255                    find(&items, name, CompletionKind::Variant).is_some(),
1256                    "{recv}.{name} missing: {:?}",
1257                    items.iter().map(|c| &c.label).collect::<Vec<_>>()
1258                );
1259            }
1260        }
1261    }
1262
1263    #[test]
1264    fn builtin_statics_are_reachable() {
1265        // ADR 0093 D5/G2: every BUILTIN_STATICS entry is reachable through the
1266        // name-receiver context — exercises the member_receiver→member_candidates
1267        // wiring for each receiver (e.g. that `Effect.`/`List.` are recognised).
1268        for &(recv, members) in BUILTIN_STATICS {
1269            let items = complete(&format!("  {recv}."), "context a.b\n", None);
1270            for &(member, _) in members {
1271                assert!(
1272                    find(&items, member, CompletionKind::Member).is_some(),
1273                    "{recv}.{member} unreachable: {:?}",
1274                    items.iter().map(|c| &c.label).collect::<Vec<_>>()
1275                );
1276            }
1277        }
1278        // The slice-1 additions specifically — guards against a table regression
1279        // (the loop above can't catch an entry being deleted).
1280        for (recv, member) in [("List", "empty"), ("Map", "empty"), ("Effect", "pure")] {
1281            let items = complete(&format!("  {recv}."), "context a.b\n", None);
1282            assert!(
1283                find(&items, member, CompletionKind::Member).is_some(),
1284                "{recv}.{member} missing from the statics table"
1285            );
1286        }
1287    }
1288
1289    #[test]
1290    fn record_value_and_decimal_receivers_yield_nothing() {
1291        // A record type has no name-receiver members (fields are value-receiver).
1292        let doc = "commons m {\n  type Point = { x: Int }\n}\n";
1293        assert!(complete("  Point.", doc, None).is_empty(), "record");
1294        // A lowercase value receiver is deferred to slice 3.
1295        assert!(complete("  let p = q.", doc, None).is_empty(), "value");
1296        // A decimal literal is not a member access.
1297        assert!(complete("  let n = 1.", doc, None).is_empty(), "decimal");
1298    }
1299
1300    #[test]
1301    fn value_receiver_rewrite_drops_the_dot_for_lowercase_receivers() {
1302        let text = "  let x = email.\n";
1303        let offset = text.find('.').unwrap() + 1; // just after the dot
1304        let (rewritten, recv) = value_receiver_rewrite(text, offset).expect("value receiver");
1305        assert_eq!(
1306            rewritten, "  let x = email\n",
1307            "the trailing dot is dropped"
1308        );
1309        assert!(
1310            text.get(recv..=recv).is_some_and(|c| c == "l"),
1311            "the receiver offset lands inside `email`"
1312        );
1313        // A partial member is dropped too.
1314        let text2 = "  let x = email.ma\n";
1315        let off2 = text2.find(".ma").unwrap() + 3;
1316        assert_eq!(
1317            value_receiver_rewrite(text2, off2).map(|(r, _)| r),
1318            Some("  let x = email\n".to_string())
1319        );
1320        // Uppercase (name receiver, slice 2), decimal, and no-dot yield None.
1321        assert!(value_receiver_rewrite("  Email.", 8).is_none());
1322        assert!(value_receiver_rewrite("  let n = 1.", 12).is_none());
1323        assert!(value_receiver_rewrite("  email", 7).is_none());
1324    }
1325
1326    #[test]
1327    fn value_member_candidates_lists_kernel_methods() {
1328        use bynk_syntax::ast::BaseType;
1329        let list = Ty::List(Box::new(Ty::Base(BaseType::Int)));
1330        let items = value_member_candidates(&list, "context a.b\n", None);
1331        assert!(find(&items, "fold", CompletionKind::Member).is_some());
1332        assert!(find(&items, "get", CompletionKind::Member).is_some());
1333
1334        let string = Ty::Base(BaseType::String);
1335        let items = value_member_candidates(&string, "context a.b\n", None);
1336        assert!(find(&items, "split", CompletionKind::Member).is_some());
1337        assert!(find(&items, "trim", CompletionKind::Member).is_some());
1338    }
1339
1340    #[test]
1341    fn expression_position_offers_locals() {
1342        // Value-expecting positions (locals offered).
1343        assert!(is_expression_position("  let y = "));
1344        assert!(is_expression_position("  let y = a + lo")); // after a binary op
1345        assert!(is_expression_position("  f("));
1346        assert!(is_expression_position("  g(a, "));
1347        assert!(is_expression_position("  xs.fold(0, (acc, x) => ac")); // lambda body
1348        // `let y = foo` is still a value position (you're typing the value).
1349        assert!(is_expression_position("  let y = foo"));
1350        // Not value positions.
1351        assert!(!is_expression_position("  let y: ")); // type annotation
1352        assert!(!is_expression_position("  on call() -> ")); // return type
1353        assert!(!is_expression_position("  tot")); // bare line start (keyword position covers it)
1354    }
1355
1356    #[test]
1357    fn value_member_candidates_lists_record_fields() {
1358        use bynk_check::checker::NamedKind;
1359        let order = Ty::Named {
1360            name: "Order".to_string(),
1361            kind: NamedKind::Record,
1362        };
1363        let doc = "commons m {\n  type Order = { id: Int, total: Int }\n}\n";
1364        let items = value_member_candidates(&order, doc, None);
1365        assert!(
1366            find(&items, "id", CompletionKind::Field).is_some(),
1367            "{items:?}",
1368            items = items.iter().map(|c| &c.label).collect::<Vec<_>>()
1369        );
1370        assert!(find(&items, "total", CompletionKind::Field).is_some());
1371    }
1372}