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; 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 lowered_prefix = partial_prefix.to_lowercase();
let matches_prefix = |s: &str| s.to_lowercase().starts_with(&lowered_prefix); 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('`')) 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 /// What the user has typed in an identifier slot whose schema
/// list contains nothing matching the prefix (ADR-0022 stage 8e /// list contains nothing matching the prefix (ADR-0022 stage 8e
/// + the user's #5). /// + the user's #5).
@@ -680,6 +743,100 @@ mod tests {
// need a separate probe mechanism (deferred — same shape as // need a separate probe mechanism (deferred — same shape as
// the post-complete-parse gap for `--create-fk` etc.). // 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] #[test]
fn show_offers_data_and_table_alphabetised() { fn show_offers_data_and_table_alphabetised() {
let cs = cands("show ", 5); let cs = cands("show ", 5);
+1
View File
@@ -133,6 +133,7 @@ pub const KEYS_AND_PLACEHOLDERS: &[(&str, &[&str])] = &[
&["kind", "found"], &["kind", "found"],
), ),
("hint.ambient_typing_name", &[]), ("hint.ambient_typing_name", &[]),
("hint.value_literal_slot", &[]),
( (
"hint.ambient_typing_name_then", "hint.ambient_typing_name_then",
&["next"], &["next"],
+7
View File
@@ -282,6 +282,13 @@ hint:
# falls through to `ambient_typing_name` instead. # falls through to `ambient_typing_name` instead.
ambient_typing_name: "Type a name" ambient_typing_name: "Type a name"
ambient_typing_name_then: "Type a name, then {next}" ambient_typing_name_then: "Type a name, then {next}"
# Value-literal slot — `insert ... values (`, `update ... set
# col=`, `where col=`. Replaces the misleading "null true
# false" keyword candidate list with format guidance for all
# accepted literal forms. Schema-aware narrowing (showing only
# the relevant format for the column's type) waits on
# ADR-0023.
value_literal_slot: "Type a value: number, 'text', true/false, null (dates as 'YYYY-MM-DD', datetimes as 'YYYY-MM-DDTHH:MM:SS')"
parse: parse:
# Wrapper around chumsky's structural error message. The # Wrapper around chumsky's structural error message. The
+7
View File
@@ -188,6 +188,13 @@ pub fn ambient_hint(
selected: None, selected: None,
}); });
} }
// Value-literal slot at empty prefix: candidates_at_cursor
// returned None (it suppresses null/true/false here to avoid
// implying they're the only options). Surface a prose hint
// that names all valid literal forms with format examples.
if let Some(prose) = crate::completion::value_literal_hint_at_cursor(input, cursor) {
return Some(AmbientHint::Prose(prose));
}
// User typing into a NewName slot — show the friendlier // User typing into a NewName slot — show the friendlier
// "type a name" hint rather than the technical "next: …" // "type a name" hint rather than the technical "next: …"
// that the post-consumed-partial parse would otherwise // that the post-consumed-partial parse would otherwise