1use venus_core::graph::{CellId, DefinitionType, MoveDirection};
6
7const MAX_UNDO_HISTORY: usize = 50;
9
10#[derive(Debug, Clone)]
12pub enum UndoableOperation {
13 InsertCell {
15 cell_name: String,
17 after_cell_name: Option<String>,
20 },
21
22 DeleteCell {
24 cell_name: String,
26 source: String,
28 after_cell_name: Option<String>,
31 },
32
33 DuplicateCell {
35 original_cell_name: String,
37 new_cell_name: String,
39 },
40
41 MoveCell {
43 cell_name: String,
45 direction: MoveDirection,
47 },
48
49 RenameCell {
51 cell_name: String,
53 old_display_name: String,
55 new_display_name: String,
57 },
58
59 EditCell {
61 cell_id: CellId,
63 start_line: usize,
65 end_line: usize,
67 old_source: String,
69 new_source: String,
71 },
72
73 InsertMarkdownCell {
75 start_line: usize,
77 end_line: usize,
79 content: String,
81 },
82
83 EditMarkdownCell {
85 start_line: usize,
87 end_line: usize,
89 old_content: String,
91 new_content: String,
93 is_module_doc: bool,
95 },
96
97 DeleteMarkdownCell {
99 start_line: usize,
101 content: String,
103 },
104
105 MoveMarkdownCell {
107 start_line: usize,
109 end_line: usize,
111 direction: MoveDirection,
113 },
114
115 InsertDefinitionCell {
117 start_line: usize,
119 end_line: usize,
121 content: String,
123 definition_type: DefinitionType,
125 },
126
127 EditDefinitionCell {
129 cell_id: CellId,
131 start_line: usize,
133 end_line: usize,
135 old_content: String,
137 new_content: String,
139 },
140
141 DeleteDefinitionCell {
143 start_line: usize,
145 end_line: usize,
147 content: String,
149 definition_type: DefinitionType,
151 },
152
153 MoveDefinitionCell {
155 start_line: usize,
157 end_line: usize,
159 direction: MoveDirection,
161 },
162}
163
164impl UndoableOperation {
165 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 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#[derive(Debug, Default)]
288pub struct UndoManager {
289 undo_stack: Vec<UndoableOperation>,
291 redo_stack: Vec<UndoableOperation>,
293}
294
295impl UndoManager {
296 pub fn new() -> Self {
298 Self::default()
299 }
300
301 pub fn record(&mut self, operation: UndoableOperation) {
305 self.redo_stack.clear();
307
308 self.undo_stack.push(operation);
310
311 while self.undo_stack.len() > MAX_UNDO_HISTORY {
313 self.undo_stack.remove(0);
314 }
315 }
316
317 pub fn pop_undo(&mut self) -> Option<UndoableOperation> {
322 self.undo_stack.pop()
323 }
324
325 pub fn record_redo(&mut self, operation: UndoableOperation) {
327 self.redo_stack.push(operation);
328 }
329
330 pub fn pop_redo(&mut self) -> Option<UndoableOperation> {
335 self.redo_stack.pop()
336 }
337
338 pub fn can_undo(&self) -> bool {
340 !self.undo_stack.is_empty()
341 }
342
343 pub fn can_redo(&self) -> bool {
345 !self.redo_stack.is_empty()
346 }
347
348 pub fn undo_description(&self) -> Option<String> {
350 self.undo_stack.last().map(|op| op.undo_description())
351 }
352
353 pub fn redo_description(&self) -> Option<String> {
355 self.redo_stack.last().map(|op| op.description())
356 }
357
358 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 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 let op = manager.pop_undo().unwrap();
386 assert!(matches!(op, UndoableOperation::InsertCell { .. }));
387
388 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 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 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}