ADR-0024 Phase F (full) step 5: walker-driven completion
Replaces the `ParseError::Invalid::expected: Vec<String>`
round-trip with structured `Expectation`s direct from the walker
(ADR-0024 §architecture). The completion engine no longer parses
formatted strings back into types — `Expectation::Ident { source,
role }`, `Expectation::Word`, `Expectation::Literal`,
`Expectation::Flag`, `Expectation::NumberLit`, and
`Expectation::StringLit` are consumed as enum variants.
New helper:
- `walker::expected_at_input(source) -> Vec<Expectation>`
consolidates the empty-input case (returns every CommandNode
entry word), unknown-command-word case (also entry words), and
walker-engaged case (Incomplete / Mismatch expectations) in one
place. ValidationFailed and Match resolve to empty.
`completion.rs` refactor:
- `expected_at(leading)` wraps the walker helper; replaces the
legacy string-based `expected_set`.
- Keyword candidates: filter `Expectation::Word(w)` /
`Expectation::Literal(s)` to alphabetic-only literals (no
more string-parsing / `strip_backticks`).
- Type names: detect `Expectation::Ident { source:
IdentSource::Types }` directly (replaces the `TYPE_SLOT_LABEL`
magic string).
- Flag candidates: read `Expectation::Flag(body)` and format
as `--{body}` (replaces backticked-string matching).
- Composite-literal candidates: match against
`Expectation::Literal("1")` (replaces the backticked-string
`` `1` ``).
- Schema identifiers: `Expectation::Ident { source, .. }`
filtered by `source.completes_from_schema()`.
- `is_value_literal_signature` checks for `Expectation::Word`
values "null"/"true"/"false" and `Expectation::NumberLit` +
`Expectation::StringLit` variants (replaces backticked-string
matching).
- `invalid_ident_at_cursor` and `typing_name_at_cursor` adopt
the same path.
The `typing_name_at_cursor` probe (substitute placeholder and
re-parse) still goes through `parse_command` because the probe
specifically wants the *post-name* expected set — `parse_command`
+ the string `expected` field carries that today. A future
follow-up could thread the structured probe through `walker`,
but the value-add is marginal.
`COMPOSITE_CANDIDATES` opener key changes from `` `1` `` (the
backticked-string the chumsky parser produced) to bare `"1"`
(the Expectation::Literal payload).
Touched modules: `dsl/walker/mod.rs` (new export),
`src/completion.rs` (refactor).
Tests: 806 passing, 0 failing, 1 ignored — every existing
completion test passes unchanged, proving the structured path
is behaviour-preserving. Clippy clean.
This commit is contained in:
+120
-114
@@ -16,27 +16,18 @@
|
|||||||
|
|
||||||
use crate::dsl::grammar::IdentSource;
|
use crate::dsl::grammar::IdentSource;
|
||||||
use crate::dsl::types::Type;
|
use crate::dsl::types::Type;
|
||||||
|
use crate::dsl::walker::outcome::Expectation;
|
||||||
use crate::dsl::{ParseError, parse_command};
|
use crate::dsl::{ParseError, parse_command};
|
||||||
|
|
||||||
/// Label emitted by `type_keyword` (in `dsl::parser`) when it
|
|
||||||
/// expects a column-type token. Matches the `.labelled("type")`
|
|
||||||
/// applied on the inner `select_ref!`. Centralised here so the
|
|
||||||
/// completion engine and the parser agree on the magic string.
|
|
||||||
const TYPE_SLOT_LABEL: &str = "type";
|
|
||||||
|
|
||||||
/// Composite literal candidates whose lexed shape is more than
|
/// Composite literal candidates whose lexed shape is more than
|
||||||
/// one token but which the user types as a single fluent piece.
|
/// one token but which the user types as a single fluent piece.
|
||||||
/// Pairs of (parser-expected-opener, full-composite-text).
|
/// Pairs of (walker-expected-literal, full-composite-text).
|
||||||
///
|
///
|
||||||
/// The opener is the first token's backticked label as it
|
/// When the walker reports `Expectation::Literal(opener)` at the
|
||||||
/// appears in `ParseError::Invalid::expected` — when present,
|
/// cursor, the engine surfaces the full composite text as a Tab
|
||||||
/// the engine surfaces the full composite text as a Tab
|
/// candidate. Today the only entry is `1:n` (the opener for
|
||||||
/// candidate.
|
/// `add 1:n relationship`) — adding more is a one-line edit.
|
||||||
///
|
const COMPOSITE_CANDIDATES: &[(&str, &str)] = &[("1", "1:n")];
|
||||||
/// Currently the only entry is `1:n` (start of
|
|
||||||
/// `add 1:n relationship`). New entries register here; no
|
|
||||||
/// parser change required.
|
|
||||||
const COMPOSITE_CANDIDATES: &[(&str, &str)] = &[("`1`", "1:n")];
|
|
||||||
|
|
||||||
/// Per-project schema lookup cache (ADR-0022 §9).
|
/// Per-project schema lookup cache (ADR-0022 §9).
|
||||||
///
|
///
|
||||||
@@ -65,6 +56,14 @@ impl SchemaCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// What the grammar would accept at the end of `leading`,
|
||||||
|
/// expressed as structured `Expectation`s direct from the
|
||||||
|
/// walker (ADR-0024 §architecture, Phase F walker-driven
|
||||||
|
/// completion). Replaces the `ParseError`-string round-trip.
|
||||||
|
fn expected_at(leading: &str) -> Vec<Expectation> {
|
||||||
|
crate::dsl::walker::expected_at_input(leading)
|
||||||
|
}
|
||||||
|
|
||||||
/// A single Tab-insertable item with its source (so the
|
/// A single Tab-insertable item with its source (so the
|
||||||
/// renderer can colour keywords differently from schema
|
/// renderer can colour keywords differently from schema
|
||||||
/// identifiers, and so the ordering can group keywords first).
|
/// identifiers, and so the ordering can group keywords first).
|
||||||
@@ -139,7 +138,7 @@ pub fn candidates_at_cursor(
|
|||||||
let partial_prefix = input[start..cursor].to_string();
|
let partial_prefix = input[start..cursor].to_string();
|
||||||
let leading = &input[..start];
|
let leading = &input[..start];
|
||||||
|
|
||||||
let expected = expected_set(leading);
|
let expected = expected_at(leading);
|
||||||
if expected.is_empty() {
|
if expected.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -149,13 +148,9 @@ pub fn candidates_at_cursor(
|
|||||||
// keyword literals — number / string-literal slots are
|
// keyword literals — number / string-literal slots are
|
||||||
// descriptive labels, not Tab candidates). Surfacing those
|
// descriptive labels, not Tab candidates). Surfacing those
|
||||||
// three is actively misleading — the user usually wants a
|
// three is actively misleading — the user usually wants a
|
||||||
// number, a quoted string, or a date, and seeing just
|
// number, a quoted string, or a date. Suppress so the
|
||||||
// "null true false" implies those are *the* options. We
|
// ambient_hint ladder falls through to a prose hint with
|
||||||
// suppress the keyword candidates here; the ambient_hint
|
// format examples instead.
|
||||||
// ladder falls through to a prose hint with format
|
|
||||||
// examples instead. Once the user starts typing a prefix
|
|
||||||
// (`n`, `tr`, `fa`) the normal keyword-completion path
|
|
||||||
// applies — the suppression only triggers at empty prefix.
|
|
||||||
if partial_prefix.is_empty() && is_value_literal_signature(&expected) {
|
if partial_prefix.is_empty() && is_value_literal_signature(&expected) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -163,35 +158,36 @@ pub fn candidates_at_cursor(
|
|||||||
let lowered_prefix = partial_prefix.to_lowercase();
|
let lowered_prefix = partial_prefix.to_lowercase();
|
||||||
let matches_prefix = |s: &str| s.to_lowercase().starts_with(&lowered_prefix);
|
let matches_prefix = |s: &str| s.to_lowercase().starts_with(&lowered_prefix);
|
||||||
|
|
||||||
// Source 1: keyword candidates from the parser's
|
// Source 1: keyword candidates direct from the walker's
|
||||||
// expected-set. Preserve `expected`'s order — it reflects
|
// expected set. `Word(primary)` and `Literal(s)` both
|
||||||
// chumsky's source-order traversal of `or_not` / `choice`
|
// surface here; we keep only the alphabetic ones —
|
||||||
// chains, which matches the canonical command shape (e.g.
|
// single-digit literals like `1` go through the composite
|
||||||
// `to` before `table` for `add column [to] [table] …`).
|
// pipeline below, and punct never surfaces as a candidate.
|
||||||
|
// Declaration order is preserved (matches the canonical
|
||||||
|
// command shape, e.g. `to` before `table` for
|
||||||
|
// `add column [to] [table] …`).
|
||||||
let mut keywords: Vec<String> = expected
|
let mut keywords: Vec<String> = expected
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|item| strip_backticks(item))
|
.filter_map(|e| match e {
|
||||||
// Backticked items are walker `Expectation::Word`s or
|
Expectation::Word(w) | Expectation::Literal(w) => Some(*w),
|
||||||
// `Expectation::Literal`s. Keywords are the
|
_ => None,
|
||||||
// alphabetic-only ones; punct (`,`, `=`) and digit
|
})
|
||||||
// literals (`1`) live in the same expected-set but
|
.filter(|w| !w.is_empty() && w.chars().all(|c| c.is_ascii_alphabetic()))
|
||||||
// surface through other candidate sources.
|
|
||||||
.filter(|name| !name.is_empty() && name.chars().all(|c| c.is_ascii_alphabetic()))
|
|
||||||
.map(str::to_string)
|
.map(str::to_string)
|
||||||
.filter(|name| matches_prefix(name))
|
.filter(|name| matches_prefix(name))
|
||||||
.collect();
|
.collect();
|
||||||
let mut seen_kw = std::collections::HashSet::new();
|
let mut seen_kw = std::collections::HashSet::new();
|
||||||
keywords.retain(|k| seen_kw.insert(k.clone()));
|
keywords.retain(|k| seen_kw.insert(k.clone()));
|
||||||
|
|
||||||
// Source 1.5: type-name candidates when the parser expects
|
// Source 1.5: type-name candidates when the walker expects
|
||||||
// a column-type slot. Type names live outside the Keyword
|
// a column-type slot. Type names are a closed set sourced
|
||||||
// enum (ADR-0020 §2 — type names stay as identifiers,
|
// from `Type::all()` (ADR-0005 declaration order:
|
||||||
// validated by Type::from_str), so they need their own
|
// text/int/real/decimal/bool/date/datetime/blob/serial/
|
||||||
// completion path. Preserve `Type::all()` declaration
|
// shortid). The walker surfaces this as
|
||||||
// order — that's text/int/real/decimal/bool/date/datetime/
|
// `Expectation::Ident { source: Types }`.
|
||||||
// blob/serial/shortid, the order a learner reads them in
|
let type_names: Vec<String> = if expected.iter().any(|e| {
|
||||||
// ADR-0005.
|
matches!(e, Expectation::Ident { source: IdentSource::Types, .. })
|
||||||
let type_names: Vec<String> = if expected.iter().any(|s| s == TYPE_SLOT_LABEL) {
|
}) {
|
||||||
Type::all()
|
Type::all()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|t| t.keyword().to_string())
|
.map(|t| t.keyword().to_string())
|
||||||
@@ -201,56 +197,66 @@ pub fn candidates_at_cursor(
|
|||||||
Vec::new()
|
Vec::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Source 1.55: flag candidates (`--name`). Like type
|
// Source 1.55: flag candidates (`--name`). Surfaced as a
|
||||||
// names, flags live outside the Keyword enum — the parser
|
// distinct CandidateKind so the hint panel can colour them
|
||||||
// labels them as backticked literals like `` `--all-rows` ``.
|
// with `tok_flag` (matching how they'll appear after
|
||||||
// Surface them as a distinct CandidateKind so the hint
|
// insertion). The standard prefix matcher walks back over
|
||||||
// panel can colour them with `tok_flag` (matching how
|
// alphanumeric + underscore, which does NOT cross `-`, so
|
||||||
// they'll appear in the input pane after insertion).
|
// when the user types `--all` the partial is `all` — match
|
||||||
//
|
// the flag's body against that. Otherwise match the full
|
||||||
// The user can either Tab from a bare cursor position
|
// `--name` against the partial (which may be empty or start
|
||||||
// (partial empty) or after typing `--` (partial = "--").
|
// with `--`).
|
||||||
// The standard prefix matcher walks back over alphanumeric +
|
|
||||||
// underscore, which does NOT cross `-`, so when the user
|
|
||||||
// types `--all` the partial is `all` — match the flag's
|
|
||||||
// body against that. Otherwise match the full `--name`
|
|
||||||
// against the partial (which may be empty or start with `--`).
|
|
||||||
let flags: Vec<String> = expected
|
let flags: Vec<String> = expected
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|item| strip_backticks(item))
|
.filter_map(|e| match e {
|
||||||
.filter(|name| name.starts_with("--"))
|
Expectation::Flag(name) => Some(*name),
|
||||||
.filter(|name| {
|
_ => None,
|
||||||
if partial_prefix.starts_with("--") || partial_prefix.is_empty() {
|
})
|
||||||
matches_prefix(name)
|
.filter(|body| {
|
||||||
|
if partial_prefix.starts_with("--") {
|
||||||
|
format!("--{body}")
|
||||||
|
.to_lowercase()
|
||||||
|
.starts_with(&lowered_prefix)
|
||||||
|
} else if partial_prefix.is_empty() {
|
||||||
|
true
|
||||||
} else {
|
} else {
|
||||||
// partial is the alphanumeric tail past `--`
|
|
||||||
let body = &name[2..];
|
|
||||||
body.to_lowercase().starts_with(&lowered_prefix)
|
body.to_lowercase().starts_with(&lowered_prefix)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.map(|name| name.to_string())
|
.map(|body| format!("--{body}"))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Source 1.6: composite-literal candidates. Some commands
|
// Source 1.6: composite-literal candidates. Some commands
|
||||||
// start with a multi-token literal sequence that the lexer
|
// start with a multi-token literal sequence that the user
|
||||||
// splits into Number/Punct/Identifier (e.g. `1:n` for
|
// types as a single fluent piece (e.g. `1:n` for
|
||||||
// `add 1:n relationship`). The parser's expected-set
|
// `add 1:n relationship`). The walker's expected-set
|
||||||
// surfaces just the first token (`` `1` ``), which would
|
// surfaces the first token only (`Expectation::Literal("1")`);
|
||||||
// otherwise be filtered out (not a Keyword variant). We
|
// the engine surfaces the full composite text so the user
|
||||||
// surface the full composite so the user can Tab through
|
// can Tab through without knowing the surface syntax.
|
||||||
// without knowing the surface syntax.
|
|
||||||
let composites: Vec<String> = COMPOSITE_CANDIDATES
|
let composites: Vec<String> = COMPOSITE_CANDIDATES
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(opener, _)| expected.iter().any(|s| s == *opener))
|
.filter(|(opener, _)| {
|
||||||
|
expected.iter().any(|e| match e {
|
||||||
|
Expectation::Literal(l) | Expectation::Word(l) => *l == *opener,
|
||||||
|
_ => false,
|
||||||
|
})
|
||||||
|
})
|
||||||
.map(|(_, text)| (*text).to_string())
|
.map(|(_, text)| (*text).to_string())
|
||||||
.filter(|s| matches_prefix(s))
|
.filter(|s| matches_prefix(s))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Source 2: schema identifiers — accumulated across every
|
// Source 2: schema identifiers — accumulated across every
|
||||||
// matching known-set slot. `NewName` slots return `&[]`.
|
// matching schema-listable `Ident { source }` expectation.
|
||||||
|
// `NewName` / `Types` / `Free` sources don't query the
|
||||||
|
// schema cache and contribute nothing here.
|
||||||
let mut identifiers: Vec<String> = expected
|
let mut identifiers: Vec<String> = expected
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|item| IdentSource::from_expected_label(item))
|
.filter_map(|e| match e {
|
||||||
|
Expectation::Ident { source, .. } if source.completes_from_schema() => {
|
||||||
|
Some(*source)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
.flat_map(|source| cache.for_source(source).iter().cloned())
|
.flat_map(|source| cache.for_source(source).iter().cloned())
|
||||||
.filter(|name| matches_prefix(name))
|
.filter(|name| matches_prefix(name))
|
||||||
.collect();
|
.collect();
|
||||||
@@ -306,18 +312,22 @@ pub fn candidates_at_cursor(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn strip_backticks(s: &str) -> Option<&str> {
|
|
||||||
s.strip_prefix('`').and_then(|s| s.strip_suffix('`'))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Detect a value-literal expected-set signature. A value-literal
|
/// Detect a value-literal expected-set signature. A value-literal
|
||||||
/// slot is the only position where chumsky's expected-set
|
/// slot is the only position where the walker's expected-set
|
||||||
/// simultaneously contains all five forms `null` / `true` /
|
/// simultaneously contains all five forms `null` / `true` /
|
||||||
/// `false` / number / string literal. See the suppression rationale
|
/// `false` / number / string literal. See the suppression
|
||||||
/// at the call site in `candidates_at_cursor`.
|
/// rationale at the call site in `candidates_at_cursor`.
|
||||||
fn is_value_literal_signature(expected: &[String]) -> bool {
|
fn is_value_literal_signature(expected: &[Expectation]) -> bool {
|
||||||
let has = |needle: &str| expected.iter().any(|e| e == needle);
|
let has_word = |needle: &str| {
|
||||||
has("`null`") && has("`true`") && has("`false`") && has("number") && has("string literal")
|
expected
|
||||||
|
.iter()
|
||||||
|
.any(|e| matches!(e, Expectation::Word(w) if *w == needle))
|
||||||
|
};
|
||||||
|
has_word("null")
|
||||||
|
&& has_word("true")
|
||||||
|
&& has_word("false")
|
||||||
|
&& expected.iter().any(|e| matches!(e, Expectation::NumberLit))
|
||||||
|
&& expected.iter().any(|e| matches!(e, Expectation::StringLit))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `Some(prose)` when the cursor sits at an empty-prefix value-literal slot.
|
/// `Some(prose)` when the cursor sits at an empty-prefix value-literal slot.
|
||||||
@@ -350,7 +360,7 @@ pub fn value_literal_hint_at_cursor(input: &str, cursor: usize) -> Option<String
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let leading = &input[..start];
|
let leading = &input[..start];
|
||||||
let expected = expected_set(leading);
|
let expected = expected_at(leading);
|
||||||
if !is_value_literal_signature(&expected) {
|
if !is_value_literal_signature(&expected) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -410,11 +420,16 @@ pub fn typing_name_at_cursor(input: &str, cursor: usize) -> Option<TypingName> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let leading = &input[..start];
|
let leading = &input[..start];
|
||||||
let expected = expected_set(leading);
|
let expected = expected_at(leading);
|
||||||
let is_new_name_slot = expected
|
let is_new_name_slot = expected.iter().any(|e| {
|
||||||
.iter()
|
matches!(
|
||||||
.filter_map(|item| IdentSource::from_expected_label(item))
|
e,
|
||||||
.any(|source| source == IdentSource::NewName);
|
Expectation::Ident {
|
||||||
|
source: IdentSource::NewName,
|
||||||
|
..
|
||||||
|
}
|
||||||
|
)
|
||||||
|
});
|
||||||
if !is_new_name_slot {
|
if !is_new_name_slot {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -485,15 +500,19 @@ pub fn invalid_ident_at_cursor(
|
|||||||
}
|
}
|
||||||
let partial = &input[start..cursor];
|
let partial = &input[start..cursor];
|
||||||
let leading = &input[..start];
|
let leading = &input[..start];
|
||||||
let expected = expected_set(leading);
|
let expected = expected_at(leading);
|
||||||
if expected.is_empty() {
|
if expected.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
// Find every known-set slot in the expected list.
|
// Find every schema-listable source in the expected list.
|
||||||
let sources: Vec<IdentSource> = expected
|
let sources: Vec<IdentSource> = expected
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|item| IdentSource::from_expected_label(item))
|
.filter_map(|e| match e {
|
||||||
.filter(|s| s.completes_from_schema())
|
Expectation::Ident { source, .. } if source.completes_from_schema() => {
|
||||||
|
Some(*source)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
if sources.is_empty() {
|
if sources.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
@@ -521,23 +540,10 @@ pub fn invalid_ident_at_cursor(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The expected-token set at the end of `leading`. Empty
|
// `expected_set` is gone: the walker-driven `expected_at` above
|
||||||
/// `leading` (whitespace only) yields every command-entry
|
// returns structured `Expectation`s with full `IdentSource`
|
||||||
/// keyword — there's no parser failure to drive this from, so
|
// information, avoiding the lossy string round-trip the
|
||||||
/// we synthesise it from the usage registry.
|
// chumsky-era completion engine relied on.
|
||||||
fn expected_set(leading: &str) -> Vec<String> {
|
|
||||||
if leading.trim().is_empty() {
|
|
||||||
return crate::dsl::grammar::entry_words_alphabetised()
|
|
||||||
.into_iter()
|
|
||||||
.map(|w| format!("`{w}`"))
|
|
||||||
.collect();
|
|
||||||
}
|
|
||||||
match parse_command(leading) {
|
|
||||||
Ok(_) => Vec::new(),
|
|
||||||
Err(ParseError::Empty) => Vec::new(),
|
|
||||||
Err(ParseError::Invalid { expected, .. }) => expected,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Snapshot of a freshly-inserted completion. The memo lives
|
/// Snapshot of a freshly-inserted completion. The memo lives
|
||||||
/// on `App::last_completion` until any non-Tab/non-Shift-Tab
|
/// on `App::last_completion` until any non-Tab/non-Shift-Tab
|
||||||
|
|||||||
@@ -30,6 +30,49 @@ use crate::dsl::walker::outcome::{
|
|||||||
pub use context::ColumnInfo;
|
pub use context::ColumnInfo;
|
||||||
pub use highlight::highlight_runs;
|
pub use highlight::highlight_runs;
|
||||||
|
|
||||||
|
/// What the grammar would accept at the end of `source`
|
||||||
|
/// (ADR-0024 §architecture, Phase F walker-driven completion).
|
||||||
|
///
|
||||||
|
/// Empty / whitespace-only input yields every command-entry word
|
||||||
|
/// as `Expectation::Word(primary)`. Otherwise the walker is
|
||||||
|
/// driven to `EndOfInput`; if the input completes a command,
|
||||||
|
/// the result is empty; if it fails or is incomplete, the
|
||||||
|
/// walker's expected-set surfaces verbatim — `Ident { source,
|
||||||
|
/// role }` carries its `IdentSource` (so the completion engine
|
||||||
|
/// can schema-look-up without a string round-trip), `Word` /
|
||||||
|
/// `Literal` carry their primary literal, etc.
|
||||||
|
///
|
||||||
|
/// Inputs whose first token is not a registered entry word
|
||||||
|
/// fall back to listing every entry word — matches the
|
||||||
|
/// synthetic "unknown command" expectation set the parser
|
||||||
|
/// produces.
|
||||||
|
#[must_use]
|
||||||
|
pub fn expected_at_input(source: &str) -> Vec<outcome::Expectation> {
|
||||||
|
use crate::dsl::grammar::REGISTRY;
|
||||||
|
|
||||||
|
if source.trim().is_empty() {
|
||||||
|
return REGISTRY
|
||||||
|
.iter()
|
||||||
|
.map(|c| outcome::Expectation::Word(c.entry.primary))
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
let mut ctx = context::WalkContext::new();
|
||||||
|
let (result, _cmd) = walk(source, outcome::WalkBound::EndOfInput, &mut ctx);
|
||||||
|
match result.map(|r| r.outcome) {
|
||||||
|
Some(outcome::WalkOutcome::Match { .. }) => Vec::new(),
|
||||||
|
Some(outcome::WalkOutcome::Incomplete { expected, .. }) => expected,
|
||||||
|
Some(outcome::WalkOutcome::Mismatch { expected, .. }) => expected,
|
||||||
|
Some(outcome::WalkOutcome::ValidationFailed { .. }) => Vec::new(),
|
||||||
|
// Walker didn't engage (unknown entry word): the
|
||||||
|
// completion engine should still surface the available
|
||||||
|
// entry words so the user can recover.
|
||||||
|
None => REGISTRY
|
||||||
|
.iter()
|
||||||
|
.map(|c| outcome::Expectation::Word(c.entry.primary))
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Public walk entry. `bound` is `EndOfInput` for parse;
|
/// Public walk entry. `bound` is `EndOfInput` for parse;
|
||||||
/// `Position(cursor)` for completion / hint (Phase A: not yet
|
/// `Position(cursor)` for completion / hint (Phase A: not yet
|
||||||
/// wired).
|
/// wired).
|
||||||
|
|||||||
Reference in New Issue
Block a user