Skip to main content

mq_edit/document/
history.rs

1use super::Cursor;
2
3/// Represents a single edit action that can be undone/redone
4#[derive(Debug, Clone)]
5pub enum EditAction {
6    /// A character was inserted at a position
7    InsertChar { line: usize, column: usize, c: char },
8    /// A string was inserted at a position
9    InsertStr {
10        line: usize,
11        column: usize,
12        text: String,
13    },
14    /// A newline was inserted at a position (line split)
15    InsertNewline { line: usize, column: usize },
16    /// A character was deleted within a line (backspace)
17    DeleteChar {
18        line: usize,
19        column: usize,
20        deleted: char,
21    },
22    /// Lines were joined by backspace at column 0
23    JoinLines {
24        /// The line that was joined into the previous line
25        line: usize,
26        /// Column position in the previous line where join happened
27        column: usize,
28    },
29    /// A range of characters was deleted on a line
30    DeleteRange {
31        line: usize,
32        start_col: usize,
33        deleted: String,
34    },
35    /// Text was replaced at a specific position
36    ReplaceAt {
37        line: usize,
38        column: usize,
39        old_text: String,
40        new_text: String,
41    },
42}
43
44/// Entry in the history stack, pairing an action with the cursor state before it
45#[derive(Debug, Clone)]
46pub struct HistoryEntry {
47    pub action: EditAction,
48    pub cursor_before: Cursor,
49}
50
51/// Manages undo/redo history for edit operations
52#[derive(Debug, Clone)]
53pub struct EditHistory {
54    undo_stack: Vec<HistoryEntry>,
55    redo_stack: Vec<HistoryEntry>,
56    max_history: usize,
57}
58
59impl EditHistory {
60    pub fn new() -> Self {
61        Self {
62            undo_stack: Vec::new(),
63            redo_stack: Vec::new(),
64            max_history: 1000,
65        }
66    }
67
68    /// Record a new edit action, clearing the redo stack
69    pub fn push(&mut self, action: EditAction, cursor_before: Cursor) {
70        self.redo_stack.clear();
71        self.undo_stack.push(HistoryEntry {
72            action,
73            cursor_before,
74        });
75        if self.undo_stack.len() > self.max_history {
76            self.undo_stack.remove(0);
77        }
78    }
79
80    /// Pop the last action from undo stack and move to redo stack
81    pub fn undo(&mut self) -> Option<HistoryEntry> {
82        let entry = self.undo_stack.pop()?;
83        self.redo_stack.push(entry.clone());
84        Some(entry)
85    }
86
87    /// Pop the last action from redo stack and move to undo stack
88    pub fn redo(&mut self) -> Option<HistoryEntry> {
89        let entry = self.redo_stack.pop()?;
90        self.undo_stack.push(entry.clone());
91        Some(entry)
92    }
93}
94
95impl Default for EditHistory {
96    fn default() -> Self {
97        Self::new()
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn test_push_and_undo() {
107        let mut history = EditHistory::new();
108        let cursor = Cursor::new();
109
110        history.push(
111            EditAction::InsertChar {
112                line: 0,
113                column: 0,
114                c: 'a',
115            },
116            cursor,
117        );
118
119        let entry = history.undo().unwrap();
120        match entry.action {
121            EditAction::InsertChar { c, .. } => assert_eq!(c, 'a'),
122            _ => panic!("Expected InsertChar"),
123        }
124    }
125
126    #[test]
127    fn test_undo_empty() {
128        let mut history = EditHistory::new();
129        assert!(history.undo().is_none());
130    }
131
132    #[test]
133    fn test_redo_after_undo() {
134        let mut history = EditHistory::new();
135        let cursor = Cursor::new();
136
137        history.push(
138            EditAction::InsertChar {
139                line: 0,
140                column: 0,
141                c: 'x',
142            },
143            cursor,
144        );
145
146        history.undo();
147        let entry = history.redo().unwrap();
148        match entry.action {
149            EditAction::InsertChar { c, .. } => assert_eq!(c, 'x'),
150            _ => panic!("Expected InsertChar"),
151        }
152    }
153
154    #[test]
155    fn test_new_action_clears_redo() {
156        let mut history = EditHistory::new();
157        let cursor = Cursor::new();
158
159        history.push(
160            EditAction::InsertChar {
161                line: 0,
162                column: 0,
163                c: 'a',
164            },
165            cursor,
166        );
167        history.undo();
168
169        // Push new action should clear redo
170        history.push(
171            EditAction::InsertChar {
172                line: 0,
173                column: 0,
174                c: 'b',
175            },
176            cursor,
177        );
178
179        assert!(history.redo().is_none());
180    }
181
182    #[test]
183    fn test_max_history() {
184        let mut history = EditHistory::new();
185        history.max_history = 3;
186        let cursor = Cursor::new();
187
188        for i in 0..5 {
189            history.push(
190                EditAction::InsertChar {
191                    line: 0,
192                    column: i,
193                    c: 'a',
194                },
195                cursor,
196            );
197        }
198
199        assert_eq!(history.undo_stack.len(), 3);
200    }
201}