Skip to main content

ronin_core/
completion.rs

1//! Structural autocomplete analysis (E005 Wave 3, US2).
2//!
3//! Given a parsed [`CstDocument`] and a cursor byte offset, this module derives a
4//! [`CompletionContext`] describing **where** the cursor sits inside the RON
5//! structure (the [`PositionKind`]), what identifier fragment the user is in the
6//! middle of typing (the `prefix`), and a deterministic, ranked list of
7//! low-confidence [`CompletionItem`] suggestions drawn **only** from what is
8//! already attested in the document.
9//!
10//! # Progressive Intelligence — structural only (project-instructions §III)
11//!
12//! Completion here is **structural**: it uses the CST syntax plus identifiers that
13//! *already appear in the document* (sibling field names, enum-variant names, map
14//! keys), plus the always-legal `Some` / `None` and the context-legal delimiters.
15//! It carries **no type information** — type-aware completion is E006. Missing or
16//! ambiguous context degrades gracefully to **zero suggestions**, never an error
17//! (FR-014). Unknown context is not a failure; it is simply "nothing to suggest".
18//!
19//! # Never corrupt (project-instructions §I)
20//!
21//! Analysis is strictly **read-only** over the CST. Every produced
22//! [`CompletionItem`] carries an `insert_text` that is syntactically valid in its
23//! position, so an accepted suggestion (spliced + re-parsed by the surface layer)
24//! round-trips through the CST. Nothing here mutates the document and nothing
25//! auto-accepts: ranks are deliberately assigned **below** the user's literal
26//! input so a suggestion is never preselected (FR-012).
27//!
28//! # WASM-clean (project-instructions §II, INV-9)
29//!
30//! This module adds **no** filesystem / UI / async / native dependency — it uses
31//! only `std` and `ronin-core`'s own CST types, so the `wasm32` build of `ronin-core`
32//! stays green.
33//!
34//! # Deferred seams
35//!
36//! Completion here is structural; the deeper intelligence is deferred to later
37//! epics and attaches around this read-only analysis:
38//!
39//! * **type-aware completion** — ranking + filtering candidates by an *expected
40//!   type* (and offering type-legal field/variant names rather than merely attested
41//!   ones) → **E006** (schema-optional type model). The [`CompletionKind`] ordering
42//!   and the [`CompletionContext`] pools are the seam a type layer refines.
43//! * **semantic / CST-backed undo-redo** of an accepted suggestion → **E007** (the
44//!   surface layer's verified splice is what an undo stack records against).
45//! * **tree / table structured editing** completions → **E008**.
46//! * **Bevy-registry-aware** completion (real component / field names from a loaded
47//!   registry, not just in-file attestation) → **E009**.
48//! * **RON⇄JSON / `derive`-driven** suggestions → **E010** (interop is outside this
49//!   pure-CST engine).
50
51use std::collections::BTreeSet;
52
53use crate::parser::CstDocument;
54use crate::syntax::{SyntaxKind, SyntaxNode, SyntaxToken};
55
56/// Where the cursor sits inside the RON structure (FR-010).
57///
58/// Derived purely from the enclosing CST construct. [`PositionKind::Value`] is the
59/// general "a value is expected here" position (top-level value, list element of a
60/// freshly-opened list, a struct/map value slot, …) where any RON value — and so
61/// `Some` / `None` / a variant name — is legal. The more specific kinds carry the
62/// extra structural knowledge that lets completion offer attested field names,
63/// keys, etc.
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
65#[non_exhaustive]
66pub enum PositionKind {
67    /// Inside a struct's parentheses, at a `field` name position (before the `:`).
68    StructField,
69    /// Inside a list `[ .. ]`, at an element position.
70    ListElement,
71    /// Inside a map `{ .. }`, at a key position (before the `:`).
72    MapKey,
73    /// Inside a map entry, at the value position (after the `:`).
74    MapValue,
75    /// Inside a tuple `( a, b )`, at a positional element position.
76    Tuple,
77    /// A generic value position (top-level value, struct/variant payload value,
78    /// or any spot where a fresh RON value is expected).
79    Value,
80}
81
82/// The classification of a single [`CompletionItem`] (FR-011/FR-012).
83///
84/// Used both as a display hint for the surface layer and as the **primary sort
85/// key** for deterministic ordering: items are ordered by kind (in the order the
86/// variants are declared here) then alphabetically by label.
87#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
88pub enum CompletionKind {
89    /// A struct field name already present on a sibling/like struct.
90    Field,
91    /// An enum-variant identifier attested elsewhere in the document.
92    Variant,
93    /// A map key attested elsewhere in the document.
94    MapKey,
95    /// The `Some` / `None` option constructors (always structurally legal).
96    Option,
97    /// A context-legal delimiter / punctuation hint (e.g. `(`, `[`, `{`).
98    Delimiter,
99}
100
101impl CompletionKind {
102    /// A stable ordinal used as the primary (kind) sort key. Lower sorts first.
103    #[inline]
104    #[must_use]
105    fn order(self) -> u8 {
106        match self {
107            Self::Field => 0,
108            Self::Variant => 1,
109            Self::MapKey => 2,
110            Self::Option => 3,
111            Self::Delimiter => 4,
112        }
113    }
114}
115
116/// A single low-confidence completion suggestion (FR-011/FR-012).
117///
118/// Always ranked **below** the user's literal input (`rank` is a strictly
119/// positive distance-from-top; the literal conceptually occupies rank `0`), so it
120/// is never preselected and never auto-accepts.
121#[derive(Debug, Clone, PartialEq, Eq)]
122pub struct CompletionItem {
123    /// The text shown to the user in the popup.
124    pub label: String,
125    /// The text inserted on acceptance — syntactically valid in its position so
126    /// the result round-trips through the CST.
127    pub insert_text: String,
128    /// The suggestion's classification (display hint + primary sort key).
129    pub kind: CompletionKind,
130    /// Deterministic rank, strictly `>= 1` (below the user's literal input at
131    /// rank `0`). Lower ranks sort first; ranks follow the kind-then-alpha order.
132    pub rank: u32,
133}
134
135/// The full completion analysis for a cursor offset (FR-010/FR-011).
136///
137/// Produced by [`completion_context`]. Holds the structural [`PositionKind`], the
138/// in-progress identifier `prefix`, the in-file attested identifier pools, and the
139/// already-filtered/ranked [`items`](Self::items). An empty / ambiguous context
140/// yields an empty `items` list and [`PositionKind::Value`] is *not* assumed —
141/// `position` is `None` (FR-014).
142#[derive(Debug, Clone, PartialEq, Eq)]
143pub struct CompletionContext {
144    /// The structural position of the cursor, or `None` for an empty / ambiguous
145    /// context where no single kind resolves (then `items` is empty).
146    pub position: Option<PositionKind>,
147    /// The identifier fragment immediately left of the cursor (may be empty).
148    pub prefix: String,
149    /// Field names already present in the enclosing (or like) struct(s).
150    pub sibling_field_names: Vec<String>,
151    /// Enum-variant identifiers seen anywhere in the document.
152    pub in_file_variant_names: Vec<String>,
153    /// Map keys (identifier-like) seen anywhere in the document.
154    pub in_file_map_keys: Vec<String>,
155    /// The ranked, prefix-filtered suggestions (empty for an empty/ambiguous
156    /// context). Always ordered by kind then alphabetically, all below the literal.
157    pub items: Vec<CompletionItem>,
158}
159
160impl CompletionContext {
161    /// An empty context: no resolvable position, no suggestions (FR-014).
162    #[must_use]
163    fn empty() -> Self {
164        Self {
165            position: None,
166            prefix: String::new(),
167            sibling_field_names: Vec::new(),
168            in_file_variant_names: Vec::new(),
169            in_file_map_keys: Vec::new(),
170            items: Vec::new(),
171        }
172    }
173}
174
175/// Analyze the cursor `offset` against `doc` and produce a [`CompletionContext`]
176/// (FR-010/FR-011/FR-012/FR-014).
177///
178/// Read-only. The offset is a byte offset into the source the document was parsed
179/// from; an out-of-range offset is clamped to the source length. An empty /
180/// whitespace-only document, a cursor with no enclosing construct, or a position
181/// whose kind cannot be uniquely resolved all yield an empty context (no items, no
182/// error) per the empty/ambiguous-suppression rule (FR-014).
183#[must_use]
184pub fn completion_context(doc: &CstDocument, offset: usize) -> CompletionContext {
185    let offset = offset.min(doc.source_len());
186    let root = doc.root();
187
188    // Empty / whitespace-only buffer: there is no construct to complete inside.
189    // (The root has no significant child token.) → empty context (FR-014).
190    if !has_significant_token(&root) {
191        return CompletionContext::empty();
192    }
193
194    // The in-progress identifier fragment left of the cursor.
195    let prefix = prefix_before(&root, offset);
196
197    // Resolve the enclosing construct + position kind. Ambiguous / no-construct
198    // → empty context (FR-014).
199    let Some(position) = resolve_position(&root, offset) else {
200        return CompletionContext::empty();
201    };
202
203    // Collect the in-file attested identifier pools (syntax + attestation only).
204    let sibling_field_names = enclosing_sibling_field_names(&root, offset);
205    let in_file_variant_names = collect_in_file(&root, SyntaxKind::EnumVariant);
206    let in_file_map_keys = collect_map_keys(&root);
207
208    let items = build_items(
209        position,
210        &prefix,
211        &sibling_field_names,
212        &in_file_variant_names,
213        &in_file_map_keys,
214    );
215
216    CompletionContext {
217        position: Some(position),
218        prefix,
219        sibling_field_names,
220        in_file_variant_names,
221        in_file_map_keys,
222        items,
223    }
224}
225
226/// Convenience: the ranked, prefix-filtered suggestions for `ctx` (FR-012).
227///
228/// This is exactly `ctx.items` — exposed as a free function so a surface layer can
229/// treat "give me the items" as a single call regardless of whether it keeps the
230/// whole context around.
231#[must_use]
232pub fn completions(ctx: &CompletionContext) -> Vec<CompletionItem> {
233    ctx.items.clone()
234}
235
236// ---- position classification -------------------------------------------------
237
238/// Find the smallest node whose range contains `offset`, descending from the root.
239///
240/// Uses a half-open `[start, end)` containment with a right-edge inclusive rule so
241/// a cursor *at* the closing position of a construct still resolves to that
242/// construct's enclosing context (the caret sits just inside the trailing edge).
243fn smallest_node_at(root: &SyntaxNode, offset: usize) -> SyntaxNode {
244    let mut node = root.clone();
245    loop {
246        // Descend into the deepest child node whose range still covers the cursor.
247        // A child may share the parent's exact range (e.g. the sole top-level
248        // value spans the whole root); descend by node identity so we still reach
249        // the inner construct rather than stopping at the root.
250        let next = node.children().find(|child| {
251            let r = child.text_range();
252            r.start() <= offset && offset <= r.end()
253        });
254        match next {
255            Some(child) if child != node => node = child,
256            _ => break,
257        }
258    }
259    node
260}
261
262/// Resolve the [`PositionKind`] for `offset`, or `None` if it cannot be uniquely
263/// determined (ambiguous / no enclosing construct) (FR-010/FR-014).
264fn resolve_position(root: &SyntaxNode, offset: usize) -> Option<PositionKind> {
265    let node = smallest_node_at(root, offset);
266
267    // Walk from the smallest containing node up to the first node whose kind names
268    // a construct we can complete inside. The *immediate* enclosing construct wins.
269    let mut cur = Some(node.clone());
270    while let Some(n) = cur {
271        match n.kind() {
272            SyntaxKind::Struct => {
273                return struct_position(&n, offset);
274            }
275            SyntaxKind::List => {
276                // The caret must be inside the brackets to be a list element.
277                return inside_brackets(&n, offset, SyntaxKind::LBracket, SyntaxKind::RBracket)
278                    .then_some(PositionKind::ListElement);
279            }
280            SyntaxKind::Tuple => {
281                return inside_brackets(&n, offset, SyntaxKind::LParen, SyntaxKind::RParen)
282                    .then_some(PositionKind::Tuple);
283            }
284            SyntaxKind::Map => {
285                return map_position(&n, offset);
286            }
287            SyntaxKind::MapEntry => {
288                return map_entry_position(&n, offset);
289            }
290            SyntaxKind::StructField => {
291                return struct_field_position(&n, offset);
292            }
293            SyntaxKind::Root => {
294                // Top-level value position: any RON value (incl. Some/None) is legal.
295                return Some(PositionKind::Value);
296            }
297            _ => cur = n.parent(),
298        }
299    }
300    None
301}
302
303/// Position inside a `Struct( .. )`: a field-name slot vs. a value slot.
304fn struct_position(node: &SyntaxNode, offset: usize) -> Option<PositionKind> {
305    if !inside_brackets(node, offset, SyntaxKind::LParen, SyntaxKind::RParen) {
306        // The caret is on the struct *name* (before `(`), not inside the body.
307        return None;
308    }
309    // If the caret sits inside an existing field, defer to that field's logic.
310    if let Some(field) = node
311        .children()
312        .find(|c| c.kind() == SyntaxKind::StructField && c.text_range().contains(offset))
313    {
314        return struct_field_position(&field, offset);
315    }
316    // Otherwise it is a fresh field-name position inside the parens.
317    Some(PositionKind::StructField)
318}
319
320/// Position inside a `StructField`: before the `:` is a field name, after is a value.
321fn struct_field_position(field: &SyntaxNode, offset: usize) -> Option<PositionKind> {
322    match colon_offset(field) {
323        Some(colon) if offset > colon => Some(PositionKind::Value),
324        _ => Some(PositionKind::StructField),
325    }
326}
327
328/// Position inside a `Map { .. }`: a key slot vs. (when inside an entry) key/value.
329fn map_position(node: &SyntaxNode, offset: usize) -> Option<PositionKind> {
330    if !inside_brackets(node, offset, SyntaxKind::LBrace, SyntaxKind::RBrace) {
331        return None;
332    }
333    if let Some(entry) = node
334        .children()
335        .find(|c| c.kind() == SyntaxKind::MapEntry && c.text_range().contains(offset))
336    {
337        return map_entry_position(&entry, offset);
338    }
339    Some(PositionKind::MapKey)
340}
341
342/// Position inside a `MapEntry`: before the `:` is the key, after is the value.
343fn map_entry_position(entry: &SyntaxNode, offset: usize) -> Option<PositionKind> {
344    match colon_offset(entry) {
345        Some(colon) if offset > colon => Some(PositionKind::MapValue),
346        _ => Some(PositionKind::MapKey),
347    }
348}
349
350/// The absolute byte offset of the entry/field's top-level `:` token, if present.
351fn colon_offset(node: &SyntaxNode) -> Option<usize> {
352    node.children_with_tokens()
353        .filter_map(|el| el.as_token().cloned())
354        .find(|t| t.kind() == SyntaxKind::Colon)
355        .map(|t| t.text_range().start())
356}
357
358/// `true` when `offset` lies strictly inside the node's open/close delimiter pair.
359///
360/// "Strictly inside" means after the opening delimiter and at-or-before the
361/// closing one (the caret may sit just before `)`/`]`/`}`). A node missing one of
362/// its delimiters (error recovery) is treated as open on that side.
363fn inside_brackets(node: &SyntaxNode, offset: usize, open: SyntaxKind, close: SyntaxKind) -> bool {
364    let open_end = node
365        .children_with_tokens()
366        .filter_map(|el| el.as_token().cloned())
367        .find(|t| t.kind() == open)
368        .map(|t| t.text_range().end());
369    let close_start = node
370        .children_with_tokens()
371        .filter_map(|el| el.as_token().cloned())
372        .find(|t| t.kind() == close)
373        .map(|t| t.text_range().start());
374
375    // MSRV 1.77: `Option::is_none_or` is 1.82+, so use `map_or(true, ..)`.
376    let after_open = open_end.map_or(true, |e| offset >= e);
377    let before_close = close_start.map_or(true, |s| offset <= s);
378    after_open && before_close
379}
380
381// ---- prefix extraction -------------------------------------------------------
382
383/// The identifier fragment immediately left of `offset` (FR-010).
384///
385/// Finds the token covering (or ending exactly at) the cursor; if it is an
386/// identifier, returns the portion of its text up to the cursor. Otherwise the
387/// prefix is empty (the cursor is at a fresh position).
388fn prefix_before(root: &SyntaxNode, offset: usize) -> String {
389    // The relevant token is the last token whose range starts before the cursor
390    // and ends at-or-after it. We want a token the cursor is "inside" or sitting
391    // at the right edge of.
392    let mut best: Option<SyntaxToken> = None;
393    for tok in root.descendant_tokens() {
394        let r = tok.text_range();
395        if r.start() < offset && offset <= r.end() {
396            best = Some(tok);
397        }
398        if r.start() >= offset {
399            break;
400        }
401    }
402    let Some(tok) = best else {
403        return String::new();
404    };
405    // Only identifier-ish tokens contribute a typed prefix. `enable`/bool keyword
406    // tokens are not user-extendable identifiers in this structural model.
407    if tok.kind() != SyntaxKind::Ident {
408        return String::new();
409    }
410    let r = tok.text_range();
411    let take = offset.saturating_sub(r.start());
412    // Slice on a char boundary defensively (idents are ASCII in practice, but
413    // never panic on a multibyte token edge).
414    let text = tok.text();
415    let take = take.min(text.len());
416    let mut end = take;
417    while end > 0 && !text.is_char_boundary(end) {
418        end -= 1;
419    }
420    text[..end].to_string()
421}
422
423// ---- in-file attestation -----------------------------------------------------
424
425/// `true` if the tree contains at least one significant (non-trivia) token.
426fn has_significant_token(root: &SyntaxNode) -> bool {
427    root.descendant_tokens().any(|t| !t.is_trivia())
428}
429
430/// Collect the field names present in the struct enclosing `offset` (FR-011).
431///
432/// "Sibling" field names are the names already typed inside the *same* struct as
433/// the cursor (so completion can suggest other-document fields while not
434/// re-suggesting one already present). When the cursor is not inside a struct, the
435/// list is empty.
436fn enclosing_sibling_field_names(root: &SyntaxNode, offset: usize) -> Vec<String> {
437    let node = smallest_node_at(root, offset);
438    let mut cur = Some(node);
439    while let Some(n) = cur {
440        if n.kind() == SyntaxKind::Struct {
441            let mut names: Vec<String> = n
442                .children()
443                .filter(|c| c.kind() == SyntaxKind::StructField)
444                .filter_map(|f| {
445                    f.first_token_of(SyntaxKind::Ident)
446                        .map(|t| t.text().to_string())
447                })
448                .collect();
449            dedup_sorted(&mut names);
450            return names;
451        }
452        cur = n.parent();
453    }
454    Vec::new()
455}
456
457/// Collect the name idents of every node of `kind` anywhere in the document.
458///
459/// Used for enum-variant names ([`SyntaxKind::EnumVariant`]). Deterministic
460/// (sorted, de-duplicated).
461fn collect_in_file(root: &SyntaxNode, kind: SyntaxKind) -> Vec<String> {
462    let mut out: Vec<String> = Vec::new();
463    collect_in_file_rec(root, kind, &mut out);
464    dedup_sorted(&mut out);
465    out
466}
467
468fn collect_in_file_rec(node: &SyntaxNode, kind: SyntaxKind, out: &mut Vec<String>) {
469    if node.kind() == kind {
470        if let Some(t) = node.first_token_of(SyntaxKind::Ident) {
471            out.push(t.text().to_string());
472        }
473    }
474    for child in node.children() {
475        collect_in_file_rec(&child, kind, out);
476    }
477}
478
479/// Collect identifier-like map keys attested anywhere in the document (FR-011).
480///
481/// Only keys whose key value is a single bare identifier (an `EnumVariant` with no
482/// payload, or a `Literal` ident — RON allows bare-ident keys) are collected, so
483/// the surface can re-offer them as completion candidates. String/number keys are
484/// not surfaced as identifier completions.
485fn collect_map_keys(root: &SyntaxNode) -> Vec<String> {
486    let mut out: Vec<String> = Vec::new();
487    collect_map_keys_rec(root, &mut out);
488    dedup_sorted(&mut out);
489    out
490}
491
492fn collect_map_keys_rec(node: &SyntaxNode, out: &mut Vec<String>) {
493    if node.kind() == SyntaxKind::MapEntry {
494        // The key is the first value-position child; before the `:`.
495        let colon = colon_offset(node);
496        if let Some(key_node) = node.children().find(|c| {
497            // MSRV 1.77: `Option::is_none_or` is 1.82+, so use `map_or(true, ..)`.
498            let before_colon = colon.map_or(true, |co| c.text_range().start() < co);
499            before_colon && is_keyable_node(c.kind())
500        }) {
501            if let Some(t) = key_node.first_token_of(SyntaxKind::Ident) {
502                out.push(t.text().to_string());
503            }
504        }
505    }
506    for child in node.children() {
507        collect_map_keys_rec(&child, out);
508    }
509}
510
511/// Whether a node kind can carry a bare-identifier key we surface for completion.
512fn is_keyable_node(kind: SyntaxKind) -> bool {
513    matches!(kind, SyntaxKind::EnumVariant | SyntaxKind::Literal)
514}
515
516/// Sort + de-duplicate a `Vec<String>` in place (deterministic ordering).
517fn dedup_sorted(v: &mut Vec<String>) {
518    let set: BTreeSet<String> = v.drain(..).collect();
519    v.extend(set);
520}
521
522// ---- item production ---------------------------------------------------------
523
524/// Build the deterministic, prefix-filtered, kind-then-alpha-ordered item list for
525/// a resolved position (FR-011/FR-012).
526///
527/// Candidate pools depend on the position kind:
528/// * `StructField` → attested sibling/like field names (minus ones already present).
529/// * `MapKey`      → attested in-file map keys.
530/// * `ListElement` / `Tuple` / `MapValue` / `Value` → `Some` / `None`, attested
531///   in-file variant names, and the legal opening delimiters.
532///
533/// Every item is ranked strictly below the user's literal input (rank `0`): the
534/// first suggestion gets rank `1`, then `2`, … in the final sorted order (FR-012).
535fn build_items(
536    position: PositionKind,
537    prefix: &str,
538    sibling_field_names: &[String],
539    in_file_variant_names: &[String],
540    in_file_map_keys: &[String],
541) -> Vec<CompletionItem> {
542    let mut raw: Vec<CompletionItem> = Vec::new();
543
544    match position {
545        PositionKind::StructField => {
546            // Offer attested field names not already present as a sibling here.
547            // (Sibling list already excludes nothing; the surface dedups by name.)
548            for name in sibling_field_names {
549                raw.push(field_item(name));
550            }
551        }
552        PositionKind::MapKey => {
553            for key in in_file_map_keys {
554                raw.push(map_key_item(key));
555            }
556        }
557        PositionKind::ListElement
558        | PositionKind::Tuple
559        | PositionKind::MapValue
560        | PositionKind::Value => {
561            // Option constructors are always structurally legal at a value slot.
562            raw.push(option_item("None", "None"));
563            raw.push(option_item("Some", "Some()"));
564            // In-file enum variants attested anywhere.
565            for v in in_file_variant_names {
566                raw.push(variant_item(v));
567            }
568            // Legal opening delimiters for a fresh composite value.
569            for (label, insert) in [("(", "()"), ("[", "[]"), ("{", "{}")] {
570                raw.push(delimiter_item(label, insert));
571            }
572        }
573    }
574
575    finalize(raw, prefix)
576}
577
578/// Filter by `prefix`, order by kind-then-alpha, drop the exact-literal match, and
579/// assign ranks `1..` (all below the user's literal at rank `0`).
580fn finalize(mut items: Vec<CompletionItem>, prefix: &str) -> Vec<CompletionItem> {
581    // Prefix filter (case-sensitive structural match). An empty prefix matches all.
582    items.retain(|it| it.label.starts_with(prefix));
583
584    // Never re-offer the user's exact literal — it already occupies rank 0.
585    if !prefix.is_empty() {
586        items.retain(|it| it.label != prefix);
587    }
588
589    // Deterministic order: kind ordinal, then alphabetical by label, then by
590    // insert_text as a final tiebreak so the order is total.
591    items.sort_by(|a, b| {
592        a.kind
593            .order()
594            .cmp(&b.kind.order())
595            .then_with(|| a.label.cmp(&b.label))
596            .then_with(|| a.insert_text.cmp(&b.insert_text))
597    });
598    items.dedup_by(|a, b| a.label == b.label && a.kind == b.kind);
599
600    // Assign ranks strictly below the literal (the literal is rank 0).
601    for (i, item) in items.iter_mut().enumerate() {
602        item.rank = (i as u32) + 1;
603    }
604    items
605}
606
607fn field_item(name: &str) -> CompletionItem {
608    CompletionItem {
609        label: name.to_string(),
610        // A field completion drops the user at the field name; the `:` is left for
611        // the user to type so we never guess a value. This is valid as a partial.
612        insert_text: name.to_string(),
613        kind: CompletionKind::Field,
614        rank: 0,
615    }
616}
617
618fn map_key_item(key: &str) -> CompletionItem {
619    CompletionItem {
620        label: key.to_string(),
621        insert_text: key.to_string(),
622        kind: CompletionKind::MapKey,
623        rank: 0,
624    }
625}
626
627fn variant_item(name: &str) -> CompletionItem {
628    CompletionItem {
629        label: name.to_string(),
630        insert_text: name.to_string(),
631        kind: CompletionKind::Variant,
632        rank: 0,
633    }
634}
635
636fn option_item(label: &str, insert: &str) -> CompletionItem {
637    CompletionItem {
638        label: label.to_string(),
639        insert_text: insert.to_string(),
640        kind: CompletionKind::Option,
641        rank: 0,
642    }
643}
644
645fn delimiter_item(label: &str, insert: &str) -> CompletionItem {
646    CompletionItem {
647        label: label.to_string(),
648        insert_text: insert.to_string(),
649        kind: CompletionKind::Delimiter,
650        rank: 0,
651    }
652}
653
654#[cfg(test)]
655mod tests {
656    use super::*;
657    use crate::parser::parse;
658
659    /// Context at the byte offset of the first occurrence of `marker` in `src`.
660    fn ctx_at(src: &str, byte_offset: usize) -> CompletionContext {
661        completion_context(&parse(src), byte_offset)
662    }
663
664    #[test]
665    fn empty_buffer_yields_no_items() {
666        let ctx = ctx_at("", 0);
667        assert_eq!(ctx.position, None);
668        assert!(ctx.items.is_empty());
669    }
670
671    #[test]
672    fn whitespace_only_buffer_yields_no_items() {
673        let ctx = ctx_at("   \n\t ", 2);
674        assert_eq!(ctx.position, None);
675        assert!(ctx.items.is_empty());
676    }
677
678    #[test]
679    fn top_level_value_offers_option_and_delimiters() {
680        // `Foo` is a bare ident value at top level; cursor right after it.
681        let ctx = ctx_at("Foo", 3);
682        assert_eq!(ctx.position, Some(PositionKind::Value));
683        // `Some`/`None`/variants/delimiters are all value-position candidates;
684        // filtered by the "Foo" prefix → only items starting with "Foo".
685        assert_eq!(ctx.prefix, "Foo");
686    }
687
688    #[test]
689    fn struct_field_position_is_classified() {
690        // `Point(x: 1, )` — caret inside the parens, just before `)`.
691        let src = "Point(x: 1, )";
692        let ctx = completion_context(&parse(src), src.len() - 1);
693        assert_eq!(ctx.position, Some(PositionKind::StructField));
694        // `x` is an attested sibling field name.
695        assert!(ctx.sibling_field_names.contains(&"x".to_string()));
696    }
697
698    #[test]
699    fn struct_field_value_position_after_colon() {
700        // Caret after the colon in `Point(x: )`.
701        let src = "Point(x: )";
702        let colon = src.find(':').unwrap();
703        let ctx = completion_context(&parse(src), colon + 2);
704        assert_eq!(ctx.position, Some(PositionKind::Value));
705    }
706
707    #[test]
708    fn list_element_position_is_classified() {
709        let src = "[1, ]";
710        let ctx = completion_context(&parse(src), 4); // before `]`
711        assert_eq!(ctx.position, Some(PositionKind::ListElement));
712    }
713
714    #[test]
715    fn tuple_position_is_classified() {
716        let src = "(1, 2)";
717        let ctx = completion_context(&parse(src), 4); // inside, before `2`
718        assert_eq!(ctx.position, Some(PositionKind::Tuple));
719    }
720
721    #[test]
722    fn map_key_and_value_positions() {
723        let src = "{ name: 1 }";
724        // Caret at the start of the key region.
725        let key_ctx = completion_context(&parse(src), 2);
726        assert_eq!(key_ctx.position, Some(PositionKind::MapKey));
727        // Caret after the colon → value.
728        let colon = src.find(':').unwrap();
729        let val_ctx = completion_context(&parse(src), colon + 2);
730        assert_eq!(val_ctx.position, Some(PositionKind::MapValue));
731    }
732
733    #[test]
734    fn collects_in_file_variant_names() {
735        // Two variants attested in a list; cursor at a fresh value slot.
736        let src = "[Alpha, Beta, ]";
737        let ctx = completion_context(&parse(src), src.len() - 1);
738        assert!(ctx.in_file_variant_names.contains(&"Alpha".to_string()));
739        assert!(ctx.in_file_variant_names.contains(&"Beta".to_string()));
740    }
741
742    #[test]
743    fn collects_in_file_map_keys() {
744        let src = "{ alpha: 1, beta: 2 }";
745        let ctx = completion_context(&parse(src), 2);
746        assert!(ctx.in_file_map_keys.contains(&"alpha".to_string()));
747        assert!(ctx.in_file_map_keys.contains(&"beta".to_string()));
748    }
749
750    #[test]
751    fn items_ordered_kind_then_alpha_below_literal() {
752        // Value position with an empty prefix: Option items come before variants
753        // before delimiters; within a kind, alphabetical.
754        let src = "[]";
755        let ctx = completion_context(&parse(src), 1); // inside the brackets
756        assert_eq!(ctx.position, Some(PositionKind::ListElement));
757        let kinds: Vec<CompletionKind> = ctx.items.iter().map(|i| i.kind).collect();
758        // Option (None, Some) precede the delimiters.
759        let first_option = kinds.iter().position(|k| *k == CompletionKind::Option);
760        let first_delim = kinds.iter().position(|k| *k == CompletionKind::Delimiter);
761        assert!(first_option < first_delim);
762        // Ranks are strictly increasing from 1 and never 0.
763        for (i, item) in ctx.items.iter().enumerate() {
764            assert_eq!(item.rank, (i as u32) + 1);
765            assert!(item.rank >= 1, "no item may share the literal's rank 0");
766        }
767        // Within the Option kind, "None" sorts before "Some".
768        let none_pos = ctx.items.iter().position(|i| i.label == "None");
769        let some_pos = ctx.items.iter().position(|i| i.label == "Some");
770        assert!(none_pos < some_pos);
771    }
772
773    #[test]
774    fn prefix_filters_candidates() {
775        // Value slot; typing `So` should keep `Some` and drop `None`.
776        let src = "So";
777        let ctx = completion_context(&parse(src), 2);
778        assert_eq!(ctx.prefix, "So");
779        assert!(ctx.items.iter().any(|i| i.label == "Some"));
780        assert!(ctx.items.iter().all(|i| i.label != "None"));
781    }
782
783    #[test]
784    fn exact_literal_is_not_reoffered() {
785        // Typing the full `Some` must not re-offer `Some` (it is the literal).
786        let src = "Some";
787        let ctx = completion_context(&parse(src), 4);
788        assert!(ctx.items.iter().all(|i| i.label != "Some"));
789    }
790
791    #[test]
792    fn some_insert_text_round_trips() {
793        // The `Some` value-slot suggestion inserts `Some()` which parses cleanly.
794        let src = "[]";
795        let ctx = completion_context(&parse(src), 1);
796        let some = ctx
797            .items
798            .iter()
799            .find(|i| i.label == "Some")
800            .expect("Some offered at a value slot");
801        let parsed = parse(&some.insert_text);
802        assert!(
803            parsed.diagnostics().is_empty(),
804            "Some insert_text must parse cleanly: {:?}",
805            parsed.diagnostics()
806        );
807    }
808
809    #[test]
810    fn delimiter_insert_text_round_trips() {
811        let src = "[]";
812        let ctx = completion_context(&parse(src), 1);
813        for item in ctx
814            .items
815            .iter()
816            .filter(|i| i.kind == CompletionKind::Delimiter)
817        {
818            let parsed = parse(&item.insert_text);
819            assert!(
820                parsed.diagnostics().is_empty(),
821                "delimiter insert_text {:?} must parse cleanly",
822                item.insert_text
823            );
824        }
825    }
826
827    #[test]
828    fn out_of_range_offset_is_clamped_not_panicking() {
829        let src = "Foo(x: 1)";
830        // An offset past the end is clamped to source_len; no panic.
831        let ctx = completion_context(&parse(src), 9_999);
832        // Cursor at EOF after the closing paren → top-level value position.
833        assert!(ctx.position.is_some() || ctx.items.is_empty());
834    }
835
836    #[test]
837    fn completions_free_fn_matches_items() {
838        let src = "[]";
839        let ctx = completion_context(&parse(src), 1);
840        assert_eq!(completions(&ctx), ctx.items);
841    }
842}