walker: keep optional trailing flags completable after --
Typing `--` to start an optional trailing flag (`--create-fk` on `add 1:n relationship`, `--cascade` on `drop column`, `--force-conversion` / `--dont-convert` on `change column`) made completion go empty: the trailing `--` turns the parse into a trailing-junk Mismatch, and the Mismatch arm of the completion expected-set resolution returned only `[EndOfInput]` — the skipped optional-flag expectations, carried in `tail_expected`, were dropped. completion_probe and expected_at_input now merge `tail_expected` into a Mismatch's expected set. `tail_expected` is empty for a genuine mid-command mismatch, so this only adds the outer shape's skipped trailing optionals — exactly the continuations the trailing `--` is starting to type. This also resolves the "wrong usage hint" symptom: with `--create-fk` offered as a candidate, the hint panel shows candidates instead of falling through to the parse-error usage block. Audit outcome (the requested scan): usage_key_for_input was verified correct for every multi-form command — add / drop / show, including the digit-led `add 1:n relationship` form — and is now regression-locked. The flag-completion fix covers the whole optional-trailing-flag class. 6 tests (3 flag-completion, 3 usage-key). 1131 passing.
This commit is contained in:
@@ -909,6 +909,48 @@ mod tests {
|
|||||||
assert!(cs.contains(&"--all-rows".to_string()), "got {cs:?}");
|
assert!(cs.contains(&"--all-rows".to_string()), "got {cs:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn typed_dashes_still_offer_an_optional_trailing_flag() {
|
||||||
|
// Regression: `add 1:n relationship … [--create-fk]` —
|
||||||
|
// at a trailing space the flag is offered, but once the
|
||||||
|
// user typed `--` the trailing-junk Mismatch dropped the
|
||||||
|
// skipped optional's expectation and completion went
|
||||||
|
// empty. Both positions must offer `--create-fk`.
|
||||||
|
let at_space = cands("add 1:n relationship from X.a to Y.b ", 37);
|
||||||
|
assert!(
|
||||||
|
at_space.contains(&"--create-fk".to_string()),
|
||||||
|
"trailing space should offer --create-fk, got {at_space:?}",
|
||||||
|
);
|
||||||
|
let at_dashes = cands("add 1:n relationship from X.a to Y.b --", 39);
|
||||||
|
assert!(
|
||||||
|
at_dashes.contains(&"--create-fk".to_string()),
|
||||||
|
"typed `--` should still offer --create-fk, got {at_dashes:?}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn typed_dashes_offer_the_optional_cascade_flag_on_drop_column() {
|
||||||
|
// The same optional-flag class: `drop column … [--cascade]`.
|
||||||
|
let at_dashes = cands("drop column from table T: c --", 30);
|
||||||
|
assert!(
|
||||||
|
at_dashes.contains(&"--cascade".to_string()),
|
||||||
|
"typed `--` should offer --cascade, got {at_dashes:?}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn typed_dashes_offer_the_change_column_conversion_flags() {
|
||||||
|
// `change column … [--force-conversion | --dont-convert]`
|
||||||
|
// — the flags sit in a `Repeated { min: 0 }`; the same
|
||||||
|
// trailing-junk-Mismatch fix must surface them.
|
||||||
|
let at_dashes = cands("change column T: c (int) --", 27);
|
||||||
|
assert!(
|
||||||
|
at_dashes.contains(&"--force-conversion".to_string())
|
||||||
|
&& at_dashes.contains(&"--dont-convert".to_string()),
|
||||||
|
"typed `--` should offer both conversion flags, got {at_dashes:?}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ---- App-lifecycle command completion (round-5 fold-in) ----
|
// ---- App-lifecycle command completion (round-5 fold-in) ----
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -498,3 +498,62 @@ pub fn command_for_entry_word(word: &str) -> Option<(usize, &'static CommandNode
|
|||||||
.find(|(_, c)| c.entry.matches(word))
|
.find(|(_, c)| c.entry.matches(word))
|
||||||
.map(|(i, c)| (i, *c))
|
.map(|(i, c)| (i, *c))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod usage_key_tests {
|
||||||
|
use super::usage_key_for_input;
|
||||||
|
|
||||||
|
/// Every multi-form command resolves a typed form to its
|
||||||
|
/// own usage key — a parse error in one form must never
|
||||||
|
/// show another form's usage (the handoff-18 `151ed08` fix;
|
||||||
|
/// regression-locked here, including the `add 1:n
|
||||||
|
/// relationship` digit-led form).
|
||||||
|
#[test]
|
||||||
|
fn multi_form_commands_resolve_to_the_typed_form() {
|
||||||
|
let cases = [
|
||||||
|
("add column to T: c (int)", "parse.usage.add_column"),
|
||||||
|
("add index on T (c)", "parse.usage.add_index"),
|
||||||
|
(
|
||||||
|
"add 1:n relationship from A.x to B.y",
|
||||||
|
"parse.usage.add_relationship",
|
||||||
|
),
|
||||||
|
// Trailing junk must not change the resolved form.
|
||||||
|
(
|
||||||
|
"add 1:n relationship from A.x to B.y --",
|
||||||
|
"parse.usage.add_relationship",
|
||||||
|
),
|
||||||
|
("drop table T", "parse.usage.drop_table"),
|
||||||
|
("drop column from table T: c", "parse.usage.drop_column"),
|
||||||
|
("drop index i", "parse.usage.drop_index"),
|
||||||
|
(
|
||||||
|
"drop relationship r",
|
||||||
|
"parse.usage.drop_relationship",
|
||||||
|
),
|
||||||
|
("show data T", "parse.usage.show_data"),
|
||||||
|
("show table T", "parse.usage.show_table"),
|
||||||
|
];
|
||||||
|
for (input, expected) in cases {
|
||||||
|
assert_eq!(
|
||||||
|
usage_key_for_input(input),
|
||||||
|
Some(expected),
|
||||||
|
"usage key for {input:?}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_bare_multi_form_entry_word_resolves_to_no_single_form() {
|
||||||
|
// `add` / `drop` alone — no form chosen; the caller
|
||||||
|
// shows the whole family rather than guessing.
|
||||||
|
assert_eq!(usage_key_for_input("add "), None);
|
||||||
|
assert_eq!(usage_key_for_input("drop "), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_single_form_command_resolves_to_its_one_key() {
|
||||||
|
assert_eq!(
|
||||||
|
usage_key_for_input("create table T with pk"),
|
||||||
|
Some("parse.usage.create_table"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+27
-4
@@ -276,8 +276,19 @@ pub fn completion_probe(
|
|||||||
};
|
};
|
||||||
let expected = match result.outcome {
|
let expected = match result.outcome {
|
||||||
outcome::WalkOutcome::Match { .. } => result.tail_expected,
|
outcome::WalkOutcome::Match { .. } => result.tail_expected,
|
||||||
outcome::WalkOutcome::Incomplete { expected, .. }
|
// A trailing-junk Mismatch (the shape matched, then the
|
||||||
| outcome::WalkOutcome::Mismatch { expected, .. } => expected,
|
// user kept typing) still carries the outer shape's
|
||||||
|
// skipped trailing optionals in `tail_expected` — e.g.
|
||||||
|
// an optional `--create-fk` flag the trailing `--` is
|
||||||
|
// starting to type. Merge them so completion still
|
||||||
|
// offers the optional continuation. A genuine
|
||||||
|
// mid-command mismatch has an empty `tail_expected`.
|
||||||
|
outcome::WalkOutcome::Mismatch { expected, .. } => {
|
||||||
|
let mut merged = expected;
|
||||||
|
merged.extend(result.tail_expected);
|
||||||
|
merged
|
||||||
|
}
|
||||||
|
outcome::WalkOutcome::Incomplete { expected, .. } => expected,
|
||||||
// Validation failure path: the walker matched the
|
// Validation failure path: the walker matched the
|
||||||
// structural shape but the AST builder rejected (e.g.
|
// structural shape but the AST builder rejected (e.g.
|
||||||
// Form C with column-shaped items). The walker still
|
// Form C with column-shaped items). The walker still
|
||||||
@@ -699,8 +710,20 @@ pub fn expected_at_input(source: &str) -> Vec<outcome::Expectation> {
|
|||||||
// optional-suffix candidates at the end of a valid
|
// optional-suffix candidates at the end of a valid
|
||||||
// command (`save` → `as`, etc.).
|
// command (`save` → `as`, etc.).
|
||||||
outcome::WalkOutcome::Match { .. } => result.tail_expected,
|
outcome::WalkOutcome::Match { .. } => result.tail_expected,
|
||||||
outcome::WalkOutcome::Incomplete { expected, .. }
|
// A trailing-junk Mismatch (the shape matched, then the
|
||||||
| outcome::WalkOutcome::Mismatch { expected, .. } => expected,
|
// user kept typing) still carries the outer shape's
|
||||||
|
// skipped trailing optionals in `tail_expected` — e.g.
|
||||||
|
// an optional `--create-fk` flag the trailing `--` is
|
||||||
|
// starting to type. Surface those alongside the
|
||||||
|
// mismatch's own expected set so completion still offers
|
||||||
|
// them. A genuine mid-command mismatch has an empty
|
||||||
|
// `tail_expected`, so this is a no-op there.
|
||||||
|
outcome::WalkOutcome::Mismatch { expected, .. } => {
|
||||||
|
let mut merged = expected;
|
||||||
|
merged.extend(result.tail_expected);
|
||||||
|
merged
|
||||||
|
}
|
||||||
|
outcome::WalkOutcome::Incomplete { expected, .. } => expected,
|
||||||
// Validation failure path: the walker matched the
|
// Validation failure path: the walker matched the
|
||||||
// structural shape but the AST builder rejected (e.g.
|
// structural shape but the AST builder rejected (e.g.
|
||||||
// Form C with column-shaped items). The walker still
|
// Form C with column-shaped items). The walker still
|
||||||
|
|||||||
Reference in New Issue
Block a user