further edit info stuff

Orual 2aa819db 9d56897a

+180 -25
+3 -3
crates/weaver-editor-core/src/text.rs
··· 56 56 fn byte_to_char(&self, byte_offset: usize) -> usize; 57 57 58 58 /// Get info about the last edit operation, if any. 59 - fn last_edit(&self) -> Option<&EditInfo>; 59 + fn last_edit(&self) -> Option<EditInfo>; 60 60 61 61 /// Check if a char offset is in the block-syntax zone (first few chars of a line). 62 62 fn is_in_block_syntax_zone(&self, offset: usize) -> bool { ··· 198 198 self.rope.byte_to_char(byte_offset) 199 199 } 200 200 201 - fn last_edit(&self) -> Option<&EditInfo> { 202 - self.last_edit.as_ref() 201 + fn last_edit(&self) -> Option<EditInfo> { 202 + self.last_edit 203 203 } 204 204 205 205 fn is_in_block_syntax_zone(&self, offset: usize) -> bool {
+1 -1
crates/weaver-editor-core/src/types.rs
··· 165 165 /// 166 166 /// This tracks enough information to determine which paragraphs need re-rendering 167 167 /// after an edit, enabling efficient incremental updates instead of full re-renders. 168 - #[derive(Clone, Debug)] 168 + #[derive(Clone, Copy, Debug)] 169 169 pub struct EditInfo { 170 170 /// Character offset where the edit occurred 171 171 pub edit_char_pos: usize,
+1 -1
crates/weaver-editor-core/src/undo.rs
··· 159 159 self.buffer.byte_to_char(byte_offset) 160 160 } 161 161 162 - fn last_edit(&self) -> Option<&crate::types::EditInfo> { 162 + fn last_edit(&self) -> Option<crate::types::EditInfo> { 163 163 self.buffer.last_edit() 164 164 } 165 165 }
+120 -20
crates/weaver-editor-crdt/src/buffer.rs
··· 4 4 use std::ops::Range; 5 5 use std::rc::Rc; 6 6 7 - use loro::{cursor::PosType, LoroDoc, LoroText, UndoManager as LoroUndoManager, VersionVector}; 7 + use loro::{ 8 + cursor::{Cursor, PosType, Side}, 9 + LoroDoc, LoroText, UndoManager as LoroUndoManager, VersionVector, 10 + }; 8 11 use smol_str::{SmolStr, ToSmolStr}; 9 12 use web_time::Instant; 10 13 use weaver_editor_core::{EditInfo, TextBuffer, UndoManager}; 11 14 12 15 use crate::CrdtError; 13 16 17 + /// Mutable state that must be shared across clones. 18 + struct LoroTextBufferInner { 19 + undo_mgr: LoroUndoManager, 20 + last_edit: Option<EditInfo>, 21 + loro_cursor: Option<Cursor>, 22 + } 23 + 14 24 /// Loro-backed text buffer with undo/redo support. 15 25 /// 16 26 /// Wraps a `LoroDoc` with a text container and provides implementations 17 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). 18 34 #[derive(Clone)] 19 35 pub struct LoroTextBuffer { 20 36 doc: LoroDoc, 21 37 content: LoroText, 22 - undo_mgr: Rc<RefCell<LoroUndoManager>>, 23 - last_edit: Option<EditInfo>, 38 + inner: Rc<RefCell<LoroTextBufferInner>>, 24 39 } 25 40 26 41 impl LoroTextBuffer { ··· 28 43 pub fn new() -> Self { 29 44 let doc = LoroDoc::new(); 30 45 let content = doc.get_text("content"); 31 - let undo_mgr = Rc::new(RefCell::new(LoroUndoManager::new(&doc))); 46 + let loro_cursor = content.get_cursor(0, Side::default()); 32 47 33 48 Self { 49 + inner: Rc::new(RefCell::new(LoroTextBufferInner { 50 + undo_mgr: LoroUndoManager::new(&doc), 51 + last_edit: None, 52 + loro_cursor, 53 + })), 34 54 doc, 35 55 content, 36 - undo_mgr, 37 - last_edit: None, 38 56 } 39 57 } 40 58 ··· 43 61 let doc = LoroDoc::new(); 44 62 doc.import(snapshot)?; 45 63 let content = doc.get_text("content"); 46 - let undo_mgr = Rc::new(RefCell::new(LoroUndoManager::new(&doc))); 64 + let loro_cursor = content.get_cursor(0, Side::default()); 47 65 48 66 Ok(Self { 67 + inner: Rc::new(RefCell::new(LoroTextBufferInner { 68 + undo_mgr: LoroUndoManager::new(&doc), 69 + last_edit: None, 70 + loro_cursor, 71 + })), 49 72 doc, 50 73 content, 51 - undo_mgr, 52 - last_edit: None, 53 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 + } 54 94 } 55 95 56 96 /// Get the underlying Loro document. ··· 104 144 pub fn version(&self) -> VersionVector { 105 145 self.doc.oplog_vv() 106 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 + } 107 175 } 108 176 109 177 impl Default for LoroTextBuffer { ··· 127 195 128 196 self.content.insert(char_offset, text).ok(); 129 197 130 - self.last_edit = Some(EditInfo { 198 + self.inner.borrow_mut().last_edit = Some(EditInfo { 131 199 edit_char_pos: char_offset, 132 200 inserted_len: text.chars().count(), 133 201 deleted_len: 0, ··· 148 216 149 217 self.content.delete(char_range.start, deleted_len).ok(); 150 218 151 - self.last_edit = Some(EditInfo { 219 + self.inner.borrow_mut().last_edit = Some(EditInfo { 152 220 edit_char_pos: char_range.start, 153 221 inserted_len: 0, 154 222 deleted_len, ··· 189 257 .unwrap_or(self.content.len_unicode()) 190 258 } 191 259 192 - fn last_edit(&self) -> Option<&EditInfo> { 193 - self.last_edit.as_ref() 260 + fn last_edit(&self) -> Option<EditInfo> { 261 + self.inner.borrow().last_edit 194 262 } 195 263 } 196 264 197 265 impl UndoManager for LoroTextBuffer { 198 266 fn can_undo(&self) -> bool { 199 - self.undo_mgr.borrow().can_undo() 267 + self.inner.borrow().undo_mgr.can_undo() 200 268 } 201 269 202 270 fn can_redo(&self) -> bool { 203 - self.undo_mgr.borrow().can_redo() 271 + self.inner.borrow().undo_mgr.can_redo() 204 272 } 205 273 206 274 fn undo(&mut self) -> bool { 207 - self.undo_mgr.borrow_mut().undo().is_ok() 275 + self.inner.borrow_mut().undo_mgr.undo().is_ok() 208 276 } 209 277 210 278 fn redo(&mut self) -> bool { 211 - self.undo_mgr.borrow_mut().redo().is_ok() 279 + self.inner.borrow_mut().undo_mgr.redo().is_ok() 212 280 } 213 281 214 282 fn clear_history(&mut self) { 215 - // Loro's UndoManager doesn't have a clear method 216 - // Create a new one to effectively clear history 217 - self.undo_mgr = Rc::new(RefCell::new(LoroUndoManager::new(&self.doc))); 283 + self.inner.borrow_mut().undo_mgr = LoroUndoManager::new(&self.doc); 218 284 } 219 285 } 220 286 ··· 267 333 268 334 assert_eq!(buffer.char_to_byte(6), 6); // before emoji 269 335 assert_eq!(buffer.char_to_byte(7), 10); // after emoji 336 + } 337 + 338 + #[test] 339 + fn test_clone_shares_state() { 340 + let mut buffer1 = LoroTextBuffer::new(); 341 + buffer1.insert(0, "Hello"); 342 + 343 + let buffer2 = buffer1.clone(); 344 + 345 + // Both should see the same last_edit 346 + assert_eq!(buffer1.last_edit(), buffer2.last_edit()); 347 + 348 + // Edit through buffer1 349 + buffer1.insert(5, " World"); 350 + 351 + // buffer2 should see the updated last_edit (shared state) 352 + assert_eq!(buffer1.last_edit(), buffer2.last_edit()); 353 + assert_eq!(buffer2.last_edit().unwrap().inserted_len, 6); 354 + } 355 + 356 + #[test] 357 + fn test_cursor_management() { 358 + let mut buffer = LoroTextBuffer::new(); 359 + buffer.insert(0, "Hello World"); 360 + 361 + // Sync cursor to position 5 362 + buffer.sync_cursor(5); 363 + assert_eq!(buffer.resolve_cursor(), Some(5)); 364 + 365 + // Insert text before cursor - cursor should shift 366 + buffer.insert(0, "Hi "); 367 + // After insert, cursor tracked by Loro should have shifted 368 + let pos = buffer.resolve_cursor().unwrap(); 369 + assert_eq!(pos, 8); // 5 + 3 = 8 270 370 } 271 371 }
+55
docs/graph-data.json
··· 1748 1748 "created_at": "2026-01-06T15:16:33.936978250-05:00", 1749 1749 "updated_at": "2026-01-06T15:16:33.936978250-05:00", 1750 1750 "metadata_json": "{\"confidence\":95}" 1751 + }, 1752 + { 1753 + "id": 161, 1754 + "change_id": "95b49ba0-0387-4bcb-b9c1-1aeea141aa46", 1755 + "node_type": "action", 1756 + "title": "Adding EditInfo tracking to LoroTextBuffer and EditorRope", 1757 + "description": null, 1758 + "status": "pending", 1759 + "created_at": "2026-01-06T15:48:29.751031863-05:00", 1760 + "updated_at": "2026-01-06T15:48:29.751031863-05:00", 1761 + "metadata_json": "{\"confidence\":85}" 1762 + }, 1763 + { 1764 + "id": 162, 1765 + "change_id": "85236b61-2d3f-4de6-bc90-71187810ca73", 1766 + "node_type": "outcome", 1767 + "title": "EditInfo tracking added to both EditorRope and LoroTextBuffer, with optimized is_in_block_syntax_zone using ropey line functions", 1768 + "description": null, 1769 + "status": "pending", 1770 + "created_at": "2026-01-06T15:49:15.888613855-05:00", 1771 + "updated_at": "2026-01-06T15:49:15.888613855-05:00", 1772 + "metadata_json": "{\"confidence\":95}" 1773 + }, 1774 + { 1775 + "id": 163, 1776 + "change_id": "5fff4fb8-ea78-4e46-9167-d3f72eea1532", 1777 + "node_type": "outcome", 1778 + "title": "LoroTextBuffer refactored with Rc<RefCell<Inner>> for shared mutable state, cursor management, from_doc constructor - clones now share all state", 1779 + "description": null, 1780 + "status": "pending", 1781 + "created_at": "2026-01-06T15:54:32.396727943-05:00", 1782 + "updated_at": "2026-01-06T15:54:32.396727943-05:00", 1783 + "metadata_json": "{\"confidence\":95}" 1751 1784 } 1752 1785 ], 1753 1786 "edges": [ ··· 3565 3598 "weight": 1.0, 3566 3599 "rationale": "refactor completed successfully", 3567 3600 "created_at": "2026-01-06T15:16:39.426610084-05:00" 3601 + }, 3602 + { 3603 + "id": 167, 3604 + "from_node_id": 161, 3605 + "to_node_id": 162, 3606 + "from_change_id": "95b49ba0-0387-4bcb-b9c1-1aeea141aa46", 3607 + "to_change_id": "85236b61-2d3f-4de6-bc90-71187810ca73", 3608 + "edge_type": "leads_to", 3609 + "weight": 1.0, 3610 + "rationale": "EditInfo implementation complete", 3611 + "created_at": "2026-01-06T15:49:15.905323972-05:00" 3612 + }, 3613 + { 3614 + "id": 168, 3615 + "from_node_id": 161, 3616 + "to_node_id": 163, 3617 + "from_change_id": "95b49ba0-0387-4bcb-b9c1-1aeea141aa46", 3618 + "to_change_id": "5fff4fb8-ea78-4e46-9167-d3f72eea1532", 3619 + "edge_type": "leads_to", 3620 + "weight": 1.0, 3621 + "rationale": "Refactoring complete with tests", 3622 + "created_at": "2026-01-06T15:54:32.412922730-05:00" 3568 3623 } 3569 3624 ] 3570 3625 }