Skip to main content

formualizer_eval/engine/graph/editor/
undo_engine.rs

1//! Basic Undo/Redo engine scaffold using ChangeLog groups.
2use super::change_log::{ChangeEvent, ChangeEventMeta, ChangeLog};
3use super::vertex_editor::VertexEditor;
4use crate::engine::graph::DependencyGraph;
5use crate::engine::graph::editor::vertex_editor::EditorError;
6
7#[derive(Debug, Clone)]
8pub struct UndoBatchItem {
9    pub event: ChangeEvent,
10    pub meta: ChangeEventMeta,
11}
12
13#[derive(Debug, Default)]
14pub struct UndoEngine {
15    /// Stack of applied groups (their last event index snapshot) for redo separation
16    undone: Vec<Vec<UndoBatchItem>>, // redo stack stores full event batches
17
18    /// Journal-based undo/redo stack for atomic actions.
19    actions_done: Vec<crate::engine::ActionJournal>,
20    actions_undone: Vec<crate::engine::ActionJournal>,
21}
22
23impl UndoEngine {
24    pub fn new() -> Self {
25        Self {
26            undone: Vec::new(),
27            actions_done: Vec::new(),
28            actions_undone: Vec::new(),
29        }
30    }
31
32    /// Record a committed atomic action journal for future undo/redo.
33    pub fn push_action(&mut self, journal: crate::engine::ActionJournal) {
34        self.actions_done.push(journal);
35        self.actions_undone.clear();
36    }
37
38    pub fn pop_undo_action(&mut self) -> Option<crate::engine::ActionJournal> {
39        self.actions_done.pop()
40    }
41
42    pub fn push_redo_action(&mut self, journal: crate::engine::ActionJournal) {
43        self.actions_undone.push(journal);
44    }
45
46    pub fn pop_redo_action(&mut self) -> Option<crate::engine::ActionJournal> {
47        self.actions_undone.pop()
48    }
49
50    pub fn push_done_action(&mut self, journal: crate::engine::ActionJournal) {
51        self.actions_done.push(journal);
52    }
53
54    /// Undo last group in the provided change log, applying inverses through a VertexEditor.
55    pub fn undo(
56        &mut self,
57        graph: &mut DependencyGraph,
58        log: &mut ChangeLog,
59    ) -> Result<Vec<UndoBatchItem>, EditorError> {
60        let idxs = log.last_group_indices();
61        if idxs.is_empty() {
62            return Ok(Vec::new());
63        }
64        let batch: Vec<UndoBatchItem> = idxs
65            .iter()
66            .map(|i| UndoBatchItem {
67                event: log.events()[*i].clone(),
68                meta: log.event_meta(*i).cloned().unwrap_or_default(),
69            })
70            .collect();
71        let max_idx = *idxs.iter().max().unwrap();
72        if max_idx + 1 == log.events().len() {
73            let truncate_to = idxs.iter().min().copied().unwrap();
74            log.truncate(truncate_to);
75        } else {
76            return Err(EditorError::TransactionFailed {
77                reason: "Non-tail undo not supported".into(),
78            });
79        }
80        graph.authority_set_replay(crate::engine::authority::history::Replay::Undo);
81        let replayed = (|| {
82            let mut editor = VertexEditor::new(graph);
83            for (i, item) in batch.iter().enumerate().rev() {
84                if let Some(description) =
85                    super::change_log::compound_start_description(i, |j| &batch[j].event)
86                {
87                    editor.inverse_compound_end(description);
88                }
89                editor.apply_inverse(item.event.clone())?;
90            }
91            Ok::<_, EditorError>(())
92        })();
93        graph.authority_set_replay(crate::engine::authority::history::Replay::Forward);
94        replayed?;
95
96        // Keep a copy for redo, but also return the batch so callers can mirror side effects.
97        self.undone.push(batch.clone());
98        Ok(batch)
99    }
100
101    pub(crate) fn pending_redo_events(&self) -> Vec<ChangeEvent> {
102        self.undone
103            .last()
104            .into_iter()
105            .flat_map(|batch| batch.iter().map(|item| item.event.clone()))
106            .collect()
107    }
108
109    pub fn redo(
110        &mut self,
111        graph: &mut DependencyGraph,
112        log: &mut ChangeLog,
113    ) -> Result<Vec<UndoBatchItem>, EditorError> {
114        if let Some(batch) = self.undone.pop() {
115            log.begin_compound("redo".to_string());
116            // Return value for callers (e.g. Arrow mirroring) must remain available even though
117            // we apply events by value below.
118            let ret = batch.clone();
119
120            graph.authority_set_replay(crate::engine::authority::history::Replay::Redo);
121            let replayed = (|| {
122                for item in batch {
123                    // Re-log original event for audit consistency
124                    log.record_with_meta(item.event.clone(), item.meta.clone());
125                    match item.event {
126                        ChangeEvent::SetValue { addr, new, .. } => {
127                            let mut editor = VertexEditor::new(graph);
128                            editor.set_cell_value(addr, new);
129                        }
130                        ChangeEvent::SetFormula { addr, new, .. } => {
131                            let mut editor = VertexEditor::new(graph);
132                            editor.set_cell_formula(addr, new);
133                        }
134                        ChangeEvent::AddVertex {
135                            coord,
136                            sheet_id,
137                            kind,
138                            ..
139                        } => {
140                            let mut editor = VertexEditor::new(graph);
141                            let meta = crate::engine::graph::editor::vertex_editor::VertexMeta::new(
142                                coord.row(),
143                                coord.col(),
144                                sheet_id,
145                                kind.unwrap_or(crate::engine::vertex::VertexKind::Cell),
146                            );
147                            editor.try_add_vertex(meta)?;
148                        }
149                        ChangeEvent::RemoveVertex {
150                            coord, sheet_id, ..
151                        } => {
152                            if let (Some(c), Some(sid)) = (coord, sheet_id) {
153                                let mut editor = VertexEditor::new(graph);
154                                let cell_ref = crate::reference::CellRef::new(
155                                    sid,
156                                    crate::reference::Coord::new(c.row(), c.col(), true, true),
157                                );
158                                let _ = editor.remove_vertex_at(cell_ref);
159                            }
160                        }
161                        ChangeEvent::VertexMoved { id, new_coord, .. } => {
162                            let mut editor = VertexEditor::new(graph);
163                            let _ = editor.move_vertex(id, new_coord);
164                        }
165                        ChangeEvent::FormulaAdjusted { id, new_ast, .. } => {
166                            // Keep it simple: apply directly by vertex id.
167                            // (This is used for structural ops formula rewrites.)
168                            let _ = graph.update_vertex_formula(id, new_ast);
169                            graph.mark_vertex_dirty(id);
170                        }
171                        ChangeEvent::DefineName {
172                            name,
173                            scope,
174                            definition,
175                        } => {
176                            let mut editor = VertexEditor::new(graph);
177                            let _ = editor.define_name(&name, definition, scope);
178                        }
179                        ChangeEvent::UpdateName {
180                            name,
181                            scope,
182                            new_definition,
183                            ..
184                        } => {
185                            let mut editor = VertexEditor::new(graph);
186                            let _ = editor.update_name(&name, new_definition, scope);
187                        }
188                        ChangeEvent::DeleteName { name, scope, .. } => {
189                            let mut editor = VertexEditor::new(graph);
190                            let _ = editor.delete_name(&name, scope);
191                        }
192                        ChangeEvent::NamedRangeAdjusted {
193                            name,
194                            scope,
195                            new_definition,
196                            ..
197                        } => {
198                            let mut editor = VertexEditor::new(graph);
199                            let _ = editor.update_name(&name, new_definition, scope);
200                        }
201                        ChangeEvent::SpillCommitted { anchor, new, .. } => {
202                            let _ = graph.commit_spill_region_atomic_with_fault(
203                                anchor,
204                                new.target_cells,
205                                new.values,
206                                None,
207                            );
208                        }
209                        ChangeEvent::SpillCleared { anchor, .. } => {
210                            graph.clear_spill_region(anchor);
211                        }
212                        ChangeEvent::SetRowVisibility { .. } => {
213                            // Engine-level sidecar metadata; applied by Engine undo/redo wrappers.
214                        }
215                        ChangeEvent::CompoundStart { description, .. } => {
216                            // A structural edit's marker shifts the retired-id
217                            // side table (decision 27).
218                            graph.replay_structural_marker(&description, true);
219                        }
220                        _ => {}
221                    }
222                }
223                Ok::<_, EditorError>(())
224            })();
225            graph.authority_set_replay(crate::engine::authority::history::Replay::Forward);
226            replayed?;
227            log.end_compound();
228            Ok(ret)
229        } else {
230            Ok(Vec::new())
231        }
232    }
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238    use crate::engine::EvalConfig;
239    use crate::engine::graph::editor::change_log::ChangeLog;
240    use crate::reference::{CellRef, Coord};
241    use formualizer_common::LiteralValue;
242
243    fn create_test_graph() -> DependencyGraph {
244        DependencyGraph::new_with_config(EvalConfig::default())
245    }
246
247    #[test]
248    fn test_undo_redo_single_value() {
249        let mut graph = create_test_graph();
250        let mut log = ChangeLog::new();
251        {
252            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
253            let cell = CellRef {
254                sheet_id: 0,
255                coord: Coord::new(1, 1, true, true),
256            };
257            editor.set_cell_value(cell, LiteralValue::Number(10.0));
258        }
259        assert_eq!(log.len(), 1);
260        let mut undo = UndoEngine::new();
261        undo.undo(&mut graph, &mut log).unwrap();
262        assert_eq!(log.len(), 0); // event removed (simplified policy)
263        // Redo
264        undo.redo(&mut graph, &mut log).unwrap();
265        assert!(!log.is_empty());
266    }
267
268    #[test]
269    fn test_undo_redo_row_shift() {
270        let mut graph = create_test_graph();
271        let mut log = ChangeLog::new();
272        {
273            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
274            // Seed some cells
275            for r in [5u32, 6u32, 10u32] {
276                let cell = CellRef {
277                    sheet_id: 0,
278                    coord: Coord::new(r, 1, true, true),
279                };
280                // Formula cells: value cells have no vertex to move
281                // (decision 27).
282                editor.set_cell_formula(
283                    cell,
284                    formualizer_parse::parser::parse(format!("={r}")).unwrap(),
285                );
286            }
287        }
288        log.clear(); // focus on shift only
289        {
290            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
291            editor.insert_rows(0, 6, 2).unwrap(); // shift rows >=6 down by 2
292        }
293        assert!(
294            log.events()
295                .iter()
296                .any(|e| matches!(e, ChangeEvent::VertexMoved { .. }))
297        );
298        let moved_count_before = log
299            .events()
300            .iter()
301            .filter(|e| matches!(e, ChangeEvent::VertexMoved { .. }))
302            .count();
303        let mut undo = UndoEngine::new();
304        undo.undo(&mut graph, &mut log).unwrap();
305        assert_eq!(log.events().len(), 0); // group removed
306        undo.redo(&mut graph, &mut log).unwrap();
307        let moved_count_after = log
308            .events()
309            .iter()
310            .filter(|e| matches!(e, ChangeEvent::VertexMoved { .. }))
311            .count();
312        assert_eq!(moved_count_before, moved_count_after);
313    }
314
315    #[test]
316    fn test_undo_redo_spill_clear_on_scalar_edit_restores_registry_and_cells() {
317        let mut graph = create_test_graph();
318        let sheet_id = graph.sheet_id_mut("Sheet1");
319
320        let anchor_cell = CellRef::new(sheet_id, Coord::new(0, 0, true, true));
321        let anchor_vid = {
322            let mut editor = VertexEditor::new(&mut graph);
323            // A formula anchors a spill (value cells have no vertex,
324            // decision 27; this used a value cell's vertex).
325            editor.set_cell_formula(anchor_cell, formualizer_parse::parser::parse("=0").unwrap())
326        };
327
328        let target_cells = vec![
329            CellRef::new(sheet_id, Coord::new(0, 0, true, true)),
330            CellRef::new(sheet_id, Coord::new(0, 1, true, true)),
331            CellRef::new(sheet_id, Coord::new(1, 0, true, true)),
332            CellRef::new(sheet_id, Coord::new(1, 1, true, true)),
333        ];
334        let values = vec![
335            vec![LiteralValue::Number(1.0), LiteralValue::Number(2.0)],
336            vec![LiteralValue::Number(3.0), LiteralValue::Number(4.0)],
337        ];
338        graph
339            .commit_spill_region_atomic_with_fault(anchor_vid, target_cells.clone(), values, None)
340            .unwrap();
341
342        assert!(graph.spill_registry_has_anchor(anchor_vid));
343
344        let mut log = ChangeLog::new();
345        {
346            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
347            // Scalar edit of the anchor should clear spill children + ownership.
348            editor.set_cell_value(anchor_cell, LiteralValue::Number(9.0));
349        }
350
351        assert!(!graph.spill_registry_has_anchor(anchor_vid));
352        assert_eq!(graph.spill_registry_counts(), (0, 0));
353
354        let mut undo = UndoEngine::new();
355        undo.undo(&mut graph, &mut log).unwrap();
356
357        assert!(graph.spill_registry_has_anchor(anchor_vid));
358        for cell in &target_cells {
359            assert_eq!(
360                graph.spill_registry_anchor_for_cell(*cell),
361                Some(anchor_vid)
362            );
363        }
364        // Graph does not cache spill child values in Arrow-truth mode; the contract here is
365        // that the spill registry ownership is restored by undo.
366
367        // Redo should clear the spill again.
368        undo.redo(&mut graph, &mut log).unwrap();
369        assert!(!graph.spill_registry_has_anchor(anchor_vid));
370        assert_eq!(graph.spill_registry_counts(), (0, 0));
371    }
372
373    #[test]
374    fn test_undo_depth_truncates_gracefully_under_changelog_cap() {
375        let mut graph = create_test_graph();
376        let sheet_id = graph.sheet_id_mut("Sheet1");
377        let mut log = ChangeLog::with_max_changelog_events(3);
378
379        // Record 5 independent edits; cap keeps only the last 3.
380        for i in 0..5u32 {
381            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
382            let cell = CellRef::new(sheet_id, Coord::new(i, 0, true, true));
383            editor.set_cell_value(cell, LiteralValue::Number(i as f64));
384        }
385        assert_eq!(log.len(), 3);
386
387        let mut undo = UndoEngine::new();
388        undo.undo(&mut graph, &mut log).unwrap();
389        undo.undo(&mut graph, &mut log).unwrap();
390        undo.undo(&mut graph, &mut log).unwrap();
391        // Beyond retained history: no-op, should not error.
392        undo.undo(&mut graph, &mut log).unwrap();
393        assert_eq!(log.len(), 0);
394    }
395
396    #[test]
397    fn test_undo_redo_column_shift() {
398        let mut graph = create_test_graph();
399        let mut log = ChangeLog::new();
400        {
401            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
402            for c in [3u32, 4u32, 8u32] {
403                let cell = CellRef {
404                    sheet_id: 0,
405                    coord: Coord::new(1, c, true, true),
406                };
407                // Formula cells: value cells have no vertex to move
408                // (decision 27).
409                editor.set_cell_formula(
410                    cell,
411                    formualizer_parse::parser::parse(format!("={c}")).unwrap(),
412                );
413            }
414        }
415        log.clear();
416        {
417            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
418            editor.insert_columns(0, 5, 2).unwrap();
419        }
420        assert!(
421            log.events()
422                .iter()
423                .any(|e| matches!(e, ChangeEvent::VertexMoved { .. }))
424        );
425        let mut undo = UndoEngine::new();
426        undo.undo(&mut graph, &mut log).unwrap();
427        assert_eq!(log.events().len(), 0);
428    }
429
430    // Reclassified (M5, internal representation): asserts legacy's edge lists
431    // through a RemoveVertex undo (`ChangeEvent::RemoveVertex` edge fields are a
432    // decision-8 removal); values after undo are covered by the undo tests.
433    #[ignore = "M5 legacy-internal: legacy edge lists across RemoveVertex undo"]
434    #[test]
435    fn test_remove_vertex_dependency_roundtrip() {
436        use formualizer_parse::parser::parse;
437        let mut graph = create_test_graph();
438        let mut log = ChangeLog::new();
439        let (a1_cell, a2_cell) = (
440            CellRef {
441                sheet_id: 0,
442                coord: Coord::new(0, 0, true, true), // A1 internal
443            },
444            CellRef {
445                sheet_id: 0,
446                coord: Coord::new(1, 0, true, true), // A2 internal
447            },
448        );
449        let a2_id;
450        {
451            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
452            editor.set_cell_value(a1_cell, LiteralValue::Number(10.0));
453            a2_id = editor.set_cell_formula(a2_cell, parse("=A1").unwrap());
454        }
455        // Ensure dependency exists
456        let deps_before = graph.get_dependencies(a2_id);
457        assert!(!deps_before.is_empty());
458        // Clear log then remove A1
459        log.clear();
460        {
461            // Obtain id prior to editor mutable borrow
462            let a1_vid = graph.get_vertex_id_for_address(&a1_cell).unwrap();
463            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
464            editor.remove_vertex(a1_vid).unwrap();
465        }
466        assert!(
467            log.events()
468                .iter()
469                .any(|e| matches!(e, ChangeEvent::RemoveVertex { .. }))
470        );
471        // After removal dependency list should be empty
472        let deps_after_remove = graph.get_dependencies(a2_id);
473        assert!(deps_after_remove.is_empty());
474        let mut undo = UndoEngine::new();
475        undo.undo(&mut graph, &mut log).unwrap();
476        // Dependency restored (may be different vertex id)
477        let deps_after_undo = graph.get_dependencies(a2_id);
478        assert!(!deps_after_undo.is_empty());
479        // Redo removal
480        undo.redo(&mut graph, &mut log).unwrap();
481        let deps_after_redo = graph.get_dependencies(a2_id);
482        assert!(deps_after_redo.is_empty());
483    }
484}