829 lines
32 KiB
Rust
829 lines
32 KiB
Rust
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<crate::selection::BlockId> = 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::<TableBlock>() {
|
|
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<crate::module::BlockInfo> {
|
|
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::<HeadingBlock>() {
|
|
(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::<TableBlock>() {
|
|
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<crate::selection::BlockId, String> = 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<String, crate::selection::BlockId> = HashMap::new();
|
|
let mut canonical: HashMap<crate::selection::BlockId, String> = 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::<TableBlock>() 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<String> = 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::<TableBlock>() {
|
|
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<Cell> = Vec::new();
|
|
let mut parse_errors: Vec<(crate::selection::BlockId, u32, u32, String)> = Vec::new();
|
|
|
|
let mut seen_blocks: std::collections::HashSet<crate::selection::BlockId> =
|
|
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::<TableBlock>() 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<usize>> = vec![Vec::new(); formulas.len()];
|
|
let mut in_degree: Vec<usize> = 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<usize> = in_degree.iter().enumerate()
|
|
.filter_map(|(i, &d)| if d == 0 { Some(i) } else { None })
|
|
.collect();
|
|
let mut order: Vec<usize> = 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<usize> = 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<acord_core::interp::TableWrite>,
|
|
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::<TableBlock>() 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<String> = 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<crate::selection::BlockId> = 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::<TextBlock>() {
|
|
let text = tb.content.text();
|
|
let use_decls = interp::extract_use_declarations(&text);
|
|
for decl in &use_decls {
|
|
let mut visited: std::collections::HashSet<String> = 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<String>,
|
|
) -> 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<String>,
|
|
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<String>,
|
|
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<String> = Vec::new();
|
|
let mut boundaries: Vec<(usize, crate::selection::BlockId)> = Vec::new();
|
|
let mut cumulative = 0usize;
|
|
let mut block_ids: Vec<crate::selection::BlockId> = 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::<Vec<Vec<String>>>(&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::<serde_json::Value>(&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<crate::module::Module> = 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<String> {
|
|
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<String> {
|
|
let block = self.registry.get(&block_id)?;
|
|
let tb = block.as_any().downcast_ref::<TextBlock>()?;
|
|
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<crate::module::BlockInfo> {
|
|
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::<HeadingBlock>() {
|
|
(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<String> {
|
|
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())
|
|
}
|