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:
+46
-26
@@ -45,6 +45,15 @@ pub enum ParseError {
|
||||
/// fires on submit. A future refinement may carry an
|
||||
/// explicit `is_definite` tag through custom errors.
|
||||
at_eof: bool,
|
||||
/// Human-rendered names of patterns the parser was
|
||||
/// looking for at the failure point: `\`create\``,
|
||||
/// `identifier`, etc. Same forms `humanise()` uses
|
||||
/// inside the `message` sentence, but as discrete
|
||||
/// items so callers (the hint panel, ADR-0022 §6)
|
||||
/// can render them in their own framing. Empty for
|
||||
/// custom errors (which have no expected-set
|
||||
/// framing).
|
||||
expected: Vec<String>,
|
||||
},
|
||||
#[error("empty input")]
|
||||
Empty,
|
||||
@@ -132,6 +141,7 @@ fn try_parse_replay_with_bare_path(
|
||||
message: "expected a path after `replay`".to_string(),
|
||||
position: after_replay,
|
||||
at_eof: true,
|
||||
expected: vec!["path".to_string()],
|
||||
}));
|
||||
}
|
||||
Some(Ok(Command::Replay {
|
||||
@@ -686,23 +696,53 @@ fn into_parse_error(errs: &[Rich<'_, Token>], tokens: &[Token], source: &str) ->
|
||||
let chumsky_span = chosen.span();
|
||||
let position = source_position_at(tokens, chumsky_span.start, source);
|
||||
let message = humanise(chosen, tokens, source);
|
||||
let at_eof = match chosen.reason() {
|
||||
let (at_eof, expected) = match chosen.reason() {
|
||||
// Structural failures know whether they ran out of
|
||||
// input — `found = None` ⇔ EOF.
|
||||
RichReason::ExpectedFound { found, .. } => found.is_none(),
|
||||
// input — `found = None` ⇔ EOF — and carry the
|
||||
// expected-pattern set chumsky was looking for.
|
||||
RichReason::ExpectedFound { expected, found } => {
|
||||
(found.is_none(), describe_expected(expected))
|
||||
}
|
||||
// Custom errors: see the docstring on
|
||||
// `ParseError::Invalid::at_eof` for why we err on the
|
||||
// side of `true` (no live overlay; on-submit error
|
||||
// still fires).
|
||||
RichReason::Custom(_) => true,
|
||||
// still fires). Custom errors have no expected-set.
|
||||
RichReason::Custom(_) => (true, Vec::new()),
|
||||
};
|
||||
ParseError::Invalid {
|
||||
message,
|
||||
position,
|
||||
at_eof,
|
||||
expected,
|
||||
}
|
||||
}
|
||||
|
||||
/// Render a chumsky expected-pattern set into the same
|
||||
/// human-readable forms `humanise()` uses, but as discrete
|
||||
/// items rather than an oxford-joined string. Stable order
|
||||
/// (sorted, deduplicated) so callers don't have to.
|
||||
fn describe_expected(expected: &[RichPattern<'_, Token>]) -> Vec<String> {
|
||||
let has_concrete = expected.iter().any(|p| {
|
||||
matches!(
|
||||
p,
|
||||
RichPattern::Token(_)
|
||||
| RichPattern::Identifier(_)
|
||||
| RichPattern::Label(_)
|
||||
| RichPattern::EndOfInput
|
||||
)
|
||||
});
|
||||
let mut items: Vec<String> = expected
|
||||
.iter()
|
||||
.filter(|p| {
|
||||
!(has_concrete && matches!(p, RichPattern::Any | RichPattern::SomethingElse))
|
||||
})
|
||||
.map(describe_pattern)
|
||||
.collect();
|
||||
items.sort();
|
||||
items.dedup();
|
||||
items
|
||||
}
|
||||
|
||||
/// Translate a chumsky token-slice index into a byte position
|
||||
/// in the original source. If the index points past the last
|
||||
/// token (an end-of-input failure), use the last token's end
|
||||
@@ -728,27 +768,7 @@ fn humanise(err: &Rich<'_, Token>, tokens: &[Token], source: &str) -> String {
|
||||
|maybe_ref| describe_token(maybe_ref),
|
||||
);
|
||||
|
||||
// If the expected set contains concrete patterns (token,
|
||||
// identifier, label), drop the generic Any/SomethingElse
|
||||
// wildcards — they add noise, not information.
|
||||
let has_concrete = expected.iter().any(|p| {
|
||||
matches!(
|
||||
p,
|
||||
RichPattern::Token(_)
|
||||
| RichPattern::Identifier(_)
|
||||
| RichPattern::Label(_)
|
||||
| RichPattern::EndOfInput
|
||||
)
|
||||
});
|
||||
let mut described: Vec<String> = expected
|
||||
.iter()
|
||||
.filter(|p| {
|
||||
!(has_concrete && matches!(p, RichPattern::Any | RichPattern::SomethingElse))
|
||||
})
|
||||
.map(describe_pattern)
|
||||
.collect();
|
||||
described.sort();
|
||||
described.dedup();
|
||||
let described = describe_expected(expected);
|
||||
let expected_str = oxford_or(&described);
|
||||
|
||||
let chumsky_span_start = err.span().start;
|
||||
|
||||
Reference in New Issue
Block a user