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