diff --git a/src/dsl/mod.rs b/src/dsl/mod.rs index 83708bd..20c03d3 100644 --- a/src/dsl/mod.rs +++ b/src/dsl/mod.rs @@ -26,3 +26,31 @@ pub use command::{ pub use parser::{ParseError, parse_command}; pub use types::Type; pub use value::Value; + +/// Prefix every echoed DSL command carries in the output +/// panel — i.e. `[simple] running: ` reads as +/// `[simple]` (tag) + `running: ` (this constant) + the +/// user's input. +/// +/// The catalog template `dsl.running` is contracted to equal +/// `format!("{ECHO_PREFIX}{{input}}")`. The constant lives +/// in code because the echo-line renderer (ADR-0022 §5) +/// peels this prefix off and re-tokenises the rest for +/// highlighting; a unit test (`echo_prefix_matches_catalog_template`) +/// pins the binding. +pub const ECHO_PREFIX: &str = "running: "; + +#[cfg(test)] +mod tests { + use super::ECHO_PREFIX; + + #[test] + fn echo_prefix_matches_catalog_template() { + // The catalog template `dsl.running` must produce + // `` so the echo-line renderer can + // peel the prefix and re-tokenise the rest. A + // translator changing the prefix breaks this test. + let rendered = crate::t!("dsl.running", input = ""); + assert_eq!(rendered, ECHO_PREFIX); + } +} diff --git a/src/input_render.rs b/src/input_render.rs index 5825b71..d950ec4 100644 --- a/src/input_render.rs +++ b/src/input_render.rs @@ -55,11 +55,20 @@ impl StyledRun { /// the cursor at `cursor_byte` (clamped to `input.len()`). #[must_use] pub fn render_input_runs(input: &str, cursor_byte: usize, theme: &Theme) -> Vec { - let mut runs = base_runs(input, theme); + let mut runs = lex_to_runs(input, theme); inject_cursor(&mut runs, input, cursor_byte, theme); runs } +/// Cursor-less variant: tokenises `input` into styled runs +/// covering the full byte range, with no inverted cursor. +/// Used by the echo-line renderer (ADR-0022 §5) where there's +/// no cursor to show. +#[must_use] +pub fn lex_to_runs(input: &str, theme: &Theme) -> Vec { + base_runs(input, theme) +} + fn base_runs(input: &str, theme: &Theme) -> Vec { let tokens = lex(input); let mut runs = Vec::with_capacity(tokens.len() * 2); diff --git a/src/ui.rs b/src/ui.rs index d705643..feab6f9 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -530,16 +530,43 @@ fn render_output_line<'a>(line: &'a OutputLine, theme: &Theme) -> Line<'a> { Mode::Simple => Style::default().fg(theme.mode_simple), Mode::Advanced => Style::default().fg(theme.mode_advanced), }; - let body_style = match line.kind { - OutputKind::Echo => Style::default().fg(theme.fg), - OutputKind::System => Style::default().fg(theme.system), - OutputKind::Error => Style::default().fg(theme.error), - }; let tag = match line.kind { OutputKind::Echo => format!("[{}] ", line.mode_at_submission.label().to_lowercase()), OutputKind::System => "[system] ".to_string(), OutputKind::Error => "[error] ".to_string(), }; + + // Simple-mode echo lines get token-class highlighting on + // their input portion (ADR-0022 §5). Echo body shape is + // contracted to ``; the prefix is + // pinned to the catalog template by + // `dsl::tests::echo_prefix_matches_catalog_template`. + if line.kind == OutputKind::Echo + && line.mode_at_submission == Mode::Simple + && let Some(rest) = line.text.strip_prefix(crate::dsl::ECHO_PREFIX) + { + let mut spans: Vec> = Vec::with_capacity(2 + rest.len() / 4); + spans.push(Span::styled(tag, tag_style)); + spans.push(Span::styled( + crate::dsl::ECHO_PREFIX, + Style::default().fg(theme.fg), + )); + for run in crate::input_render::lex_to_runs(rest, theme) { + spans.push(Span::styled( + &rest[run.byte_range.0..run.byte_range.1], + run.style, + )); + } + return Line::from(spans); + } + // Echo body without the expected prefix, or any non-echo + // line, falls through to the plain rendering below. + + let body_style = match line.kind { + OutputKind::Echo => Style::default().fg(theme.fg), + OutputKind::System => Style::default().fg(theme.system), + OutputKind::Error => Style::default().fg(theme.error), + }; Line::from(vec![ Span::styled(tag, tag_style), Span::styled(line.text.as_str(), body_style),