at main 414 lines 12 kB view raw
1//! Loro-backed text buffer implementing core editor traits. 2 3use std::cell::RefCell; 4use std::ops::Range; 5use std::rc::Rc; 6 7use loro::{ 8 cursor::{Cursor, PosType, Side}, 9 LoroDoc, LoroText, UndoManager as LoroUndoManager, VersionVector, 10}; 11use smol_str::{SmolStr, ToSmolStr}; 12use web_time::Instant; 13use weaver_editor_core::{EditInfo, TextBuffer, UndoManager}; 14 15use crate::CrdtError; 16 17/// Mutable state that must be shared across clones. 18struct LoroTextBufferInner { 19 undo_mgr: LoroUndoManager, 20 last_edit: Option<EditInfo>, 21 loro_cursor: Option<Cursor>, 22} 23 24/// Loro-backed text buffer with undo/redo support. 25/// 26/// Wraps a `LoroDoc` with a text container and provides implementations 27/// of the `TextBuffer` and `UndoManager` traits from weaver-editor-core. 28/// 29/// Also provides CRDT-aware cursor tracking that survives remote edits 30/// and undo/redo operations. 31/// 32/// Cloning is cheap and clones share all mutable state (undo history, 33/// last edit info, cursor position). 34#[derive(Clone)] 35pub struct LoroTextBuffer { 36 doc: LoroDoc, 37 content: LoroText, 38 inner: Rc<RefCell<LoroTextBufferInner>>, 39} 40 41impl LoroTextBuffer { 42 /// Create a new empty buffer. 43 pub fn new() -> Self { 44 let doc = LoroDoc::new(); 45 let content = doc.get_text("content"); 46 let loro_cursor = content.get_cursor(0, Side::default()); 47 48 Self { 49 inner: Rc::new(RefCell::new(LoroTextBufferInner { 50 undo_mgr: LoroUndoManager::new(&doc), 51 last_edit: None, 52 loro_cursor, 53 })), 54 doc, 55 content, 56 } 57 } 58 59 /// Create a buffer from an existing Loro snapshot. 60 pub fn from_snapshot(snapshot: &[u8]) -> Result<Self, CrdtError> { 61 let doc = LoroDoc::new(); 62 doc.import(snapshot)?; 63 let content = doc.get_text("content"); 64 let loro_cursor = content.get_cursor(0, Side::default()); 65 66 Ok(Self { 67 inner: Rc::new(RefCell::new(LoroTextBufferInner { 68 undo_mgr: LoroUndoManager::new(&doc), 69 last_edit: None, 70 loro_cursor, 71 })), 72 doc, 73 content, 74 }) 75 } 76 77 /// Create a buffer from an existing LoroDoc with a specific text container key. 78 /// 79 /// Useful for shared documents where multiple text fields exist in the same doc. 80 /// The doc is cloned (cheap - Arc-backed) so the buffer shares state with the original. 81 pub fn from_doc(doc: LoroDoc, key: &str) -> Self { 82 let content = doc.get_text(key); 83 let loro_cursor = content.get_cursor(0, Side::default()); 84 85 Self { 86 inner: Rc::new(RefCell::new(LoroTextBufferInner { 87 undo_mgr: LoroUndoManager::new(&doc), 88 last_edit: None, 89 loro_cursor, 90 })), 91 doc, 92 content, 93 } 94 } 95 96 /// Get the underlying Loro document. 97 pub fn doc(&self) -> &LoroDoc { 98 &self.doc 99 } 100 101 /// Get the text container. 102 pub fn content(&self) -> &LoroText { 103 &self.content 104 } 105 106 /// Export full snapshot. 107 pub fn export_snapshot(&self) -> Vec<u8> { 108 self.doc 109 .export(loro::ExportMode::Snapshot) 110 .expect("snapshot export should not fail") 111 } 112 113 /// Export updates since given version. 114 pub fn export_updates_since(&self, version: &VersionVector) -> Option<Vec<u8>> { 115 use std::borrow::Cow; 116 117 let current_vv = self.doc.oplog_vv(); 118 119 if *version == current_vv { 120 return None; 121 } 122 123 let updates = self 124 .doc 125 .export(loro::ExportMode::Updates { 126 from: Cow::Owned(version.clone()), 127 }) 128 .ok()?; 129 130 if updates.is_empty() { 131 return None; 132 } 133 134 Some(updates) 135 } 136 137 /// Import remote changes. 138 pub fn import(&mut self, data: &[u8]) -> Result<(), CrdtError> { 139 self.doc.import(data)?; 140 Ok(()) 141 } 142 143 /// Get current version vector. 144 pub fn version(&self) -> VersionVector { 145 self.doc.oplog_vv() 146 } 147 148 // --- Cursor management --- 149 150 /// Sync the Loro cursor to track a specific char offset. 151 /// Call this after local edits where you know the new cursor position. 152 pub fn sync_cursor(&self, offset: usize) { 153 self.inner.borrow_mut().loro_cursor = self.content.get_cursor(offset, Side::default()); 154 } 155 156 /// Resolve the Loro cursor to its current char offset. 157 /// Call this after undo/redo or remote edits where the position may have shifted. 158 /// Returns None if no cursor is set or resolution fails. 159 pub fn resolve_cursor(&self) -> Option<usize> { 160 let inner = self.inner.borrow(); 161 let cursor = inner.loro_cursor.as_ref()?; 162 let result = self.doc.get_cursor_pos(cursor).ok()?; 163 Some(result.current.pos.min(self.content.len_unicode())) 164 } 165 166 /// Get a clone of the Loro cursor for serialization. 167 pub fn loro_cursor(&self) -> Option<Cursor> { 168 self.inner.borrow().loro_cursor.clone() 169 } 170 171 /// Set the Loro cursor (used when restoring from storage). 172 pub fn set_loro_cursor(&self, cursor: Option<Cursor>) { 173 self.inner.borrow_mut().loro_cursor = cursor; 174 } 175} 176 177impl Default for LoroTextBuffer { 178 fn default() -> Self { 179 Self::new() 180 } 181} 182 183impl TextBuffer for LoroTextBuffer { 184 fn len_bytes(&self) -> usize { 185 self.content.len_utf8() 186 } 187 188 fn len_chars(&self) -> usize { 189 self.content.len_unicode() 190 } 191 192 fn insert(&mut self, char_offset: usize, text: &str) { 193 let in_block_syntax_zone = self.is_in_block_syntax_zone(char_offset); 194 let contains_newline = text.contains('\n'); 195 196 self.content.insert(char_offset, text).ok(); 197 198 self.inner.borrow_mut().last_edit = Some(EditInfo { 199 edit_char_pos: char_offset, 200 inserted_len: text.chars().count(), 201 deleted_len: 0, 202 contains_newline, 203 in_block_syntax_zone, 204 doc_len_after: self.content.len_unicode(), 205 timestamp: Instant::now(), 206 }); 207 } 208 209 fn delete(&mut self, char_range: Range<usize>) { 210 let in_block_syntax_zone = self.is_in_block_syntax_zone(char_range.start); 211 let contains_newline = self 212 .slice(char_range.clone()) 213 .map(|s| s.contains('\n')) 214 .unwrap_or(false); 215 let deleted_len = char_range.len(); 216 217 self.content.delete(char_range.start, deleted_len).ok(); 218 219 self.inner.borrow_mut().last_edit = Some(EditInfo { 220 edit_char_pos: char_range.start, 221 inserted_len: 0, 222 deleted_len, 223 contains_newline, 224 in_block_syntax_zone, 225 doc_len_after: self.content.len_unicode(), 226 timestamp: Instant::now(), 227 }); 228 } 229 230 fn replace(&mut self, char_range: Range<usize>, text: &str) { 231 let in_block_syntax_zone = self.is_in_block_syntax_zone(char_range.start); 232 let delete_has_newline = self 233 .slice(char_range.clone()) 234 .map(|s| s.contains('\n')) 235 .unwrap_or(false); 236 let deleted_len = char_range.len(); 237 let inserted_len = text.chars().count(); 238 239 // Use Loro's atomic splice operation 240 self.content 241 .splice(char_range.start, deleted_len, text) 242 .ok(); 243 244 self.inner.borrow_mut().last_edit = Some(EditInfo { 245 edit_char_pos: char_range.start, 246 inserted_len, 247 deleted_len, 248 contains_newline: delete_has_newline || text.contains('\n'), 249 in_block_syntax_zone, 250 doc_len_after: self.content.len_unicode(), 251 timestamp: Instant::now(), 252 }); 253 } 254 255 fn push(&mut self, text: &str) { 256 let char_offset = self.content.len_unicode(); 257 let contains_newline = text.contains('\n'); 258 let in_block_syntax_zone = self.is_in_block_syntax_zone(char_offset); 259 260 self.content.push_str(text).ok(); 261 262 self.inner.borrow_mut().last_edit = Some(EditInfo { 263 edit_char_pos: char_offset, 264 inserted_len: text.chars().count(), 265 deleted_len: 0, 266 contains_newline, 267 in_block_syntax_zone, 268 doc_len_after: self.content.len_unicode(), 269 timestamp: Instant::now(), 270 }); 271 } 272 273 fn slice(&self, char_range: Range<usize>) -> Option<SmolStr> { 274 if char_range.end > self.content.len_unicode() { 275 return None; 276 } 277 self.content 278 .slice(char_range.start, char_range.end) 279 .ok() 280 .map(|s| s.to_smolstr()) 281 } 282 283 fn char_at(&self, char_offset: usize) -> Option<char> { 284 self.content.char_at(char_offset).ok() 285 } 286 287 fn to_string(&self) -> String { 288 self.content.to_string() 289 } 290 291 fn char_to_byte(&self, char_offset: usize) -> usize { 292 self.content 293 .convert_pos(char_offset, PosType::Unicode, PosType::Bytes) 294 .unwrap_or(self.content.len_utf8()) 295 } 296 297 fn byte_to_char(&self, byte_offset: usize) -> usize { 298 self.content 299 .convert_pos(byte_offset, PosType::Bytes, PosType::Unicode) 300 .unwrap_or(self.content.len_unicode()) 301 } 302 303 fn last_edit(&self) -> Option<EditInfo> { 304 self.inner.borrow().last_edit 305 } 306} 307 308impl UndoManager for LoroTextBuffer { 309 fn can_undo(&self) -> bool { 310 self.inner.borrow().undo_mgr.can_undo() 311 } 312 313 fn can_redo(&self) -> bool { 314 self.inner.borrow().undo_mgr.can_redo() 315 } 316 317 fn undo(&mut self) -> bool { 318 self.inner.borrow_mut().undo_mgr.undo().is_ok() 319 } 320 321 fn redo(&mut self) -> bool { 322 self.inner.borrow_mut().undo_mgr.redo().is_ok() 323 } 324 325 fn clear_history(&mut self) { 326 self.inner.borrow_mut().undo_mgr = LoroUndoManager::new(&self.doc); 327 } 328} 329 330#[cfg(test)] 331mod tests { 332 use super::*; 333 334 #[test] 335 fn test_basic_operations() { 336 let mut buffer = LoroTextBuffer::new(); 337 338 buffer.insert(0, "Hello"); 339 assert_eq!(buffer.to_string(), "Hello"); 340 341 buffer.insert(5, " World"); 342 assert_eq!(buffer.to_string(), "Hello World"); 343 344 buffer.delete(5..6); 345 assert_eq!(buffer.to_string(), "HelloWorld"); 346 } 347 348 #[test] 349 fn test_snapshot_roundtrip() { 350 let mut buffer = LoroTextBuffer::new(); 351 buffer.insert(0, "Test content"); 352 353 let snapshot = buffer.export_snapshot(); 354 let restored = LoroTextBuffer::from_snapshot(&snapshot).unwrap(); 355 356 assert_eq!(restored.to_string(), "Test content"); 357 } 358 359 #[test] 360 fn test_slice() { 361 let mut buffer = LoroTextBuffer::new(); 362 buffer.insert(0, "Hello World"); 363 364 assert_eq!(buffer.slice(0..5).as_deref(), Some("Hello")); 365 assert_eq!(buffer.slice(6..11).as_deref(), Some("World")); 366 assert_eq!(buffer.slice(0..100), None); 367 } 368 369 #[test] 370 fn test_offset_conversion() { 371 let mut buffer = LoroTextBuffer::new(); 372 buffer.insert(0, "hello 🌍"); 373 374 assert_eq!(buffer.len_chars(), 7); // h e l l o 🌍 375 assert_eq!(buffer.len_bytes(), 10); // 6 + 4 376 377 assert_eq!(buffer.char_to_byte(6), 6); // before emoji 378 assert_eq!(buffer.char_to_byte(7), 10); // after emoji 379 } 380 381 #[test] 382 fn test_clone_shares_state() { 383 let mut buffer1 = LoroTextBuffer::new(); 384 buffer1.insert(0, "Hello"); 385 386 let buffer2 = buffer1.clone(); 387 388 // Both should see the same last_edit 389 assert_eq!(buffer1.last_edit(), buffer2.last_edit()); 390 391 // Edit through buffer1 392 buffer1.insert(5, " World"); 393 394 // buffer2 should see the updated last_edit (shared state) 395 assert_eq!(buffer1.last_edit(), buffer2.last_edit()); 396 assert_eq!(buffer2.last_edit().unwrap().inserted_len, 6); 397 } 398 399 #[test] 400 fn test_cursor_management() { 401 let mut buffer = LoroTextBuffer::new(); 402 buffer.insert(0, "Hello World"); 403 404 // Sync cursor to position 5 405 buffer.sync_cursor(5); 406 assert_eq!(buffer.resolve_cursor(), Some(5)); 407 408 // Insert text before cursor - cursor should shift 409 buffer.insert(0, "Hi "); 410 // After insert, cursor tracked by Loro should have shifted 411 let pos = buffer.resolve_cursor().unwrap(); 412 assert_eq!(pos, 8); // 5 + 3 = 8 413 } 414}