use std::collections::HashMap; use std::path::PathBuf; use std::sync::Arc; use std::time::Instant; use iced_widget::markdown; use crate::blocks; use crate::table_block::TableBlock; use crate::text_block::TextBlock; use crate::text_widget::{self, Action, Motion}; use super::types::{ Anchor, ComputedTable, ComputedTree, InlineResult, TableIndex, EVAL_DEBOUNCE_MS, LONG_PRESS_MS, }; use super::types::{resolve_ref_key, ERROR_PREFIX, RESULT_PREFIX}; use super::RenderMode; impl super::EditorState { /// maps a line number in concatenated module source back to a per-block anchor pub(super) fn map_line_to_anchor( boundaries: &[(usize, crate::selection::BlockId)], global_line: usize, ) -> Anchor { let mut best_id = boundaries.first().map(|b| b.1).unwrap_or(0); let mut best_start = 0; for &(start, id) in boundaries { if start <= global_line { best_id = id; best_start = start; } else { break; } } Anchor { block_id: best_id, after_line: global_line - best_start, } } 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; self.run_eval(); } { let block_start = self.layout.get(self.focused_block) .and_then(|id| self.registry.get(id)) .map(|b| b.start_line()) .unwrap_or(0); let intra = self.content().cursor().position.line; let global_line = block_start + intra; if global_line != self.prev_cursor_line { self.prev_cursor_line = global_line; if !self.eval_dirty { self.run_eval(); } } } let due = self.inline_press.as_ref().is_some_and(|s| { !s.fired_long_press && s.started_at.elapsed().as_millis() >= LONG_PRESS_MS }); if due { if let Some(s) = self.inline_press.as_mut() { s.fired_long_press = true; let bid = s.block_id; let line = s.after_line; self.copy_inline_result(bid, line); } } let block_ids: Vec = self.layout.clone(); for id in block_ids { if let Some(block) = self.registry.get_mut(&id) { if let Some(tb) = block.as_any_mut().downcast_mut::() { tb.check_hover_spillover(); } } } } pub(super) fn reparse(&mut self) { let text = self.get_clean_text(); 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 { use crate::heading_block::HeadingBlock; use crate::module::BlockInfo; self.layout.iter().filter_map(|&id| { let block = self.registry.get(&id)?; let tag = block.kind_tag(); let (heading_level, heading_text) = if let Some(hb) = block.as_any().downcast_ref::() { (hb.level.as_u8(), hb.text.clone()) } else { (0, String::new()) }; let text_content = if tag == "text" { block.to_md() } else { String::new() }; Some(BlockInfo { id, kind_tag: tag, heading_level, heading_text, text_content }) }).collect() } /// rebuilds the module list and applies heading-based table names pub(super) fn rebuild_modules(&mut self) { use crate::module::{compute_modules, detect_table_names}; let infos = self.build_block_infos(); self.modules = compute_modules(&infos); let names = detect_table_names(&infos); for assignment in names { if let Some(block) = self.registry.get_mut(&assignment.table_id) { if let Some(tb) = block.as_any_mut().downcast_mut::() { tb.table_name = Some(assignment.name); } } } } /// registers every non-eval-result table on the interpreter and returns the alias index pub(super) fn register_visible_tables( &self, interp: &mut acord_core::interp::Interpreter, focused_block_idx: usize, ) -> TableIndex { use crate::module::{ compute_positional_ids, detect_table_names, normalize_name, TableNameScope, }; let infos = self.build_block_infos(); let table_names = detect_table_names(&infos); let pos_ids = compute_positional_ids(&infos); let mut block_to_module: HashMap = HashMap::new(); for m in &self.modules { for &bid in &m.block_ids { block_to_module.insert(bid, m.name.clone()); } } let focused_id = self.layout.get(focused_block_idx).copied(); let focused_module_name = focused_id.and_then(|id| block_to_module.get(&id).cloned()); interp.set_current_block(focused_module_name.as_deref()); let mut keys_map: HashMap = HashMap::new(); let mut canonical: HashMap = HashMap::new(); for (table_id, pos_name, _pos_block_pos) in &pos_ids.tables { let Some(block) = self.registry.get(table_id) else { continue }; let Some(tb) = block.as_any().downcast_ref::() else { continue }; if tb.is_eval_result { continue; } let rows = tb.rows.clone(); let heading = table_names.iter().find(|a| a.table_id == *table_id); let module_name = block_to_module.get(table_id).cloned(); let canonical_key = match heading { Some(h) => { let hname = normalize_name(&h.name); match h.scope { TableNameScope::Global => hname, TableNameScope::BlockScoped => { if let Some(ref m) = module_name { format!("{}::{}", m, hname) } else { hname } } } } None => pos_name.to_lowercase(), }; canonical.insert(*table_id, canonical_key.clone()); let mut keys: Vec = vec![pos_name.to_lowercase(), canonical_key.clone()]; if let Some(h) = heading { let hname = normalize_name(&h.name); if h.scope == TableNameScope::BlockScoped { if module_name.as_deref() == focused_module_name.as_deref() { keys.push(hname); } } } if let Some(ref m) = module_name { keys.push(format!("{}::{}", m, pos_name.to_lowercase())); } keys.sort(); keys.dedup(); for k in &keys { interp.register_table(k, rows.clone()); keys_map.insert(k.clone(), *table_id); } } TableIndex { keys: keys_map, canonical } } /// returns true if any visible table contains a `/=` formula cell pub(super) fn any_visible_cell_formulas(&self) -> bool { for block in self.registry.values() { if let Some(tb) = block.as_any().downcast_ref::() { if tb.is_eval_result { continue; } if tb.rows.iter().any(|row| row.iter().any(|c| c.trim_start().starts_with("/="))) { return true; } } } false } /// parses, topo-sorts, and evaluates every visible cell formula pub(super) fn evaluate_cell_formulas( &mut self, interp: &mut acord_core::interp::Interpreter, table_index: &TableIndex, ) { use acord_core::interp::{parse_formula_with_spice, ParsedFormula, Value}; struct Cell { table_key: String, col: u32, row: u32, block_id: crate::selection::BlockId, ast: ParsedFormula, } let mut formulas: Vec = Vec::new(); let mut parse_errors: Vec<(crate::selection::BlockId, u32, u32, String)> = Vec::new(); let mut seen_blocks: std::collections::HashSet = std::collections::HashSet::new(); for (_, &block_id) in &table_index.keys { if !seen_blocks.insert(block_id) { continue; } let Some(block) = self.registry.get(&block_id) else { continue }; let Some(tb) = block.as_any().downcast_ref::() else { continue }; let canonical = match table_index.canonical.get(&block_id) { Some(k) => k.clone(), None => continue, }; for (r, row) in tb.rows.iter().enumerate() { for (c, cell) in row.iter().enumerate() { let trimmed = cell.trim_start(); let Some(body) = trimmed.strip_prefix("/=") else { continue }; match parse_formula_with_spice(body, interp.spice_enabled()) { Ok(ast) => formulas.push(Cell { table_key: canonical.clone(), col: c as u32, row: r as u32, block_id, ast, }), Err(e) => parse_errors.push((block_id, c as u32, r as u32, e)), } } } } self.computed_cells.retain(|k, _| !seen_blocks.contains(&k.0)); for (bid, c, r, e) in parse_errors { self.computed_cells.insert((bid, c, r), Value::Error(format!("parse: {}", e))); } if formulas.is_empty() { return; } let node_key: HashMap<(String, u32, u32), usize> = formulas.iter().enumerate() .map(|(i, f)| ((f.table_key.clone(), f.col, f.row), i)) .collect(); let mut edges: Vec> = vec![Vec::new(); formulas.len()]; let mut in_degree: Vec = vec![0; formulas.len()]; for (i, f) in formulas.iter().enumerate() { let refs = f.ast.refs(&f.table_key); for r in refs { let resolved = resolve_ref_key(&r, table_index); if let Some(key) = resolved { if let Some(&dep) = node_key.get(&(key, r.cell.0, r.cell.1)) { if dep != i { edges[dep].push(i); in_degree[i] += 1; } } } } } let mut queue: std::collections::VecDeque = in_degree.iter().enumerate() .filter_map(|(i, &d)| if d == 0 { Some(i) } else { None }) .collect(); let mut order: Vec = Vec::new(); while let Some(i) = queue.pop_front() { order.push(i); let next = std::mem::take(&mut edges[i]); for j in next { in_degree[j] -= 1; if in_degree[j] == 0 { queue.push_back(j); } } } let ordered: std::collections::HashSet = order.iter().copied().collect(); for i in &order { let f = &formulas[*i]; interp.set_current_table(Some(&f.table_key)); let result = match interp.eval_formula(&f.ast) { Ok(v) => v, Err(e) => Value::Error(e), }; interp.set_current_table(None); if !result.is_error() { let display = result.display(); for (alias_key, &bid) in &table_index.keys { if bid == f.block_id { interp.write_cell_raw(alias_key, f.col, f.row, &display); } } } self.computed_cells.insert((f.block_id, f.col, f.row), result); } for i in 0..formulas.len() { if ordered.contains(&i) { continue; } let f = &formulas[i]; self.computed_cells.insert((f.block_id, f.col, f.row), Value::Error("cycle".into())); } } /// applies cell writes logged by the interpreter to live tables pub(super) fn apply_table_writes( &mut self, writes: Vec, table_index: &TableIndex, ) { for w in writes { let Some(&block_id) = table_index.keys.get(&w.table_key) else { continue }; let Some(block) = self.registry.get_mut(&block_id) else { continue }; let Some(tb) = block.as_any_mut().downcast_mut::() else { continue }; let (c, r) = (w.cell.0 as usize, w.cell.1 as usize); while tb.rows.len() <= r { tb.rows.push(Vec::new()); } let target_cols = (c + 1).max(tb.col_widths.len()); while tb.col_widths.len() < target_cols { tb.col_widths.push(120.0); } while tb.row_heights.len() < tb.rows.len() { tb.row_heights.push(None); } while tb.rows[r].len() <= c { tb.rows[r].push(String::new()); } tb.rows[r][c] = w.value; } } /// reparses the full text and reconciles block structure changes. pub(super) fn check_block_structure(&mut self) { let cursor = self.content().cursor(); let full = self.full_text(); let lang = self.lang_str(); let old_count = self.block_count(); { let mut block_vec = self.registry_to_vec(); blocks::reparse_incremental(&mut block_vec, &full, &lang); self.replace_blocks(block_vec); } if self.focused_block >= self.block_count() { self.set_focused_block(self.block_count().saturating_sub(1)); } if self.block_count() != old_count { if let Some(bi) = self.block_index_at_line(cursor.position.line) { self.set_focused_block(bi); } } self.rebuild_modules(); self.refresh_text_caches(); } /// returns the concatenated text of all text blocks in a module pub(super) fn module_source_text(&self, module: &crate::module::Module) -> String { let mut parts = Vec::new(); for &bid in &module.block_ids { if let Some(block) = self.registry.get(&bid) { if block.kind_tag() == "text" { parts.push(block.to_md()); } } } parts.join("\n") } /// builds an interpreter pre-populated with root and use-declared module exports. pub(super) fn build_eval_interpreter(&self, block_idx: usize) -> acord_core::interp::Interpreter { use acord_core::interp; let mut eval_interp = interp::Interpreter::new(); let block_id = match self.layout.get(block_idx) { Some(&id) => id, None => return eval_interp, }; let my_module = self.modules.iter().find(|m| m.block_ids.contains(&block_id)); let is_root = my_module.map(|m| m.is_root).unwrap_or(false); if !is_root { if let Some(root) = self.modules.iter().find(|m| m.is_root) { let mut visited: std::collections::HashSet = std::collections::HashSet::new(); let root_exports = self.resolve_module_exports(root, &mut visited); eval_interp.import_all(&root_exports); } } let use_block_ids: Vec = my_module .map(|m| m.block_ids.clone()) .unwrap_or_default(); let my_module_name = my_module.map(|m| m.name.clone()).unwrap_or_default(); for &bid in &use_block_ids { if let Some(block) = self.registry.get(&bid) { if let Some(tb) = block.as_any().downcast_ref::() { let text = tb.content.text(); let use_decls = interp::extract_use_declarations(&text); for decl in &use_decls { let mut visited: std::collections::HashSet = std::collections::HashSet::new(); if !my_module_name.is_empty() { visited.insert(my_module_name.clone()); } self.apply_use_decl(decl, &mut visited, &mut eval_interp); } } } } eval_interp } /// recursively evaluates a module with use declarations resolved. pub(super) fn resolve_module_exports( &self, module: &crate::module::Module, visited: &mut std::collections::HashSet, ) -> acord_core::interp::ModuleExports { use acord_core::interp; if !module.name.is_empty() && !visited.insert(module.name.clone()) { return interp::ModuleExports::default(); } let mut interp = interp::Interpreter::new(); if !module.is_root { if let Some(root) = self.modules.iter().find(|m| m.is_root) { if root.name != module.name { let root_exports = self.resolve_module_exports(root, visited); interp.import_all(&root_exports); } } } let module_text = self.module_source_text(module); let use_decls = interp::extract_use_declarations(&module_text); for decl in &use_decls { self.apply_use_decl(decl, visited, &mut interp); } crate::eval::evaluate_document_with_interp(&mut interp, &module_text); interp.exports() } /// flags use targets the language handles internally. pub(super) fn is_builtin_use(module: &str) -> bool { module == "spice" || module.starts_with("spice::") || module == "ring" || module.starts_with("ring::") } /// merges a single use decl's exports into the given interpreter. pub(super) fn apply_use_decl( &self, decl: &acord_core::interp::UseDecl, visited: &mut std::collections::HashSet, target: &mut acord_core::interp::Interpreter, ) { let root = decl.root(); if Self::is_builtin_use(root) { return; } if let Some(local) = self.modules.iter().find(|m| m.name == root) { let exports = self.resolve_module_exports(local, visited); if decl.wildcard || decl.segments.len() < 2 { target.import_all(&exports); } else if let Some(item) = decl.segments.last() { target.import_item(&exports, item); } return; } self.apply_external_use_decl(decl, visited, target); } /// resolves a sibling-note use decl against either the whole file or a named H2 submodule. fn apply_external_use_decl( &self, decl: &acord_core::interp::UseDecl, visited: &mut std::collections::HashSet, target: &mut acord_core::interp::Interpreter, ) { use acord_core::interp; let root = decl.root(); let cycle_key = format!("__ext__{}", root); if !visited.insert(cycle_key) { return; } let dir = self.notes_dir(); let path = dir.join(format!("{}.md", root)); let Ok(bytes) = std::fs::read(&path) else { return }; let (text_bytes, _archive) = crate::sidecar::extract_from_md(&bytes); let Ok(raw) = String::from_utf8(text_bytes) else { return }; let loaded = crate::sidecar::extract_archive(&raw); let clean = super::strip_result_lines(&loaded.markdown); let lang = self.lang_str(); let ext_blocks = blocks::parse_blocks(&clean, &lang); let ext_infos = build_block_infos_from(&ext_blocks); let ext_modules = crate::module::compute_modules(&ext_infos); let item_name: Option<&str> = if decl.wildcard { None } else if decl.segments.len() >= 2 { Some(decl.segments[1].as_str()) } else { None }; let submodule_match = item_name.and_then(|item| { ext_modules.iter().find(|m| m.name == item) }); if let Some(submod) = submodule_match { let mut nested = interp::Interpreter::new(); for d in interp::extract_use_declarations(&clean) { self.apply_use_decl(&d, visited, &mut nested); } if !submod.is_root { if let Some(root) = ext_modules.iter().find(|m| m.is_root) { let root_text = module_text_from_blocks(root, &ext_blocks); crate::eval::evaluate_document_with_interp(&mut nested, &root_text); } } let submod_text = module_text_from_blocks(submod, &ext_blocks); crate::eval::evaluate_document_with_interp(&mut nested, &submod_text); target.import_all(&nested.exports()); return; } let mut nested = interp::Interpreter::new(); for d in interp::extract_use_declarations(&clean) { self.apply_use_decl(&d, visited, &mut nested); } crate::eval::evaluate_document_with_interp(&mut nested, &clean); let exports = nested.exports(); if decl.wildcard || item_name.is_none() { target.import_all(&exports); } else if let Some(item) = item_name { target.import_item(&exports, item); } } /// resolves the directory sibling notes live in. pub(super) fn notes_dir(&self) -> PathBuf { let from_settings = self.settings_view.auto_save_dir.trim(); if !from_settings.is_empty() { return PathBuf::from(from_settings); } if let Ok(home) = std::env::var("HOME") { return PathBuf::from(home).join(".acord").join("notes"); } PathBuf::from(".") } pub(super) fn run_eval(&mut self) { self.rebuild_modules(); let focused_id = match self.layout.get(self.focused_block) { Some(&id) => id, None => return, }; let module = match self.modules.iter().find(|m| m.block_ids.contains(&focused_id)) { Some(m) => m.clone(), None => return, }; let mut source_parts: Vec = Vec::new(); let mut boundaries: Vec<(usize, crate::selection::BlockId)> = Vec::new(); let mut cumulative = 0usize; let mut block_ids: Vec = Vec::new(); for &bid in &module.block_ids { if let Some(block) = self.registry.get(&bid) { if block.kind_tag() == "text" { boundaries.push((cumulative, bid)); block_ids.push(bid); let text = block.to_md(); let lc = text.lines().count().max(1); source_parts.push(text); cumulative += lc; } } } let source = source_parts.join("\n"); self.scan_images(&boundaries, &block_ids); let has_text_eval = source.lines().any(|l| l.trim_start().starts_with("/=")); let has_cell_formulas = self.any_visible_cell_formulas(); if !has_text_eval && !has_cell_formulas { self.clear_layers_for_blocks(&block_ids); self.computed_cells.clear(); return; } let mut interp = self.build_eval_interpreter(self.focused_block); let table_keys = self.register_visible_tables(&mut interp, self.focused_block); self.evaluate_cell_formulas(&mut interp, &table_keys); let doc = crate::eval::evaluate_document_with_interp(&mut interp, &source); let writes = interp.drain_table_writes(); self.apply_table_writes(writes, &table_keys); self.clear_layers_for_blocks(&block_ids); for r in &doc.results { let anchor = Self::map_line_to_anchor(&boundaries, r.line); if r.format == "table" { match serde_json::from_str::>>(&r.result) { Ok(rows) if !rows.is_empty() => { let col_count = rows.iter().map(|r| r.len()).max().unwrap_or(0); let mut col_widths = vec![120.0f32; col_count]; for row in &rows { for (ci, cell) in row.iter().enumerate() { let w = cell.len() as f32 * 8.0 + 16.0; if ci < col_widths.len() && w > col_widths[ci] { col_widths[ci] = w; } } } self.computed_tables.push(ComputedTable { anchor, rows, col_widths, }); continue; } _ => {} } self.eval_results.push(InlineResult { anchor, text: format!("{}{}", RESULT_PREFIX, r.result), is_error: false, }); } else if r.format == "tree" { match serde_json::from_str::(&r.result) { Ok(data) => { self.computed_trees.push(ComputedTree { anchor, data }); } Err(_) => { self.eval_results.push(InlineResult { anchor, text: format!("{}{}", RESULT_PREFIX, r.result), is_error: false, }); } } } else { self.eval_results.push(InlineResult { anchor, text: format!("{}{}", RESULT_PREFIX, r.result), is_error: false, }); } } for e in &doc.errors { let anchor = Self::map_line_to_anchor(&boundaries, e.line); self.eval_results.push(InlineResult { anchor, text: format!("{}{}", ERROR_PREFIX, e.error), is_error: true, }); } } /// evaluates every module in document order pub(super) fn run_eval_all(&mut self) { self.rebuild_modules(); self.eval_results.clear(); self.computed_tables.clear(); self.computed_trees.clear(); self.computed_cells.clear(); let saved = self.focused_block; let modules: Vec = self.modules.clone(); for module in &modules { let anchor_idx = module.block_ids.iter() .find_map(|bid| self.layout.iter().position(|id| id == bid)); if let Some(idx) = anchor_idx { self.focused_block = idx; self.run_eval(); } } self.focused_block = saved; } /// returns the inline result text for a given anchor pub(super) fn inline_result_value(&self, block_id: crate::selection::BlockId, after_line: usize) -> Option { let r = self.eval_results.iter().find(|r| { r.anchor.block_id == block_id && r.anchor.after_line == after_line && !r.is_error })?; Some(r.text.trim_start_matches(RESULT_PREFIX).trim().to_string()) } /// reads one line from the text block with the given id. pub(super) fn read_line_at(&self, block_id: crate::selection::BlockId, line_idx: usize) -> Option { let block = self.registry.get(&block_id)?; let tb = block.as_any().downcast_ref::()?; tb.content.line(line_idx).map(|l| l.text.to_string()) } /// copies the source line and result value pair to the clipboard. pub(super) fn copy_inline_result(&mut self, block_id: crate::selection::BlockId, after_line: usize) { let value = match self.inline_result_value(block_id, after_line) { Some(v) => v, None => return, }; let line = self.read_line_at(block_id, after_line).unwrap_or_default(); let trimmed = line.trim_end(); self.pending_clipboard = Some(format!("{trimmed} {RESULT_PREFIX}{value}")); } /// copies the result and inserts a let binding below the source line. pub(super) fn handle_result_extract(&mut self, block_id: crate::selection::BlockId, after_line: usize) { let value = match self.inline_result_value(block_id, after_line) { Some(v) => v, None => return, }; self.copy_inline_result(block_id, after_line); let block_idx = match self.layout.iter().position(|id| *id == block_id) { Some(i) => i, None => return, }; if self.text_block_at(block_idx).is_none() { return; } self.push_undo_snapshot(); self.redo_stack.clear(); self.set_focused_block(block_idx); let content = self.content_mut(); content.perform(Action::Move(Motion::DocumentStart)); for _ in 0..after_line { content.perform(Action::Move(Motion::Down)); } content.perform(Action::Move(Motion::End)); let paste = format!("\n\nlet = {value}"); content.perform(Action::Edit(text_widget::Edit::Paste(Arc::new(paste)))); let back = 3 + value.chars().count(); for _ in 0..back { content.perform(Action::Move(Motion::Left)); } self.last_edit = Instant::now(); self.eval_dirty = true; self.reparse(); } } /// builds BlockInfos from a free-standing block list. fn build_block_infos_from(blocks: &[crate::blocks::BoxedBlock]) -> Vec { use crate::heading_block::HeadingBlock; use crate::module::BlockInfo; blocks.iter().map(|block| { let tag = block.kind_tag(); let (heading_level, heading_text) = if let Some(hb) = block.as_any().downcast_ref::() { (hb.level.as_u8(), hb.text.clone()) } else { (0, String::new()) }; let text_content = if tag == "text" { block.to_md() } else { String::new() }; BlockInfo { id: block.id(), kind_tag: tag, heading_level, heading_text, text_content } }).collect() } /// concatenates the text-block sources within a module against an arbitrary block list. fn module_text_from_blocks(module: &crate::module::Module, blocks: &[crate::blocks::BoxedBlock]) -> String { let mut parts = Vec::new(); for &bid in &module.block_ids { if let Some(block) = blocks.iter().find(|b| b.id() == bid) { if block.kind_tag() == "text" { parts.push(block.to_md()); } } } parts.join("\n") } pub(super) fn parse_let_binding(line: &str) -> Option { let rest = line.strip_prefix("let ")?; let eq_pos = rest.find('=')?; if rest.as_bytes().get(eq_pos + 1) == Some(&b'=') { return None; } let name_part = rest[..eq_pos].trim(); let name = if let Some(colon) = name_part.find(':') { name_part[..colon].trim() } else { name_part }; if name.is_empty() || !name.chars().all(|c| c.is_alphanumeric() || c == '_') { return None; } Some(name.to_string()) }