Syntax highlighting bug fix for external.
Also timing for tree-sitter checks
This commit is contained in:
parent
9b5dbbdf2b
commit
24958d4896
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
|
|
||||||
|
|
@ -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> {
|
||||||
|
|
|
||||||
|
|
@ -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>(
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(')') {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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>(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue