ADR-0022 stage 4/8: render-time parse + error overlay
Add `classify_input(&str) -> InputState` that returns one of
{Empty, Valid, IncompleteAtEof, DefiniteErrorAt(byte)}.
The renderer uses this to overlay tok_error on the failing
token of mid-typed input that can never be valid.
ParseError::Invalid gains an `at_eof: bool` field populated
by `into_parse_error`:
- structural failures: at_eof = found.is_none()
(chumsky's own "ran out of input" discriminator);
- custom errors from try_map: at_eof = true,
conservatively.
The conservative custom-error classification is a deliberate
under-highlighting bias. It means three classes of error
currently DO NOT get a live red overlay (only on submit):
- "tables need at least one column" (correct: this is
genuinely an incomplete state — adding `with pk ...` fixes it);
- "unknown type 'varchar'" (sub-optimal: should overlay);
- "--force-conversion and --dont-convert are mutually
exclusive" (sub-optimal: should overlay).
The trade-off is documented inline on the at_eof field. A
future refinement could carry an explicit definite/incomplete
tag through Custom errors (would change RichReason::Custom's
payload from String to a typed value).
render_input_runs now applies the overlay on the failing
token's run before injecting the cursor. Tokens after the
error keep their lex-class colour — fixes one thing at a
time per ADR-0022 §4. Lex errors continue to render in
tok_error from stage 2.
Pattern-matches on ParseError::Invalid throughout the
codebase use `..` and are unaffected; only the two
constructions in parser.rs needed updating.
Tests: 693 passing, 0 failing, 1 ignored (683 baseline →
+10: 7 classify + overlay tests, +1 adapted full-command
test, +2 valid-vs-incomplete coverage). Clippy clean.
Stage 5 lights up the hint panel as the verbose-feedback
surface — needs the InputState classifier from this stage.
This commit is contained in:
+45
-2
@@ -25,7 +25,27 @@ use crate::dsl::value::Value;
|
||||
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
|
||||
pub enum ParseError {
|
||||
#[error("could not parse command: {message}")]
|
||||
Invalid { message: String, position: usize },
|
||||
Invalid {
|
||||
message: String,
|
||||
position: usize,
|
||||
/// True when the parse failed because more input was
|
||||
/// expected — i.e. a structural failure with no
|
||||
/// next-token to point at. Used by the input renderer
|
||||
/// (ADR-0022 §4) to distinguish "incomplete but
|
||||
/// plausible" from "definite error" mid-typing.
|
||||
///
|
||||
/// Custom errors raised by `try_map` are conservatively
|
||||
/// classified as `at_eof = true` because we cannot, at
|
||||
/// this layer, tell apart "tables need at least one
|
||||
/// column" (incomplete: more input would help) from
|
||||
/// "--force-conversion and --dont-convert are mutually
|
||||
/// exclusive" (definite: user must remove a token).
|
||||
/// Erring on `true` means custom-error inputs do not
|
||||
/// get a live error overlay; the parse error still
|
||||
/// fires on submit. A future refinement may carry an
|
||||
/// explicit `is_definite` tag through custom errors.
|
||||
at_eof: bool,
|
||||
},
|
||||
#[error("empty input")]
|
||||
Empty,
|
||||
}
|
||||
@@ -38,6 +58,14 @@ impl ParseError {
|
||||
Self::Empty => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn at_eof(&self) -> bool {
|
||||
match self {
|
||||
Self::Invalid { at_eof, .. } => *at_eof,
|
||||
Self::Empty => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a single DSL command end-to-end.
|
||||
@@ -103,6 +131,7 @@ fn try_parse_replay_with_bare_path(
|
||||
return Some(Err(ParseError::Invalid {
|
||||
message: "expected a path after `replay`".to_string(),
|
||||
position: after_replay,
|
||||
at_eof: true,
|
||||
}));
|
||||
}
|
||||
Some(Ok(Command::Replay {
|
||||
@@ -657,7 +686,21 @@ 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);
|
||||
ParseError::Invalid { message, position }
|
||||
let at_eof = match chosen.reason() {
|
||||
// Structural failures know whether they ran out of
|
||||
// input — `found = None` ⇔ EOF.
|
||||
RichReason::ExpectedFound { found, .. } => found.is_none(),
|
||||
// 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,
|
||||
};
|
||||
ParseError::Invalid {
|
||||
message,
|
||||
position,
|
||||
at_eof,
|
||||
}
|
||||
}
|
||||
|
||||
/// Translate a chumsky token-slice index into a byte position
|
||||
|
||||
Reference in New Issue
Block a user