diff --git a/core/src/doc.rs b/core/src/doc.rs index c214c4b..068768a 100644 --- a/core/src/doc.rs +++ b/core/src/doc.rs @@ -122,6 +122,17 @@ fn is_ident(s: &str) -> bool { } pub fn classify_document(text: &str) -> Vec { + classify_document_with(text, |_| None) +} + +/// classifies a document, letting a caller override the Cordial/Markdown verdict per line. +/// the override feeds the same decision point as the built-in classifier, so brace tracking +/// engages for overridden Cordial openers and multi-line blocks follow. the `/=` eval sigil +/// is never overridable. +pub fn classify_document_with( + text: &str, + classify_override: impl Fn(&str) -> Option, +) -> Vec { let mut result = Vec::new(); let mut comment_depth: usize = 0; let mut brace_depth: i32 = 0; @@ -140,7 +151,16 @@ pub fn classify_document(text: &str) -> Vec { if brace_depth < 0 { brace_depth = 0; } result.push(ClassifiedLine { index: i, kind: LineKind::Cordial, content: line.to_string() }); } else { - let cl = classify_line(i, line); + let base = classify_line(i, line); + let cl = if base.kind == LineKind::Eval { + base + } else { + match classify_override(line.trim()) { + Some(true) => ClassifiedLine { index: i, kind: LineKind::Cordial, content: line.to_string() }, + Some(false) => ClassifiedLine { index: i, kind: LineKind::Markdown, content: line.to_string() }, + None => base, + } + }; if cl.kind == LineKind::Cordial { let trimmed = line.trim(); let opens = trimmed.matches('{').count() as i32; diff --git a/core/src/eval.rs b/core/src/eval.rs index b29d815..d0dd52a 100644 --- a/core/src/eval.rs +++ b/core/src/eval.rs @@ -240,7 +240,10 @@ pub fn evaluate_modules(sources: &[ModuleSource]) -> Vec { /// evaluates a document's text using a pre-populated interpreter. pub fn evaluate_document_with_interp(interp: &mut interp::Interpreter, text: &str) -> DocumentResult { - let classified = classify_document(text); + let classified = { + let interp_ref: &interp::Interpreter = interp; + crate::doc::classify_document_with(text, |line| interp_ref.hook_classify_line(line)) + }; let mut results = Vec::new(); let mut errors = Vec::new(); diff --git a/core/src/interp/hooks.rs b/core/src/interp/hooks.rs index d2a8aac..9b963a0 100644 --- a/core/src/interp/hooks.rs +++ b/core/src/interp/hooks.rs @@ -39,6 +39,11 @@ pub trait InterpreterHook { -> Option> { None } fn extern_unop(&self, _i: &mut Interpreter, _op: Op, _operand: &Value, _depth: u32) -> Option> { None } + + /// line-classification override: None defers, true = Cordial, false = Markdown. + fn classify_line(&self, _trimmed: &str) -> Option { None } + /// names the binding on a custom assignment line. + fn eval_binding_name(&self, _trimmed: &str) -> Option { None } } pub(crate) struct HookList { diff --git a/core/src/interp/mod.rs b/core/src/interp/mod.rs index d62eab6..c72f873 100644 --- a/core/src/interp/mod.rs +++ b/core/src/interp/mod.rs @@ -158,6 +158,22 @@ impl Interpreter { .collect() } + /// first hook with a line-classification verdict. + pub fn hook_classify_line(&self, trimmed: &str) -> Option { + for h in &self.hooks.hooks { + if let Some(v) = h.classify_line(trimmed) { return Some(v); } + } + None + } + + /// first hook to name a custom binding line. + pub fn hook_eval_binding_name(&self, trimmed: &str) -> Option { + for h in &self.hooks.hooks { + if let Some(v) = h.eval_binding_name(trimmed) { return Some(v); } + } + None + } + /// hook-aware Value rendering for inline/table/tree formats. pub fn display_value(&self, v: &Value, fmt: DisplayFormat) -> String { if let Value::Extern(h) = v { diff --git a/viewport/src/editor/mod.rs b/viewport/src/editor/mod.rs index e3f7d71..817e116 100644 --- a/viewport/src/editor/mod.rs +++ b/viewport/src/editor/mod.rs @@ -571,15 +571,38 @@ impl EditorState { weight: iced_wgpu::core::font::Weight::Bold, ..syntax::EDITOR_FONT }; + let value_mid: Element<'a, Message, Theme, iced_wgpu::Renderer> = match &self.eval_highlight { + Some(rules) => { + let spans = syntax::highlight_eval_spans(&value, rules); + let mut rich: Vec> = Vec::new(); + let mut cursor = 0usize; + for (range, kind) in &spans { + if range.start > cursor { + rich.push(iced_widget::span(value[cursor..range.start].to_string()).color(value_color)); + } + let mut s = iced_widget::span(value[range.start..range.end].to_string()) + .color(syntax::highlight_color(*kind)); + if let Some(f) = syntax::highlight_font(*kind) { s = s.font(f); } + rich.push(s); + cursor = range.end; + } + if cursor < value.len() { + rich.push(iced_widget::span(value[cursor..].to_string()).color(value_color)); + } + iced_widget::text::Rich::with_spans(rich).size(self.font_size).into() + } + None => iced_widget::text(value) + .font(bold) + .size(self.font_size) + .color(value_color) + .into(), + }; let row = iced_widget::row![ iced_widget::text("→ ") .font(syntax::EDITOR_FONT) .size(self.font_size) .color(arrow_color), - iced_widget::text(value) - .font(bold) - .size(self.font_size) - .color(value_color), + value_mid, iced_widget::text(" ←") .font(syntax::EDITOR_FONT) .size(self.font_size) diff --git a/viewport/src/editor/state.rs b/viewport/src/editor/state.rs index 1c3ac78..4e06143 100644 --- a/viewport/src/editor/state.rs +++ b/viewport/src/editor/state.rs @@ -111,6 +111,8 @@ pub struct EditorState { pub(super) cached_minimap_lines: Vec, /// custom keyword/builtin/type table layered on top of Cordial. pub syntax_rules: crate::syntax::SyntaxRules, + /// optional rules for highlighting inline eval output. + pub(super) eval_highlight: Option, /// tree-sitter rebuild gate, bumped on idle. pub(super) heavy_token: u64, /// deferred-rebuild arm; set on edit, cleared on debounce fire. @@ -187,9 +189,15 @@ impl EditorState { heavy_token: 0, heavy_pending: false, interp_setup: None, + eval_highlight: None, } } + /// sets the rules for highlighting inline eval output. + pub fn set_eval_highlight(&mut self, rules: Option) { + self.eval_highlight = rules; + } + /// installs a routine run on each fresh eval interpreter — register hooks, /// provide shared state, seed vars, add module paths. pub fn set_interpreter_setup(&mut self, setup: F) diff --git a/viewport/src/editor/update.rs b/viewport/src/editor/update.rs index 39423a0..f9e08a3 100644 --- a/viewport/src/editor/update.rs +++ b/viewport/src/editor/update.rs @@ -458,7 +458,10 @@ impl super::EditorState { let line_idx = cursor.position.line; if line_idx < lines.len() { let line = lines[line_idx].trim(); - if let Some(varname) = parse_let_binding(line) { + let varname = self.new_eval_interpreter() + .hook_eval_binding_name(line) + .or_else(|| parse_let_binding(line)); + if let Some(varname) = varname { let insert = format!("\n/= {varname}"); self.content_mut().perform(text_widget::Action::Move(Motion::End)); self.content_mut().perform(text_widget::Action::Edit( diff --git a/viewport/src/syntax.rs b/viewport/src/syntax.rs index b401903..3739919 100644 --- a/viewport/src/syntax.rs +++ b/viewport/src/syntax.rs @@ -512,6 +512,15 @@ fn extract_paren_idents(s: &str, map: &mut HashMap, slot: &mut u32) } } +/// highlights a single eval-output line with the given rules into (range, kind) spans. +pub fn highlight_eval_spans(line: &str, rules: &SyntaxRules) -> Vec<(Range, u8)> { + let idents: HashMap = HashMap::new(); + highlight_cordial(line, &idents, rules) + .into_iter() + .map(|(r, SyntaxHighlight { kind })| (r, kind)) + .collect() +} + fn highlight_cordial(line: &str, user_idents: &HashMap, rules: &SyntaxRules) -> Vec<(Range, SyntaxHighlight)> { let bytes = line.as_bytes(); let len = bytes.len();