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;
|
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 partial_prefix = input[start..cursor].to_string();
|
||||||
let leading = &input[..start];
|
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
|
// Source 1.55: flag candidates (`--name`). Surfaced as a
|
||||||
// distinct CandidateKind so the hint panel can colour them
|
// distinct CandidateKind so the hint panel can colour them
|
||||||
// with `tok_flag` (matching how they'll appear after
|
// with `tok_flag` (matching how they'll appear after
|
||||||
// insertion). The standard prefix matcher walks back over
|
// insertion). The flag-aware partial detection above captures any
|
||||||
// alphanumeric + underscore, which does NOT cross `-`, so
|
// leading dash-run, so the partial is one of: empty, all-dashes
|
||||||
// when the user types `--all` the partial is `all` — match
|
// (`-` / `--`), or `[-]+body`. Stripping the leading dashes and
|
||||||
// the flag's body against that. Otherwise match the full
|
// matching the remainder against the flag *body* handles all of
|
||||||
// `--name` against the partial (which may be empty or start
|
// them uniformly (empty / all-dashes → match every flag).
|
||||||
// with `--`).
|
let flag_needle = partial_prefix.trim_start_matches('-').to_lowercase();
|
||||||
let flags: Vec<String> = expected
|
let flags: Vec<String> = expected
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|e| match e {
|
.filter_map(|e| match e {
|
||||||
Expectation::Flag(name) => Some(*name),
|
Expectation::Flag(name) => Some(*name),
|
||||||
_ => None,
|
_ => None,
|
||||||
})
|
})
|
||||||
.filter(|body| {
|
.filter(|body| body.to_lowercase().starts_with(&flag_needle))
|
||||||
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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.map(|body| format!("--{body}"))
|
.map(|body| format!("--{body}"))
|
||||||
.collect();
|
.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]
|
#[test]
|
||||||
fn typed_dashes_offer_the_optional_cascade_flag_on_drop_column() {
|
fn typed_dashes_offer_the_optional_cascade_flag_on_drop_column() {
|
||||||
// The same optional-flag class: `drop column … [--cascade]`.
|
// The same optional-flag class: `drop column … [--cascade]`.
|
||||||
|
|||||||
+2
-2
@@ -24,10 +24,10 @@ Assessment {
|
|||||||
completion: Some(
|
completion: Some(
|
||||||
Completion {
|
Completion {
|
||||||
replaced_range: (
|
replaced_range: (
|
||||||
24,
|
22,
|
||||||
27,
|
27,
|
||||||
),
|
),
|
||||||
partial_prefix: "all",
|
partial_prefix: "--all",
|
||||||
candidates: [
|
candidates: [
|
||||||
Candidate {
|
Candidate {
|
||||||
text: "--all-rows",
|
text: "--all-rows",
|
||||||
|
|||||||
+2
-2
@@ -24,10 +24,10 @@ Assessment {
|
|||||||
completion: Some(
|
completion: Some(
|
||||||
Completion {
|
Completion {
|
||||||
replaced_range: (
|
replaced_range: (
|
||||||
33,
|
31,
|
||||||
36,
|
36,
|
||||||
),
|
),
|
||||||
partial_prefix: "all",
|
partial_prefix: "--all",
|
||||||
candidates: [
|
candidates: [
|
||||||
Candidate {
|
Candidate {
|
||||||
text: "--all-rows",
|
text: "--all-rows",
|
||||||
|
|||||||
Reference in New Issue
Block a user