atproto blogging
1//! Undo/redo management for editor operations.
2//!
3//! Provides:
4//! - `UndoManager` trait for abstracting undo implementations
5//! - `UndoableBuffer<T>` - wraps a TextBuffer and provides undo/redo
6
7use std::ops::Range;
8
9use smol_str::{SmolStr, ToSmolStr};
10
11use crate::text::TextBuffer;
12
13/// Trait for managing undo/redo operations.
14///
15/// Implementations must actually perform the undo/redo, not just track state.
16/// For local editing, use `UndoableBuffer<T>` which wraps a TextBuffer.
17/// For Loro, wrap LoroText + loro::UndoManager together.
18pub trait UndoManager {
19 /// Check if undo is available.
20 fn can_undo(&self) -> bool;
21
22 /// Check if redo is available.
23 fn can_redo(&self) -> bool;
24
25 /// Perform undo. Returns true if successful.
26 fn undo(&mut self) -> bool;
27
28 /// Perform redo. Returns true if successful.
29 fn redo(&mut self) -> bool;
30
31 /// Clear all undo/redo history.
32 fn clear_history(&mut self);
33}
34
35/// A recorded edit operation for undo/redo.
36#[derive(Debug, Clone)]
37struct EditOperation {
38 /// Character position where edit occurred
39 pos: usize,
40 /// Text that was deleted (empty for pure insertions)
41 deleted: SmolStr,
42 /// Text that was inserted (empty for pure deletions)
43 inserted: SmolStr,
44}
45
46/// A TextBuffer wrapper that tracks edits and provides undo/redo.
47///
48/// This is the standard way to get undo support for local editing.
49/// All mutations go through this wrapper, which records them for undo.
50pub struct UndoableBuffer<T> {
51 buffer: T,
52 undo_stack: Vec<EditOperation>,
53 redo_stack: Vec<EditOperation>,
54 max_steps: usize,
55}
56
57impl<T: Clone> Clone for UndoableBuffer<T> {
58 fn clone(&self) -> Self {
59 Self {
60 buffer: self.buffer.clone(),
61 undo_stack: self.undo_stack.clone(),
62 redo_stack: self.redo_stack.clone(),
63 max_steps: self.max_steps,
64 }
65 }
66}
67
68impl<T: TextBuffer + Default> Default for UndoableBuffer<T> {
69 fn default() -> Self {
70 Self::new(T::default(), 100)
71 }
72}
73
74impl<T: TextBuffer> UndoableBuffer<T> {
75 /// Create a new undoable buffer wrapping the given buffer.
76 pub fn new(buffer: T, max_steps: usize) -> Self {
77 Self {
78 buffer,
79 undo_stack: Vec::new(),
80 redo_stack: Vec::new(),
81 max_steps,
82 }
83 }
84
85 /// Get a reference to the inner buffer.
86 pub fn inner(&self) -> &T {
87 &self.buffer
88 }
89
90 /// Get a mutable reference to the inner buffer.
91 /// WARNING: Edits made directly bypass undo tracking!
92 pub fn inner_mut(&mut self) -> &mut T {
93 &mut self.buffer
94 }
95
96 /// Record an operation (called internally by TextBuffer impl).
97 fn record_op(&mut self, pos: usize, deleted: &str, inserted: &str) {
98 // Clear redo stack on new edit
99 self.redo_stack.clear();
100
101 let op = EditOperation {
102 pos,
103 deleted: deleted.to_smolstr(),
104 inserted: inserted.to_smolstr(),
105 };
106
107 self.undo_stack.push(op);
108
109 // Trim if over max
110 while self.undo_stack.len() > self.max_steps {
111 self.undo_stack.remove(0);
112 }
113 }
114}
115
116// Implement TextBuffer by delegating to inner buffer + recording operations
117impl<T: TextBuffer> TextBuffer for UndoableBuffer<T> {
118 fn len_bytes(&self) -> usize {
119 self.buffer.len_bytes()
120 }
121
122 fn len_chars(&self) -> usize {
123 self.buffer.len_chars()
124 }
125
126 fn insert(&mut self, char_offset: usize, text: &str) {
127 self.record_op(char_offset, "", text);
128 self.buffer.insert(char_offset, text);
129 }
130
131 fn delete(&mut self, char_range: Range<usize>) {
132 // Get the text being deleted for undo
133 let deleted = self
134 .buffer
135 .slice(char_range.clone())
136 .map(|s| s.to_string())
137 .unwrap_or_default();
138 self.record_op(char_range.start, &deleted, "");
139 self.buffer.delete(char_range);
140 }
141
142 fn slice(&self, char_range: Range<usize>) -> Option<SmolStr> {
143 self.buffer.slice(char_range)
144 }
145
146 fn char_at(&self, char_offset: usize) -> Option<char> {
147 self.buffer.char_at(char_offset)
148 }
149
150 fn to_string(&self) -> String {
151 self.buffer.to_string()
152 }
153
154 fn char_to_byte(&self, char_offset: usize) -> usize {
155 self.buffer.char_to_byte(char_offset)
156 }
157
158 fn byte_to_char(&self, byte_offset: usize) -> usize {
159 self.buffer.byte_to_char(byte_offset)
160 }
161
162 fn last_edit(&self) -> Option<crate::types::EditInfo> {
163 self.buffer.last_edit()
164 }
165}
166
167impl<T: TextBuffer> UndoManager for UndoableBuffer<T> {
168 fn can_undo(&self) -> bool {
169 !self.undo_stack.is_empty()
170 }
171
172 fn can_redo(&self) -> bool {
173 !self.redo_stack.is_empty()
174 }
175
176 fn undo(&mut self) -> bool {
177 let Some(op) = self.undo_stack.pop() else {
178 return false;
179 };
180
181 // Apply inverse: delete what was inserted, insert what was deleted
182 let inserted_chars = op.inserted.chars().count();
183 if inserted_chars > 0 {
184 self.buffer.delete(op.pos..op.pos + inserted_chars);
185 }
186 if !op.deleted.is_empty() {
187 self.buffer.insert(op.pos, &op.deleted);
188 }
189
190 self.redo_stack.push(op);
191 true
192 }
193
194 fn redo(&mut self) -> bool {
195 let Some(op) = self.redo_stack.pop() else {
196 return false;
197 };
198
199 // Re-apply original: delete what was deleted, insert what was inserted
200 let deleted_chars = op.deleted.chars().count();
201 if deleted_chars > 0 {
202 self.buffer.delete(op.pos..op.pos + deleted_chars);
203 }
204 if !op.inserted.is_empty() {
205 self.buffer.insert(op.pos, &op.inserted);
206 }
207
208 self.undo_stack.push(op);
209 true
210 }
211
212 fn clear_history(&mut self) {
213 self.undo_stack.clear();
214 self.redo_stack.clear();
215 }
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221 use crate::EditorRope;
222
223 #[test]
224 fn test_undoable_buffer_insert_undo() {
225 let rope = EditorRope::from_str("hello");
226 let mut buf = UndoableBuffer::new(rope, 100);
227
228 assert_eq!(buf.to_string(), "hello");
229 assert!(!buf.can_undo());
230
231 // Insert " world"
232 buf.insert(5, " world");
233 assert_eq!(buf.to_string(), "hello world");
234 assert!(buf.can_undo());
235
236 // Undo
237 assert!(buf.undo());
238 assert_eq!(buf.to_string(), "hello");
239 assert!(!buf.can_undo());
240 assert!(buf.can_redo());
241
242 // Redo
243 assert!(buf.redo());
244 assert_eq!(buf.to_string(), "hello world");
245 assert!(buf.can_undo());
246 assert!(!buf.can_redo());
247 }
248
249 #[test]
250 fn test_undoable_buffer_delete_undo() {
251 let rope = EditorRope::from_str("hello world");
252 let mut buf = UndoableBuffer::new(rope, 100);
253
254 // Delete " world"
255 buf.delete(5..11);
256 assert_eq!(buf.to_string(), "hello");
257 assert!(buf.can_undo());
258
259 // Undo
260 assert!(buf.undo());
261 assert_eq!(buf.to_string(), "hello world");
262 }
263
264 #[test]
265 fn test_undoable_buffer_replace_undo() {
266 let rope = EditorRope::from_str("hello world");
267 let mut buf = UndoableBuffer::new(rope, 100);
268
269 // Replace "world" with "rust"
270 buf.delete(6..11);
271 buf.insert(6, "rust");
272 assert_eq!(buf.to_string(), "hello rust");
273
274 // Undo insert
275 assert!(buf.undo());
276 assert_eq!(buf.to_string(), "hello ");
277
278 // Undo delete
279 assert!(buf.undo());
280 assert_eq!(buf.to_string(), "hello world");
281 }
282
283 #[test]
284 fn test_new_edit_clears_redo() {
285 let rope = EditorRope::from_str("abc");
286 let mut buf = UndoableBuffer::new(rope, 100);
287
288 buf.insert(3, "d");
289 assert!(buf.undo());
290 assert!(buf.can_redo());
291
292 // New edit should clear redo
293 buf.insert(3, "e");
294 assert!(!buf.can_redo());
295 }
296
297 #[test]
298 fn test_max_steps() {
299 let rope = EditorRope::from_str("");
300 let mut buf = UndoableBuffer::new(rope, 3);
301
302 buf.insert(0, "a");
303 buf.insert(1, "b");
304 buf.insert(2, "c");
305 buf.insert(3, "d"); // should evict "a"
306
307 assert_eq!(buf.to_string(), "abcd");
308
309 // Should only be able to undo 3 times
310 assert!(buf.undo()); // removes d
311 assert!(buf.undo()); // removes c
312 assert!(buf.undo()); // removes b
313 assert!(!buf.undo()); // a was evicted
314
315 assert_eq!(buf.to_string(), "a");
316 }
317
318 #[test]
319 fn test_multiple_undo_redo_cycles() {
320 let rope = EditorRope::from_str("");
321 let mut buf = UndoableBuffer::new(rope, 100);
322
323 buf.insert(0, "a");
324 buf.insert(1, "b");
325 buf.insert(2, "c");
326 assert_eq!(buf.to_string(), "abc");
327
328 // Undo all
329 assert!(buf.undo());
330 assert!(buf.undo());
331 assert!(buf.undo());
332 assert_eq!(buf.to_string(), "");
333
334 // Redo all
335 assert!(buf.redo());
336 assert!(buf.redo());
337 assert!(buf.redo());
338 assert_eq!(buf.to_string(), "abc");
339
340 // Partial undo then new edit
341 assert!(buf.undo()); // "ab"
342 assert!(buf.undo()); // "a"
343 buf.insert(1, "x");
344 assert_eq!(buf.to_string(), "ax");
345 assert!(!buf.can_redo()); // redo cleared
346 }
347}