edit tracking more integral

Orual 9d56897a 3d475648

+126 -5
+1
Cargo.lock
··· 12262 "weaver-common", 12263 "weaver-editor-core", 12264 "weaver-renderer", 12265 ] 12266 12267 [[package]]
··· 12262 "weaver-common", 12263 "weaver-editor-core", 12264 "weaver-renderer", 12265 + "web-time", 12266 ] 12267 12268 [[package]]
+79 -2
crates/weaver-editor-core/src/text.rs
··· 6 7 use smol_str::{SmolStr, ToSmolStr}; 8 use std::ops::Range; 9 10 /// A text buffer that supports efficient editing and offset conversion. 11 /// ··· 51 52 /// Convert byte offset to char offset. 53 fn byte_to_char(&self, byte_offset: usize) -> usize; 54 } 55 56 /// Ropey-backed text buffer for local editing. 57 /// 58 /// Provides O(log n) editing operations and offset conversions. 59 - #[derive(Clone, Default)] 60 pub struct EditorRope { 61 rope: ropey::Rope, 62 } 63 64 impl EditorRope { ··· 71 pub fn from_str(s: &str) -> Self { 72 Self { 73 rope: ropey::Rope::from_str(s), 74 } 75 } 76 ··· 101 } 102 103 fn insert(&mut self, char_offset: usize, text: &str) { 104 self.rope.insert(char_offset, text); 105 } 106 107 fn delete(&mut self, char_range: Range<usize>) { 108 - self.rope.remove(char_range); 109 } 110 111 fn slice(&self, char_range: Range<usize>) -> Option<SmolStr> { ··· 132 133 fn byte_to_char(&self, byte_offset: usize) -> usize { 134 self.rope.byte_to_char(byte_offset) 135 } 136 } 137
··· 6 7 use smol_str::{SmolStr, ToSmolStr}; 8 use std::ops::Range; 9 + use web_time::Instant; 10 + 11 + use crate::types::{EditInfo, BLOCK_SYNTAX_ZONE}; 12 13 /// A text buffer that supports efficient editing and offset conversion. 14 /// ··· 54 55 /// Convert byte offset to char offset. 56 fn byte_to_char(&self, byte_offset: usize) -> usize; 57 + 58 + /// Get info about the last edit operation, if any. 59 + fn last_edit(&self) -> Option<&EditInfo>; 60 + 61 + /// Check if a char offset is in the block-syntax zone (first few chars of a line). 62 + fn is_in_block_syntax_zone(&self, offset: usize) -> bool { 63 + if offset <= BLOCK_SYNTAX_ZONE { 64 + return true; 65 + } 66 + 67 + // Get slice of the search range and look for newline. 68 + let search_start = offset.saturating_sub(BLOCK_SYNTAX_ZONE + 1); 69 + match self.slice(search_start..offset) { 70 + Some(s) => match s.rfind('\n') { 71 + Some(pos) => (offset - search_start - pos - 1) <= BLOCK_SYNTAX_ZONE, 72 + None => false, // No newline in range, offset > BLOCK_SYNTAX_ZONE. 73 + }, 74 + None => false, 75 + } 76 + } 77 } 78 79 /// Ropey-backed text buffer for local editing. 80 /// 81 /// Provides O(log n) editing operations and offset conversions. 82 + #[derive(Clone)] 83 pub struct EditorRope { 84 rope: ropey::Rope, 85 + last_edit: Option<EditInfo>, 86 + } 87 + 88 + impl Default for EditorRope { 89 + fn default() -> Self { 90 + Self { 91 + rope: ropey::Rope::default(), 92 + last_edit: None, 93 + } 94 + } 95 } 96 97 impl EditorRope { ··· 104 pub fn from_str(s: &str) -> Self { 105 Self { 106 rope: ropey::Rope::from_str(s), 107 + last_edit: None, 108 } 109 } 110 ··· 135 } 136 137 fn insert(&mut self, char_offset: usize, text: &str) { 138 + let in_block_syntax_zone = self.is_in_block_syntax_zone(char_offset); 139 + let contains_newline = text.contains('\n'); 140 + 141 self.rope.insert(char_offset, text); 142 + 143 + self.last_edit = Some(EditInfo { 144 + edit_char_pos: char_offset, 145 + inserted_len: text.chars().count(), 146 + deleted_len: 0, 147 + contains_newline, 148 + in_block_syntax_zone, 149 + doc_len_after: self.rope.len_chars(), 150 + timestamp: Instant::now(), 151 + }); 152 } 153 154 fn delete(&mut self, char_range: Range<usize>) { 155 + let in_block_syntax_zone = self.is_in_block_syntax_zone(char_range.start); 156 + let contains_newline = self 157 + .slice(char_range.clone()) 158 + .map(|s| s.contains('\n')) 159 + .unwrap_or(false); 160 + let deleted_len = char_range.len(); 161 + 162 + self.rope.remove(char_range.clone()); 163 + 164 + self.last_edit = Some(EditInfo { 165 + edit_char_pos: char_range.start, 166 + inserted_len: 0, 167 + deleted_len, 168 + contains_newline, 169 + in_block_syntax_zone, 170 + doc_len_after: self.rope.len_chars(), 171 + timestamp: Instant::now(), 172 + }); 173 } 174 175 fn slice(&self, char_range: Range<usize>) -> Option<SmolStr> { ··· 196 197 fn byte_to_char(&self, byte_offset: usize) -> usize { 198 self.rope.byte_to_char(byte_offset) 199 + } 200 + 201 + fn last_edit(&self) -> Option<&EditInfo> { 202 + self.last_edit.as_ref() 203 + } 204 + 205 + fn is_in_block_syntax_zone(&self, offset: usize) -> bool { 206 + if offset > self.rope.len_chars() { 207 + return false; 208 + } 209 + let line_num = self.rope.char_to_line(offset); 210 + let line_start = self.rope.line_to_char(line_num); 211 + (offset - line_start) <= BLOCK_SYNTAX_ZONE 212 } 213 } 214
+4
crates/weaver-editor-core/src/undo.rs
··· 158 fn byte_to_char(&self, byte_offset: usize) -> usize { 159 self.buffer.byte_to_char(byte_offset) 160 } 161 } 162 163 impl<T: TextBuffer> UndoManager for UndoableBuffer<T> {
··· 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 167 impl<T: TextBuffer> UndoManager for UndoableBuffer<T> {
+1 -1
crates/weaver-editor-core/src/writer/embed.rs
··· 6 7 use jacquard::IntoStatic; 8 use jacquard::types::{ident::AtIdentifier, string::Rkey}; 9 - use markdown_weaver::{CowStr, Event, Tag}; 10 use markdown_weaver_escape::{StrWrite, escape_html}; 11 use smol_str::SmolStr; 12
··· 6 7 use jacquard::IntoStatic; 8 use jacquard::types::{ident::AtIdentifier, string::Rkey}; 9 + use markdown_weaver::{Event, Tag}; 10 use markdown_weaver_escape::{StrWrite, escape_html}; 11 use smol_str::SmolStr; 12
+1
crates/weaver-editor-crdt/Cargo.toml
··· 23 loro = "1.9" 24 serde = { workspace = true } 25 smol_str = "0.3" 26 tracing = { workspace = true } 27 thiserror = "2" 28 futures-util = "0.3"
··· 23 loro = "1.9" 24 serde = { workspace = true } 25 smol_str = "0.3" 26 + web-time = "1" 27 tracing = { workspace = true } 28 thiserror = "2" 29 futures-util = "0.3"
+40 -2
crates/weaver-editor-crdt/src/buffer.rs
··· 6 7 use loro::{cursor::PosType, LoroDoc, LoroText, UndoManager as LoroUndoManager, VersionVector}; 8 use smol_str::{SmolStr, ToSmolStr}; 9 - use weaver_editor_core::{TextBuffer, UndoManager}; 10 11 use crate::CrdtError; 12 ··· 19 doc: LoroDoc, 20 content: LoroText, 21 undo_mgr: Rc<RefCell<LoroUndoManager>>, 22 } 23 24 impl LoroTextBuffer { ··· 32 doc, 33 content, 34 undo_mgr, 35 } 36 } 37 ··· 46 doc, 47 content, 48 undo_mgr, 49 }) 50 } 51 ··· 118 } 119 120 fn insert(&mut self, char_offset: usize, text: &str) { 121 self.content.insert(char_offset, text).ok(); 122 } 123 124 fn delete(&mut self, char_range: Range<usize>) { 125 - self.content.delete(char_range.start, char_range.len()).ok(); 126 } 127 128 fn slice(&self, char_range: Range<usize>) -> Option<SmolStr> { ··· 153 self.content 154 .convert_pos(byte_offset, PosType::Bytes, PosType::Unicode) 155 .unwrap_or(self.content.len_unicode()) 156 } 157 } 158
··· 6 7 use loro::{cursor::PosType, LoroDoc, LoroText, UndoManager as LoroUndoManager, VersionVector}; 8 use smol_str::{SmolStr, ToSmolStr}; 9 + use web_time::Instant; 10 + use weaver_editor_core::{EditInfo, TextBuffer, UndoManager}; 11 12 use crate::CrdtError; 13 ··· 20 doc: LoroDoc, 21 content: LoroText, 22 undo_mgr: Rc<RefCell<LoroUndoManager>>, 23 + last_edit: Option<EditInfo>, 24 } 25 26 impl LoroTextBuffer { ··· 34 doc, 35 content, 36 undo_mgr, 37 + last_edit: None, 38 } 39 } 40 ··· 49 doc, 50 content, 51 undo_mgr, 52 + last_edit: None, 53 }) 54 } 55 ··· 122 } 123 124 fn insert(&mut self, char_offset: usize, text: &str) { 125 + let in_block_syntax_zone = self.is_in_block_syntax_zone(char_offset); 126 + let contains_newline = text.contains('\n'); 127 + 128 self.content.insert(char_offset, text).ok(); 129 + 130 + self.last_edit = Some(EditInfo { 131 + edit_char_pos: char_offset, 132 + inserted_len: text.chars().count(), 133 + deleted_len: 0, 134 + contains_newline, 135 + in_block_syntax_zone, 136 + doc_len_after: self.content.len_unicode(), 137 + timestamp: Instant::now(), 138 + }); 139 } 140 141 fn delete(&mut self, char_range: Range<usize>) { 142 + let in_block_syntax_zone = self.is_in_block_syntax_zone(char_range.start); 143 + let contains_newline = self 144 + .slice(char_range.clone()) 145 + .map(|s| s.contains('\n')) 146 + .unwrap_or(false); 147 + let deleted_len = char_range.len(); 148 + 149 + self.content.delete(char_range.start, deleted_len).ok(); 150 + 151 + self.last_edit = Some(EditInfo { 152 + edit_char_pos: char_range.start, 153 + inserted_len: 0, 154 + deleted_len, 155 + contains_newline, 156 + in_block_syntax_zone, 157 + doc_len_after: self.content.len_unicode(), 158 + timestamp: Instant::now(), 159 + }); 160 } 161 162 fn slice(&self, char_range: Range<usize>) -> Option<SmolStr> { ··· 187 self.content 188 .convert_pos(byte_offset, PosType::Bytes, PosType::Unicode) 189 .unwrap_or(self.content.len_unicode()) 190 + } 191 + 192 + fn last_edit(&self) -> Option<&EditInfo> { 193 + self.last_edit.as_ref() 194 } 195 } 196