venus_server/
undo.rs

1//! Undo/redo manager for cell management operations.
2//!
3//! Tracks operations on the notebook and allows undoing/redoing them.
4
5use venus_core::graph::{CellId, DefinitionType, MoveDirection};
6
7/// Maximum number of undo operations to track.
8const MAX_UNDO_HISTORY: usize = 50;
9
10/// An undoable operation on the notebook.
11#[derive(Debug, Clone)]
12pub enum UndoableOperation {
13    /// A cell was inserted. Undo = delete this cell.
14    InsertCell {
15        /// Name of the inserted cell.
16        cell_name: String,
17        /// Name of the cell after which this was inserted (for redo).
18        /// None if inserted at the beginning.
19        after_cell_name: Option<String>,
20    },
21
22    /// A cell was deleted. Undo = restore it.
23    DeleteCell {
24        /// Name of the deleted cell.
25        cell_name: String,
26        /// Full source code of the cell (including doc comments and attributes).
27        source: String,
28        /// Name of the cell before this one (for position restoration).
29        /// None if this was the first cell.
30        after_cell_name: Option<String>,
31    },
32
33    /// A cell was duplicated. Undo = delete the new cell.
34    DuplicateCell {
35        /// Name of the original cell.
36        original_cell_name: String,
37        /// Name of the new duplicated cell.
38        new_cell_name: String,
39    },
40
41    /// A cell was moved. Undo = move in opposite direction.
42    MoveCell {
43        /// Name of the moved cell.
44        cell_name: String,
45        /// Original direction (undo reverses it).
46        direction: MoveDirection,
47    },
48
49    /// A cell's display name was renamed. Undo = restore old name.
50    RenameCell {
51        /// Name of the cell (function name).
52        cell_name: String,
53        /// Old display name (for undo).
54        old_display_name: String,
55        /// New display name.
56        new_display_name: String,
57    },
58
59    /// A code cell was edited. Undo = restore old source.
60    EditCell {
61        /// Cell ID that was edited.
62        cell_id: CellId,
63        /// Start line of the cell.
64        start_line: usize,
65        /// End line of the cell.
66        end_line: usize,
67        /// Old source code (for undo).
68        old_source: String,
69        /// New source code.
70        new_source: String,
71    },
72
73    /// A markdown cell was inserted. Undo = delete it.
74    InsertMarkdownCell {
75        /// Start line of the inserted markdown cell.
76        start_line: usize,
77        /// End line of the inserted markdown cell.
78        end_line: usize,
79        /// Content of the inserted cell (for redo).
80        content: String,
81    },
82
83    /// A markdown cell was edited. Undo = restore old content.
84    EditMarkdownCell {
85        /// Start line of the markdown cell.
86        start_line: usize,
87        /// End line of the markdown cell.
88        end_line: usize,
89        /// Old content (for undo).
90        old_content: String,
91        /// New content.
92        new_content: String,
93        /// Whether this is a module-level doc comment (//! vs ///).
94        is_module_doc: bool,
95    },
96
97    /// A markdown cell was deleted. Undo = restore it.
98    DeleteMarkdownCell {
99        /// Start line where the cell was located.
100        start_line: usize,
101        /// Content of the deleted cell.
102        content: String,
103    },
104
105    /// A markdown cell was moved. Undo = move in opposite direction.
106    MoveMarkdownCell {
107        /// Start line of the moved cell.
108        start_line: usize,
109        /// End line of the moved cell.
110        end_line: usize,
111        /// Direction it was moved.
112        direction: MoveDirection,
113    },
114
115    /// A definition cell was inserted. Undo = delete it.
116    InsertDefinitionCell {
117        /// Start line of the inserted definition cell.
118        start_line: usize,
119        /// End line of the inserted definition cell.
120        end_line: usize,
121        /// Content of the inserted cell (for redo).
122        content: String,
123        /// Type of definition.
124        definition_type: DefinitionType,
125    },
126
127    /// A definition cell was edited. Undo = restore old content.
128    EditDefinitionCell {
129        /// Cell ID that was edited.
130        cell_id: CellId,
131        /// Start line of the definition cell.
132        start_line: usize,
133        /// End line of the definition cell.
134        end_line: usize,
135        /// Old content (for undo).
136        old_content: String,
137        /// New content.
138        new_content: String,
139    },
140
141    /// A definition cell was deleted. Undo = restore it.
142    DeleteDefinitionCell {
143        /// Start line of the deleted definition cell.
144        start_line: usize,
145        /// End line of the deleted definition cell.
146        end_line: usize,
147        /// Content of the deleted cell (for undo).
148        content: String,
149        /// Type of definition.
150        definition_type: DefinitionType,
151    },
152
153    /// A definition cell was moved. Undo = move in opposite direction.
154    MoveDefinitionCell {
155        /// Start line of the moved cell.
156        start_line: usize,
157        /// End line of the moved cell.
158        end_line: usize,
159        /// Direction it was moved.
160        direction: MoveDirection,
161    },
162}
163
164impl UndoableOperation {
165    /// Get a human-readable description of this operation.
166    pub fn description(&self) -> String {
167        match self {
168            Self::InsertCell { cell_name, .. } => {
169                format!("Insert cell '{}'", cell_name)
170            }
171            Self::DeleteCell { cell_name, .. } => {
172                format!("Delete cell '{}'", cell_name)
173            }
174            Self::DuplicateCell { new_cell_name, .. } => {
175                format!("Duplicate to '{}'", new_cell_name)
176            }
177            Self::MoveCell { cell_name, direction } => {
178                let dir_str = match direction {
179                    MoveDirection::Up => "up",
180                    MoveDirection::Down => "down",
181                };
182                format!("Move '{}' {}", cell_name, dir_str)
183            }
184            Self::RenameCell { cell_name, new_display_name, .. } => {
185                format!("Rename '{}' to '{}'", cell_name, new_display_name)
186            }
187            Self::EditCell { start_line, .. } => {
188                format!("Edit cell at line {}", start_line)
189            }
190            Self::InsertMarkdownCell { start_line, .. } => {
191                format!("Insert markdown cell at line {}", start_line)
192            }
193            Self::EditMarkdownCell { start_line, .. } => {
194                format!("Edit markdown cell at line {}", start_line)
195            }
196            Self::DeleteMarkdownCell { start_line, .. } => {
197                format!("Delete markdown cell at line {}", start_line)
198            }
199            Self::MoveMarkdownCell { start_line, direction, .. } => {
200                let dir_str = match direction {
201                    MoveDirection::Up => "up",
202                    MoveDirection::Down => "down",
203                };
204                format!("Move markdown cell at line {} {}", start_line, dir_str)
205            }
206            Self::InsertDefinitionCell { start_line, .. } => {
207                format!("Insert definition cell at line {}", start_line)
208            }
209            Self::EditDefinitionCell { start_line, .. } => {
210                format!("Edit definition cell at line {}", start_line)
211            }
212            Self::DeleteDefinitionCell { start_line, .. } => {
213                format!("Delete definition cell at line {}", start_line)
214            }
215            Self::MoveDefinitionCell { start_line, direction, .. } => {
216                let dir_str = match direction {
217                    MoveDirection::Up => "up",
218                    MoveDirection::Down => "down",
219                };
220                format!("Move definition cell at line {} {}", start_line, dir_str)
221            }
222        }
223    }
224
225    /// Get the reverse operation (what undo would do).
226    pub fn undo_description(&self) -> String {
227        match self {
228            Self::InsertCell { cell_name, .. } => {
229                format!("Delete cell '{}'", cell_name)
230            }
231            Self::DeleteCell { cell_name, .. } => {
232                format!("Restore cell '{}'", cell_name)
233            }
234            Self::DuplicateCell { new_cell_name, .. } => {
235                format!("Delete cell '{}'", new_cell_name)
236            }
237            Self::MoveCell { cell_name, direction } => {
238                let dir_str = match direction {
239                    MoveDirection::Up => "down",
240                    MoveDirection::Down => "up",
241                };
242                format!("Move '{}' {}", cell_name, dir_str)
243            }
244            Self::RenameCell { cell_name, old_display_name, .. } => {
245                format!("Rename '{}' back to '{}'", cell_name, old_display_name)
246            }
247            Self::EditCell { start_line, .. } => {
248                format!("Restore cell at line {}", start_line)
249            }
250            Self::InsertMarkdownCell { start_line, .. } => {
251                format!("Delete markdown cell at line {}", start_line)
252            }
253            Self::EditMarkdownCell { start_line, .. } => {
254                format!("Restore markdown cell at line {}", start_line)
255            }
256            Self::DeleteMarkdownCell { start_line, .. } => {
257                format!("Restore markdown cell at line {}", start_line)
258            }
259            Self::MoveMarkdownCell { start_line, direction, .. } => {
260                let dir_str = match direction {
261                    MoveDirection::Up => "down",
262                    MoveDirection::Down => "up",
263                };
264                format!("Move markdown cell at line {} {}", start_line, dir_str)
265            }
266            Self::InsertDefinitionCell { start_line, .. } => {
267                format!("Delete definition cell at line {}", start_line)
268            }
269            Self::EditDefinitionCell { start_line, .. } => {
270                format!("Restore definition cell at line {}", start_line)
271            }
272            Self::DeleteDefinitionCell { start_line, .. } => {
273                format!("Restore definition cell at line {}", start_line)
274            }
275            Self::MoveDefinitionCell { start_line, direction, .. } => {
276                let dir_str = match direction {
277                    MoveDirection::Up => "down",
278                    MoveDirection::Down => "up",
279                };
280                format!("Move definition cell at line {} {}", start_line, dir_str)
281            }
282        }
283    }
284}
285
286/// Manages undo/redo stacks for cell operations.
287#[derive(Debug, Default)]
288pub struct UndoManager {
289    /// Stack of operations that can be undone.
290    undo_stack: Vec<UndoableOperation>,
291    /// Stack of operations that can be redone.
292    redo_stack: Vec<UndoableOperation>,
293}
294
295impl UndoManager {
296    /// Create a new undo manager.
297    pub fn new() -> Self {
298        Self::default()
299    }
300
301    /// Record an operation that was just performed.
302    ///
303    /// This clears the redo stack (can't redo after a new operation).
304    pub fn record(&mut self, operation: UndoableOperation) {
305        // Clear redo stack - new operation invalidates redo history
306        self.redo_stack.clear();
307
308        // Add to undo stack
309        self.undo_stack.push(operation);
310
311        // Trim if too long
312        while self.undo_stack.len() > MAX_UNDO_HISTORY {
313            self.undo_stack.remove(0);
314        }
315    }
316
317    /// Pop the last operation from the undo stack.
318    ///
319    /// Returns the operation to undo, or None if stack is empty.
320    /// The caller should execute the reverse operation, then call `record_redo`.
321    pub fn pop_undo(&mut self) -> Option<UndoableOperation> {
322        self.undo_stack.pop()
323    }
324
325    /// Record an operation that was just undone (for redo).
326    pub fn record_redo(&mut self, operation: UndoableOperation) {
327        self.redo_stack.push(operation);
328    }
329
330    /// Pop the last operation from the redo stack.
331    ///
332    /// Returns the operation to redo, or None if stack is empty.
333    /// The caller should execute the operation, then call `record` as normal.
334    pub fn pop_redo(&mut self) -> Option<UndoableOperation> {
335        self.redo_stack.pop()
336    }
337
338    /// Check if undo is available.
339    pub fn can_undo(&self) -> bool {
340        !self.undo_stack.is_empty()
341    }
342
343    /// Check if redo is available.
344    pub fn can_redo(&self) -> bool {
345        !self.redo_stack.is_empty()
346    }
347
348    /// Get description of what will be undone (for UI).
349    pub fn undo_description(&self) -> Option<String> {
350        self.undo_stack.last().map(|op| op.undo_description())
351    }
352
353    /// Get description of what will be redone (for UI).
354    pub fn redo_description(&self) -> Option<String> {
355        self.redo_stack.last().map(|op| op.description())
356    }
357
358    /// Clear all undo/redo history.
359    ///
360    /// Called when the file is externally modified.
361    pub fn clear(&mut self) {
362        self.undo_stack.clear();
363        self.redo_stack.clear();
364    }
365}
366
367#[cfg(test)]
368mod tests {
369    use super::*;
370
371    #[test]
372    fn test_record_and_undo() {
373        let mut manager = UndoManager::new();
374
375        // Record an insert
376        manager.record(UndoableOperation::InsertCell {
377            cell_name: "test_cell".to_string(),
378            after_cell_name: None,
379        });
380
381        assert!(manager.can_undo());
382        assert!(!manager.can_redo());
383
384        // Undo it
385        let op = manager.pop_undo().unwrap();
386        assert!(matches!(op, UndoableOperation::InsertCell { .. }));
387
388        // Record for redo
389        manager.record_redo(op);
390
391        assert!(!manager.can_undo());
392        assert!(manager.can_redo());
393    }
394
395    #[test]
396    fn test_new_operation_clears_redo() {
397        let mut manager = UndoManager::new();
398
399        // Record and undo
400        manager.record(UndoableOperation::InsertCell {
401            cell_name: "cell1".to_string(),
402            after_cell_name: None,
403        });
404        let op = manager.pop_undo().unwrap();
405        manager.record_redo(op);
406
407        assert!(manager.can_redo());
408
409        // New operation should clear redo
410        manager.record(UndoableOperation::InsertCell {
411            cell_name: "cell2".to_string(),
412            after_cell_name: Some("cell1".to_string()),
413        });
414
415        assert!(!manager.can_redo());
416    }
417
418    #[test]
419    fn test_descriptions() {
420        let op = UndoableOperation::DeleteCell {
421            cell_name: "foo".to_string(),
422            source: "".to_string(),
423            after_cell_name: None,
424        };
425
426        assert_eq!(op.description(), "Delete cell 'foo'");
427        assert_eq!(op.undo_description(), "Restore cell 'foo'");
428    }
429
430    #[test]
431    fn test_move_descriptions() {
432        let op = UndoableOperation::MoveCell {
433            cell_name: "bar".to_string(),
434            direction: MoveDirection::Up,
435        };
436
437        assert_eq!(op.description(), "Move 'bar' up");
438        assert_eq!(op.undo_description(), "Move 'bar' down");
439    }
440}