at main 347 lines 9.4 kB view raw
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}