diff --git a/src/completion.rs b/src/completion.rs index aa9d605..f5ad278 100644 --- a/src/completion.rs +++ b/src/completion.rs @@ -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 = 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]`. diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__delete_all_rows__delete_partial_flag_is_incomplete@partial_flag.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__delete_all_rows__delete_partial_flag_is_incomplete@partial_flag.snap index f0261f3..d5dfb13 100644 --- a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__delete_all_rows__delete_partial_flag_is_incomplete@partial_flag.snap +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__delete_all_rows__delete_partial_flag_is_incomplete@partial_flag.snap @@ -24,10 +24,10 @@ Assessment { completion: Some( Completion { replaced_range: ( - 24, + 22, 27, ), - partial_prefix: "all", + partial_prefix: "--all", candidates: [ Candidate { text: "--all-rows", diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__update_all_rows__update_partial_flag_name_is_incomplete@partial_flag.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__update_all_rows__update_partial_flag_name_is_incomplete@partial_flag.snap index e10cfab..7b43f80 100644 --- a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__update_all_rows__update_partial_flag_name_is_incomplete@partial_flag.snap +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__update_all_rows__update_partial_flag_name_is_incomplete@partial_flag.snap @@ -24,10 +24,10 @@ Assessment { completion: Some( Completion { replaced_range: ( - 33, + 31, 36, ), - partial_prefix: "all", + partial_prefix: "--all", candidates: [ Candidate { text: "--all-rows",