Syntax highlighting bug fix for external.

Also timing for tree-sitter checks
This commit is contained in:
jess 2026-05-28 02:54:31 -07:00
parent 9b5dbbdf2b
commit 24958d4896
10 changed files with 228 additions and 79 deletions

View File

@ -2007,18 +2007,30 @@ impl Interpreter {
Value::Array(a) => a, Value::Array(a) => a,
_ => return Err("for loop requires an array or range".into()), _ => return Err("for loop requires an array or range".into()),
}; };
let prev = self.vars.remove(var);
let prev_type = self.var_types.remove(var);
let mut iterations = 0; let mut iterations = 0;
let mut last = Value::Void; let mut last = Value::Void;
let mut loop_err: Option<String> = None;
for item in &items { for item in &items {
iterations += 1; iterations += 1;
if iterations > MAX_ITERATIONS { if iterations > MAX_ITERATIONS {
return Err(format!("loop exceeded {} iterations", MAX_ITERATIONS)); loop_err = Some(format!("loop exceeded {} iterations", MAX_ITERATIONS));
break;
} }
self.vars.insert(var.clone(), item.clone()); self.vars.insert(var.clone(), item.clone());
for s in body { for s in body {
last = self.exec_stmt(s, depth)?; match self.exec_stmt(s, depth) {
Ok(v) => last = v,
Err(e) => { loop_err = Some(e); break; }
}
} }
if loop_err.is_some() { break; }
} }
self.vars.remove(var);
if let Some(v) = prev { self.vars.insert(var.clone(), v); }
if let Some(t) = prev_type { self.var_types.insert(var.clone(), t); }
if let Some(e) = loop_err { return Err(e); }
Ok(last) Ok(last)
} }
Stmt::FnDef { name, params, return_type, body } => { Stmt::FnDef { name, params, return_type, body } => {
@ -2213,17 +2225,28 @@ impl Interpreter {
} }
_ => None, _ => None,
}; };
let type_tag = type_tag.ok_or_else(|| // typed receiver → look up method on the type
format!("cannot call .{}() — receiver has no __type", method) if let Some(tag) = type_tag {
)?; if let Some(fndef) = self.methods.get(&(tag.clone(), method.clone())).cloned() {
let fndef = self.methods.get(&(type_tag.clone(), method.clone())) let mut eval_args = vec![recv_val];
.cloned() for a in args {
.ok_or_else(|| format!("no method '{}' on type '{}'", method, type_tag))?; eval_args.push(self.eval_expr(a, depth)?);
let mut eval_args = vec![recv_val]; }
for a in args { return self.call_fndef(&fndef, &eval_args, depth);
eval_args.push(self.eval_expr(a, depth)?); }
return Err(format!("no method '{}' on type '{}'", method, tag));
} }
self.call_fndef(&fndef, &eval_args, depth) // untyped receiver → desugar to method(receiver, args...)
let mut call_args: Vec<Expr> = Vec::with_capacity(args.len() + 1);
let placeholder = format!("__method_recv_{}", depth);
self.vars.insert(placeholder.clone(), recv_val);
call_args.push(Expr::Ident(placeholder.clone()));
for a in args {
call_args.push(a.clone());
}
let result = self.eval_call(method, &call_args, depth);
self.vars.remove(&placeholder);
result
} }
Expr::StaticCall(type_name_str, method, args) => { Expr::StaticCall(type_name_str, method, args) => {
let fndef = self.methods.get(&(type_name_str.clone(), method.clone())) let fndef = self.methods.get(&(type_name_str.clone(), method.clone()))
@ -3430,25 +3453,33 @@ fn builtin_constant(name: &str) -> Option<Value> {
} }
fn value_is_kind(v: &Value, kind: &str) -> bool { fn value_is_kind(v: &Value, kind: &str) -> bool {
match (kind, v) { match (canonical_type(kind), v) {
("int", Value::Number(n)) => *n == n.trunc() && n.is_finite(), ("int", Value::Number(n)) => *n == n.trunc() && n.is_finite(),
("float", Value::Number(_)) => true, ("float", Value::Number(_)) => true,
("number", Value::Number(_)) => true, ("number", Value::Number(_)) => true,
("bool", Value::Bool(_)) => true, ("bool", Value::Bool(_)) => true,
("str", Value::Str(_)) => true, ("str", Value::Str(_)) => true,
("array", Value::Array(_)) => true, ("array", Value::Array(_)) => true,
("struct", Value::Struct(_)) => true,
("ring", Value::Ring(_)) => true,
("void", Value::Void) => true,
_ => false, _ => false,
} }
} }
fn try_cast(v: &Value, target: &str) -> Option<Value> { fn try_cast(v: &Value, target: &str) -> Option<Value> {
match (target, v) { match (canonical_type(target), v) {
("int", Value::Number(n)) if *n == n.trunc() && n.is_finite() => { ("int", Value::Number(n)) if *n == n.trunc() && n.is_finite() => {
Some(Value::Number(*n)) Some(Value::Number(*n))
} }
("float", Value::Number(_)) => Some(v.clone()), ("float", Value::Number(_)) => Some(v.clone()),
("number", Value::Number(_)) => Some(v.clone()),
("bool", Value::Bool(_)) => Some(v.clone()), ("bool", Value::Bool(_)) => Some(v.clone()),
("str", Value::Str(_)) => Some(v.clone()), ("str", Value::Str(_)) => Some(v.clone()),
("array", Value::Array(_)) => Some(v.clone()),
("struct", Value::Struct(_)) => Some(v.clone()),
("ring", Value::Ring(_)) => Some(v.clone()),
("void", Value::Void) => Some(Value::Void),
("bool", Value::Number(n)) => { ("bool", Value::Number(n)) => {
if *n == 0.0 { if *n == 0.0 {
@ -3476,6 +3507,7 @@ fn try_cast(v: &Value, target: &str) -> Option<Value> {
.filter(|n| *n == n.trunc() && n.is_finite()) .filter(|n| *n == n.trunc() && n.is_finite())
.map(Value::Number), .map(Value::Number),
("float", Value::Str(s)) => s.parse::<f64>().ok().map(Value::Number), ("float", Value::Str(s)) => s.parse::<f64>().ok().map(Value::Number),
("str", Value::Array(_)) => Some(Value::Str(v.display())),
_ => None, _ => None,
} }
@ -3486,7 +3518,13 @@ fn coerce_to(val: &Value, target: &str) -> Result<Value, String> {
return Err(format!("unknown type annotation: {}", target)); return Err(format!("unknown type annotation: {}", target));
} }
let t1 = match try_cast(val, target) { let canon = canonical_type(target);
if value_is_kind(val, canon) {
return Ok(val.clone());
}
let t1 = match try_cast(val, canon) {
Some(v) => v, Some(v) => v,
None => { None => {
return Err(format!( return Err(format!(
@ -3498,6 +3536,10 @@ fn coerce_to(val: &Value, target: &str) -> Result<Value, String> {
} }
}; };
if matches!(canon, "array" | "struct" | "ring" | "void") {
return Ok(t1);
}
if values_equal(&t1, val) { if values_equal(&t1, val) {
return Ok(t1); return Ok(t1);
} }
@ -3536,7 +3578,25 @@ fn coerce_to(val: &Value, target: &str) -> Result<Value, String> {
} }
fn is_known_type(t: &str) -> bool { fn is_known_type(t: &str) -> bool {
matches!(t, "int" | "float" | "bool" | "str") matches!(canonical_type(t),
"int" | "float" | "bool" | "str" | "number"
| "array" | "struct" | "void" | "ring")
}
/// folds aliases onto a canonical type name.
fn canonical_type(t: &str) -> &str {
match t {
"arr" | "array" | "vec" | "[]" | "list" => "array",
"int" | "integer" => "int",
"float" | "f64" | "f32" => "float",
"bool" | "boolean" => "bool",
"str" | "string" | "String" => "str",
"number" | "num" => "number",
"void" | "null" | "nil" | "none" | "unit" | "()" => "void",
"struct" | "obj" | "object" | "map" | "dict" => "struct",
"ring" => "ring",
other => other,
}
} }
fn values_equal(a: &Value, b: &Value) -> bool { fn values_equal(a: &Value, b: &Value) -> bool {
@ -5962,6 +6022,97 @@ fn find(arr, target) {
assert!(result.unwrap_err().contains("missing required method 'bounds'")); assert!(result.unwrap_err().contains("missing required method 'bounds'"));
} }
#[test]
fn fn_return_type_arr_accepts_array() {
let mut i = Interpreter::new();
i.exec_line("fn which(degree) -> arr {\n return [0, 0]\n}").unwrap();
let v = i.eval_expr_str("which(5)").unwrap();
match v {
Value::Array(a) => assert_eq!(a.len(), 2),
other => panic!("expected array, got {:?}", other),
}
}
#[test]
fn fn_return_type_array_aliases() {
let mut i = Interpreter::new();
for ty in ["arr", "array", "vec", "list"] {
i.exec_line(&format!("fn f_{}() -> {} {{ return [1, 2, 3] }}", ty, ty)).unwrap();
let v = i.eval_expr_str(&format!("f_{}()", ty)).unwrap();
assert!(matches!(v, Value::Array(_)), "{} return type rejected", ty);
}
}
#[test]
fn fn_return_type_null_accepts_void() {
let mut i = Interpreter::new();
i.exec_line("fn noop() -> null { }").unwrap();
let v = i.eval_expr_str("noop()").unwrap();
assert!(matches!(v, Value::Void));
}
#[test]
fn let_with_array_type_annotation() {
let mut i = Interpreter::new();
i.exec_line("let pos: arr = [1, 2, 3, 4]").unwrap();
let v = i.get_var("pos").unwrap();
assert!(matches!(v, Value::Array(_)));
}
#[test]
fn let_array_type_aliases() {
for ty in ["arr", "array", "vec", "list"] {
let mut i = Interpreter::new();
i.exec_line(&format!("let x: {} = [1, 2]", ty)).unwrap();
assert!(matches!(i.get_var("x").unwrap(), Value::Array(_)),
"alias {} failed", ty);
}
}
#[test]
fn method_len_on_array() {
let mut i = Interpreter::new();
i.exec_line("let pos = [1, 2, 3, 4]").unwrap();
let v = i.eval_expr_str("pos.len()").unwrap();
assert!(matches!(v, Value::Number(n) if n == 4.0));
}
#[test]
fn method_call_on_array_literal() {
let mut i = Interpreter::new();
let v = i.eval_expr_str("[1, 2, 3].len()").unwrap();
assert!(matches!(v, Value::Number(n) if n == 3.0));
}
#[test]
fn for_loop_does_not_leak_var() {
let mut i = Interpreter::new();
i.exec_line("let x = [1, 2, 3]").unwrap();
i.exec_line("for x in 0..3 { let _ = x }").unwrap();
let after = i.get_var("x").unwrap();
assert!(matches!(after, Value::Array(_)), "for loop leaked: x is now {:?}", after);
}
#[test]
fn for_loop_in_while_with_shadowing() {
let mut i = Interpreter::new();
i.exec_line("let pos = [10, 20, 30, 40]").unwrap();
i.exec_line("let count = 0").unwrap();
i.exec_line("let iters = 0").unwrap();
i.exec_line("while iters < 2 {\n for pos in 0..len(pos) {\n count = count + 1\n }\n iters = iters + 1\n}").unwrap();
let count = i.get_var("count").unwrap();
assert!(matches!(count, Value::Number(n) if n == 8.0),
"expected 8 inner iterations, got {:?}", count);
}
#[test]
fn fn_return_type_struct_accepts_struct() {
let mut i = Interpreter::new();
i.exec_line("fn make() -> struct {\n return {x: 1, y: 2}\n}").unwrap();
let v = i.eval_expr_str("make()").unwrap();
assert!(matches!(v, Value::Struct(_)));
}
#[test] #[test]
fn method_on_untyped_struct_errors() { fn method_on_untyped_struct_errors() {
let mut i = Interpreter::new(); let mut i = Interpreter::new();

View File

@ -19,6 +19,7 @@ pub fn highlight_preview(source: &str) -> Vec<PreviewLine> {
source: source.to_string(), source: source.to_string(),
user_idents: crate::syntax::scan_user_idents_in(source), user_idents: crate::syntax::scan_user_idents_in(source),
rules: crate::syntax::SyntaxRules::cordial(), rules: crate::syntax::SyntaxRules::cordial(),
heavy_token: 0,
}; };
let mut highlighter = SyntaxHighlighter::new(&settings); let mut highlighter = SyntaxHighlighter::new(&settings);

View File

@ -398,14 +398,6 @@ impl super::EditorState {
!tb.selection.is_empty() || tb.spillover.is_some() !tb.selection.is_empty() || tb.spillover.is_some()
} }
/// jumps the focused text block's internal scroll to the given fraction (0.0..1.0).
pub(super) fn jump_to_fraction(&mut self, frac: f32) {
let frac = frac.clamp(0.0, 1.0);
let line_count = self.content().line_count().max(1);
let target = ((line_count as f32 * frac) as usize).min(line_count.saturating_sub(1));
self.content_mut().jump_to_line(target);
}
/// scrolls the viewport and places the cursor at the given line. /// scrolls the viewport and places the cursor at the given line.
pub(super) fn jump_to_line(&mut self, line: usize) { pub(super) fn jump_to_line(&mut self, line: usize) {
let clamped = line.min(self.content().line_count().saturating_sub(1)); let clamped = line.min(self.content().line_count().saturating_sub(1));

View File

@ -40,6 +40,10 @@ impl super::EditorState {
} }
pub fn tick(&mut self) { pub fn tick(&mut self) {
if self.heavy_pending && self.last_edit.elapsed().as_millis() >= 300 {
self.heavy_token = self.heavy_token.wrapping_add(1);
self.heavy_pending = false;
}
if self.render_mode != RenderMode::Live { return; } if self.render_mode != RenderMode::Live { return; }
if self.eval_dirty && self.last_edit.elapsed().as_millis() >= EVAL_DEBOUNCE_MS { if self.eval_dirty && self.last_edit.elapsed().as_millis() >= EVAL_DEBOUNCE_MS {
self.eval_dirty = false; self.eval_dirty = false;
@ -85,6 +89,7 @@ impl super::EditorState {
self.parsed = markdown::parse(&text).collect(); self.parsed = markdown::parse(&text).collect();
self.rebuild_modules(); self.rebuild_modules();
self.refresh_text_caches(); self.refresh_text_caches();
self.heavy_pending = true;
} }
pub(super) fn build_block_infos(&self) -> Vec<crate::module::BlockInfo> { pub(super) fn build_block_infos(&self) -> Vec<crate::module::BlockInfo> {

View File

@ -247,6 +247,7 @@ impl EditorState {
source: tb.content.text(), source: tb.content.text(),
user_idents: self.cached_user_idents.clone(), user_idents: self.cached_user_idents.clone(),
rules: self.syntax_rules.clone(), rules: self.syntax_rules.clone(),
heavy_token: self.heavy_token,
}; };
let editor_el: Element<'_, Message, Theme, iced_wgpu::Renderer> = editor let editor_el: Element<'_, Message, Theme, iced_wgpu::Renderer> = editor
.highlight_with::<SyntaxHighlighter>( .highlight_with::<SyntaxHighlighter>(
@ -715,6 +716,7 @@ impl EditorState {
source: tb.content.text(), source: tb.content.text(),
user_idents: self.cached_user_idents.clone(), user_idents: self.cached_user_idents.clone(),
rules: self.syntax_rules.clone(), rules: self.syntax_rules.clone(),
heavy_token: self.heavy_token,
}; };
editor editor
.highlight_with::<SyntaxHighlighter>( .highlight_with::<SyntaxHighlighter>(

View File

@ -111,6 +111,10 @@ 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,
/// tree-sitter rebuild gate, bumped on idle.
pub(super) heavy_token: u64,
/// deferred-rebuild arm; set on edit, cleared on debounce fire.
pub(super) heavy_pending: bool,
} }
impl EditorState { impl EditorState {
@ -178,6 +182,8 @@ impl EditorState {
cached_user_idents: HashMap::new(), cached_user_idents: HashMap::new(),
cached_minimap_lines: Vec::new(), cached_minimap_lines: Vec::new(),
syntax_rules: crate::syntax::SyntaxRules::cordial(), syntax_rules: crate::syntax::SyntaxRules::cordial(),
heavy_token: 0,
heavy_pending: false,
} }
} }

View File

@ -1,60 +1,21 @@
//! helpers for embedding EditorState inside an external iced application. //! embedding helpers for EditorState inside an external iced application.
//!
//! the editor produces a fully-functional iced widget via EditorState::view().
//! callers also need to drive periodic state work (eval debounce, autosave hints)
//! by calling EditorState::tick() once per frame, and drain pending clipboard or
//! shell-action output. these helpers wrap the common patterns.
//!
//! example:
//!
//! ```ignore
//! use acord_viewport::{EditorState, embed};
//!
//! struct App { editor: EditorState }
//!
//! enum Msg { Acord(acord_viewport::Message), Tick }
//!
//! fn update(&mut self, msg: Msg) {
//! match msg {
//! Msg::Acord(m) => self.editor.update(m),
//! Msg::Tick => {
//! self.editor.tick();
//! let pending = self.editor.drain_pending();
//! if let Some(text) = pending.clipboard {
//! // copy to host clipboard
//! }
//! }
//! }
//! }
//!
//! fn view(&self) -> Element<'_, Msg> {
//! self.editor.view().map(Msg::Acord)
//! }
//!
//! fn subscription(&self) -> Subscription<Msg> {
//! iced::time::every(embed::TICK_INTERVAL).map(|_| Msg::Tick)
//! }
//! ```
use std::time::Duration; use std::time::Duration;
use crate::editor::EditorState; use crate::editor::EditorState;
/// recommended tick interval for embedded use (60 fps cadence). /// tick cadence for embedded use.
pub const TICK_INTERVAL: Duration = Duration::from_millis(16); pub const TICK_INTERVAL: Duration = Duration::from_millis(16);
/// snapshot of host-handled state the editor produced this frame. /// host-handled output produced during the current frame.
pub struct Pending { pub struct Pending {
/// text the editor wants written to the host clipboard, if any.
pub clipboard: Option<String>, pub clipboard: Option<String>,
/// numeric command the host shell should act on, if any.
pub shell_action: Option<crate::editor::ShellAction>, pub shell_action: Option<crate::editor::ShellAction>,
/// widget that should receive iced focus this frame, if any.
pub focus: Option<iced_wgpu::core::widget::Id>, pub focus: Option<iced_wgpu::core::widget::Id>,
} }
impl EditorState { impl EditorState {
/// pulls every host-handled output for this frame and clears it from the editor. /// pulls clipboard, shell-action, and focus output and clears it.
pub fn drain_pending(&mut self) -> Pending { pub fn drain_pending(&mut self) -> Pending {
Pending { Pending {
clipboard: self.pending_clipboard.take(), clipboard: self.pending_clipboard.take(),
@ -63,8 +24,7 @@ impl EditorState {
} }
} }
/// records the surface size so the minimap, scrollable math, and free-layer /// records the current surface size.
/// placement can size themselves correctly.
pub fn set_viewport_size(&mut self, width: f32, height: f32) { pub fn set_viewport_size(&mut self, width: f32, height: f32) {
self.viewport_size = (width, height); self.viewport_size = (width, height);
} }

View File

@ -1,6 +1,4 @@
use iced_wgpu::core::{ use iced_wgpu::core::{Color, Element, Font, Length, Theme};
alignment, mouse, Color, Element, Font, Length, Theme,
};
use iced_widget::{column, container, mouse_area, text}; use iced_widget::{column, container, mouse_area, text};
use crate::palette; use crate::palette;
@ -143,7 +141,7 @@ fn parse_impl_entry(rest: &str, line: usize) -> MapEntry {
} }
} }
/// strips `pub`, `pub(crate)`, `pub(super)`, `pub(in path)` from the front. /// strips a leading Rust visibility modifier.
fn strip_visibility(s: &str) -> &str { fn strip_visibility(s: &str) -> &str {
if let Some(rest) = s.strip_prefix("pub(") { if let Some(rest) = s.strip_prefix("pub(") {
if let Some(end) = rest.find(')') { if let Some(end) = rest.find(')') {

View File

@ -78,6 +78,8 @@ pub struct SyntaxSettings {
pub user_idents: HashMap<String, u8>, pub user_idents: HashMap<String, u8>,
/// optional extra keywords/builtins layered on top of Cordial. /// optional extra keywords/builtins layered on top of Cordial.
pub rules: SyntaxRules, pub rules: SyntaxRules,
/// rebuild gate; tree-sitter re-runs only when this changes.
pub heavy_token: u64,
} }
/// extensible token table for languages built on top of Cordial. /// extensible token table for languages built on top of Cordial.
@ -88,6 +90,8 @@ pub struct SyntaxRules {
pub extra_types: std::collections::BTreeSet<String>, pub extra_types: std::collections::BTreeSet<String>,
/// when true, the hardcoded Cordial keywords/builtins remain active. /// when true, the hardcoded Cordial keywords/builtins remain active.
pub include_cordial: bool, pub include_cordial: bool,
/// routes every non-fenced line through the Cordial scanner, bypassing the line classifier.
pub assume_cordial: bool,
} }
impl SyntaxRules { impl SyntaxRules {
@ -98,6 +102,7 @@ impl SyntaxRules {
extra_builtins: Default::default(), extra_builtins: Default::default(),
extra_types: Default::default(), extra_types: Default::default(),
include_cordial: true, include_cordial: true,
assume_cordial: false,
} }
} }
@ -108,9 +113,16 @@ impl SyntaxRules {
extra_builtins: Default::default(), extra_builtins: Default::default(),
extra_types: Default::default(), extra_types: Default::default(),
include_cordial: false, include_cordial: false,
assume_cordial: false,
} }
} }
/// builder for the assume_cordial flag.
pub fn assume_cordial(mut self, on: bool) -> Self {
self.assume_cordial = on;
self
}
pub fn keyword(mut self, w: impl Into<String>) -> Self { pub fn keyword(mut self, w: impl Into<String>) -> Self {
self.extra_keywords.insert(w.into()); self.extra_keywords.insert(w.into());
self self
@ -171,11 +183,11 @@ pub struct SyntaxHighlighter {
/// per-line tree-sitter spans for fenced code body lines, keyed by absolute line index. /// per-line tree-sitter spans for fenced code body lines, keyed by absolute line index.
code_block_spans: HashMap<usize, Vec<(Range<usize>, SyntaxHighlight)>>, code_block_spans: HashMap<usize, Vec<(Range<usize>, SyntaxHighlight)>>,
rules: SyntaxRules, rules: SyntaxRules,
last_heavy_token: Option<u64>,
} }
impl SyntaxHighlighter { impl SyntaxHighlighter {
fn rebuild(&mut self, source: &str) { fn rebuild(&mut self, source: &str, heavy_token: u64) {
self.spans = highlight_source(source, &self.lang);
self.line_offsets.clear(); self.line_offsets.clear();
let mut offset = 0; let mut offset = 0;
for line in source.split('\n') { for line in source.split('\n') {
@ -212,7 +224,11 @@ impl SyntaxHighlighter {
self.in_fenced_code = false; self.in_fenced_code = false;
self.current_line = 0; self.current_line = 0;
self.scan_fenced_code_blocks(source); if self.last_heavy_token != Some(heavy_token) {
self.spans = highlight_source(source, &self.lang);
self.scan_fenced_code_blocks(source);
self.last_heavy_token = Some(heavy_token);
}
} }
/// highlights language-tagged fenced blocks via tree-sitter, stashing per-line spans. /// highlights language-tagged fenced blocks via tree-sitter, stashing per-line spans.
@ -718,7 +734,17 @@ fn is_cordial_builtin(w: &str) -> bool {
} }
fn is_cordial_type_annotation(w: &str) -> bool { fn is_cordial_type_annotation(w: &str) -> bool {
matches!(w, "int" | "float" | "bool" | "str" | "number" | "array" | "vec") matches!(w,
"int" | "integer"
| "float" | "f64" | "f32"
| "number" | "num"
| "bool" | "boolean"
| "str" | "string"
| "array" | "arr" | "vec" | "list"
| "struct" | "obj" | "object" | "map" | "dict"
| "void" | "null" | "nil" | "none" | "unit"
| "ring"
)
} }
fn last_token_is_colon(spans: &[(Range<usize>, SyntaxHighlight)]) -> bool { fn last_token_is_colon(spans: &[(Range<usize>, SyntaxHighlight)]) -> bool {
@ -969,8 +995,9 @@ impl highlighter::Highlighter for SyntaxHighlighter {
user_idents: settings.user_idents.clone(), user_idents: settings.user_idents.clone(),
code_block_spans: HashMap::new(), code_block_spans: HashMap::new(),
rules: settings.rules.clone(), rules: settings.rules.clone(),
last_heavy_token: None,
}; };
h.rebuild(&settings.source); h.rebuild(&settings.source, settings.heavy_token);
h h
} }
@ -978,7 +1005,7 @@ impl highlighter::Highlighter for SyntaxHighlighter {
self.lang = new_settings.lang.clone(); self.lang = new_settings.lang.clone();
self.user_idents = new_settings.user_idents.clone(); self.user_idents = new_settings.user_idents.clone();
self.rules = new_settings.rules.clone(); self.rules = new_settings.rules.clone();
self.rebuild(&new_settings.source); self.rebuild(&new_settings.source, new_settings.heavy_token);
} }
fn change_line(&mut self, line: usize) { fn change_line(&mut self, line: usize) {
@ -1003,6 +1030,12 @@ impl highlighter::Highlighter for SyntaxHighlighter {
// pure-code mode bypasses cordial and markdown classifiers. // pure-code mode bypasses cordial and markdown classifiers.
let is_pure_code = !self.lang.is_empty(); let is_pure_code = !self.lang.is_empty();
if !is_pure_code && self.rules.assume_cordial && !self.in_fenced_code {
if !trimmed.starts_with("```") {
return highlight_cordial(line, &self.user_idents, &self.rules).into_iter();
}
}
if !is_pure_code if !is_pure_code
&& ln < self.line_kinds.len() && ln < self.line_kinds.len()
&& matches!(self.line_kinds[ln], LineKind::Cordial | LineKind::Eval | LineKind::Comment) && matches!(self.line_kinds[ln], LineKind::Cordial | LineKind::Eval | LineKind::Comment)

View File

@ -94,6 +94,7 @@ impl<Message: Clone + 'static> Block<Message> for TextBlock {
user_idents: syntax::scan_user_idents_in(&source), user_idents: syntax::scan_user_idents_in(&source),
rules: syntax::SyntaxRules::cordial(), rules: syntax::SyntaxRules::cordial(),
source, source,
heavy_token: 0,
}; };
let editor_el: Element<'a, Message, Theme, iced_wgpu::Renderer> = editor let editor_el: Element<'a, Message, Theme, iced_wgpu::Renderer> = editor
.highlight_with::<SyntaxHighlighter>( .highlight_with::<SyntaxHighlighter>(