|
|
|
@@ -146,6 +146,22 @@ pub fn candidates_at_cursor(
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Value-literal slot: at an empty-prefix position the only
|
|
|
|
|
// candidates we'd surface are `null`/`true`/`false` (the
|
|
|
|
|
// keyword literals — number / string-literal slots are
|
|
|
|
|
// descriptive labels, not Tab candidates). Surfacing those
|
|
|
|
|
// three is actively misleading — the user usually wants a
|
|
|
|
|
// number, a quoted string, or a date, and seeing just
|
|
|
|
|
// "null true false" implies those are *the* options. We
|
|
|
|
|
// suppress the keyword candidates here; the ambient_hint
|
|
|
|
|
// ladder falls through to a prose hint with format
|
|
|
|
|
// examples instead. Once the user starts typing a prefix
|
|
|
|
|
// (`n`, `tr`, `fa`) the normal keyword-completion path
|
|
|
|
|
// applies — the suppression only triggers at empty prefix.
|
|
|
|
|
if partial_prefix.is_empty() && is_value_literal_signature(&expected) {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let lowered_prefix = partial_prefix.to_lowercase();
|
|
|
|
|
let matches_prefix = |s: &str| s.to_lowercase().starts_with(&lowered_prefix);
|
|
|
|
|
|
|
|
|
@@ -290,6 +306,53 @@ fn strip_backticks(s: &str) -> Option<&str> {
|
|
|
|
|
s.strip_prefix('`').and_then(|s| s.strip_suffix('`'))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Detect a value-literal expected-set signature. A value-literal
|
|
|
|
|
/// slot is the only position where chumsky's expected-set
|
|
|
|
|
/// simultaneously contains all five forms `null` / `true` /
|
|
|
|
|
/// `false` / number / string literal. See the suppression rationale
|
|
|
|
|
/// at the call site in `candidates_at_cursor`.
|
|
|
|
|
fn is_value_literal_signature(expected: &[String]) -> bool {
|
|
|
|
|
let has = |needle: &str| expected.iter().any(|e| e == needle);
|
|
|
|
|
has("`null`") && has("`true`") && has("`false`") && has("number") && has("string literal")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// `Some(prose)` when the cursor sits at an empty-prefix value-literal slot.
|
|
|
|
|
///
|
|
|
|
|
/// The hint panel surfaces format guidance (number, quoted text,
|
|
|
|
|
/// date, datetime, bool, null) instead of the misleading "null
|
|
|
|
|
/// true false" keyword-only candidate list.
|
|
|
|
|
///
|
|
|
|
|
/// Note: this is a stopgap until ADR-0023 lands schema-aware
|
|
|
|
|
/// completion (which would surface format examples specific to
|
|
|
|
|
/// the column's type — e.g. just the datetime format at a
|
|
|
|
|
/// datetime column). Today the hint lists all valid literal
|
|
|
|
|
/// shapes regardless of context.
|
|
|
|
|
#[must_use]
|
|
|
|
|
pub fn value_literal_hint_at_cursor(input: &str, cursor: usize) -> Option<String> {
|
|
|
|
|
let cursor = cursor.min(input.len());
|
|
|
|
|
let bytes = input.as_bytes();
|
|
|
|
|
let mut start = cursor;
|
|
|
|
|
while start > 0 {
|
|
|
|
|
let prev = bytes[start - 1];
|
|
|
|
|
if prev.is_ascii_alphanumeric() || prev == b'_' {
|
|
|
|
|
start -= 1;
|
|
|
|
|
} else {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if start != cursor {
|
|
|
|
|
// Partial prefix is non-empty — keyword completion
|
|
|
|
|
// handles it (e.g. `n` → `null`).
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
let leading = &input[..start];
|
|
|
|
|
let expected = expected_set(leading);
|
|
|
|
|
if !is_value_literal_signature(&expected) {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
Some(crate::t!("hint.value_literal_slot"))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// What the user has typed in an identifier slot whose schema
|
|
|
|
|
/// list contains nothing matching the prefix (ADR-0022 stage 8e
|
|
|
|
|
/// + the user's #5).
|
|
|
|
@@ -680,6 +743,100 @@ mod tests {
|
|
|
|
|
// need a separate probe mechanism (deferred — same shape as
|
|
|
|
|
// the post-complete-parse gap for `--create-fk` etc.).
|
|
|
|
|
|
|
|
|
|
// ---- Value-literal slot suppression (round-6) -----------
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn value_literal_slot_suppresses_keyword_candidates_at_empty_prefix() {
|
|
|
|
|
// After `insert into T values (` the parser's expected
|
|
|
|
|
// set contains null/true/false/number/string literal.
|
|
|
|
|
// The keyword pipeline would otherwise surface `null`,
|
|
|
|
|
// `true`, `false` as Tab candidates — actively
|
|
|
|
|
// misleading at a slot where the user is more likely
|
|
|
|
|
// entering a number / text / date. Suppress.
|
|
|
|
|
let cs = cands("insert into T values (", 22);
|
|
|
|
|
assert!(cs.is_empty(), "got misleading candidates {cs:?}");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn value_literal_slot_with_partial_prefix_still_completes() {
|
|
|
|
|
// Once the user types a prefix, normal keyword
|
|
|
|
|
// completion applies — `n` → `null`, `tr` → `true`,
|
|
|
|
|
// `fa` → `false`.
|
|
|
|
|
assert_eq!(
|
|
|
|
|
cands("insert into T values (n", 23),
|
|
|
|
|
vec!["null".to_string()],
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
cands("insert into T values (tr", 24),
|
|
|
|
|
vec!["true".to_string()],
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
cands("insert into T values (fa", 24),
|
|
|
|
|
vec!["false".to_string()],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn value_literal_slot_after_first_value_also_suppresses() {
|
|
|
|
|
// Comma-separated value positions all hit the same slot
|
|
|
|
|
// signature. `insert into T values (1, ` → expected:
|
|
|
|
|
// null/true/false/number/string. Suppress.
|
|
|
|
|
let cs = cands("insert into T values (1, ", 25);
|
|
|
|
|
assert!(cs.is_empty(), "got {cs:?}");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn update_set_value_slot_suppresses() {
|
|
|
|
|
// `update T set col=` is also a value-literal slot.
|
|
|
|
|
let cs = cands("update T set col=", 17);
|
|
|
|
|
assert!(cs.is_empty(), "got {cs:?}");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn where_value_slot_suppresses() {
|
|
|
|
|
// `where col=` is also a value-literal slot.
|
|
|
|
|
let cs = cands("delete from T where col=", 24);
|
|
|
|
|
assert!(cs.is_empty(), "got {cs:?}");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn value_literal_hint_fires_at_empty_value_slot() {
|
|
|
|
|
let hint = value_literal_hint_at_cursor("insert into T values (", 22);
|
|
|
|
|
let s = hint.expect("hint should fire at value-literal slot");
|
|
|
|
|
// Lists each literal form so the user sees the full set
|
|
|
|
|
// of valid inputs rather than just three keywords.
|
|
|
|
|
assert!(s.contains("number"), "got {s:?}");
|
|
|
|
|
assert!(s.contains("text") || s.contains("'"), "got {s:?}");
|
|
|
|
|
assert!(s.contains("true"), "got {s:?}");
|
|
|
|
|
assert!(s.contains("false") || s.contains("/false"), "got {s:?}");
|
|
|
|
|
assert!(s.contains("null"), "got {s:?}");
|
|
|
|
|
// Format examples for the cases users typically can't
|
|
|
|
|
// guess (date, datetime).
|
|
|
|
|
assert!(
|
|
|
|
|
s.contains("YYYY-MM-DD"),
|
|
|
|
|
"should include date format, got {s:?}",
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
s.contains("HH:MM:SS"),
|
|
|
|
|
"should include datetime format, got {s:?}",
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn value_literal_hint_does_not_fire_at_partial_prefix() {
|
|
|
|
|
// With a partial prefix the keyword-completion path
|
|
|
|
|
// handles it; the prose hint short-circuit only
|
|
|
|
|
// applies to empty-prefix positions.
|
|
|
|
|
assert!(value_literal_hint_at_cursor("insert into T values (n", 23).is_none());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn value_literal_hint_does_not_fire_at_keyword_slot() {
|
|
|
|
|
// Entry keyword position is not a value-literal slot.
|
|
|
|
|
assert!(value_literal_hint_at_cursor("", 0).is_none());
|
|
|
|
|
assert!(value_literal_hint_at_cursor("insert ", 7).is_none());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn show_offers_data_and_table_alphabetised() {
|
|
|
|
|
let cs = cands("show ", 5);
|
|
|
|
|