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, ChangeLog};
3use super::vertex_editor::VertexEditor;
4use crate::engine::graph::DependencyGraph;
5use crate::engine::graph::editor::vertex_editor::EditorError;
6
7#[derive(Debug, Default)]
8pub struct UndoEngine {
9    /// Stack of applied groups (their last event index snapshot) for redo separation
10    undone: Vec<Vec<ChangeEvent>>, // redo stack stores full event batches
11}
12
13impl UndoEngine {
14    pub fn new() -> Self {
15        Self { undone: Vec::new() }
16    }
17
18    /// Undo last group in the provided change log, applying inverses through a VertexEditor.
19    pub fn undo(
20        &mut self,
21        graph: &mut DependencyGraph,
22        log: &mut ChangeLog,
23    ) -> Result<(), EditorError> {
24        let idxs = log.last_group_indices();
25        if idxs.is_empty() {
26            return Ok(());
27        }
28        let batch: Vec<ChangeEvent> = idxs.iter().map(|i| log.events()[*i].clone()).collect();
29        let max_idx = *idxs.iter().max().unwrap();
30        if max_idx + 1 == log.events().len() {
31            let truncate_to = idxs.iter().min().copied().unwrap();
32            log.truncate(truncate_to);
33        } else {
34            return Err(EditorError::TransactionFailed {
35                reason: "Non-tail undo not supported".into(),
36            });
37        }
38        let mut editor = VertexEditor::new(graph);
39        for ev in batch.iter().rev() {
40            editor.apply_inverse(ev.clone())?;
41        }
42        self.undone.push(batch);
43        Ok(())
44    }
45
46    pub fn redo(
47        &mut self,
48        graph: &mut DependencyGraph,
49        log: &mut ChangeLog,
50    ) -> Result<(), EditorError> {
51        if let Some(batch) = self.undone.pop() {
52            let mut editor = VertexEditor::new(graph);
53            log.begin_compound("redo".to_string());
54            for ev in batch {
55                // Re-log original event for audit consistency
56                log.record(ev.clone());
57                match ev {
58                    ChangeEvent::SetValue { addr, new, .. } => {
59                        editor.set_cell_value(addr, new);
60                    }
61                    ChangeEvent::SetFormula { addr, new, .. } => {
62                        editor.set_cell_formula(addr, new);
63                    }
64                    ChangeEvent::AddVertex {
65                        coord,
66                        sheet_id,
67                        kind,
68                        ..
69                    } => {
70                        let meta = crate::engine::graph::editor::vertex_editor::VertexMeta::new(
71                            coord.row(),
72                            coord.col(),
73                            sheet_id,
74                            kind.unwrap_or(crate::engine::vertex::VertexKind::Cell),
75                        );
76                        editor.add_vertex(meta);
77                    }
78                    ChangeEvent::RemoveVertex {
79                        coord, sheet_id, ..
80                    } => {
81                        if let (Some(c), Some(sid)) = (coord, sheet_id) {
82                            let cell_ref = crate::reference::CellRef::new(
83                                sid,
84                                crate::reference::Coord::new(c.row(), c.col(), true, true),
85                            );
86                            let _ = editor.remove_vertex_at(cell_ref);
87                        }
88                    }
89                    ChangeEvent::VertexMoved {
90                        id,
91                        old_coord: _,
92                        new_coord,
93                    } => {
94                        let _ = editor.move_vertex(id, new_coord);
95                    }
96                    ChangeEvent::DefineName {
97                        name,
98                        scope,
99                        definition,
100                    } => {
101                        let _ = editor.define_name(&name, definition, scope);
102                    }
103                    ChangeEvent::UpdateName {
104                        name,
105                        scope,
106                        new_definition,
107                        ..
108                    } => {
109                        let _ = editor.update_name(&name, new_definition, scope);
110                    }
111                    ChangeEvent::DeleteName { name, scope, .. } => {
112                        let _ = editor.delete_name(&name, scope);
113                    }
114                    ChangeEvent::NamedRangeAdjusted {
115                        name,
116                        scope,
117                        new_definition,
118                        ..
119                    } => {
120                        let _ = editor.update_name(&name, new_definition, scope);
121                    }
122                    _ => {}
123                }
124            }
125            log.end_compound();
126        }
127        Ok(())
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134    use crate::engine::graph::editor::change_log::ChangeLog;
135    use crate::reference::{CellRef, Coord};
136    use formualizer_common::LiteralValue;
137
138    #[test]
139    fn test_undo_redo_single_value() {
140        let mut graph = DependencyGraph::new();
141        let mut log = ChangeLog::new();
142        {
143            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
144            let cell = CellRef {
145                sheet_id: 0,
146                coord: Coord::new(1, 1, true, true),
147            };
148            editor.set_cell_value(cell, LiteralValue::Number(10.0));
149        }
150        assert_eq!(log.len(), 1);
151        let mut undo = UndoEngine::new();
152        undo.undo(&mut graph, &mut log).unwrap();
153        assert_eq!(log.len(), 0); // event removed (simplified policy)
154        // Redo
155        undo.redo(&mut graph, &mut log).unwrap();
156        assert!(!log.is_empty());
157    }
158
159    #[test]
160    fn test_undo_redo_row_shift() {
161        let mut graph = DependencyGraph::new();
162        let mut log = ChangeLog::new();
163        {
164            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
165            // Seed some cells
166            for r in [5u32, 6u32, 10u32] {
167                let cell = CellRef {
168                    sheet_id: 0,
169                    coord: Coord::new(r, 1, true, true),
170                };
171                editor.set_cell_value(cell, LiteralValue::Number(r as f64));
172            }
173        }
174        log.clear(); // focus on shift only
175        {
176            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
177            editor.insert_rows(0, 6, 2).unwrap(); // shift rows >=6 down by 2
178        }
179        assert!(
180            log.events()
181                .iter()
182                .any(|e| matches!(e, ChangeEvent::VertexMoved { .. }))
183        );
184        let moved_count_before = log
185            .events()
186            .iter()
187            .filter(|e| matches!(e, ChangeEvent::VertexMoved { .. }))
188            .count();
189        let mut undo = UndoEngine::new();
190        undo.undo(&mut graph, &mut log).unwrap();
191        assert_eq!(log.events().len(), 0); // group removed
192        undo.redo(&mut graph, &mut log).unwrap();
193        let moved_count_after = log
194            .events()
195            .iter()
196            .filter(|e| matches!(e, ChangeEvent::VertexMoved { .. }))
197            .count();
198        assert_eq!(moved_count_before, moved_count_after);
199    }
200
201    #[test]
202    fn test_undo_redo_column_shift() {
203        let mut graph = DependencyGraph::new();
204        let mut log = ChangeLog::new();
205        {
206            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
207            for c in [3u32, 4u32, 8u32] {
208                let cell = CellRef {
209                    sheet_id: 0,
210                    coord: Coord::new(1, c, true, true),
211                };
212                editor.set_cell_value(cell, LiteralValue::Number(c as f64));
213            }
214        }
215        log.clear();
216        {
217            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
218            editor.insert_columns(0, 5, 2).unwrap();
219        }
220        assert!(
221            log.events()
222                .iter()
223                .any(|e| matches!(e, ChangeEvent::VertexMoved { .. }))
224        );
225        let mut undo = UndoEngine::new();
226        undo.undo(&mut graph, &mut log).unwrap();
227        assert_eq!(log.events().len(), 0);
228    }
229
230    #[test]
231    fn test_remove_vertex_dependency_roundtrip() {
232        use formualizer_parse::parser::parse;
233        let mut graph = DependencyGraph::new();
234        let mut log = ChangeLog::new();
235        let (a1_cell, a2_cell) = (
236            CellRef {
237                sheet_id: 0,
238                coord: Coord::new(0, 0, true, true), // A1 internal
239            },
240            CellRef {
241                sheet_id: 0,
242                coord: Coord::new(1, 0, true, true), // A2 internal
243            },
244        );
245        let a2_id;
246        {
247            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
248            editor.set_cell_value(a1_cell, LiteralValue::Number(10.0));
249            a2_id = editor.set_cell_formula(a2_cell, parse("=A1").unwrap());
250        }
251        // Ensure dependency exists
252        let deps_before = graph.get_dependencies(a2_id);
253        assert!(!deps_before.is_empty());
254        // Clear log then remove A1
255        log.clear();
256        {
257            // Obtain id prior to editor mutable borrow
258            let a1_vid = graph.get_vertex_id_for_address(&a1_cell).copied().unwrap();
259            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
260            editor.remove_vertex(a1_vid).unwrap();
261        }
262        assert!(
263            log.events()
264                .iter()
265                .any(|e| matches!(e, ChangeEvent::RemoveVertex { .. }))
266        );
267        // After removal dependency list should be empty
268        let deps_after_remove = graph.get_dependencies(a2_id);
269        assert!(deps_after_remove.is_empty());
270        let mut undo = UndoEngine::new();
271        undo.undo(&mut graph, &mut log).unwrap();
272        // Dependency restored (may be different vertex id)
273        let deps_after_undo = graph.get_dependencies(a2_id);
274        assert!(!deps_after_undo.is_empty());
275        // Redo removal
276        undo.redo(&mut graph, &mut log).unwrap();
277        let deps_after_redo = graph.get_dependencies(a2_id);
278        assert!(deps_after_redo.is_empty());
279    }
280}