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                    _ => {}
97                }
98            }
99            log.end_compound();
100        }
101        Ok(())
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use crate::engine::graph::editor::change_log::ChangeLog;
109    use crate::reference::{CellRef, Coord};
110    use formualizer_common::LiteralValue;
111
112    #[test]
113    fn test_undo_redo_single_value() {
114        let mut graph = DependencyGraph::new();
115        let mut log = ChangeLog::new();
116        {
117            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
118            let cell = CellRef {
119                sheet_id: 0,
120                coord: Coord::new(1, 1, true, true),
121            };
122            editor.set_cell_value(cell, LiteralValue::Number(10.0));
123        }
124        assert_eq!(log.len(), 1);
125        let mut undo = UndoEngine::new();
126        undo.undo(&mut graph, &mut log).unwrap();
127        assert_eq!(log.len(), 0); // event removed (simplified policy)
128        // Redo
129        undo.redo(&mut graph, &mut log).unwrap();
130        assert!(!log.is_empty());
131    }
132
133    #[test]
134    fn test_undo_redo_row_shift() {
135        let mut graph = DependencyGraph::new();
136        let mut log = ChangeLog::new();
137        {
138            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
139            // Seed some cells
140            for r in [5u32, 6u32, 10u32] {
141                let cell = CellRef {
142                    sheet_id: 0,
143                    coord: Coord::new(r, 1, true, true),
144                };
145                editor.set_cell_value(cell, LiteralValue::Number(r as f64));
146            }
147        }
148        log.clear(); // focus on shift only
149        {
150            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
151            editor.insert_rows(0, 6, 2).unwrap(); // shift rows >=6 down by 2
152        }
153        assert!(
154            log.events()
155                .iter()
156                .any(|e| matches!(e, ChangeEvent::VertexMoved { .. }))
157        );
158        let moved_count_before = log
159            .events()
160            .iter()
161            .filter(|e| matches!(e, ChangeEvent::VertexMoved { .. }))
162            .count();
163        let mut undo = UndoEngine::new();
164        undo.undo(&mut graph, &mut log).unwrap();
165        assert_eq!(log.events().len(), 0); // group removed
166        undo.redo(&mut graph, &mut log).unwrap();
167        let moved_count_after = log
168            .events()
169            .iter()
170            .filter(|e| matches!(e, ChangeEvent::VertexMoved { .. }))
171            .count();
172        assert_eq!(moved_count_before, moved_count_after);
173    }
174
175    #[test]
176    fn test_undo_redo_column_shift() {
177        let mut graph = DependencyGraph::new();
178        let mut log = ChangeLog::new();
179        {
180            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
181            for c in [3u32, 4u32, 8u32] {
182                let cell = CellRef {
183                    sheet_id: 0,
184                    coord: Coord::new(1, c, true, true),
185                };
186                editor.set_cell_value(cell, LiteralValue::Number(c as f64));
187            }
188        }
189        log.clear();
190        {
191            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
192            editor.insert_columns(0, 5, 2).unwrap();
193        }
194        assert!(
195            log.events()
196                .iter()
197                .any(|e| matches!(e, ChangeEvent::VertexMoved { .. }))
198        );
199        let mut undo = UndoEngine::new();
200        undo.undo(&mut graph, &mut log).unwrap();
201        assert_eq!(log.events().len(), 0);
202    }
203
204    #[test]
205    fn test_remove_vertex_dependency_roundtrip() {
206        use formualizer_parse::parser::parse;
207        let mut graph = DependencyGraph::new();
208        let mut log = ChangeLog::new();
209        let (a1_cell, a2_cell) = (
210            CellRef {
211                sheet_id: 0,
212                coord: Coord::new(0, 0, true, true), // A1 internal
213            },
214            CellRef {
215                sheet_id: 0,
216                coord: Coord::new(1, 0, true, true), // A2 internal
217            },
218        );
219        let a2_id;
220        {
221            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
222            editor.set_cell_value(a1_cell, LiteralValue::Number(10.0));
223            a2_id = editor.set_cell_formula(a2_cell, parse("=A1").unwrap());
224        }
225        // Ensure dependency exists
226        let deps_before = graph.get_dependencies(a2_id);
227        assert!(!deps_before.is_empty());
228        // Clear log then remove A1
229        log.clear();
230        {
231            // Obtain id prior to editor mutable borrow
232            let a1_vid = graph.get_vertex_id_for_address(&a1_cell).copied().unwrap();
233            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
234            editor.remove_vertex(a1_vid).unwrap();
235        }
236        assert!(
237            log.events()
238                .iter()
239                .any(|e| matches!(e, ChangeEvent::RemoveVertex { .. }))
240        );
241        // After removal dependency list should be empty
242        let deps_after_remove = graph.get_dependencies(a2_id);
243        assert!(deps_after_remove.is_empty());
244        let mut undo = UndoEngine::new();
245        undo.undo(&mut graph, &mut log).unwrap();
246        // Dependency restored (may be different vertex id)
247        let deps_after_undo = graph.get_dependencies(a2_id);
248        assert!(!deps_after_undo.is_empty());
249        // Redo removal
250        undo.redo(&mut graph, &mut log).unwrap();
251        let deps_after_redo = graph.get_dependencies(a2_id);
252        assert!(deps_after_redo.is_empty());
253    }
254}