fix(completion): flag-aware partial so a dash completes flags, not keywords
The partial-token walk stopped at `-`, so after typing `-` (or `--`) the partial was empty and the replaced range was a zero-width point *after* the dash. Two bugs followed at a flag position (e.g. `add 1:n relationship … -`): the `on` keyword was offered (it prefix-matched the empty partial), and accepting a candidate inserted after the dash — `-on`, `---create-fk`, `----all-rows`. Detect a dash-prefixed token at a word boundary as a flag-in-progress and fold the whole dash-run into the partial, gated on a flag actually being expected there (so `where x = -5` stays a signed number, not a flag). The flag matcher now strips leading dashes and matches the body uniformly (empty / `-` / `--` → all flags; `--cr` → create-fk). Keywords like `on` no longer appear after a dash, and accept replaces the dash(es) so `-` → `--create-fk` and `--all` → `--all-rows`. Two partial-flag snapshots updated (they had captured the old behaviour).
This commit is contained in:
+103
-17
@@ -333,6 +333,37 @@ pub fn candidates_at_cursor_with_in_mode(
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Flag-aware extension. The plain walk above stops at `-`, so a
|
||||
// flag the user is mid-typing (`-`, `--`, `--all`, `--create-fk`)
|
||||
// leaves an *empty* partial sitting just after the dash(es) — which
|
||||
// made the engine offer every keyword (a `-` prefix-matches nothing,
|
||||
// so the empty-prefix path let `on` through) and, worse, replace an
|
||||
// empty range so accepting produced `-on` / `---create-fk`. When a
|
||||
// dash-prefixed token sits at a word boundary AND a flag is actually
|
||||
// expected here, treat the whole dash-run-plus-body as the partial so
|
||||
// it is matched and replaced wholesale. The "flag is expected" gate
|
||||
// (one cheap probe on the pre-dash prefix) keeps a signed number /
|
||||
// minus (`where x = -5`) from being mis-read as a flag.
|
||||
{
|
||||
let mut run = cursor;
|
||||
while run > 0 {
|
||||
let p = bytes[run - 1];
|
||||
if p.is_ascii_alphanumeric() || p == b'_' || p == b'-' {
|
||||
run -= 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let word_boundary = run == 0 || bytes[run - 1].is_ascii_whitespace();
|
||||
if run < cursor && bytes[run] == b'-' && word_boundary && run < start {
|
||||
let pre = crate::dsl::walker::completion_probe_in_mode(&input[..run], cache, mode);
|
||||
if pre.expected.iter().any(|e| matches!(e, Expectation::Flag(_))) {
|
||||
start = run;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let partial_prefix = input[start..cursor].to_string();
|
||||
let leading = &input[..start];
|
||||
|
||||
@@ -629,29 +660,19 @@ pub fn candidates_at_cursor_with_in_mode(
|
||||
// Source 1.55: flag candidates (`--name`). Surfaced as a
|
||||
// distinct CandidateKind so the hint panel can colour them
|
||||
// with `tok_flag` (matching how they'll appear after
|
||||
// insertion). 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 `--`).
|
||||
// insertion). The flag-aware partial detection above captures any
|
||||
// leading dash-run, so the partial is one of: empty, all-dashes
|
||||
// (`-` / `--`), or `[-]+body`. Stripping the leading dashes and
|
||||
// matching the remainder against the flag *body* handles all of
|
||||
// them uniformly (empty / all-dashes → match every flag).
|
||||
let flag_needle = partial_prefix.trim_start_matches('-').to_lowercase();
|
||||
let flags: Vec<String> = expected
|
||||
.iter()
|
||||
.filter_map(|e| match e {
|
||||
Expectation::Flag(name) => Some(*name),
|
||||
_ => None,
|
||||
})
|
||||
.filter(|body| {
|
||||
if partial_prefix.starts_with("--") {
|
||||
format!("--{body}")
|
||||
.to_lowercase()
|
||||
.starts_with(&lowered_prefix)
|
||||
} else if partial_prefix.is_empty() {
|
||||
true
|
||||
} else {
|
||||
body.to_lowercase().starts_with(&lowered_prefix)
|
||||
}
|
||||
})
|
||||
.filter(|body| body.to_lowercase().starts_with(&flag_needle))
|
||||
.map(|body| format!("--{body}"))
|
||||
.collect();
|
||||
|
||||
@@ -1528,6 +1549,71 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_dash_offers_flags_not_keywords_and_replaces_the_dash() {
|
||||
// Bug (manual testing): `add 1:n relationship … -` (one dash)
|
||||
// offered the `on` keyword *and* `--create-fk`, and accepting
|
||||
// produced `-on` / `---create-fk` because the lone `-` was not
|
||||
// part of the replaced range. A dash at a flag position is a
|
||||
// flag-in-progress: offer flags, exclude keywords, replace the
|
||||
// dash on accept.
|
||||
let input = "add 1:n relationship from X.a to Y.b -";
|
||||
let c = candidates_at_cursor(input, input.len(), &SchemaCache::default())
|
||||
.expect("a `-` at a flag position offers candidates");
|
||||
let texts: Vec<&str> = c.candidates.iter().map(|x| x.text.as_str()).collect();
|
||||
assert!(texts.contains(&"--create-fk"), "should offer --create-fk: {texts:?}");
|
||||
assert!(!texts.contains(&"on"), "must NOT offer `on` after a dash: {texts:?}");
|
||||
assert_eq!(
|
||||
c.replaced_range,
|
||||
(input.len() - 1, input.len()),
|
||||
"the `-` must be inside the replaced range so accept yields `--create-fk`",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn double_dash_replaces_both_dashes_on_accept() {
|
||||
let input = "delete from T --";
|
||||
let c = candidates_at_cursor_in_mode(
|
||||
input,
|
||||
input.len(),
|
||||
&SchemaCache::default(),
|
||||
Mode::Simple,
|
||||
)
|
||||
.expect("`--` offers the flag");
|
||||
assert!(c.candidates.iter().any(|x| x.text == "--all-rows"));
|
||||
assert_eq!(
|
||||
c.replaced_range,
|
||||
(input.len() - 2, input.len()),
|
||||
"both dashes are replaced so accept yields `--all-rows`, not `----all-rows`",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dash_at_a_value_position_is_not_treated_as_a_flag() {
|
||||
// `show data T where x = -5` — the `-` is a sign, not a flag.
|
||||
// No flag is expected here, so the dash must not be swallowed
|
||||
// into a flag partial: the partial stays `5` (the original
|
||||
// value-operand behaviour), and no `--…` candidate appears.
|
||||
let mut s = SchemaCache::default();
|
||||
s.tables.push("T".into());
|
||||
s.columns.push("x".into());
|
||||
let input = "show data T where x = -5";
|
||||
if let Some(c) =
|
||||
candidates_at_cursor_in_mode(input, input.len(), &s, Mode::Simple)
|
||||
{
|
||||
assert!(
|
||||
!c.candidates.iter().any(|x| x.text.starts_with("--")),
|
||||
"no flags at a value position: {:?}",
|
||||
c.candidates,
|
||||
);
|
||||
assert_eq!(
|
||||
c.replaced_range,
|
||||
(input.len() - 1, input.len()),
|
||||
"only the `5` is the partial; the `-` (sign) is not captured",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typed_dashes_offer_the_optional_cascade_flag_on_drop_column() {
|
||||
// The same optional-flag class: `drop column … [--cascade]`.
|
||||
|
||||
Reference in New Issue
Block a user