1#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
7#![forbid(unsafe_code)]
8
9pub mod catalog;
10pub mod cf;
11mod chart_svg;
12mod clip_html;
13pub mod cmd;
14pub mod display;
15pub mod fill;
16pub mod io;
17pub mod links;
18pub mod macros;
19pub mod outline;
20pub mod paper;
21pub mod pivot;
22pub mod pivot_calc;
23pub mod recent;
24pub mod record;
25pub mod sample;
26pub mod selection;
27pub mod tables;
28pub mod windows;
29
30use std::sync::Arc;
31
32use crate::base::{CellRef, RangeRef};
33use crate::calc::Calc;
34use crate::model::Workbook;
35use serde::Serialize;
36use serde_json::Value as Json;
37
38pub use crate::base as core;
39pub use crate::calc;
40pub use crate::formula;
41pub use crate::math;
42pub use crate::model;
43pub use crate::numfmt;
44pub use cmd::{CommandInfo, CommandSpec, command_specs, find_command, register_test_command};
45pub use selection::Selection;
46pub use windows::{Target, TargetGuard, WindowState, WindowView};
47
48#[derive(Debug, thiserror::Error)]
49pub enum EngineError {
50 #[error("unknown command `{0}`")]
51 UnknownCommand(String),
52 #[error("command `{0}` is not available right now: {1}")]
53 Disabled(String, String),
54 #[error("invalid parameters for `{cmd}`: {msg}")]
55 BadParams { cmd: String, msg: String },
56 #[error("no workbook open")]
57 NoDocument,
58 #[error("{0}")]
59 Other(String),
60 #[error("confirm: {0} (run again with `overwrite: true`)")]
62 Confirm(String),
63 #[error("approve: {0} (run again with `approve: true`)")]
65 Approve(String),
66 #[error("internal error in `{0}` (the workbook was kept as it was): {1}")]
67 Internal(String, String),
68}
69
70pub type Result<T> = std::result::Result<T, EngineError>;
71
72#[derive(Clone, Debug)]
73pub struct HistoryEntry {
74 pub label: String,
75 pub wb: Arc<Workbook>,
76 pub selection: Selection,
77 pub retained_bytes: usize,
79}
80
81#[derive(Debug, PartialEq, Eq)]
84#[must_use = "an undo group stays open until it is ended or rolled back"]
85pub struct GroupToken {
86 id: u64,
87 doc: u64,
88}
89
90impl GroupToken {
91 pub fn doc(&self) -> u64 {
93 self.doc
94 }
95}
96
97#[derive(Clone, Debug)]
99struct OpenGroup {
100 id: u64,
101 doc: u64,
102 label: String,
103 start: usize,
105 wb: Arc<Workbook>,
106 selection: Selection,
107 redo: Vec<HistoryEntry>,
109 recorded: Option<(u64, u64)>,
112}
113
114#[derive(Debug)]
121struct GroupStep {
122 doc: u64,
123 after: Arc<Workbook>,
124 selection: Selection,
125 len: usize,
126 stepped: bool,
127}
128
129#[derive(Clone, Debug)]
131pub struct Clipboard {
132 pub wb: Arc<Workbook>,
134 pub sheet: usize,
135 pub range: RangeRef,
136 pub selected: RangeRef,
139 pub cut: bool,
140 pub doc_uid: u64,
142 pub text: String,
144 pub areas: Vec<RangeRef>,
147 pub trust: Trust,
149}
150
151#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
155#[serde(rename_all = "camelCase")]
156pub enum Trust {
157 Local,
159 FromFile,
162}
163
164impl Trust {
165 pub fn allows_code(self) -> bool {
167 self == Trust::Local
168 }
169
170 pub fn receive(self, source: Trust) -> Trust {
173 if source == Trust::FromFile { Trust::FromFile } else { self }
174 }
175}
176
177#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
179pub enum Mode {
180 #[default]
181 Ready,
182 Enter,
183 Edit,
184 Point,
185}
186
187#[derive(Clone, Debug)]
188pub struct DocState {
189 pub wb: Arc<Workbook>,
190 pub calc: Calc,
191 pub selection: Selection,
192 pub undo: Vec<HistoryEntry>,
193 pub redo: Vec<HistoryEntry>,
194 pub path: Option<String>,
195 pub saved: Arc<Workbook>,
196 pub revision: u64,
197 pub uid: u64,
200 pub title: String,
201 pub sheet_selections: Vec<Selection>,
203 pub unlocked_ranges: Vec<(String, String)>,
205 pub content_kind: Option<io::FileKind>,
208 pub scripts_trusted: bool,
213 pub trust: Trust,
218 pub touched: Option<Vec<(String, CellRef)>>,
221 pub edits: Option<Vec<(String, crate::formula::adjust::Edit)>>,
224 pub go_to_recent: Vec<String>,
227}
228
229static NEXT_UID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
230const UNDO_LIMIT: usize = 100;
231const JOURNAL_MAX_ENTRIES: usize = 10_000;
233pub(crate) const JOURNAL_MAX_BYTES: usize = 64 * 1024 * 1024;
238const UNDO_BYTES_LIMIT: usize = 128 * 1024 * 1024;
239
240fn history_retained_bytes(older: &Workbook, newer: &Workbook) -> usize {
241 let mut bytes = std::mem::size_of::<Workbook>();
242 for (index, sheet) in older.sheets.iter().enumerate() {
243 if newer.sheets.iter().any(|next| Arc::ptr_eq(sheet, next)) {
245 continue;
246 }
247 bytes = bytes.saturating_add(std::mem::size_of::<crate::model::Sheet>());
248 let empty = crate::model::CellStore::new();
249 let next = newer.sheets.get(index).map_or(&empty, |next| &next.cells);
250 bytes = bytes.saturating_add(sheet.cells.retained_bytes_against(next));
251 }
252 if !older.macros.shares(&newer.macros) {
254 bytes = bytes.saturating_add(older.macros.estimated_bytes());
255 }
256 bytes
257}
258
259fn push_history(history: &mut Vec<HistoryEntry>, label: String, wb: Arc<Workbook>, next: &Workbook, selection: Selection) {
260 push_history_with_budget(history, label, wb, next, selection, UNDO_BYTES_LIMIT);
261}
262
263fn push_history_with_budget(
264 history: &mut Vec<HistoryEntry>,
265 label: String,
266 wb: Arc<Workbook>,
267 next: &Workbook,
268 selection: Selection,
269 byte_budget: usize,
270) {
271 push_entry(history, label, wb, next, selection);
272 trim_to_budget(history, byte_budget);
273}
274
275fn push_entry(history: &mut Vec<HistoryEntry>, label: String, wb: Arc<Workbook>, next: &Workbook, selection: Selection) {
278 let retained_bytes = history_retained_bytes(&wb, next);
279 history.push(HistoryEntry { label, wb, selection, retained_bytes });
280}
281
282fn trim_to_budget(history: &mut Vec<HistoryEntry>, byte_budget: usize) {
285 let mut total = history.iter().fold(0usize, |sum, entry| sum.saturating_add(entry.retained_bytes));
286 while history.len() > 1 && (history.len() > UNDO_LIMIT || total > byte_budget) {
287 total = total.saturating_sub(history.remove(0).retained_bytes);
288 }
289}
290
291impl DocState {
292 pub fn new(mut wb: Workbook, path: Option<String>, title: String, trust: Trust) -> DocState {
296 let mut calc = Calc::new();
297 calc.recalc_all(&mut wb);
298 Self::with_calc(wb, calc, path, title, trust)
299 }
300 pub fn from_saved(wb: Workbook, path: Option<String>, title: String, trust: Trust) -> DocState {
304 let mut calc = Calc::new();
305 calc.rebuild(&wb);
306 Self::with_calc(wb, calc, path, title, trust)
307 }
308 fn with_calc(mut wb: Workbook, calc: Calc, path: Option<String>, title: String, trust: Trust) -> DocState {
309 wb.refresh_equations();
311 let scripts_trusted = !cmd::extra::has_scripts(&wb);
312 let wb = Arc::new(wb);
313 let sel = Selection::at(wb.active().map(|s| s.view_active).unwrap_or_default());
314 DocState {
315 saved: wb.clone(),
316 wb,
317 calc,
318 selection: sel,
319 undo: vec![],
320 redo: vec![],
321 path,
322 revision: 1,
323 uid: NEXT_UID.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
324 title,
325 sheet_selections: vec![],
326 unlocked_ranges: vec![],
327 content_kind: None,
328 scripts_trusted,
329 trust,
330 touched: None,
331 edits: None,
332 go_to_recent: vec![],
333 }
334 }
335 pub fn is_dirty(&self) -> bool {
336 !Arc::ptr_eq(&self.wb, &self.saved)
337 }
338 pub fn sheet_index(&self) -> usize {
339 self.wb.active_sheet()
340 }
341 pub fn store_view(&mut self, tops: &[(usize, CellRef)]) {
345 let active = self.selection.active;
346 let si = self.wb.active_sheet();
347 let changed = tops.iter().any(|(i, c)| self.wb.sheets.get(*i).is_some_and(|sh| sh.view_top_left != *c))
348 || self.wb.sheets.get(si).is_some_and(|sh| sh.view_active != active);
349 if !changed {
350 return;
351 }
352 let dirty = self.is_dirty();
353 let wb = Arc::make_mut(&mut self.wb);
354 for (i, c) in tops {
355 if let Some(sh) = wb.sheets.get_mut(*i)
356 && sh.view_top_left != *c
357 {
358 Arc::make_mut(sh).view_top_left = *c;
359 }
360 }
361 if let Some(sh) = wb.sheets.get_mut(si)
362 && sh.view_active != active
363 {
364 Arc::make_mut(sh).view_active = active;
365 }
366 if !dirty {
367 self.saved = self.wb.clone();
368 }
369 }
370 pub fn display_title(&self) -> String {
371 self.path
372 .as_deref()
373 .and_then(|p| std::path::Path::new(p).file_name())
374 .map(|n| n.to_string_lossy().to_string())
375 .unwrap_or_else(|| self.title.clone())
376 }
377}
378
379#[derive(Clone, Debug, Serialize, serde::Deserialize)]
381#[serde(default, rename_all = "camelCase")]
382pub struct Prefs {
383 pub enter_direction: String,
385 pub autocomplete: bool,
386 pub user_name: String,
387 pub default_font: String,
388 pub default_font_size: f32,
389 pub sheets_in_new_workbook: usize,
390 pub r1c1: bool,
391 pub user_dictionary: Vec<String>,
393 pub formula_bar: bool,
395 pub status_bar: StatusBarStats,
397 #[serde(deserialize_with = "recent::lenient")]
400 pub recent_functions: Vec<String>,
401}
402
403#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, serde::Deserialize)]
406#[serde(default, rename_all = "camelCase")]
407pub struct StatusBarStats {
408 pub average: bool,
409 pub count: bool,
410 pub numerical_count: bool,
411 pub minimum: bool,
412 pub maximum: bool,
413 pub sum: bool,
414}
415
416impl Default for StatusBarStats {
417 fn default() -> Self {
418 StatusBarStats { average: true, count: true, numerical_count: false, minimum: false, maximum: false, sum: true }
419 }
420}
421
422impl Default for Prefs {
423 fn default() -> Self {
424 Prefs {
425 enter_direction: "down".into(),
426 autocomplete: true,
427 user_name: "GridCraft User".into(),
428 default_font: crate::model::DEFAULT_FONT.into(),
429 default_font_size: crate::model::DEFAULT_FONT_SIZE,
430 sheets_in_new_workbook: 1,
431 r1c1: false,
432 user_dictionary: vec![],
433 formula_bar: true,
434 status_bar: StatusBarStats::default(),
435 recent_functions: recent::defaults(),
436 }
437 }
438}
439
440#[derive(Clone, Debug, PartialEq, Serialize)]
442pub enum UiRequest {
443 Dialog(String, Json),
444 Message(String),
445 EditCell(Option<String>),
447 InsertName(String),
450 OpenUrl(String),
451}
452
453pub type TextWidth = Arc<dyn Fn(&str, &model::Font, bool) -> f32 + Send + Sync>;
459
460#[derive(Default)]
461pub struct Session {
462 docs: Vec<DocState>,
463 active: usize,
465 windows: Vec<WindowState>,
467 active_window: Option<u64>,
468 next_window: u64,
470 activation: u64,
472 pub clipboard: Option<Clipboard>,
473 pub prefs: Prefs,
474 pub mode: Mode,
475 pub journal: Vec<(String, Json)>,
477 journal_sizes: Vec<usize>,
479 journal_bytes: usize,
480 pub(crate) journal_paused: u32,
483 pub ui_requests: Vec<UiRequest>,
484 pub format_painter: Option<(Arc<Workbook>, usize, RangeRef, bool)>,
486 pub last_find: Option<Json>,
488 pub hide_ruler: bool,
490 pub recording: Option<usize>,
492 pub recorder: Option<record::Recorder>,
495 depth: u32,
497 pub(crate) script_ran: Option<Vec<(String, Json)>>,
501 pub macro_approvals: std::collections::HashSet<(u64, String)>,
504 pub macro_debug: Option<macros::debug::Debugger>,
506 pub macro_recording: Option<macros::recorder::RecordingStart>,
508 pub draw_tool: String,
510 pub draw_color: String,
511 pub draw_width: f32,
512 pub comments_pane: bool,
514 pub show_all_notes: bool,
517 pub link_policy: links::LinkPolicy,
520 groups: Vec<OpenGroup>,
522 next_group: u64,
523 group_epoch: u64,
525 group_step: Option<GroupStep>,
527 marks: Vec<(u64, usize)>,
531 pub text_width: Option<TextWidth>,
534}
535
536impl Session {
537 pub fn new() -> Session {
538 Session::default()
539 }
540 pub fn documents(&self) -> &[DocState] {
541 &self.docs
542 }
543 pub fn active_index(&self) -> usize {
544 self.active
545 }
546 pub fn set_active(&mut self, i: usize) {
549 let Some(uid) = self.docs.get(i).map(|d| d.uid) else { return };
550 if self.active == i && self.active_window.and_then(|w| self.window(w)).is_some_and(|w| w.doc_uid == uid) {
551 return;
552 }
553 if let Some(w) = self.doc_window(uid) {
554 self.activate_window(w);
555 }
556 }
557 pub fn active(&self) -> Option<&DocState> {
558 self.docs.get(self.active)
559 }
560 pub fn document_index(&self, uid: u64) -> Option<usize> {
562 self.docs.iter().position(|d| d.uid == uid)
563 }
564 pub fn active_mut(&mut self) -> Option<&mut DocState> {
565 self.docs.get_mut(self.active)
566 }
567 pub fn doc(&self) -> Result<&DocState> {
568 self.active().ok_or(EngineError::NoDocument)
569 }
570 pub fn doc_mut(&mut self) -> Result<&mut DocState> {
571 self.active_mut().ok_or(EngineError::NoDocument)
572 }
573 pub fn locked_cell_message(&self, sheet: usize, at: CellRef) -> Option<String> {
577 let d = self.active()?;
578 let sh = d.wb.sheet(sheet)?;
579 if !sh.is_protected() || !d.wb.styles.get(sh.style_id(at)).protection.locked {
580 return None;
581 }
582 cmd::locked_cell_error(sh, at, &d.unlocked_ranges)
583 }
584 pub fn add_document(&mut self, d: DocState) -> usize {
586 let uid = d.uid;
587 self.docs.push(d);
588 let w = self.push_window(uid);
589 self.activate_window(w);
590 self.docs.len() - 1
591 }
592 pub fn close_document(&mut self, i: usize) {
594 if i < self.docs.len() {
595 let d = self.docs.remove(i);
596 self.groups.retain(|g| g.doc != d.uid);
597 if self.macro_debug.as_ref().is_some_and(|m| m.doc == d.uid) {
599 self.macro_debug = None;
600 }
601 self.windows.retain(|w| w.doc_uid != d.uid);
602 if self.active_window.is_some_and(|w| self.window(w).is_none()) {
603 self.activate_recent();
604 } else if let Some(di) = self.active_window.and_then(|w| self.window(w)).and_then(|w| self.document_index(w.doc_uid)) {
605 self.active = di;
606 }
607 }
608 }
609 pub fn new_workbook(&mut self) -> usize {
611 let n = (1..).find(|n| !self.docs.iter().any(|d| d.title == format!("Book{n}") && d.path.is_none())).unwrap_or(1);
612 let mut wb = Workbook::new();
613 wb.props.author = self.prefs.user_name.clone();
614 for _ in 1..self.prefs.sheets_in_new_workbook.clamp(1, 255) {
615 let name = wb.next_sheet_name();
616 wb.sheets.push(Arc::new(crate::model::Sheet::new(name)));
617 }
618 self.add_document(DocState::new(wb, None, format!("Book{n}"), Trust::Local))
619 }
620
621 fn push_journal(&mut self, id: &str, params: Json) {
624 let size = |e: &(String, Json)| serde_json::to_vec(e).map_or(0, |v| v.len());
625 if self.journal_sizes.len() != self.journal.len() {
627 self.journal_sizes = self.journal.iter().map(size).collect();
628 self.journal_bytes = self.journal_sizes.iter().sum();
629 }
630 let entry = (id.to_string(), params);
631 let entry_size = size(&entry);
632 self.journal.push(entry);
633 self.journal_sizes.push(entry_size);
634 self.journal_bytes = self.journal_bytes.saturating_add(entry_size);
635 let len = self.journal.len();
636 let mut n = if len > JOURNAL_MAX_ENTRIES { len - JOURNAL_MAX_ENTRIES / 2 } else { 0 };
637 let mut kept = self.journal_bytes.saturating_sub(self.journal_sizes.iter().take(n).sum());
638 if self.journal_bytes > JOURNAL_MAX_BYTES {
639 while kept > JOURNAL_MAX_BYTES / 2 && n + 1 < len {
641 kept = kept.saturating_sub(self.journal_sizes.get(n).copied().unwrap_or(0));
642 n += 1;
643 }
644 }
645 if n > 0 {
646 self.journal.drain(..n);
647 self.journal_sizes.drain(..n);
648 self.journal_bytes = kept;
649 self.recording = self.recording.map(|r| r.saturating_sub(n));
650 }
651 }
652
653 pub(crate) fn truncate_journal(&mut self, keep: usize) {
655 if self.journal_sizes.len() == self.journal.len() {
656 self.journal_sizes.truncate(keep);
657 self.journal_bytes = self.journal_sizes.iter().sum();
658 }
659 self.journal.truncate(keep);
660 self.recording = self.recording.map(|r| r.min(self.journal.len()));
661 }
662
663 pub fn commands(&self) -> Vec<CommandInfo> {
664 command_specs().iter().map(|c| c.info(self)).collect()
665 }
666
667 pub fn execute(&mut self, id: &str, params: Json) -> Result<Json> {
671 let spec = find_command(id).ok_or_else(|| EngineError::UnknownCommand(id.to_string()))?;
672 if let Err(why) = (spec.enabled)(self) {
673 return Err(EngineError::Disabled(id.to_string(), why));
674 }
675 if id != "macro.debug" {
678 macros::debug::drop_if_ended(self);
679 }
680 let query = !spec.journal && !spec.undoable;
681 let locked = self.active().is_some_and(|d| self.macro_debug.as_ref().is_some_and(|m| m.locks(d.uid) && !m.stepping()));
687 if locked && !macros::debug::runs_while_locked(id, query) {
688 return Err(EngineError::Disabled(id.to_string(), macros::debug::BUSY.into()));
689 }
690 let before = self.active().map(|d| (d.uid, d.wb.clone(), d.selection.clone()));
691 let paused_before = self.journal_paused;
694 let params = if params.is_null() { Json::Object(Default::default()) } else { params };
695 let (first_group, group_epoch) = (self.next_group, self.group_epoch);
696 let start = before.as_ref().and_then(|(uid, ..)| self.docs.iter().find(|d| d.uid == *uid).map(|d| (self.in_group(*uid), d.undo.len())));
698 let history_before = before.as_ref().filter(|_| !query).and_then(|(uid, ..)| {
704 #[cfg(test)]
705 tests::HISTORY_COPIES.with(|n| n.set(n.get().saturating_add(1)));
706 self.docs.iter().find(|d| d.uid == *uid).map(|d| (d.undo.clone(), d.redo.clone()))
707 });
708 let groups_before: Vec<(u64, String, Arc<Workbook>, Selection)> = match &before {
710 Some((uid, ..)) => {
711 self.groups.iter().filter(|g| g.doc == *uid).map(|g| (g.id, g.label.clone(), g.wb.clone(), g.selection.clone())).collect()
712 }
713 None => Vec::new(),
714 };
715 let place = (self.depth == 0 && self.recorder.is_some()).then(|| match self.active() {
717 Some(d) => (d.uid, d.wb.active_sheet(), d.wb.active().map(|sh| sh.name.clone()).unwrap_or_default(), d.selection.clone()),
718 None => (0, 0, String::new(), Selection::default()),
719 });
720 let doc_uid = before.as_ref().map(|(uid, ..)| *uid);
721 let requests_before = self.ui_requests.len();
722 let marked = doc_uid.zip(start).map(|(uid, (_, len))| (uid, len));
723 self.marks.extend(marked);
724 self.depth = self.depth.saturating_add(1);
725 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| (spec.run)(self, ¶ms)));
726 self.depth = self.depth.saturating_sub(1);
727 if marked.is_some() {
728 self.marks.pop();
729 }
730 let result = match result {
731 Ok(r) => r,
732 Err(p) => {
733 let msg = p
734 .downcast_ref::<&str>()
735 .map(ToString::to_string)
736 .or_else(|| p.downcast_ref::<String>().cloned())
737 .unwrap_or_else(|| "panic".into());
738 self.groups.retain(|g| g.id < first_group);
741 let group_open = before.as_ref().is_some_and(|(uid, ..)| self.groups.iter().any(|g| g.doc == *uid));
742 if let Some((uid, wb, sel)) = &before
743 && let Some(d) = self.docs.iter_mut().find(|d| d.uid == *uid)
744 {
745 d.wb = wb.clone();
746 d.selection = sel.clone();
747 if let Some((undo, redo)) = history_before {
748 d.undo = undo;
749 d.redo = redo;
750 }
751 if !group_open
755 && let Some((_, label, wb, selection)) = groups_before.into_iter().min_by_key(|g| g.0)
756 && !Arc::ptr_eq(&wb, &d.wb)
757 {
758 push_entry(&mut d.undo, label, wb, &d.wb, selection);
759 d.redo.clear();
760 }
761 d.calc.rebuild(&d.wb);
762 }
763 if self.journal_paused != paused_before {
764 self.journal_paused = paused_before;
765 for d in self.docs.iter_mut() {
766 d.touched = None;
767 }
768 }
769 if self.depth == 0 {
770 macros::debug::recover(self);
773 self.trim_history();
774 self.group_step = None;
775 }
776 return Err(EngineError::Internal(id.to_string(), msg));
777 }
778 };
779 if result.is_err() {
783 self.rollback_groups_from(first_group);
784 }
785 if spec.journal && result.is_ok() {
786 let opened_dialog = self.ui_requests.get(requests_before..).is_some_and(|new| new.iter().any(|q| matches!(q, UiRequest::Dialog(..))));
794 let changed = before.as_ref().is_some_and(|(uid, wb, _)| self.docs.iter().any(|d| d.uid == *uid && !Arc::ptr_eq(&d.wb, wb)));
795 if let Some((doc, sheet, sheet_name, selection)) = place.filter(|_| (!opened_dialog || changed) && !record::NOT_RECORDED.contains(&id))
796 && let Some(rec) = self.recorder.as_mut()
797 {
798 let params = match &result {
801 Ok(r) if params.is_object() => record::with_toggle_state(id, cmd::journal_params(¶ms), r),
802 _ => cmd::journal_params(¶ms),
803 };
804 let kept = rec.push(record::RecordedStep { doc, sheet, sheet_name, selection, id: id.to_string(), params });
805 if let Some(ran) = self.script_ran.take()
808 && kept
809 && matches!(id, "automate.runScript" | "macro.run")
810 {
811 rec.attach_ran(ran);
812 }
813 }
814 if self.journal_paused == 0 {
815 self.push_journal(id, cmd::journal_params(¶ms));
816 recent::note(&mut self.prefs.recent_functions, id, ¶ms);
817 }
818 }
819 let epoch_changed = self.group_epoch != group_epoch;
824 let in_group_now = before.as_ref().is_some_and(|(uid, ..)| self.in_group(*uid));
825 let was_open = start.is_some_and(|(was_open, _)| was_open);
826 if spec.undoable
827 && let Some((uid, wb, sel)) = before
828 && let Some(d) = self.docs.iter_mut().find(|d| d.uid == uid)
829 && !Arc::ptr_eq(&d.wb, &wb)
830 {
831 let label = spec.label.trim_end_matches('…').to_string();
832 if in_group_now {
833 if !was_open
837 && let Some((_, len)) = start
838 && let Some(g) = self.groups.iter_mut().filter(|g| g.doc == uid && g.id >= first_group).min_by_key(|g| g.id)
839 {
840 g.start = len;
841 g.wb = wb;
842 g.selection = sel;
843 }
844 } else if epoch_changed && was_open {
845 if let Some(step) = self.group_step.as_ref().filter(|g| g.doc == uid)
848 && (d.undo.len() > step.len || !Arc::ptr_eq(&step.after, &d.wb))
849 {
850 d.undo.truncate(step.len);
851 push_entry(&mut d.undo, label, step.after.clone(), &d.wb, step.selection.clone());
852 d.redo.clear();
853 }
854 } else {
855 let mut label = label;
856 if epoch_changed && let Some((_, len)) = start {
857 if d.undo.len() == len + 1
863 && let Some(last) = d.undo.last()
864 && Arc::ptr_eq(&last.wb, &wb)
865 && self.group_step.as_ref().is_some_and(|g| g.doc == uid && g.stepped && Arc::ptr_eq(&g.after, &d.wb))
866 {
867 label = last.label.clone();
868 }
869 d.undo.truncate(len);
870 }
871 push_entry(&mut d.undo, label, wb, &d.wb, sel);
872 d.redo.clear();
873 }
874 d.revision += 1;
877 }
878 if self.depth == 0 {
881 self.trim_history();
882 self.group_step = None;
883 } else if let Some(uid) = doc_uid {
884 self.bound_tail(uid);
885 }
886 result
887 }
888
889 pub fn grouped<R>(&mut self, label: &str, f: impl FnOnce(&mut Session) -> Result<R>) -> Result<R> {
893 let token = self.group_begin(label)?;
894 let doc = token.doc();
895 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| f(self))) {
898 Ok(Ok(r)) => match self.group_end(token) {
899 Ok(_) => Ok(r),
900 Err(_) if !self.docs.iter().any(|d| d.uid == doc) => Ok(r),
903 Err(e) => Err(e),
904 },
905 Ok(Err(e)) => {
906 let _ = self.group_rollback(token);
909 Err(e)
910 }
911 Err(panic) => {
912 let _ = self.group_rollback(token);
913 std::panic::resume_unwind(panic)
914 }
915 }
916 }
917
918 pub fn group_begin(&mut self, label: &str) -> Result<GroupToken> {
923 let d = self.doc()?;
924 let g = OpenGroup {
925 id: self.next_group,
926 doc: d.uid,
927 label: label.trim_end_matches('…').to_string(),
928 start: d.undo.len(),
929 wb: d.wb.clone(),
930 selection: d.selection.clone(),
931 redo: d.redo.clone(),
932 recorded: self.recorder.as_ref().map(record::Recorder::mark),
933 };
934 let token = GroupToken { id: g.id, doc: g.doc };
935 self.next_group = self.next_group.saturating_add(1);
936 self.group_epoch = self.group_epoch.wrapping_add(1);
937 self.groups.push(g);
938 Ok(token)
939 }
940
941 pub fn group_end(&mut self, token: GroupToken) -> Result<bool> {
945 let g = self.take_group(&token)?;
946 let outer = self.in_group(g.doc);
947 let Some(d) = self.docs.iter_mut().find(|d| d.uid == g.doc) else { return Ok(false) };
948 d.undo.truncate(g.start);
950 let changed = !Arc::ptr_eq(&d.wb, &g.wb);
951 if changed && !outer {
952 push_entry(&mut d.undo, g.label, g.wb, &d.wb, g.selection);
953 d.redo.clear();
954 d.revision += 1;
955 }
956 if !outer {
957 self.group_step =
958 Some(GroupStep { doc: g.doc, after: d.wb.clone(), selection: d.selection.clone(), len: d.undo.len(), stepped: changed });
959 }
960 if self.depth == 0 {
962 self.trim_history();
963 }
964 Ok(changed)
965 }
966
967 pub fn abort_groups(&mut self, doc: u64) -> bool {
973 let Some(id) = self.groups.iter().filter(|g| g.doc == doc).map(|g| g.id).min() else { return false };
974 self.group_rollback(GroupToken { id, doc }).is_ok()
976 }
977
978 fn rollback_groups_from(&mut self, first: u64) {
981 let opened: Vec<(u64, u64)> = self.groups.iter().filter(|g| g.id >= first).map(|g| (g.id, g.doc)).collect();
982 for (id, doc) in opened {
983 if self.groups.iter().any(|g| g.id == id) {
985 let _ = self.group_rollback(GroupToken { id, doc });
986 }
987 }
988 }
989
990 fn bound_tail(&mut self, uid: u64) {
996 let Some(d) = self.docs.iter_mut().find(|d| d.uid == uid) else { return };
997 let marks = self.marks.iter().filter(|(doc, _)| *doc == uid).map(|(_, len)| *len);
998 let groups = self.groups.iter().filter(|g| g.doc == uid).map(|g| g.start);
999 let step = self.group_step.as_ref().filter(|g| g.doc == uid).map(|g| g.len);
1000 let mark = marks.chain(groups).chain(step).max().unwrap_or(0).min(d.undo.len());
1001 let mut bytes = d.undo.iter().skip(mark).fold(0usize, |sum, e| sum.saturating_add(e.retained_bytes));
1002 while d.undo.len() > mark + 1 && (d.undo.len() - mark > UNDO_LIMIT || bytes > UNDO_BYTES_LIMIT) {
1003 bytes = bytes.saturating_sub(d.undo.remove(mark).retained_bytes);
1004 }
1005 }
1006
1007 fn trim_history(&mut self) {
1010 let groups = &self.groups;
1011 for d in self.docs.iter_mut().filter(|d| !groups.iter().any(|g| g.doc == d.uid)) {
1012 trim_to_budget(&mut d.undo, UNDO_BYTES_LIMIT);
1013 }
1014 }
1015
1016 pub fn group_rollback(&mut self, token: GroupToken) -> Result<()> {
1019 let g = self.take_group(&token)?;
1020 let Some(d) = self.docs.iter_mut().find(|d| d.uid == g.doc) else { return Ok(()) };
1021 if !Arc::ptr_eq(&d.wb, &g.wb) {
1022 d.wb = g.wb;
1023 d.calc.rebuild(&d.wb);
1024 }
1025 d.selection = g.selection;
1026 d.undo.truncate(g.start);
1027 d.redo = g.redo;
1028 d.revision += 1;
1029 if let Some(rec) = self.recorder.as_mut() {
1032 rec.cut_back(g.recorded.unwrap_or((0, 0)), g.doc);
1033 }
1034 let kept = self.group_step.as_ref().is_some_and(|st| st.doc == g.doc && st.len == g.start && Arc::ptr_eq(&st.after, &d.wb));
1037 if !kept && !self.groups.iter().any(|o| o.doc == g.doc) {
1038 self.group_step = Some(GroupStep { doc: g.doc, after: d.wb.clone(), selection: d.selection.clone(), len: g.start, stepped: false });
1039 }
1040 Ok(())
1041 }
1042
1043 pub fn start_recording(&mut self) {
1046 self.recorder = Some(record::Recorder::default());
1047 }
1048
1049 pub fn stop_recording(&mut self) -> Option<record::Recorder> {
1051 self.recorder.take()
1052 }
1053
1054 pub fn is_recording(&self) -> bool {
1056 self.recorder.is_some()
1057 }
1058
1059 pub fn depth(&self) -> u32 {
1062 self.depth
1063 }
1064
1065 #[cfg(test)]
1068 pub(crate) fn execute_nested(&mut self, id: &str, params: Json) -> Result<Json> {
1069 self.depth = self.depth.saturating_add(1);
1070 let r = self.execute(id, params);
1071 self.depth = self.depth.saturating_sub(1);
1072 r
1073 }
1074
1075 pub fn group_is_open(&self, token: &GroupToken) -> bool {
1077 self.groups.iter().any(|g| g.id == token.id)
1078 }
1079
1080 pub fn in_group(&self, doc: u64) -> bool {
1082 self.groups.iter().any(|g| g.doc == doc)
1083 }
1084
1085 fn take_group(&mut self, token: &GroupToken) -> Result<OpenGroup> {
1087 let ended = || EngineError::Other("That undo group has already ended.".into());
1088 let at = self.groups.iter().position(|g| g.id == token.id).ok_or_else(ended)?;
1089 let mut later = self.groups.split_off(at).into_iter();
1090 let g = later.next().ok_or_else(ended)?;
1091 self.groups.extend(later.filter(|o| o.doc != g.doc));
1092 self.group_epoch = self.group_epoch.wrapping_add(1);
1093 Ok(g)
1094 }
1095
1096 pub fn run(&mut self, id: &str, params: Json) -> std::result::Result<Json, String> {
1097 self.execute(id, params).map_err(|e| e.to_string())
1098 }
1099
1100 pub fn take_ui_requests(&mut self) -> Vec<UiRequest> {
1101 std::mem::take(&mut self.ui_requests)
1102 }
1103
1104 pub fn execute_headless(&mut self, id: &str, params: Json) -> Result<Json> {
1108 let r = self.execute(id, params);
1109 let requests = self.take_ui_requests();
1110 let asked_for_file = requests.iter().any(|q| matches!(q, UiRequest::Dialog(name, _) if name == "open" || name == "saveAs"));
1111 if r.is_ok() && asked_for_file {
1114 let what = match id {
1115 "file.open" => "`path` or `base64`",
1116 "data.getData" | "data.fromTextCsv" => "`path` or `text`",
1117 _ => "`path`",
1118 };
1119 let msg = format!("missing {what}: programmatic calls never open dialogs");
1120 return Err(EngineError::BadParams { cmd: id.to_string(), msg });
1121 }
1122 r
1123 }
1124}
1125
1126pub fn active_sheet(s: &Session) -> Option<&crate::model::Sheet> {
1128 s.active().and_then(|d| d.wb.active())
1129}
1130
1131pub fn cell(r: u32, c: u32) -> CellRef {
1132 CellRef::new(r, c)
1133}
1134
1135#[cfg(test)]
1136mod tests;