From 9d56897a51abcd1531c2698a4824349c8c4bf1f5 Mon Sep 17 00:00:00 2001 From: Orual Date: Tue, 6 Jan 2026 15:49:16 -0500 Subject: [PATCH] edit tracking more integral --- Cargo.lock | 1 + crates/weaver-editor-core/src/text.rs | 81 ++++++++++++++++++- crates/weaver-editor-core/src/undo.rs | 4 + crates/weaver-editor-core/src/writer/embed.rs | 2 +- crates/weaver-editor-crdt/Cargo.toml | 1 + crates/weaver-editor-crdt/src/buffer.rs | 42 +++++++++- 6 files changed, 126 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c062f9e..1224f57 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12262,6 +12262,7 @@ dependencies = [ "weaver-common", "weaver-editor-core", "weaver-renderer", + "web-time", ] [[package]] diff --git a/crates/weaver-editor-core/src/text.rs b/crates/weaver-editor-core/src/text.rs index c2fb2c4..7a02d02 100644 --- a/crates/weaver-editor-core/src/text.rs +++ b/crates/weaver-editor-core/src/text.rs @@ -6,6 +6,9 @@ use smol_str::{SmolStr, ToSmolStr}; use std::ops::Range; +use web_time::Instant; + +use crate::types::{EditInfo, BLOCK_SYNTAX_ZONE}; /// A text buffer that supports efficient editing and offset conversion. /// @@ -51,14 +54,44 @@ pub trait TextBuffer { /// Convert byte offset to char offset. fn byte_to_char(&self, byte_offset: usize) -> usize; + + /// Get info about the last edit operation, if any. + fn last_edit(&self) -> Option<&EditInfo>; + + /// Check if a char offset is in the block-syntax zone (first few chars of a line). + fn is_in_block_syntax_zone(&self, offset: usize) -> bool { + if offset <= BLOCK_SYNTAX_ZONE { + return true; + } + + // Get slice of the search range and look for newline. + let search_start = offset.saturating_sub(BLOCK_SYNTAX_ZONE + 1); + match self.slice(search_start..offset) { + Some(s) => match s.rfind('\n') { + Some(pos) => (offset - search_start - pos - 1) <= BLOCK_SYNTAX_ZONE, + None => false, // No newline in range, offset > BLOCK_SYNTAX_ZONE. + }, + None => false, + } + } } /// Ropey-backed text buffer for local editing. /// /// Provides O(log n) editing operations and offset conversions. -#[derive(Clone, Default)] +#[derive(Clone)] pub struct EditorRope { rope: ropey::Rope, + last_edit: Option, +} + +impl Default for EditorRope { + fn default() -> Self { + Self { + rope: ropey::Rope::default(), + last_edit: None, + } + } } impl EditorRope { @@ -71,6 +104,7 @@ impl EditorRope { pub fn from_str(s: &str) -> Self { Self { rope: ropey::Rope::from_str(s), + last_edit: None, } } @@ -101,11 +135,41 @@ impl TextBuffer for EditorRope { } fn insert(&mut self, char_offset: usize, text: &str) { + let in_block_syntax_zone = self.is_in_block_syntax_zone(char_offset); + let contains_newline = text.contains('\n'); + self.rope.insert(char_offset, text); + + self.last_edit = Some(EditInfo { + edit_char_pos: char_offset, + inserted_len: text.chars().count(), + deleted_len: 0, + contains_newline, + in_block_syntax_zone, + doc_len_after: self.rope.len_chars(), + timestamp: Instant::now(), + }); } fn delete(&mut self, char_range: Range) { - self.rope.remove(char_range); + let in_block_syntax_zone = self.is_in_block_syntax_zone(char_range.start); + let contains_newline = self + .slice(char_range.clone()) + .map(|s| s.contains('\n')) + .unwrap_or(false); + let deleted_len = char_range.len(); + + self.rope.remove(char_range.clone()); + + self.last_edit = Some(EditInfo { + edit_char_pos: char_range.start, + inserted_len: 0, + deleted_len, + contains_newline, + in_block_syntax_zone, + doc_len_after: self.rope.len_chars(), + timestamp: Instant::now(), + }); } fn slice(&self, char_range: Range) -> Option { @@ -133,6 +197,19 @@ impl TextBuffer for EditorRope { fn byte_to_char(&self, byte_offset: usize) -> usize { self.rope.byte_to_char(byte_offset) } + + fn last_edit(&self) -> Option<&EditInfo> { + self.last_edit.as_ref() + } + + fn is_in_block_syntax_zone(&self, offset: usize) -> bool { + if offset > self.rope.len_chars() { + return false; + } + let line_num = self.rope.char_to_line(offset); + let line_start = self.rope.line_to_char(line_num); + (offset - line_start) <= BLOCK_SYNTAX_ZONE + } } impl From<&str> for EditorRope { diff --git a/crates/weaver-editor-core/src/undo.rs b/crates/weaver-editor-core/src/undo.rs index ef2b005..f7f32f3 100644 --- a/crates/weaver-editor-core/src/undo.rs +++ b/crates/weaver-editor-core/src/undo.rs @@ -158,6 +158,10 @@ impl TextBuffer for UndoableBuffer { fn byte_to_char(&self, byte_offset: usize) -> usize { self.buffer.byte_to_char(byte_offset) } + + fn last_edit(&self) -> Option<&crate::types::EditInfo> { + self.buffer.last_edit() + } } impl UndoManager for UndoableBuffer { diff --git a/crates/weaver-editor-core/src/writer/embed.rs b/crates/weaver-editor-core/src/writer/embed.rs index 94f6cd4..6e6518b 100644 --- a/crates/weaver-editor-core/src/writer/embed.rs +++ b/crates/weaver-editor-core/src/writer/embed.rs @@ -6,7 +6,7 @@ use std::ops::Range; use jacquard::IntoStatic; use jacquard::types::{ident::AtIdentifier, string::Rkey}; -use markdown_weaver::{CowStr, Event, Tag}; +use markdown_weaver::{Event, Tag}; use markdown_weaver_escape::{StrWrite, escape_html}; use smol_str::SmolStr; diff --git a/crates/weaver-editor-crdt/Cargo.toml b/crates/weaver-editor-crdt/Cargo.toml index 3afb045..d8bc296 100644 --- a/crates/weaver-editor-crdt/Cargo.toml +++ b/crates/weaver-editor-crdt/Cargo.toml @@ -23,6 +23,7 @@ jacquard = { workspace = true } loro = "1.9" serde = { workspace = true } smol_str = "0.3" +web-time = "1" tracing = { workspace = true } thiserror = "2" futures-util = "0.3" diff --git a/crates/weaver-editor-crdt/src/buffer.rs b/crates/weaver-editor-crdt/src/buffer.rs index 33c5777..1b43892 100644 --- a/crates/weaver-editor-crdt/src/buffer.rs +++ b/crates/weaver-editor-crdt/src/buffer.rs @@ -6,7 +6,8 @@ use std::rc::Rc; use loro::{cursor::PosType, LoroDoc, LoroText, UndoManager as LoroUndoManager, VersionVector}; use smol_str::{SmolStr, ToSmolStr}; -use weaver_editor_core::{TextBuffer, UndoManager}; +use web_time::Instant; +use weaver_editor_core::{EditInfo, TextBuffer, UndoManager}; use crate::CrdtError; @@ -19,6 +20,7 @@ pub struct LoroTextBuffer { doc: LoroDoc, content: LoroText, undo_mgr: Rc>, + last_edit: Option, } impl LoroTextBuffer { @@ -32,6 +34,7 @@ impl LoroTextBuffer { doc, content, undo_mgr, + last_edit: None, } } @@ -46,6 +49,7 @@ impl LoroTextBuffer { doc, content, undo_mgr, + last_edit: None, }) } @@ -118,11 +122,41 @@ impl TextBuffer for LoroTextBuffer { } fn insert(&mut self, char_offset: usize, text: &str) { + let in_block_syntax_zone = self.is_in_block_syntax_zone(char_offset); + let contains_newline = text.contains('\n'); + self.content.insert(char_offset, text).ok(); + + self.last_edit = Some(EditInfo { + edit_char_pos: char_offset, + inserted_len: text.chars().count(), + deleted_len: 0, + contains_newline, + in_block_syntax_zone, + doc_len_after: self.content.len_unicode(), + timestamp: Instant::now(), + }); } fn delete(&mut self, char_range: Range) { - self.content.delete(char_range.start, char_range.len()).ok(); + let in_block_syntax_zone = self.is_in_block_syntax_zone(char_range.start); + let contains_newline = self + .slice(char_range.clone()) + .map(|s| s.contains('\n')) + .unwrap_or(false); + let deleted_len = char_range.len(); + + self.content.delete(char_range.start, deleted_len).ok(); + + self.last_edit = Some(EditInfo { + edit_char_pos: char_range.start, + inserted_len: 0, + deleted_len, + contains_newline, + in_block_syntax_zone, + doc_len_after: self.content.len_unicode(), + timestamp: Instant::now(), + }); } fn slice(&self, char_range: Range) -> Option { @@ -154,6 +188,10 @@ impl TextBuffer for LoroTextBuffer { .convert_pos(byte_offset, PosType::Bytes, PosType::Unicode) .unwrap_or(self.content.len_unicode()) } + + fn last_edit(&self) -> Option<&EditInfo> { + self.last_edit.as_ref() + } } impl UndoManager for LoroTextBuffer { -- 2.51.2