ADR-0024 round-5 follow-up: surface tail-Optional expectations
Pre-Phase-D, `save ` parsed as a complete `save` command, so the completion engine had nothing to mine: `as` never surfaced as a Tab candidate. This is the round-5 gap the handoffs have been tracking. `WalkResult` gains a `tail_expected: Vec<Expectation>` field. The walker's top-level `Matched` branch copies the outer shape's skipped-Optional expectations into it. `expected_at_input` returns `tail_expected` on `Match` so the completion engine sees the optional-suffix continuations and offers them as Tab candidates. `hint_mode_at_input` deliberately does NOT consume `tail_expected` — surfacing prose like "Type a name" at the end of a valid command would be misleading. A new private `expected_for_hint` returns empty on `Match` to preserve this. The split distinguishes "valid + could continue" (completion helps) from "invalid + must continue" (hint resolver helps). Tests: - `save ` Tab → `as` (new test, the original round-5 gap). - `messages ` Tab → `short` and `verbose` (same shape). - Existing `hint_mode_none_for_complete_command` stays green because hint resolver ignores tail_expected. - 830 total passing, 0 failing, 1 ignored. Clippy clean.
This commit is contained in:
+21
-8
@@ -814,14 +814,27 @@ mod tests {
|
||||
assert!(cs.contains(&"advanced".to_string()), "got {cs:?}");
|
||||
}
|
||||
|
||||
// Note: `save ` and `messages ` are deliberately NOT tested
|
||||
// here. Both commands accept their bare form as a valid parse
|
||||
// — `save` opens the save modal, `messages` shows the current
|
||||
// verbosity — so the parser returns Ok at those positions
|
||||
// and the completion engine has no expected-set to mine. The
|
||||
// optional-suffix candidates (`as`, `short`, `verbose`) would
|
||||
// need a separate probe mechanism (deferred — same shape as
|
||||
// the post-complete-parse gap for `--create-fk` etc.).
|
||||
// ---- Optional-suffix completion (round-5 gap, closed in Phase D) ----
|
||||
//
|
||||
// Pre-Phase-D: `save ` parsed as a valid `save` command, so
|
||||
// the completion engine had no expected-set to mine and the
|
||||
// `as` suffix never surfaced as a Tab candidate. Phase D's
|
||||
// `WalkResult::tail_expected` carries the outer shape's
|
||||
// skipped-Optional expectations even on `Match`, so these
|
||||
// surface without a separate probe mechanism.
|
||||
|
||||
#[test]
|
||||
fn save_space_offers_as_via_tail_expected() {
|
||||
let cs = cands("save ", 5);
|
||||
assert_eq!(cs, vec!["as".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn messages_space_offers_short_and_verbose_via_tail_expected() {
|
||||
let cs = cands("messages ", 9);
|
||||
assert!(cs.contains(&"short".to_string()), "got {cs:?}");
|
||||
assert!(cs.contains(&"verbose".to_string()), "got {cs:?}");
|
||||
}
|
||||
|
||||
// ---- Value-literal slot suppression (round-6) -----------
|
||||
|
||||
|
||||
+70
-8
@@ -52,7 +52,14 @@ pub fn hint_mode_at_input(source: &str) -> Option<crate::dsl::grammar::HintMode>
|
||||
use crate::dsl::grammar::{HintMode, IdentSource};
|
||||
use crate::dsl::walker::outcome::Expectation;
|
||||
|
||||
let expected = expected_at_input(source);
|
||||
// Hint mode is only meaningful at *required* slot positions
|
||||
// (Incomplete / Mismatch outcomes). For complete commands
|
||||
// (Match), `tail_expected` may carry optional-suffix
|
||||
// expectations — completion surfaces those as Tab
|
||||
// candidates, but the hint resolver should stay silent so
|
||||
// we don't push prose like "Type a name" at the end of a
|
||||
// valid command.
|
||||
let expected = expected_for_hint(source);
|
||||
if expected.is_empty() {
|
||||
return None;
|
||||
}
|
||||
@@ -125,18 +132,56 @@ pub fn expected_at_input(source: &str) -> Vec<outcome::Expectation> {
|
||||
}
|
||||
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(),
|
||||
let Some(result) = result else {
|
||||
// Walker didn't engage (unknown entry word): the
|
||||
// completion engine should still surface the available
|
||||
// entry words so the user can recover.
|
||||
None => REGISTRY
|
||||
return REGISTRY
|
||||
.iter()
|
||||
.map(|c| outcome::Expectation::Word(c.entry.primary))
|
||||
.collect(),
|
||||
.collect();
|
||||
};
|
||||
match result.outcome {
|
||||
// On Match, surface the outer-shape's skipped-Optional
|
||||
// expectations so the completion engine can offer
|
||||
// optional-suffix candidates at the end of a valid
|
||||
// command (`save` → `as`, etc.).
|
||||
outcome::WalkOutcome::Match { .. } => result.tail_expected,
|
||||
outcome::WalkOutcome::Incomplete { expected, .. }
|
||||
| outcome::WalkOutcome::Mismatch { expected, .. } => expected,
|
||||
outcome::WalkOutcome::ValidationFailed { .. } => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Strict-required expected set at the end of `source`. Like
|
||||
/// `expected_at_input` but returns empty on `WalkOutcome::Match`
|
||||
/// — optional-suffix continuations are not surfaced. Used by
|
||||
/// the hint resolver to distinguish "must type more" from
|
||||
/// "could continue".
|
||||
#[must_use]
|
||||
fn expected_for_hint(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);
|
||||
let Some(result) = result else {
|
||||
return REGISTRY
|
||||
.iter()
|
||||
.map(|c| outcome::Expectation::Word(c.entry.primary))
|
||||
.collect();
|
||||
};
|
||||
match result.outcome {
|
||||
outcome::WalkOutcome::Match { .. } | outcome::WalkOutcome::ValidationFailed { .. } => {
|
||||
Vec::new()
|
||||
}
|
||||
outcome::WalkOutcome::Incomplete { expected, .. }
|
||||
| outcome::WalkOutcome::Mismatch { expected, .. } => expected,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,6 +241,7 @@ pub fn walk<'a>(
|
||||
class: grammar::HighlightClass::Keyword,
|
||||
});
|
||||
|
||||
let mut tail_expected: Vec<Expectation> = Vec::new();
|
||||
let outcome = match walk_node(
|
||||
effective_source,
|
||||
kw_end,
|
||||
@@ -204,6 +250,21 @@ pub fn walk<'a>(
|
||||
&mut path,
|
||||
&mut per_byte,
|
||||
) {
|
||||
NodeWalkResult::Matched { end, skipped } => {
|
||||
// Carry the outer shape's skipped-Optional
|
||||
// expectations into WalkResult so completion can
|
||||
// surface optional-suffix candidates (`save` →
|
||||
// `as`). Empty for shapes with no trailing
|
||||
// optionals.
|
||||
tail_expected = skipped;
|
||||
NodeWalkResult::Matched {
|
||||
end,
|
||||
skipped: Vec::new(),
|
||||
}
|
||||
}
|
||||
other => other,
|
||||
};
|
||||
let outcome = match outcome {
|
||||
NodeWalkResult::Matched { end, .. } => {
|
||||
let trailing = skip_whitespace(effective_source, end);
|
||||
if trailing < effective_source.len() {
|
||||
@@ -272,6 +333,7 @@ pub fn walk<'a>(
|
||||
outcome: final_outcome,
|
||||
matched_path: path,
|
||||
per_byte_class: per_byte,
|
||||
tail_expected,
|
||||
};
|
||||
(Some(result), cmd)
|
||||
}
|
||||
|
||||
@@ -164,4 +164,16 @@ pub struct WalkResult {
|
||||
pub outcome: WalkOutcome,
|
||||
pub matched_path: MatchedPath,
|
||||
pub per_byte_class: Vec<ByteClass>,
|
||||
/// Optional-Optional expectations the walker could have
|
||||
/// accepted but didn't because the outer shape ran out at a
|
||||
/// node boundary (ADR-0024 §architecture, round-5 follow-up).
|
||||
///
|
||||
/// Populated on `WalkOutcome::Match` so completion can offer
|
||||
/// optional-suffix candidates at the end of a valid command
|
||||
/// — e.g., after typing `save` the walker matches the
|
||||
/// Optional `as` as skipped, the suffix carries it here, and
|
||||
/// the completion engine surfaces `as` as a Tab candidate.
|
||||
/// Empty on the non-Match outcomes — those carry expected
|
||||
/// information inside the outcome variant itself.
|
||||
pub tail_expected: Vec<Expectation>,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user