ADR-0022 stage 5/8: hint panel ambient typing assistance
ParseError::Invalid gains an `expected: Vec<String>` field —
the human-rendered names of the patterns chumsky was looking
for at the failure point (`\`create\``, `identifier`, etc.).
Empty for custom errors, which have no expected-set framing.
Populated by a new `describe_expected()` helper in parser.rs
that humanise() also delegates to (eliminates duplication).
`input_render::ambient_hint(input) -> Option<String>` returns
the hint-panel content per ADR-0022 §6:
- empty input → None (caller falls back to panel.hint_empty);
- Valid → t!("hint.ambient_complete") ("submit with Enter");
- IncompleteAtEof → t!("hint.ambient_expected", expected = …)
listing the parser's expected next tokens, oxford-joined;
- DefiniteErrorAt → t!("hint.ambient_error_with_usage", …)
composing the parse-error message with the matching
parse.usage.* template if a known entry keyword was
consumed, else the bare message.
Catalog gains the three hint.ambient_* keys + validator
declarations.
ui::render_hint_panel resolution order:
1. explicit app.hint (modal contexts) wins;
2. simple-mode + non-empty input → ambient_hint;
3. fallback to panel.hint_empty.
Advanced mode (persistent + one-shot `:`) bypasses ambient
hinting per ADR-0022 §12.
Snapshot: highlighted_input_all_token_classes rebaselined
because the hint panel now displays an ambient hint instead
of the empty placeholder when input is non-empty.
Tests: 698 passing, 0 failing, 1 ignored (693 baseline →
+5 ambient_hint cases). Clippy clean.
Stage 6 introduces the IdentSlot taxonomy + parser audit so
identifier-typed slots can yield schema-aware completion
candidates in stage 8.
This commit is contained in:
@@ -113,6 +113,73 @@ pub fn classify_input(input: &str) -> InputState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Ambient hint-panel content for the user's current input
|
||||
/// (ADR-0022 §6). Returns `None` for empty input — the caller
|
||||
/// then falls back to the existing `panel.hint_empty` content.
|
||||
///
|
||||
/// One of three sub-states:
|
||||
/// - **Valid** — `t!("hint.ambient_complete")` (e.g. "submit
|
||||
/// with Enter").
|
||||
/// - **IncompleteAtEof** — `t!("hint.ambient_expected", …)`
|
||||
/// listing what the parser was looking for next.
|
||||
/// - **DefiniteErrorAt** — `t!("hint.ambient_error_with_usage", …)`
|
||||
/// composing the parse-error message with the matching
|
||||
/// `parse.usage.*` template if a known command-entry
|
||||
/// keyword was consumed; falls back to the bare message
|
||||
/// otherwise.
|
||||
#[must_use]
|
||||
pub fn ambient_hint(input: &str) -> Option<String> {
|
||||
if input.trim().is_empty() {
|
||||
return None;
|
||||
}
|
||||
match parse_command(input) {
|
||||
Ok(_) => Some(crate::t!("hint.ambient_complete")),
|
||||
Err(ParseError::Empty) => None,
|
||||
Err(ParseError::Invalid {
|
||||
message,
|
||||
position,
|
||||
at_eof,
|
||||
expected,
|
||||
}) => {
|
||||
if at_eof {
|
||||
if expected.is_empty() {
|
||||
Some(message)
|
||||
} else {
|
||||
let joined = oxford_or(&expected);
|
||||
Some(crate::t!("hint.ambient_expected", expected = joined))
|
||||
}
|
||||
} else {
|
||||
let tokens = lex(input);
|
||||
let usage = crate::dsl::usage::matched_entry(&tokens, position)
|
||||
.and_then(|(_, keys)| keys.first().copied())
|
||||
.map(|key| crate::friendly::translate(key, &[]));
|
||||
match usage {
|
||||
Some(u) => Some(crate::t!(
|
||||
"hint.ambient_error_with_usage",
|
||||
message = message,
|
||||
usage = u,
|
||||
)),
|
||||
None => Some(message),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// "A, B, or C" / "A or B" / "A". Local copy because the
|
||||
/// parser's identical helper is private.
|
||||
fn oxford_or(items: &[String]) -> String {
|
||||
match items {
|
||||
[] => String::new(),
|
||||
[a] => a.clone(),
|
||||
[a, b] => format!("{a} or {b}"),
|
||||
rest => {
|
||||
let (last, head) = rest.split_last().expect("len >= 3");
|
||||
format!("{}, or {last}", head.join(", "))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn overlay_error(runs: &mut [StyledRun], error_byte: usize, theme: &Theme) {
|
||||
// Failing tokens have their byte_range starting exactly at
|
||||
// `error_byte`. Override the fg colour while preserving any
|
||||
@@ -323,6 +390,59 @@ mod tests {
|
||||
assert!(reversed(last));
|
||||
}
|
||||
|
||||
// ---- ambient_hint (stage 5) ----
|
||||
|
||||
#[test]
|
||||
fn ambient_hint_is_none_for_empty_input() {
|
||||
assert_eq!(ambient_hint(""), None);
|
||||
assert_eq!(ambient_hint(" "), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ambient_hint_for_valid_input_invites_submit() {
|
||||
let h = ambient_hint("create table T with pk").expect("some hint");
|
||||
assert!(h.contains("Enter"), "got {h:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ambient_hint_for_partial_keyword_lists_expected_set() {
|
||||
let h = ambient_hint("show").expect("some hint");
|
||||
assert!(h.starts_with("expected:"), "got {h:?}");
|
||||
assert!(h.contains("`data`"), "got {h:?}");
|
||||
assert!(h.contains("`table`"), "got {h:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ambient_hint_for_definite_error_includes_usage_template() {
|
||||
let h = ambient_hint("insert into T extra").expect("some hint");
|
||||
// Definite error after `insert into T` (parser expected
|
||||
// values clause / column list / values keyword).
|
||||
// Composes message with insert usage template.
|
||||
assert!(
|
||||
h.contains("usage:"),
|
||||
"definite-error hint should include usage template, got {h:?}",
|
||||
);
|
||||
assert!(
|
||||
h.contains("insert into <Table>"),
|
||||
"should reference the insert usage template, got {h:?}",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ambient_hint_for_unknown_command_falls_back_to_message() {
|
||||
// No consumed entry keyword → no usage template; the
|
||||
// message itself is shown.
|
||||
let h = ambient_hint("frobulate widgets").expect("some hint");
|
||||
assert!(
|
||||
!h.contains("usage:"),
|
||||
"no entry keyword consumed → no usage template; got {h:?}",
|
||||
);
|
||||
assert!(
|
||||
h.contains("frobulate"),
|
||||
"message should mention the unknown word; got {h:?}",
|
||||
);
|
||||
}
|
||||
|
||||
// ---- classify_input + error overlay (stage 4) ----
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user