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}