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        let mut editor = VertexEditor::new(graph);
81        for item in batch.iter().rev() {
82            editor.apply_inverse(item.event.clone())?;
83        }
84
85        // Keep a copy for redo, but also return the batch so callers can mirror side effects.
86        self.undone.push(batch.clone());
87        Ok(batch)
88    }
89
90    pub(crate) fn pending_redo_events(&self) -> Vec<ChangeEvent> {
91        self.undone
92            .last()
93            .into_iter()
94            .flat_map(|batch| batch.iter().map(|item| item.event.clone()))
95            .collect()
96    }
97
98    pub fn redo(
99        &mut self,
100        graph: &mut DependencyGraph,
101        log: &mut ChangeLog,
102    ) -> Result<Vec<UndoBatchItem>, EditorError> {
103        if let Some(batch) = self.undone.pop() {
104            log.begin_compound("redo".to_string());
105            // Return value for callers (e.g. Arrow mirroring) must remain available even though
106            // we apply events by value below.
107            let ret = batch.clone();
108
109            for item in batch {
110                // Re-log original event for audit consistency
111                log.record_with_meta(item.event.clone(), item.meta.clone());
112                match item.event {
113                    ChangeEvent::SetValue { addr, new, .. } => {
114                        let mut editor = VertexEditor::new(graph);
115                        editor.set_cell_value(addr, new);
116                    }
117                    ChangeEvent::SetFormula { addr, new, .. } => {
118                        let mut editor = VertexEditor::new(graph);
119                        editor.set_cell_formula(addr, new);
120                    }
121                    ChangeEvent::AddVertex {
122                        coord,
123                        sheet_id,
124                        kind,
125                        ..
126                    } => {
127                        let mut editor = VertexEditor::new(graph);
128                        let meta = crate::engine::graph::editor::vertex_editor::VertexMeta::new(
129                            coord.row(),
130                            coord.col(),
131                            sheet_id,
132                            kind.unwrap_or(crate::engine::vertex::VertexKind::Cell),
133                        );
134                        editor.try_add_vertex(meta)?;
135                    }
136                    ChangeEvent::RemoveVertex {
137                        coord, sheet_id, ..
138                    } => {
139                        if let (Some(c), Some(sid)) = (coord, sheet_id) {
140                            let mut editor = VertexEditor::new(graph);
141                            let cell_ref = crate::reference::CellRef::new(
142                                sid,
143                                crate::reference::Coord::new(c.row(), c.col(), true, true),
144                            );
145                            let _ = editor.remove_vertex_at(cell_ref);
146                        }
147                    }
148                    ChangeEvent::VertexMoved { id, new_coord, .. } => {
149                        let mut editor = VertexEditor::new(graph);
150                        let _ = editor.move_vertex(id, new_coord);
151                    }
152                    ChangeEvent::FormulaAdjusted { id, new_ast, .. } => {
153                        // Keep it simple: apply directly by vertex id.
154                        // (This is used for structural ops formula rewrites.)
155                        let _ = graph.update_vertex_formula(id, new_ast);
156                        graph.mark_vertex_dirty(id);
157                    }
158                    ChangeEvent::DefineName {
159                        name,
160                        scope,
161                        definition,
162                    } => {
163                        let mut editor = VertexEditor::new(graph);
164                        let _ = editor.define_name(&name, definition, scope);
165                    }
166                    ChangeEvent::UpdateName {
167                        name,
168                        scope,
169                        new_definition,
170                        ..
171                    } => {
172                        let mut editor = VertexEditor::new(graph);
173                        let _ = editor.update_name(&name, new_definition, scope);
174                    }
175                    ChangeEvent::DeleteName { name, scope, .. } => {
176                        let mut editor = VertexEditor::new(graph);
177                        let _ = editor.delete_name(&name, scope);
178                    }
179                    ChangeEvent::NamedRangeAdjusted {
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::SpillCommitted { anchor, new, .. } => {
189                        let _ = graph.commit_spill_region_atomic_with_fault(
190                            anchor,
191                            new.target_cells,
192                            new.values,
193                            None,
194                        );
195                    }
196                    ChangeEvent::SpillCleared { anchor, .. } => {
197                        graph.clear_spill_region(anchor);
198                    }
199                    ChangeEvent::SetRowVisibility { .. } => {
200                        // Engine-level sidecar metadata; applied by Engine undo/redo wrappers.
201                    }
202                    _ => {}
203                }
204            }
205            log.end_compound();
206            Ok(ret)
207        } else {
208            Ok(Vec::new())
209        }
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216    use crate::engine::EvalConfig;
217    use crate::engine::graph::editor::change_log::ChangeLog;
218    use crate::reference::{CellRef, Coord};
219    use formualizer_common::LiteralValue;
220
221    fn create_test_graph() -> DependencyGraph {
222        DependencyGraph::new_with_config(EvalConfig::default())
223    }
224
225    #[test]
226    fn test_undo_redo_single_value() {
227        let mut graph = create_test_graph();
228        let mut log = ChangeLog::new();
229        {
230            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
231            let cell = CellRef {
232                sheet_id: 0,
233                coord: Coord::new(1, 1, true, true),
234            };
235            editor.set_cell_value(cell, LiteralValue::Number(10.0));
236        }
237        assert_eq!(log.len(), 1);
238        let mut undo = UndoEngine::new();
239        undo.undo(&mut graph, &mut log).unwrap();
240        assert_eq!(log.len(), 0); // event removed (simplified policy)
241        // Redo
242        undo.redo(&mut graph, &mut log).unwrap();
243        assert!(!log.is_empty());
244    }
245
246    #[test]
247    fn test_undo_redo_row_shift() {
248        let mut graph = create_test_graph();
249        let mut log = ChangeLog::new();
250        {
251            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
252            // Seed some cells
253            for r in [5u32, 6u32, 10u32] {
254                let cell = CellRef {
255                    sheet_id: 0,
256                    coord: Coord::new(r, 1, true, true),
257                };
258                editor.set_cell_value(cell, LiteralValue::Number(r as f64));
259            }
260        }
261        log.clear(); // focus on shift only
262        {
263            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
264            editor.insert_rows(0, 6, 2).unwrap(); // shift rows >=6 down by 2
265        }
266        assert!(
267            log.events()
268                .iter()
269                .any(|e| matches!(e, ChangeEvent::VertexMoved { .. }))
270        );
271        let moved_count_before = log
272            .events()
273            .iter()
274            .filter(|e| matches!(e, ChangeEvent::VertexMoved { .. }))
275            .count();
276        let mut undo = UndoEngine::new();
277        undo.undo(&mut graph, &mut log).unwrap();
278        assert_eq!(log.events().len(), 0); // group removed
279        undo.redo(&mut graph, &mut log).unwrap();
280        let moved_count_after = log
281            .events()
282            .iter()
283            .filter(|e| matches!(e, ChangeEvent::VertexMoved { .. }))
284            .count();
285        assert_eq!(moved_count_before, moved_count_after);
286    }
287
288    #[test]
289    fn test_undo_redo_spill_clear_on_scalar_edit_restores_registry_and_cells() {
290        let mut graph = create_test_graph();
291        let sheet_id = graph.sheet_id_mut("Sheet1");
292
293        let anchor_cell = CellRef::new(sheet_id, Coord::new(0, 0, true, true));
294        let anchor_vid = {
295            let mut editor = VertexEditor::new(&mut graph);
296            editor.set_cell_value(anchor_cell, LiteralValue::Number(0.0))
297        };
298
299        let target_cells = vec![
300            CellRef::new(sheet_id, Coord::new(0, 0, true, true)),
301            CellRef::new(sheet_id, Coord::new(0, 1, true, true)),
302            CellRef::new(sheet_id, Coord::new(1, 0, true, true)),
303            CellRef::new(sheet_id, Coord::new(1, 1, true, true)),
304        ];
305        let values = vec![
306            vec![LiteralValue::Number(1.0), LiteralValue::Number(2.0)],
307            vec![LiteralValue::Number(3.0), LiteralValue::Number(4.0)],
308        ];
309        graph
310            .commit_spill_region_atomic_with_fault(anchor_vid, target_cells.clone(), values, None)
311            .unwrap();
312
313        assert!(graph.spill_registry_has_anchor(anchor_vid));
314
315        let mut log = ChangeLog::new();
316        {
317            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
318            // Scalar edit of the anchor should clear spill children + ownership.
319            editor.set_cell_value(anchor_cell, LiteralValue::Number(9.0));
320        }
321
322        assert!(!graph.spill_registry_has_anchor(anchor_vid));
323        assert_eq!(graph.spill_registry_counts(), (0, 0));
324
325        let mut undo = UndoEngine::new();
326        undo.undo(&mut graph, &mut log).unwrap();
327
328        assert!(graph.spill_registry_has_anchor(anchor_vid));
329        for cell in &target_cells {
330            assert_eq!(
331                graph.spill_registry_anchor_for_cell(*cell),
332                Some(anchor_vid)
333            );
334        }
335        // Graph does not cache spill child values in Arrow-truth mode; the contract here is
336        // that the spill registry ownership is restored by undo.
337
338        // Redo should clear the spill again.
339        undo.redo(&mut graph, &mut log).unwrap();
340        assert!(!graph.spill_registry_has_anchor(anchor_vid));
341        assert_eq!(graph.spill_registry_counts(), (0, 0));
342    }
343
344    #[test]
345    fn test_undo_depth_truncates_gracefully_under_changelog_cap() {
346        let mut graph = create_test_graph();
347        let sheet_id = graph.sheet_id_mut("Sheet1");
348        let mut log = ChangeLog::with_max_changelog_events(3);
349
350        // Record 5 independent edits; cap keeps only the last 3.
351        for i in 0..5u32 {
352            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
353            let cell = CellRef::new(sheet_id, Coord::new(i, 0, true, true));
354            editor.set_cell_value(cell, LiteralValue::Number(i as f64));
355        }
356        assert_eq!(log.len(), 3);
357
358        let mut undo = UndoEngine::new();
359        undo.undo(&mut graph, &mut log).unwrap();
360        undo.undo(&mut graph, &mut log).unwrap();
361        undo.undo(&mut graph, &mut log).unwrap();
362        // Beyond retained history: no-op, should not error.
363        undo.undo(&mut graph, &mut log).unwrap();
364        assert_eq!(log.len(), 0);
365    }
366
367    #[test]
368    fn test_undo_redo_column_shift() {
369        let mut graph = create_test_graph();
370        let mut log = ChangeLog::new();
371        {
372            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
373            for c in [3u32, 4u32, 8u32] {
374                let cell = CellRef {
375                    sheet_id: 0,
376                    coord: Coord::new(1, c, true, true),
377                };
378                editor.set_cell_value(cell, LiteralValue::Number(c as f64));
379            }
380        }
381        log.clear();
382        {
383            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
384            editor.insert_columns(0, 5, 2).unwrap();
385        }
386        assert!(
387            log.events()
388                .iter()
389                .any(|e| matches!(e, ChangeEvent::VertexMoved { .. }))
390        );
391        let mut undo = UndoEngine::new();
392        undo.undo(&mut graph, &mut log).unwrap();
393        assert_eq!(log.events().len(), 0);
394    }
395
396    #[test]
397    fn test_remove_vertex_dependency_roundtrip() {
398        use formualizer_parse::parser::parse;
399        let mut graph = create_test_graph();
400        let mut log = ChangeLog::new();
401        let (a1_cell, a2_cell) = (
402            CellRef {
403                sheet_id: 0,
404                coord: Coord::new(0, 0, true, true), // A1 internal
405            },
406            CellRef {
407                sheet_id: 0,
408                coord: Coord::new(1, 0, true, true), // A2 internal
409            },
410        );
411        let a2_id;
412        {
413            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
414            editor.set_cell_value(a1_cell, LiteralValue::Number(10.0));
415            a2_id = editor.set_cell_formula(a2_cell, parse("=A1").unwrap());
416        }
417        // Ensure dependency exists
418        let deps_before = graph.get_dependencies(a2_id);
419        assert!(!deps_before.is_empty());
420        // Clear log then remove A1
421        log.clear();
422        {
423            // Obtain id prior to editor mutable borrow
424            let a1_vid = graph.get_vertex_id_for_address(&a1_cell).copied().unwrap();
425            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
426            editor.remove_vertex(a1_vid).unwrap();
427        }
428        assert!(
429            log.events()
430                .iter()
431                .any(|e| matches!(e, ChangeEvent::RemoveVertex { .. }))
432        );
433        // After removal dependency list should be empty
434        let deps_after_remove = graph.get_dependencies(a2_id);
435        assert!(deps_after_remove.is_empty());
436        let mut undo = UndoEngine::new();
437        undo.undo(&mut graph, &mut log).unwrap();
438        // Dependency restored (may be different vertex id)
439        let deps_after_undo = graph.get_dependencies(a2_id);
440        assert!(!deps_after_undo.is_empty());
441        // Redo removal
442        undo.redo(&mut graph, &mut log).unwrap();
443        let deps_after_redo = graph.get_dependencies(a2_id);
444        assert!(deps_after_redo.is_empty());
445    }
446}