Skip to main content

kode_core/
history.rs

1use crate::transaction::Transaction;
2
3/// Undo/redo history using invertible transactions.
4///
5/// Consecutive single-character edits are coalesced into groups so that
6/// undoing "typed a word" undoes the whole word, not one char at a time.
7#[derive(Debug)]
8pub struct History {
9    undo_stack: Vec<Transaction>,
10    redo_stack: Vec<Transaction>,
11    /// Undo stack depth at the last clean point. None if never saved or
12    /// the clean state has been diverged from (new edit after undo past clean).
13    clean_depth: Option<usize>,
14    /// When true, the next push will not coalesce with the previous entry.
15    force_new_group: bool,
16}
17
18impl History {
19    pub fn new() -> Self {
20        Self {
21            undo_stack: Vec::new(),
22            redo_stack: Vec::new(),
23            clean_depth: Some(0),
24            force_new_group: false,
25        }
26    }
27
28    /// Record a transaction. Clears the redo stack.
29    /// Tries to coalesce with the most recent undo entry.
30    pub fn push(&mut self, tx: Transaction) {
31        // If we had a clean point in the redo future, it's now unreachable
32        if let Some(depth) = self.clean_depth {
33            if depth > self.undo_stack.len() {
34                self.clean_depth = None;
35            }
36        }
37        self.redo_stack.clear();
38
39        if !self.force_new_group {
40            if let Some(last) = self.undo_stack.last_mut() {
41                if last.can_coalesce(&tx) {
42                    last.merge(&tx);
43                    return;
44                }
45            }
46        }
47        self.force_new_group = false;
48
49        self.undo_stack.push(tx);
50    }
51
52    /// Pop the most recent transaction for undo. Returns its inverse
53    /// (which should be applied to revert the change).
54    pub fn undo(&mut self) -> Option<Transaction> {
55        let tx = self.undo_stack.pop()?;
56        let inverse = tx.inverse();
57        self.redo_stack.push(tx);
58        Some(inverse)
59    }
60
61    /// Pop the most recent redo transaction. Returns it
62    /// (which should be re-applied).
63    pub fn redo(&mut self) -> Option<Transaction> {
64        let tx = self.redo_stack.pop()?;
65        let re_apply = tx.clone();
66        self.undo_stack.push(tx);
67        Some(re_apply)
68    }
69
70    /// Mark the current state as clean (e.g., after save).
71    /// Also forces the next edit into a new undo group so that
72    /// undoing back to this point is always possible.
73    pub fn mark_clean(&mut self) {
74        self.clean_depth = Some(self.undo_stack.len());
75        self.force_new_group = true;
76    }
77
78    /// Check if the document is dirty (modified since last save).
79    pub fn is_dirty(&self) -> bool {
80        self.clean_depth != Some(self.undo_stack.len())
81    }
82
83    /// True if there are entries to undo.
84    pub fn can_undo(&self) -> bool {
85        !self.undo_stack.is_empty()
86    }
87
88    /// True if there are entries to redo.
89    pub fn can_redo(&self) -> bool {
90        !self.redo_stack.is_empty()
91    }
92
93    /// Clear all history and reset to clean state.
94    pub fn clear(&mut self) {
95        self.undo_stack.clear();
96        self.redo_stack.clear();
97        self.clean_depth = Some(0);
98        self.force_new_group = false;
99    }
100}
101
102impl Default for History {
103    fn default() -> Self {
104        Self::new()
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111    use crate::transaction::EditStep;
112
113    #[test]
114    fn undo_redo_basic() {
115        let mut history = History::new();
116        let tx = Transaction::single(EditStep::insert(0, "hello"));
117        history.push(tx);
118
119        assert!(history.can_undo());
120        assert!(!history.can_redo());
121
122        let undo_tx = history.undo().unwrap();
123        assert_eq!(undo_tx.steps[0].offset, 0);
124        assert_eq!(undo_tx.steps[0].deleted, "hello");
125        assert!(undo_tx.steps[0].inserted.is_empty());
126
127        assert!(!history.can_undo());
128        assert!(history.can_redo());
129
130        let redo_tx = history.redo().unwrap();
131        assert_eq!(redo_tx.steps[0].inserted, "hello");
132    }
133
134    #[test]
135    fn new_edit_clears_redo() {
136        let mut history = History::new();
137        history.push(Transaction::single(EditStep::insert(0, "a")));
138        history.push(Transaction::single(EditStep::insert(1, "b")));
139        history.undo(); // undo "b" (coalesced with "a" so undoes "ab")
140
141        // Can redo
142        assert!(history.can_redo());
143
144        // New edit clears redo
145        history.push(Transaction::single(EditStep::insert(0, "x")));
146        assert!(!history.can_redo());
147    }
148
149    #[test]
150    fn coalescing_inserts() {
151        let mut history = History::new();
152        history.push(Transaction::single(EditStep::insert(0, "h")));
153        history.push(Transaction::single(EditStep::insert(1, "i")));
154
155        // Should be coalesced into one entry
156        assert_eq!(history.undo_stack.len(), 1);
157        assert_eq!(history.undo_stack[0].steps[0].inserted, "hi");
158    }
159
160    #[test]
161    fn newline_breaks_coalescing() {
162        let mut history = History::new();
163        history.push(Transaction::single(EditStep::insert(0, "a")));
164        history.push(Transaction::single(EditStep::insert(1, "\n")));
165
166        // Should NOT coalesce across newline
167        assert_eq!(history.undo_stack.len(), 2);
168    }
169
170    #[test]
171    fn dirty_tracking() {
172        let mut history = History::new();
173        assert!(!history.is_dirty());
174
175        history.push(Transaction::single(EditStep::insert(0, "x")));
176        assert!(history.is_dirty());
177
178        history.mark_clean();
179        assert!(!history.is_dirty());
180
181        // After mark_clean, next push starts a new group even if coalescible
182        history.push(Transaction::single(EditStep::insert(1, "y")));
183        assert!(history.is_dirty());
184
185        // Undo back to clean point
186        history.undo();
187        assert!(!history.is_dirty());
188    }
189}