Compare commits

...

2 Commits

Author SHA1 Message Date
jess 2111fcac62 rand() and .rand()
see wiki for details
2026-05-28 03:09:36 -07:00
jess 24958d4896 Syntax highlighting bug fix for external.
Also timing for tree-sitter checks
2026-05-28 02:56:36 -07:00
10 changed files with 384 additions and 79 deletions

View File

@ -1671,6 +1671,40 @@ pub struct TableWrite {
const MAX_ITERATIONS: usize = 10_000;
const MAX_CALL_DEPTH: u32 = 256;
thread_local! {
static RNG_STATE: std::cell::Cell<u64> = std::cell::Cell::new(seed_from_time());
}
fn seed_from_time() -> u64 {
use std::time::{SystemTime, UNIX_EPOCH};
let nanos = SystemTime::now().duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos() as u64).unwrap_or(0x9E3779B97F4A7C15);
nanos.wrapping_mul(0x9E3779B97F4A7C15) ^ 0xDEADBEEFCAFEBABE
}
/// xorshift64* — pulls the next u64 from the thread-local PRNG.
fn rng_next_u64() -> u64 {
RNG_STATE.with(|s| {
let mut x = s.get();
if x == 0 { x = 0x9E3779B97F4A7C15; }
x ^= x >> 12;
x ^= x << 25;
x ^= x >> 27;
s.set(x);
x.wrapping_mul(0x2545F4914F6CDD1D)
})
}
/// uniform f64 in [0, 1).
fn rng_next_unit() -> f64 {
(rng_next_u64() >> 11) as f64 / (1u64 << 53) as f64
}
/// seeds the thread-local PRNG to a deterministic value.
pub fn seed_rng(seed: u64) {
RNG_STATE.with(|s| s.set(if seed == 0 { 0x9E3779B97F4A7C15 } else { seed }));
}
impl Interpreter {
pub fn new() -> Self {
Interpreter {
@ -2007,18 +2041,30 @@ impl Interpreter {
Value::Array(a) => a,
_ => 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 last = Value::Void;
let mut loop_err: Option<String> = None;
for item in &items {
iterations += 1;
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());
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)
}
Stmt::FnDef { name, params, return_type, body } => {
@ -2213,17 +2259,28 @@ impl Interpreter {
}
_ => None,
};
let type_tag = type_tag.ok_or_else(||
format!("cannot call .{}() — receiver has no __type", method)
)?;
let fndef = self.methods.get(&(type_tag.clone(), method.clone()))
.cloned()
.ok_or_else(|| format!("no method '{}' on type '{}'", method, type_tag))?;
// typed receiver → look up method on the type
if let Some(tag) = type_tag {
if let Some(fndef) = self.methods.get(&(tag.clone(), method.clone())).cloned() {
let mut eval_args = vec![recv_val];
for a in args {
eval_args.push(self.eval_expr(a, depth)?);
}
self.call_fndef(&fndef, &eval_args, depth)
return self.call_fndef(&fndef, &eval_args, depth);
}
return Err(format!("no method '{}' on type '{}'", method, tag));
}
// 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) => {
let fndef = self.methods.get(&(type_name_str.clone(), method.clone()))
@ -2380,6 +2437,52 @@ impl Interpreter {
};
return Ok(retag_spice(Value::Number(result), unit));
}
"rand" => {
match args.len() {
0 => return Ok(Value::Number(rng_next_unit())),
1 => {
let v = self.eval_expr(&args[0], depth)?;
match v {
Value::Number(n) => {
if n <= 0.0 { return Err("rand(n) expects n > 0".into()); }
return Ok(Value::Number((rng_next_unit() * n).floor()));
}
Value::Array(a) => {
if a.is_empty() { return Err("rand(array) on empty array".into()); }
let idx = (rng_next_unit() * a.len() as f64) as usize;
return Ok(a[idx.min(a.len() - 1)].clone());
}
_ => return Err("rand(x) expects a number or array".into()),
}
}
2 => {
let lo = match self.eval_expr(&args[0], depth)? {
Value::Number(n) => n,
_ => return Err("rand(lo, hi) expects numbers".into()),
};
let hi = match self.eval_expr(&args[1], depth)? {
Value::Number(n) => n,
_ => return Err("rand(lo, hi) expects numbers".into()),
};
if hi <= lo { return Err("rand(lo, hi) requires hi > lo".into()); }
return Ok(Value::Number(lo + rng_next_unit() * (hi - lo)));
}
_ => return Err("rand() takes 0, 1, or 2 arguments".into()),
}
}
"seed" => {
if args.len() != 1 {
return Err("seed() expects 1 argument".into());
}
let v = self.eval_expr(&args[0], depth)?;
match v {
Value::Number(n) => {
seed_rng(n.to_bits());
return Ok(Value::Void);
}
_ => return Err("seed() expects a number".into()),
}
}
"floor" | "ceil" | "round" => {
if args.is_empty() || args.len() > 2 {
return Err(format!("{}() expects 1 or 2 arguments", name));
@ -3430,25 +3533,33 @@ fn builtin_constant(name: &str) -> Option<Value> {
}
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(),
("float", Value::Number(_)) => true,
("number", Value::Number(_)) => true,
("bool", Value::Bool(_)) => true,
("str", Value::Str(_)) => true,
("array", Value::Array(_)) => true,
("struct", Value::Struct(_)) => true,
("ring", Value::Ring(_)) => true,
("void", Value::Void) => true,
_ => false,
}
}
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() => {
Some(Value::Number(*n))
}
("float", Value::Number(_)) => Some(v.clone()),
("number", Value::Number(_)) => Some(v.clone()),
("bool", Value::Bool(_)) => 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)) => {
if *n == 0.0 {
@ -3476,6 +3587,7 @@ fn try_cast(v: &Value, target: &str) -> Option<Value> {
.filter(|n| *n == n.trunc() && n.is_finite())
.map(Value::Number),
("float", Value::Str(s)) => s.parse::<f64>().ok().map(Value::Number),
("str", Value::Array(_)) => Some(Value::Str(v.display())),
_ => None,
}
@ -3486,7 +3598,13 @@ fn coerce_to(val: &Value, target: &str) -> Result<Value, String> {
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,
None => {
return Err(format!(
@ -3498,6 +3616,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) {
return Ok(t1);
}
@ -3536,7 +3658,25 @@ fn coerce_to(val: &Value, target: &str) -> Result<Value, String> {
}
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 {
@ -5962,6 +6102,171 @@ fn find(arr, target) {
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 rand_unit_range() {
let mut i = Interpreter::new();
i.exec_line("seed(42)").unwrap();
for _ in 0..20 {
let v = i.eval_expr_str("rand()").unwrap();
match v {
Value::Number(n) => assert!(n >= 0.0 && n < 1.0, "rand() = {}", n),
_ => panic!("rand() should return Number"),
}
}
}
#[test]
fn rand_integer_bound() {
let mut i = Interpreter::new();
i.exec_line("seed(7)").unwrap();
for _ in 0..30 {
let v = i.eval_expr_str("rand(10)").unwrap();
match v {
Value::Number(n) => assert!(n >= 0.0 && n < 10.0 && n == n.trunc()),
_ => panic!(),
}
}
}
#[test]
fn rand_lo_hi_range() {
let mut i = Interpreter::new();
i.exec_line("seed(99)").unwrap();
for _ in 0..30 {
let v = i.eval_expr_str("rand(5, 8)").unwrap();
match v {
Value::Number(n) => assert!(n >= 5.0 && n < 8.0),
_ => panic!(),
}
}
}
#[test]
fn rand_picks_from_array() {
let mut i = Interpreter::new();
i.exec_line("seed(1)").unwrap();
let v = i.eval_expr_str("rand([10, 20, 30])").unwrap();
match v {
Value::Number(n) => assert!(n == 10.0 || n == 20.0 || n == 30.0),
_ => panic!(),
}
}
#[test]
fn rand_seed_is_deterministic() {
let mut a = Interpreter::new();
a.exec_line("seed(12345)").unwrap();
let va = a.eval_expr_str("rand()").unwrap();
a.exec_line("seed(12345)").unwrap();
let vb = a.eval_expr_str("rand()").unwrap();
match (va, vb) {
(Value::Number(x), Value::Number(y)) => assert_eq!(x.to_bits(), y.to_bits()),
_ => panic!(),
}
}
#[test]
fn rand_method_call_on_array() {
let mut i = Interpreter::new();
i.exec_line("seed(3)").unwrap();
let v = i.eval_expr_str("[1, 2, 3, 4, 5].rand()").unwrap();
match v {
Value::Number(n) => assert!((1.0..=5.0).contains(&n)),
_ => panic!(),
}
}
#[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]
fn method_on_untyped_struct_errors() {
let mut i = Interpreter::new();

View File

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

View File

@ -398,14 +398,6 @@ impl super::EditorState {
!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.
pub(super) fn jump_to_line(&mut self, line: usize) {
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) {
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.eval_dirty && self.last_edit.elapsed().as_millis() >= EVAL_DEBOUNCE_MS {
self.eval_dirty = false;
@ -85,6 +89,7 @@ impl super::EditorState {
self.parsed = markdown::parse(&text).collect();
self.rebuild_modules();
self.refresh_text_caches();
self.heavy_pending = true;
}
pub(super) fn build_block_infos(&self) -> Vec<crate::module::BlockInfo> {

View File

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

View File

@ -111,6 +111,10 @@ pub struct EditorState {
pub(super) cached_minimap_lines: Vec<crate::minimap::MinimapLine>,
/// custom keyword/builtin/type table layered on top of Cordial.
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 {
@ -178,6 +182,8 @@ impl EditorState {
cached_user_idents: HashMap::new(),
cached_minimap_lines: Vec::new(),
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.
//!
//! 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)
//! }
//! ```
//! embedding helpers for EditorState inside an external iced application.
use std::time::Duration;
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);
/// snapshot of host-handled state the editor produced this frame.
/// host-handled output produced during the current frame.
pub struct Pending {
/// text the editor wants written to the host clipboard, if any.
pub clipboard: Option<String>,
/// numeric command the host shell should act on, if any.
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>,
}
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 {
Pending {
clipboard: self.pending_clipboard.take(),
@ -63,8 +24,7 @@ impl EditorState {
}
}
/// records the surface size so the minimap, scrollable math, and free-layer
/// placement can size themselves correctly.
/// records the current surface size.
pub fn set_viewport_size(&mut self, width: f32, height: f32) {
self.viewport_size = (width, height);
}

View File

@ -1,6 +1,4 @@
use iced_wgpu::core::{
alignment, mouse, Color, Element, Font, Length, Theme,
};
use iced_wgpu::core::{Color, Element, Font, Length, Theme};
use iced_widget::{column, container, mouse_area, text};
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 {
if let Some(rest) = s.strip_prefix("pub(") {
if let Some(end) = rest.find(')') {

View File

@ -78,6 +78,8 @@ pub struct SyntaxSettings {
pub user_idents: HashMap<String, u8>,
/// optional extra keywords/builtins layered on top of Cordial.
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.
@ -88,6 +90,8 @@ pub struct SyntaxRules {
pub extra_types: std::collections::BTreeSet<String>,
/// when true, the hardcoded Cordial keywords/builtins remain active.
pub include_cordial: bool,
/// routes every non-fenced line through the Cordial scanner, bypassing the line classifier.
pub assume_cordial: bool,
}
impl SyntaxRules {
@ -98,6 +102,7 @@ impl SyntaxRules {
extra_builtins: Default::default(),
extra_types: Default::default(),
include_cordial: true,
assume_cordial: false,
}
}
@ -108,9 +113,16 @@ impl SyntaxRules {
extra_builtins: Default::default(),
extra_types: Default::default(),
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 {
self.extra_keywords.insert(w.into());
self
@ -171,11 +183,11 @@ pub struct SyntaxHighlighter {
/// per-line tree-sitter spans for fenced code body lines, keyed by absolute line index.
code_block_spans: HashMap<usize, Vec<(Range<usize>, SyntaxHighlight)>>,
rules: SyntaxRules,
last_heavy_token: Option<u64>,
}
impl SyntaxHighlighter {
fn rebuild(&mut self, source: &str) {
self.spans = highlight_source(source, &self.lang);
fn rebuild(&mut self, source: &str, heavy_token: u64) {
self.line_offsets.clear();
let mut offset = 0;
for line in source.split('\n') {
@ -212,7 +224,11 @@ impl SyntaxHighlighter {
self.in_fenced_code = false;
self.current_line = 0;
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.
@ -712,13 +728,25 @@ fn is_cordial_builtin(w: &str) -> bool {
| "ring" | "iter" | "peek" | "history"
// aggregates
| "sum" | "avg" | "min" | "max" | "count" | "std_devp" | "std_devs"
// random
| "rand" | "seed"
// constants
| "pi"
)
}
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 {
@ -969,8 +997,9 @@ impl highlighter::Highlighter for SyntaxHighlighter {
user_idents: settings.user_idents.clone(),
code_block_spans: HashMap::new(),
rules: settings.rules.clone(),
last_heavy_token: None,
};
h.rebuild(&settings.source);
h.rebuild(&settings.source, settings.heavy_token);
h
}
@ -978,7 +1007,7 @@ impl highlighter::Highlighter for SyntaxHighlighter {
self.lang = new_settings.lang.clone();
self.user_idents = new_settings.user_idents.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) {
@ -1003,6 +1032,12 @@ impl highlighter::Highlighter for SyntaxHighlighter {
// pure-code mode bypasses cordial and markdown classifiers.
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
&& ln < self.line_kinds.len()
&& 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),
rules: syntax::SyntaxRules::cordial(),
source,
heavy_token: 0,
};
let editor_el: Element<'a, Message, Theme, iced_wgpu::Renderer> = editor
.highlight_with::<SyntaxHighlighter>(