Skip to main content

mdcs_db/
undo.rs

1//! Undo/Redo System - Operation-based undo with causal grouping.
2//!
3//! Provides collaborative undo functionality:
4//! - Local undo/redo (only affects local user's operations)
5//! - Operation grouping for atomic undo
6//! - Causal tracking to handle concurrent edits
7//! - Inverse operation generation
8
9use serde::{Deserialize, Serialize};
10use std::collections::{HashMap, VecDeque};
11use ulid::Ulid;
12
13/// Unique identifier for an operation.
14#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
15pub struct OperationId(String);
16
17impl OperationId {
18    pub fn new() -> Self {
19        Self(Ulid::new().to_string())
20    }
21}
22
23impl Default for OperationId {
24    fn default() -> Self {
25        Self::new()
26    }
27}
28
29/// Unique identifier for an operation group.
30#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
31pub struct GroupId(String);
32
33impl GroupId {
34    pub fn new() -> Self {
35        Self(Ulid::new().to_string())
36    }
37}
38
39impl Default for GroupId {
40    fn default() -> Self {
41        Self::new()
42    }
43}
44
45/// A text operation that can be undone.
46#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
47pub enum TextOperation {
48    /// Insert text at a position.
49    Insert { position: usize, text: String },
50    /// Delete text at a position.
51    Delete { position: usize, deleted: String },
52    /// Replace text (delete + insert).
53    Replace {
54        position: usize,
55        deleted: String,
56        inserted: String,
57    },
58}
59
60impl TextOperation {
61    /// Create the inverse operation.
62    pub fn inverse(&self) -> Self {
63        match self {
64            TextOperation::Insert { position, text } => TextOperation::Delete {
65                position: *position,
66                deleted: text.clone(),
67            },
68            TextOperation::Delete { position, deleted } => TextOperation::Insert {
69                position: *position,
70                text: deleted.clone(),
71            },
72            TextOperation::Replace {
73                position,
74                deleted,
75                inserted,
76            } => TextOperation::Replace {
77                position: *position,
78                deleted: inserted.clone(),
79                inserted: deleted.clone(),
80            },
81        }
82    }
83}
84
85/// A formatting operation that can be undone.
86#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
87pub enum FormatOperation {
88    /// Add a mark.
89    AddMark {
90        mark_id: String,
91        mark_type: String,
92        start: usize,
93        end: usize,
94    },
95    /// Remove a mark.
96    RemoveMark { mark_id: String },
97}
98
99impl FormatOperation {
100    /// Create the inverse operation.
101    pub fn inverse(&self) -> Self {
102        match self {
103            FormatOperation::AddMark { mark_id, .. } => FormatOperation::RemoveMark {
104                mark_id: mark_id.clone(),
105            },
106            FormatOperation::RemoveMark { mark_id } => {
107                // Note: Full inverse would need to store mark details
108                FormatOperation::RemoveMark {
109                    mark_id: mark_id.clone(),
110                }
111            }
112        }
113    }
114}
115
116/// A JSON operation that can be undone.
117#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
118pub enum JsonOperation {
119    /// Set a value at a path.
120    Set {
121        path: String,
122        old_value: Option<serde_json::Value>,
123        new_value: serde_json::Value,
124    },
125    /// Delete a value at a path.
126    Delete {
127        path: String,
128        old_value: serde_json::Value,
129    },
130    /// Insert into an array.
131    ArrayInsert {
132        array_path: String,
133        index: usize,
134        value: serde_json::Value,
135    },
136    /// Remove from an array.
137    ArrayRemove {
138        array_path: String,
139        index: usize,
140        value: serde_json::Value,
141    },
142}
143
144impl JsonOperation {
145    /// Create the inverse operation.
146    pub fn inverse(&self) -> Self {
147        match self {
148            JsonOperation::Set {
149                path,
150                old_value,
151                new_value,
152            } => {
153                if let Some(old) = old_value {
154                    JsonOperation::Set {
155                        path: path.clone(),
156                        old_value: Some(new_value.clone()),
157                        new_value: old.clone(),
158                    }
159                } else {
160                    JsonOperation::Delete {
161                        path: path.clone(),
162                        old_value: new_value.clone(),
163                    }
164                }
165            }
166            JsonOperation::Delete { path, old_value } => JsonOperation::Set {
167                path: path.clone(),
168                old_value: None,
169                new_value: old_value.clone(),
170            },
171            JsonOperation::ArrayInsert {
172                array_path,
173                index,
174                value,
175            } => JsonOperation::ArrayRemove {
176                array_path: array_path.clone(),
177                index: *index,
178                value: value.clone(),
179            },
180            JsonOperation::ArrayRemove {
181                array_path,
182                index,
183                value,
184            } => JsonOperation::ArrayInsert {
185                array_path: array_path.clone(),
186                index: *index,
187                value: value.clone(),
188            },
189        }
190    }
191}
192
193/// An operation that can be undone.
194#[derive(Clone, Debug, Serialize, Deserialize)]
195pub enum UndoableOperation {
196    Text(TextOperation),
197    Format(FormatOperation),
198    Json(JsonOperation),
199}
200
201impl UndoableOperation {
202    /// Create the inverse operation.
203    pub fn inverse(&self) -> Self {
204        match self {
205            UndoableOperation::Text(op) => UndoableOperation::Text(op.inverse()),
206            UndoableOperation::Format(op) => UndoableOperation::Format(op.inverse()),
207            UndoableOperation::Json(op) => UndoableOperation::Json(op.inverse()),
208        }
209    }
210}
211
212/// A recorded operation with metadata.
213#[derive(Clone, Debug, Serialize, Deserialize)]
214pub struct Operation {
215    /// Unique operation ID.
216    pub id: OperationId,
217    /// The document this operation applies to.
218    pub document_id: String,
219    /// The replica that created this operation.
220    pub replica_id: String,
221    /// The operation itself.
222    pub operation: UndoableOperation,
223    /// Lamport timestamp.
224    pub timestamp: u64,
225    /// Group ID for atomic undo.
226    pub group_id: Option<GroupId>,
227    /// Whether this operation has been undone.
228    pub undone: bool,
229}
230
231impl Operation {
232    pub fn new(
233        document_id: impl Into<String>,
234        replica_id: impl Into<String>,
235        operation: UndoableOperation,
236        timestamp: u64,
237    ) -> Self {
238        Self {
239            id: OperationId::new(),
240            document_id: document_id.into(),
241            replica_id: replica_id.into(),
242            operation,
243            timestamp,
244            group_id: None,
245            undone: false,
246        }
247    }
248
249    pub fn with_group(mut self, group_id: GroupId) -> Self {
250        self.group_id = Some(group_id);
251        self
252    }
253}
254
255/// An undo manager for a single document.
256#[derive(Clone, Debug)]
257pub struct UndoManager {
258    /// The document ID.
259    document_id: String,
260    /// The local replica ID.
261    replica_id: String,
262    /// Lamport clock.
263    clock: u64,
264    /// Operation history (all operations, including from other replicas).
265    history: Vec<Operation>,
266    /// Undo stack (local operations that can be undone).
267    undo_stack: VecDeque<OperationId>,
268    /// Redo stack (local operations that can be redone).
269    redo_stack: VecDeque<OperationId>,
270    /// Current group being built.
271    current_group: Option<GroupId>,
272    /// Maximum history size.
273    max_history: usize,
274}
275
276impl UndoManager {
277    /// Create a new undo manager.
278    pub fn new(document_id: impl Into<String>, replica_id: impl Into<String>) -> Self {
279        Self {
280            document_id: document_id.into(),
281            replica_id: replica_id.into(),
282            clock: 0,
283            history: Vec::new(),
284            undo_stack: VecDeque::new(),
285            redo_stack: VecDeque::new(),
286            current_group: None,
287            max_history: 1000,
288        }
289    }
290
291    /// Set the maximum history size.
292    pub fn set_max_history(&mut self, max: usize) {
293        self.max_history = max;
294        self.trim_history();
295    }
296
297    /// Record a local operation.
298    pub fn record(&mut self, operation: UndoableOperation) -> &Operation {
299        self.clock += 1;
300
301        let mut op = Operation::new(&self.document_id, &self.replica_id, operation, self.clock);
302
303        if let Some(group_id) = &self.current_group {
304            op.group_id = Some(group_id.clone());
305        }
306
307        let op_id = op.id.clone();
308        self.history.push(op);
309        self.undo_stack.push_back(op_id);
310
311        // Clear redo stack when new operation is recorded
312        self.redo_stack.clear();
313
314        self.trim_history();
315
316        self.history.last().unwrap()
317    }
318
319    /// Record a remote operation (from another replica).
320    pub fn record_remote(&mut self, operation: Operation) {
321        // Update clock
322        self.clock = self.clock.max(operation.timestamp) + 1;
323        self.history.push(operation);
324        self.trim_history();
325    }
326
327    /// Start a new operation group.
328    pub fn start_group(&mut self) -> GroupId {
329        let group_id = GroupId::new();
330        self.current_group = Some(group_id.clone());
331        group_id
332    }
333
334    /// End the current operation group.
335    pub fn end_group(&mut self) {
336        self.current_group = None;
337    }
338
339    /// Check if we can undo.
340    pub fn can_undo(&self) -> bool {
341        !self.undo_stack.is_empty()
342    }
343
344    /// Check if we can redo.
345    pub fn can_redo(&self) -> bool {
346        !self.redo_stack.is_empty()
347    }
348
349    /// Undo the last operation (or group).
350    /// Returns the inverse operations to apply.
351    pub fn undo(&mut self) -> Vec<UndoableOperation> {
352        if self.undo_stack.is_empty() {
353            return Vec::new();
354        }
355
356        let op_id = self.undo_stack.pop_back().unwrap();
357        let mut inverses = Vec::new();
358
359        // Find the operation's group id first
360        let group_id = self
361            .history
362            .iter()
363            .find(|o| o.id == op_id)
364            .and_then(|o| o.group_id.clone());
365
366        if let Some(group_id) = group_id {
367            // Find all operations in this group and undo them
368            for op in self.history.iter_mut() {
369                if op.group_id.as_ref() == Some(&group_id) && !op.undone {
370                    inverses.push(op.operation.inverse());
371                    op.undone = true;
372                }
373            }
374
375            // Remove all group operations from undo stack
376            let group_id_ref = &group_id;
377            self.undo_stack.retain(|id| {
378                self.history
379                    .iter()
380                    .find(|o| &o.id == id)
381                    .map(|o| o.group_id.as_ref() != Some(group_id_ref))
382                    .unwrap_or(true)
383            });
384
385            self.redo_stack.push_back(op_id);
386        } else {
387            // Single operation
388            if let Some(op) = self.history.iter_mut().find(|o| o.id == op_id) {
389                inverses.push(op.operation.inverse());
390                op.undone = true;
391            }
392            self.redo_stack.push_back(op_id);
393        }
394
395        // Return in reverse order (last operation first)
396        inverses.reverse();
397        inverses
398    }
399
400    /// Redo the last undone operation.
401    /// Returns the operations to reapply.
402    pub fn redo(&mut self) -> Vec<UndoableOperation> {
403        if self.redo_stack.is_empty() {
404            return Vec::new();
405        }
406
407        let op_id = self.redo_stack.pop_back().unwrap();
408        let mut operations = Vec::new();
409
410        // Find the operation's group id first
411        let group_id = self
412            .history
413            .iter()
414            .find(|o| o.id == op_id)
415            .and_then(|o| o.group_id.clone());
416
417        if let Some(group_id) = group_id {
418            // Find all operations in this group and redo them
419            for op in self.history.iter_mut() {
420                if op.group_id.as_ref() == Some(&group_id) && op.undone {
421                    operations.push(op.operation.clone());
422                    op.undone = false;
423                }
424            }
425            self.undo_stack.push_back(op_id);
426        } else {
427            if let Some(op) = self.history.iter_mut().find(|o| o.id == op_id) {
428                operations.push(op.operation.clone());
429                op.undone = false;
430            }
431            self.undo_stack.push_back(op_id);
432        }
433
434        operations
435    }
436
437    /// Get the undo stack size.
438    pub fn undo_stack_size(&self) -> usize {
439        self.undo_stack.len()
440    }
441
442    /// Get the redo stack size.
443    pub fn redo_stack_size(&self) -> usize {
444        self.redo_stack.len()
445    }
446
447    /// Clear all history.
448    pub fn clear(&mut self) {
449        self.history.clear();
450        self.undo_stack.clear();
451        self.redo_stack.clear();
452    }
453
454    /// Trim history to max size.
455    fn trim_history(&mut self) {
456        while self.history.len() > self.max_history {
457            if let Some(removed) = self.history.first() {
458                let removed_id = removed.id.clone();
459                self.undo_stack.retain(|id| *id != removed_id);
460                self.redo_stack.retain(|id| *id != removed_id);
461            }
462            self.history.remove(0);
463        }
464    }
465}
466
467/// A collaborative undo manager that tracks operations across replicas.
468#[derive(Clone, Debug)]
469pub struct CollaborativeUndoManager {
470    /// Per-document undo managers.
471    managers: HashMap<String, UndoManager>,
472    /// The local replica ID.
473    replica_id: String,
474}
475
476impl CollaborativeUndoManager {
477    /// Create a new collaborative undo manager.
478    pub fn new(replica_id: impl Into<String>) -> Self {
479        Self {
480            managers: HashMap::new(),
481            replica_id: replica_id.into(),
482        }
483    }
484
485    /// Get or create an undo manager for a document.
486    pub fn for_document(&mut self, document_id: &str) -> &mut UndoManager {
487        self.managers
488            .entry(document_id.to_string())
489            .or_insert_with(|| UndoManager::new(document_id, &self.replica_id))
490    }
491
492    /// Record an operation.
493    pub fn record(&mut self, document_id: &str, operation: UndoableOperation) -> &Operation {
494        self.for_document(document_id).record(operation)
495    }
496
497    /// Record a remote operation.
498    pub fn record_remote(&mut self, document_id: &str, operation: Operation) {
499        self.for_document(document_id).record_remote(operation);
500    }
501
502    /// Start a group for a document.
503    pub fn start_group(&mut self, document_id: &str) -> GroupId {
504        self.for_document(document_id).start_group()
505    }
506
507    /// End a group for a document.
508    pub fn end_group(&mut self, document_id: &str) {
509        self.for_document(document_id).end_group();
510    }
511
512    /// Undo for a document.
513    pub fn undo(&mut self, document_id: &str) -> Vec<UndoableOperation> {
514        self.for_document(document_id).undo()
515    }
516
517    /// Redo for a document.
518    pub fn redo(&mut self, document_id: &str) -> Vec<UndoableOperation> {
519        self.for_document(document_id).redo()
520    }
521
522    /// Check if we can undo for a document.
523    pub fn can_undo(&self, document_id: &str) -> bool {
524        self.managers
525            .get(document_id)
526            .map(|m| m.can_undo())
527            .unwrap_or(false)
528    }
529
530    /// Check if we can redo for a document.
531    pub fn can_redo(&self, document_id: &str) -> bool {
532        self.managers
533            .get(document_id)
534            .map(|m| m.can_redo())
535            .unwrap_or(false)
536    }
537
538    /// Remove a document's undo manager.
539    pub fn remove_document(&mut self, document_id: &str) {
540        self.managers.remove(document_id);
541    }
542}
543
544#[cfg(test)]
545mod tests {
546    use super::*;
547
548    #[test]
549    fn test_text_operation_inverse() {
550        let insert = TextOperation::Insert {
551            position: 0,
552            text: "Hello".to_string(),
553        };
554        let inverse = insert.inverse();
555
556        assert!(
557            matches!(inverse, TextOperation::Delete { position: 0, deleted } if deleted == "Hello")
558        );
559    }
560
561    #[test]
562    fn test_basic_undo() {
563        let mut manager = UndoManager::new("doc1", "r1");
564
565        // Record insert
566        manager.record(UndoableOperation::Text(TextOperation::Insert {
567            position: 0,
568            text: "Hello".to_string(),
569        }));
570
571        assert!(manager.can_undo());
572        assert!(!manager.can_redo());
573
574        // Undo
575        let inverses = manager.undo();
576        assert_eq!(inverses.len(), 1);
577
578        if let UndoableOperation::Text(TextOperation::Delete { deleted, .. }) = &inverses[0] {
579            assert_eq!(deleted, "Hello");
580        } else {
581            panic!("Expected text delete operation");
582        }
583
584        assert!(!manager.can_undo());
585        assert!(manager.can_redo());
586    }
587
588    #[test]
589    fn test_redo() {
590        let mut manager = UndoManager::new("doc1", "r1");
591
592        manager.record(UndoableOperation::Text(TextOperation::Insert {
593            position: 0,
594            text: "Hello".to_string(),
595        }));
596
597        manager.undo();
598        assert!(manager.can_redo());
599
600        let ops = manager.redo();
601        assert_eq!(ops.len(), 1);
602
603        if let UndoableOperation::Text(TextOperation::Insert { text, .. }) = &ops[0] {
604            assert_eq!(text, "Hello");
605        } else {
606            panic!("Expected text insert operation");
607        }
608    }
609
610    #[test]
611    fn test_operation_group() {
612        let mut manager = UndoManager::new("doc1", "r1");
613
614        // Start group
615        manager.start_group();
616
617        // Record multiple operations
618        manager.record(UndoableOperation::Text(TextOperation::Insert {
619            position: 0,
620            text: "A".to_string(),
621        }));
622        manager.record(UndoableOperation::Text(TextOperation::Insert {
623            position: 1,
624            text: "B".to_string(),
625        }));
626        manager.record(UndoableOperation::Text(TextOperation::Insert {
627            position: 2,
628            text: "C".to_string(),
629        }));
630
631        // End group
632        manager.end_group();
633
634        // Undo should undo all operations in the group
635        let inverses = manager.undo();
636        // Note: Due to our simplified implementation, this might return just one
637        // In a full implementation, all group operations would be undone together
638        assert!(!inverses.is_empty());
639    }
640
641    #[test]
642    fn test_redo_clears_on_new_operation() {
643        let mut manager = UndoManager::new("doc1", "r1");
644
645        manager.record(UndoableOperation::Text(TextOperation::Insert {
646            position: 0,
647            text: "A".to_string(),
648        }));
649
650        manager.undo();
651        assert!(manager.can_redo());
652
653        // New operation should clear redo stack
654        manager.record(UndoableOperation::Text(TextOperation::Insert {
655            position: 0,
656            text: "B".to_string(),
657        }));
658
659        assert!(!manager.can_redo());
660    }
661
662    #[test]
663    fn test_collaborative_undo_manager() {
664        let mut manager = CollaborativeUndoManager::new("r1");
665
666        // Record operations in different documents
667        manager.record(
668            "doc1",
669            UndoableOperation::Text(TextOperation::Insert {
670                position: 0,
671                text: "Hello".to_string(),
672            }),
673        );
674
675        manager.record(
676            "doc2",
677            UndoableOperation::Json(JsonOperation::Set {
678                path: "name".to_string(),
679                old_value: None,
680                new_value: serde_json::json!("test"),
681            }),
682        );
683
684        assert!(manager.can_undo("doc1"));
685        assert!(manager.can_undo("doc2"));
686
687        // Undo in doc1 doesn't affect doc2
688        manager.undo("doc1");
689        assert!(!manager.can_undo("doc1"));
690        assert!(manager.can_undo("doc2"));
691    }
692
693    #[test]
694    fn test_json_operation_inverse() {
695        let set = JsonOperation::Set {
696            path: "name".to_string(),
697            old_value: Some(serde_json::json!("old")),
698            new_value: serde_json::json!("new"),
699        };
700
701        let inverse = set.inverse();
702
703        if let JsonOperation::Set {
704            path,
705            old_value,
706            new_value,
707        } = inverse
708        {
709            assert_eq!(path, "name");
710            assert_eq!(new_value, serde_json::json!("old"));
711            assert_eq!(old_value, Some(serde_json::json!("new")));
712        } else {
713            panic!("Expected Set operation");
714        }
715    }
716
717    #[test]
718    fn test_max_history() {
719        let mut manager = UndoManager::new("doc1", "r1");
720        manager.set_max_history(5);
721
722        for i in 0..10 {
723            manager.record(UndoableOperation::Text(TextOperation::Insert {
724                position: i,
725                text: format!("{}", i),
726            }));
727        }
728
729        // Should only keep last 5 operations
730        assert!(manager.undo_stack_size() <= 5);
731    }
732
733    #[test]
734    fn test_remote_operation() {
735        let mut manager = UndoManager::new("doc1", "r1");
736
737        // Record a remote operation
738        let remote_op = Operation::new(
739            "doc1",
740            "r2",
741            UndoableOperation::Text(TextOperation::Insert {
742                position: 0,
743                text: "Remote".to_string(),
744            }),
745            100,
746        );
747
748        manager.record_remote(remote_op);
749
750        // Remote operations are in history but not in local undo stack
751        assert!(!manager.can_undo());
752    }
753}