extend eval and highlight rule permissivity

This commit is contained in:
jess 2026-05-30 23:04:26 -07:00
parent 48a871186b
commit 7112564d9d
8 changed files with 94 additions and 7 deletions

View File

@ -122,6 +122,17 @@ fn is_ident(s: &str) -> bool {
} }
pub fn classify_document(text: &str) -> Vec<ClassifiedLine> { pub fn classify_document(text: &str) -> Vec<ClassifiedLine> {
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<bool>,
) -> Vec<ClassifiedLine> {
let mut result = Vec::new(); let mut result = Vec::new();
let mut comment_depth: usize = 0; let mut comment_depth: usize = 0;
let mut brace_depth: i32 = 0; let mut brace_depth: i32 = 0;
@ -140,7 +151,16 @@ pub fn classify_document(text: &str) -> Vec<ClassifiedLine> {
if brace_depth < 0 { brace_depth = 0; } if brace_depth < 0 { brace_depth = 0; }
result.push(ClassifiedLine { index: i, kind: LineKind::Cordial, content: line.to_string() }); result.push(ClassifiedLine { index: i, kind: LineKind::Cordial, content: line.to_string() });
} else { } 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 { if cl.kind == LineKind::Cordial {
let trimmed = line.trim(); let trimmed = line.trim();
let opens = trimmed.matches('{').count() as i32; let opens = trimmed.matches('{').count() as i32;

View File

@ -240,7 +240,10 @@ pub fn evaluate_modules(sources: &[ModuleSource]) -> Vec<ModuleResult> {
/// evaluates a document's text using a pre-populated interpreter. /// evaluates a document's text using a pre-populated interpreter.
pub fn evaluate_document_with_interp(interp: &mut interp::Interpreter, text: &str) -> DocumentResult { 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 results = Vec::new();
let mut errors = Vec::new(); let mut errors = Vec::new();

View File

@ -39,6 +39,11 @@ pub trait InterpreterHook {
-> Option<Result<Value, String>> { None } -> Option<Result<Value, String>> { None }
fn extern_unop(&self, _i: &mut Interpreter, _op: Op, _operand: &Value, _depth: u32) fn extern_unop(&self, _i: &mut Interpreter, _op: Op, _operand: &Value, _depth: u32)
-> Option<Result<Value, String>> { None } -> Option<Result<Value, String>> { None }
/// line-classification override: None defers, true = Cordial, false = Markdown.
fn classify_line(&self, _trimmed: &str) -> Option<bool> { None }
/// names the binding on a custom assignment line.
fn eval_binding_name(&self, _trimmed: &str) -> Option<String> { None }
} }
pub(crate) struct HookList { pub(crate) struct HookList {

View File

@ -158,6 +158,22 @@ impl Interpreter {
.collect() .collect()
} }
/// first hook with a line-classification verdict.
pub fn hook_classify_line(&self, trimmed: &str) -> Option<bool> {
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<String> {
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. /// hook-aware Value rendering for inline/table/tree formats.
pub fn display_value(&self, v: &Value, fmt: DisplayFormat) -> String { pub fn display_value(&self, v: &Value, fmt: DisplayFormat) -> String {
if let Value::Extern(h) = v { if let Value::Extern(h) = v {

View File

@ -571,15 +571,38 @@ impl EditorState {
weight: iced_wgpu::core::font::Weight::Bold, weight: iced_wgpu::core::font::Weight::Bold,
..syntax::EDITOR_FONT ..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<iced_wgpu::core::text::Span<'a, ()>> = 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![ let row = iced_widget::row![
iced_widget::text("") iced_widget::text("")
.font(syntax::EDITOR_FONT) .font(syntax::EDITOR_FONT)
.size(self.font_size) .size(self.font_size)
.color(arrow_color), .color(arrow_color),
iced_widget::text(value) value_mid,
.font(bold)
.size(self.font_size)
.color(value_color),
iced_widget::text("") iced_widget::text("")
.font(syntax::EDITOR_FONT) .font(syntax::EDITOR_FONT)
.size(self.font_size) .size(self.font_size)

View File

@ -111,6 +111,8 @@ pub struct EditorState {
pub(super) cached_minimap_lines: Vec<crate::minimap::MinimapLine>, pub(super) cached_minimap_lines: Vec<crate::minimap::MinimapLine>,
/// custom keyword/builtin/type table layered on top of Cordial. /// custom keyword/builtin/type table layered on top of Cordial.
pub syntax_rules: crate::syntax::SyntaxRules, pub syntax_rules: crate::syntax::SyntaxRules,
/// optional rules for highlighting inline eval output.
pub(super) eval_highlight: Option<crate::syntax::SyntaxRules>,
/// tree-sitter rebuild gate, bumped on idle. /// tree-sitter rebuild gate, bumped on idle.
pub(super) heavy_token: u64, pub(super) heavy_token: u64,
/// deferred-rebuild arm; set on edit, cleared on debounce fire. /// deferred-rebuild arm; set on edit, cleared on debounce fire.
@ -187,9 +189,15 @@ impl EditorState {
heavy_token: 0, heavy_token: 0,
heavy_pending: false, heavy_pending: false,
interp_setup: None, interp_setup: None,
eval_highlight: None,
} }
} }
/// sets the rules for highlighting inline eval output.
pub fn set_eval_highlight(&mut self, rules: Option<crate::syntax::SyntaxRules>) {
self.eval_highlight = rules;
}
/// installs a routine run on each fresh eval interpreter — register hooks, /// installs a routine run on each fresh eval interpreter — register hooks,
/// provide shared state, seed vars, add module paths. /// provide shared state, seed vars, add module paths.
pub fn set_interpreter_setup<F>(&mut self, setup: F) pub fn set_interpreter_setup<F>(&mut self, setup: F)

View File

@ -458,7 +458,10 @@ impl super::EditorState {
let line_idx = cursor.position.line; let line_idx = cursor.position.line;
if line_idx < lines.len() { if line_idx < lines.len() {
let line = lines[line_idx].trim(); 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}"); let insert = format!("\n/= {varname}");
self.content_mut().perform(text_widget::Action::Move(Motion::End)); self.content_mut().perform(text_widget::Action::Move(Motion::End));
self.content_mut().perform(text_widget::Action::Edit( self.content_mut().perform(text_widget::Action::Edit(

View File

@ -512,6 +512,15 @@ fn extract_paren_idents(s: &str, map: &mut HashMap<String, u8>, 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<usize>, u8)> {
let idents: HashMap<String, u8> = HashMap::new();
highlight_cordial(line, &idents, rules)
.into_iter()
.map(|(r, SyntaxHighlight { kind })| (r, kind))
.collect()
}
fn highlight_cordial(line: &str, user_idents: &HashMap<String, u8>, rules: &SyntaxRules) -> Vec<(Range<usize>, SyntaxHighlight)> { fn highlight_cordial(line: &str, user_idents: &HashMap<String, u8>, rules: &SyntaxRules) -> Vec<(Range<usize>, SyntaxHighlight)> {
let bytes = line.as_bytes(); let bytes = line.as_bytes();
let len = bytes.len(); let len = bytes.len();