hint: replace misleading "null true false" suggestions at value slots

At value-literal slots (`insert into T values (`, `update T set
col=`, `where col=`, comma positions) the expected-token set
contains null/true/false/number/string-literal. The completion
engine was surfacing the three keyword candidates as Tab options
— actively misleading because the user is usually about to enter
a number, quoted text, or date, and seeing "null true false"
implies those are *the* options. User report (round-6 testing):
"especially not when I'm trying to insert a datetime value and
don't know the correct format for the literal".

Fix: detect the value-literal slot by its expected-set
fingerprint. Suppress Tab candidates at empty prefix. Surface a
prose hint listing all literal forms with format examples
('YYYY-MM-DD' for dates, 'YYYY-MM-DDTHH:MM:SS' for datetimes).
Once the user starts typing a prefix (n / tr / fa), normal
keyword completion still applies.

Schema-aware narrowing (show ONLY the datetime format at a
datetime column) waits on ADR-0023.

Tests: 769 -> 777 passing (+8). Clippy clean.
This commit is contained in:
claude@clouddev1
2026-05-14 20:40:19 +00:00
parent d6e138169f
commit 3b36bbb4d6
4 changed files with 172 additions and 0 deletions
+157
View File
@@ -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);