1use crate::cursor::CursorPosition;
8
9#[derive(Clone, Debug, PartialEq, Eq)]
11pub enum EditOperation {
12 Insert {
14 pos: CursorPosition,
16 text: String,
18 },
19 Delete {
21 pos: CursorPosition,
23 text: String,
25 },
26 Replace {
28 pos: CursorPosition,
30 old_text: String,
32 new_text: String,
34 },
35}
36
37impl EditOperation {
38 pub fn inverse(&self) -> Self {
44 match self {
45 Self::Insert { pos, text } => Self::Delete {
46 pos: *pos,
47 text: text.clone(),
48 },
49 Self::Delete { pos, text } => Self::Insert {
50 pos: *pos,
51 text: text.clone(),
52 },
53 Self::Replace {
54 pos,
55 old_text,
56 new_text,
57 } => Self::Replace {
58 pos: *pos,
59 old_text: new_text.clone(),
60 new_text: old_text.clone(),
61 },
62 }
63 }
64}
65
66#[derive(Clone, Debug)]
72pub struct UndoStack {
73 undo_stack: Vec<EditOperation>,
74 redo_stack: Vec<EditOperation>,
75 max_history: usize,
76}
77
78impl UndoStack {
79 pub fn new(max_history: usize) -> Self {
81 Self {
82 undo_stack: Vec::new(),
83 redo_stack: Vec::new(),
84 max_history,
85 }
86 }
87
88 pub fn push(&mut self, op: EditOperation) {
93 self.redo_stack.clear();
94 self.undo_stack.push(op);
95 if self.undo_stack.len() > self.max_history {
96 self.undo_stack.remove(0);
97 }
98 }
99
100 pub fn undo(&mut self) -> Option<EditOperation> {
104 let op = self.undo_stack.pop()?;
105 let inverse = op.inverse();
106 self.redo_stack.push(op);
107 Some(inverse)
108 }
109
110 pub fn redo(&mut self) -> Option<EditOperation> {
114 let op = self.redo_stack.pop()?;
115 let result = op.clone();
116 self.undo_stack.push(op);
117 Some(result)
118 }
119
120 pub fn can_undo(&self) -> bool {
122 !self.undo_stack.is_empty()
123 }
124
125 pub fn can_redo(&self) -> bool {
127 !self.redo_stack.is_empty()
128 }
129
130 pub fn clear(&mut self) {
132 self.undo_stack.clear();
133 self.redo_stack.clear();
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 fn insert_op(line: usize, col: usize, text: &str) -> EditOperation {
142 EditOperation::Insert {
143 pos: CursorPosition::new(line, col),
144 text: text.to_string(),
145 }
146 }
147
148 fn delete_op(line: usize, col: usize, text: &str) -> EditOperation {
149 EditOperation::Delete {
150 pos: CursorPosition::new(line, col),
151 text: text.to_string(),
152 }
153 }
154
155 #[test]
156 fn push_and_undo() {
157 let mut stack = UndoStack::new(100);
158 stack.push(insert_op(0, 0, "hello"));
159 assert!(stack.can_undo());
160 let inv = stack.undo();
161 match inv {
162 Some(EditOperation::Delete { ref text, .. }) if text == "hello" => {}
163 other => unreachable!("expected Delete('hello'), got {other:?}"),
164 }
165 }
166
167 #[test]
168 fn undo_then_redo() {
169 let mut stack = UndoStack::new(100);
170 stack.push(insert_op(0, 0, "a"));
171 let _inv = stack.undo();
172 assert!(stack.can_redo());
173 let redo = stack.redo();
174 match redo {
175 Some(EditOperation::Insert { ref text, .. }) if text == "a" => {}
176 other => unreachable!("expected Insert('a'), got {other:?}"),
177 }
178 }
179
180 #[test]
181 fn push_clears_redo() {
182 let mut stack = UndoStack::new(100);
183 stack.push(insert_op(0, 0, "a"));
184 let _inv = stack.undo();
185 assert!(stack.can_redo());
186 stack.push(insert_op(0, 0, "b"));
187 assert!(!stack.can_redo());
188 }
189
190 #[test]
191 fn undo_multiple() {
192 let mut stack = UndoStack::new(100);
193 stack.push(insert_op(0, 0, "a"));
194 stack.push(insert_op(0, 1, "b"));
195 stack.push(insert_op(0, 2, "c"));
196
197 match stack.undo() {
199 Some(EditOperation::Delete { ref text, .. }) if text == "c" => {}
200 other => unreachable!("expected Delete('c'), got {other:?}"),
201 }
202 match stack.undo() {
203 Some(EditOperation::Delete { ref text, .. }) if text == "b" => {}
204 other => unreachable!("expected Delete('b'), got {other:?}"),
205 }
206 match stack.undo() {
207 Some(EditOperation::Delete { ref text, .. }) if text == "a" => {}
208 other => unreachable!("expected Delete('a'), got {other:?}"),
209 }
210 assert!(!stack.can_undo());
211 }
212
213 #[test]
214 fn max_history_limit() {
215 let mut stack = UndoStack::new(3);
216 stack.push(insert_op(0, 0, "a"));
217 stack.push(insert_op(0, 1, "b"));
218 stack.push(insert_op(0, 2, "c"));
219 stack.push(insert_op(0, 3, "d")); let _d = stack.undo();
223 let _c = stack.undo();
224 match stack.undo() {
225 Some(EditOperation::Delete { ref text, .. }) if text == "b" => {}
226 other => unreachable!("expected Delete('b'), got {other:?}"),
227 }
228 assert!(!stack.can_undo()); }
230
231 #[test]
232 fn inverse_insert() {
233 let op = insert_op(1, 5, "hello");
234 match op.inverse() {
235 EditOperation::Delete { pos, ref text } => {
236 assert!(pos == CursorPosition::new(1, 5));
237 assert!(text == "hello");
238 }
239 other => unreachable!("expected Delete, got {other:?}"),
240 }
241 }
242
243 #[test]
244 fn inverse_delete() {
245 let op = delete_op(2, 3, "world");
246 match op.inverse() {
247 EditOperation::Insert { pos, ref text } => {
248 assert!(pos == CursorPosition::new(2, 3));
249 assert!(text == "world");
250 }
251 other => unreachable!("expected Insert, got {other:?}"),
252 }
253 }
254
255 #[test]
256 fn inverse_replace() {
257 let op = EditOperation::Replace {
258 pos: CursorPosition::new(0, 0),
259 old_text: "foo".to_string(),
260 new_text: "bar".to_string(),
261 };
262 match op.inverse() {
263 EditOperation::Replace {
264 ref old_text,
265 ref new_text,
266 ..
267 } => {
268 assert!(old_text == "bar");
269 assert!(new_text == "foo");
270 }
271 other => unreachable!("expected Replace, got {other:?}"),
272 }
273 }
274
275 #[test]
276 fn clear_resets_both_stacks() {
277 let mut stack = UndoStack::new(100);
278 stack.push(insert_op(0, 0, "a"));
279 let _inv = stack.undo();
280 assert!(stack.can_redo());
281 stack.clear();
282 assert!(!stack.can_undo());
283 assert!(!stack.can_redo());
284 }
285
286 #[test]
287 fn empty_stack_undo_redo_returns_none() {
288 let mut stack = UndoStack::new(100);
289 assert!(stack.undo().is_none());
290 assert!(stack.redo().is_none());
291 }
292}