edit tracking more integral

Orual 9d56897a 3d475648

+126 -5
+1
Cargo.lock
··· 12262 12262 "weaver-common", 12263 12263 "weaver-editor-core", 12264 12264 "weaver-renderer", 12265 + "web-time", 12265 12266 ] 12266 12267 12267 12268 [[package]]
+79 -2
crates/weaver-editor-core/src/text.rs
··· 6 6 7 7 use smol_str::{SmolStr, ToSmolStr}; 8 8 use std::ops::Range; 9 + use web_time::Instant; 10 + 11 + use crate::types::{EditInfo, BLOCK_SYNTAX_ZONE}; 9 12 10 13 /// A text buffer that supports efficient editing and offset conversion. 11 14 /// ··· 51 54 52 55 /// Convert byte offset to char offset. 53 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 + } 54 77 } 55 78 56 79 /// Ropey-backed text buffer for local editing. 57 80 /// 58 81 /// Provides O(log n) editing operations and offset conversions. 59 - #[derive(Clone, Default)] 82 + #[derive(Clone)] 60 83 pub struct EditorRope { 61 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 + } 62 95 } 63 96 64 97 impl EditorRope { ··· 71 104 pub fn from_str(s: &str) -> Self { 72 105 Self { 73 106 rope: ropey::Rope::from_str(s), 107 + last_edit: None, 74 108 } 75 109 } 76 110 ··· 101 135 } 102 136 103 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 + 104 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 + }); 105 152 } 106 153 107 154 fn delete(&mut self, char_range: Range<usize>) { 108 - self.rope.remove(char_range); 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 + }); 109 173 } 110 174 111 175 fn slice(&self, char_range: Range<usize>) -> Option<SmolStr> { ··· 132 196 133 197 fn byte_to_char(&self, byte_offset: usize) -> usize { 134 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 135 212 } 136 213 } 137 214
+4
crates/weaver-editor-core/src/undo.rs
··· 158 158 fn byte_to_char(&self, byte_offset: usize) -> usize { 159 159 self.buffer.byte_to_char(byte_offset) 160 160 } 161 + 162 + fn last_edit(&self) -> Option<&crate::types::EditInfo> { 163 + self.buffer.last_edit() 164 + } 161 165 } 162 166 163 167 impl<T: TextBuffer> UndoManager for UndoableBuffer<T> {
+1 -1
crates/weaver-editor-core/src/writer/embed.rs
··· 6 6 7 7 use jacquard::IntoStatic; 8 8 use jacquard::types::{ident::AtIdentifier, string::Rkey}; 9 - use markdown_weaver::{CowStr, Event, Tag}; 9 + use markdown_weaver::{Event, Tag}; 10 10 use markdown_weaver_escape::{StrWrite, escape_html}; 11 11 use smol_str::SmolStr; 12 12
+1
crates/weaver-editor-crdt/Cargo.toml
··· 23 23 loro = "1.9" 24 24 serde = { workspace = true } 25 25 smol_str = "0.3" 26 + web-time = "1" 26 27 tracing = { workspace = true } 27 28 thiserror = "2" 28 29 futures-util = "0.3"
+40 -2
crates/weaver-editor-crdt/src/buffer.rs
··· 6 6 7 7 use loro::{cursor::PosType, LoroDoc, LoroText, UndoManager as LoroUndoManager, VersionVector}; 8 8 use smol_str::{SmolStr, ToSmolStr}; 9 - use weaver_editor_core::{TextBuffer, UndoManager}; 9 + use web_time::Instant; 10 + use weaver_editor_core::{EditInfo, TextBuffer, UndoManager}; 10 11 11 12 use crate::CrdtError; 12 13 ··· 19 20 doc: LoroDoc, 20 21 content: LoroText, 21 22 undo_mgr: Rc<RefCell<LoroUndoManager>>, 23 + last_edit: Option<EditInfo>, 22 24 } 23 25 24 26 impl LoroTextBuffer { ··· 32 34 doc, 33 35 content, 34 36 undo_mgr, 37 + last_edit: None, 35 38 } 36 39 } 37 40 ··· 46 49 doc, 47 50 content, 48 51 undo_mgr, 52 + last_edit: None, 49 53 }) 50 54 } 51 55 ··· 118 122 } 119 123 120 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 + 121 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 + }); 122 139 } 123 140 124 141 fn delete(&mut self, char_range: Range<usize>) { 125 - self.content.delete(char_range.start, char_range.len()).ok(); 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 + }); 126 160 } 127 161 128 162 fn slice(&self, char_range: Range<usize>) -> Option<SmolStr> { ··· 153 187 self.content 154 188 .convert_pos(byte_offset, PosType::Bytes, PosType::Unicode) 155 189 .unwrap_or(self.content.len_unicode()) 190 + } 191 + 192 + fn last_edit(&self) -> Option<&EditInfo> { 193 + self.last_edit.as_ref() 156 194 } 157 195 } 158 196