From fb318728e6c2542a846a1006ae5ecbdd679087fa Mon Sep 17 00:00:00 2001 From: adz Date: Fri, 21 Aug 2026 12:11:57 +0200 Subject: [PATCH 1/2] loro: Delta should be chars count, not bytes len This fixes a bug where we've converted the Loro text deltas wrongly by using the bytes len of the inserted string instead of the number of characters. When inserting a non-ASCII character this would lead to a bytes len > 1, scrambling text documents. Co-authored-by: niklaswimmer --- reflection-doc/src/document.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/reflection-doc/src/document.rs b/reflection-doc/src/document.rs index b0f776a4..d57786f5 100644 --- a/reflection-doc/src/document.rs +++ b/reflection-doc/src/document.rs @@ -382,13 +382,13 @@ mod imp { self.subscription().is_some() } - fn emit_text_inserted(&self, pos: i32, text: String) { - if pos <= DOCUMENT_NAME_LENGTH as i32 { + fn emit_text_inserted(&self, offset: i32, text: String) { + if offset <= DOCUMENT_NAME_LENGTH as i32 { self.update_name(); } self.obj() - .emit_by_name::<()>("text-inserted", &[&pos, &text]); + .emit_by_name::<()>("text-inserted", &[&offset, &text]); } fn emit_range_deleted(&self, start: i32, end: i32) { @@ -475,7 +475,7 @@ mod imp { index += retain; } loro::TextDelta::Insert { insert, .. } => { - let len = insert.len(); + let len = insert.chars().count(); obj.imp().emit_text_inserted(index as i32, insert); index += len; } From 36496dd589d3968907d316c276b24f6175391576 Mon Sep 17 00:00:00 2001 From: adz Date: Sun, 23 Aug 2026 15:01:22 +0200 Subject: [PATCH 2/2] chore: GTK offset naming for char-based positions Stay consistent with how GTK names char-based positioning. Co-authored-by: niklaswimmer --- reflection-app/src/textbuffer.rs | 8 ++++---- reflection-doc/src/document.rs | 29 ++++++++++++++++------------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/reflection-app/src/textbuffer.rs b/reflection-app/src/textbuffer.rs index 8f23d070..31aad780 100644 --- a/reflection-app/src/textbuffer.rs +++ b/reflection-app/src/textbuffer.rs @@ -143,15 +143,15 @@ mod imp { #[upgrade_or] None, move |values| { - let pos: i32 = values.get(1).unwrap().get().unwrap(); + let offset: i32 = values.get(1).unwrap().get().unwrap(); let text: &str = values.get(2).unwrap().get().unwrap(); if buffer.inhibit_text_change() { return None; } - let mut pos_iter = buffer.iter_at_offset(pos); + let mut iter = buffer.iter_at_offset(offset); buffer.set_inhibit_text_change(true); - buffer.insert(&mut pos_iter, text); + buffer.insert(&mut iter, text); buffer.set_inhibit_text_change(false); None @@ -311,7 +311,7 @@ mod imp { if let Err(error) = result { error!("Failed to submit changes to the document: {error}"); } else { - info!("inserting new text {} at pos {}", new_text, offset); + info!("inserting new text {} at offset {}", new_text, offset); self.parent_insert_text(iter, new_text); } } diff --git a/reflection-doc/src/document.rs b/reflection-doc/src/document.rs index d57786f5..b2df83d5 100644 --- a/reflection-doc/src/document.rs +++ b/reflection-doc/src/document.rs @@ -258,21 +258,21 @@ mod imp { } } - pub fn insert_text(&self, index: usize, chunk: &str) -> Result<()> { + pub fn insert_text(&self, offset: usize, chunk: &str) -> Result<()> { let doc = self.crdt_doc.get().expect("crdt_doc to be set"); let text = doc.get_text(&*TEXT_CONTAINER_ID); - text.insert(index, chunk)?; + text.insert(offset, chunk)?; doc.commit(); Ok(()) } - pub fn delete_text(&self, index: usize, len: usize) -> Result<()> { + pub fn delete_text(&self, offset: usize, len: usize) -> Result<()> { let doc = self.crdt_doc.get().expect("crdt_doc to be set"); let text = doc.get_text(&*TEXT_CONTAINER_ID); - text.delete(index, len)?; + text.delete(offset, len)?; doc.commit(); Ok(()) @@ -468,21 +468,24 @@ mod imp { // Loro's text deltas are represented as QuillJS "Deltas" // See: https://quilljs.com/docs/delta/ for commit in text_deltas { - let mut index = 0; + // The `retain` and `delete` integers coming from Loro are the number of + // unicode codepoints (_not_ utf8 bytes or "characters"). In GTK this is + // called an "offset". + let mut offset = 0; for delta in commit { match delta { loro::TextDelta::Retain { retain, .. } => { - index += retain; + offset += retain; } loro::TextDelta::Insert { insert, .. } => { - let len = insert.chars().count(); - obj.imp().emit_text_inserted(index as i32, insert); - index += len; + let chars_count = insert.chars().count(); + obj.imp().emit_text_inserted(offset as i32, insert); + offset += chars_count; } loro::TextDelta::Delete { delete } => { obj.imp().emit_range_deleted( - index as i32, - (index + delete) as i32, + offset as i32, + (offset + delete) as i32, ); } } @@ -739,8 +742,8 @@ impl Document { .build() } - pub fn insert_text(&self, pos: i32, text: &str) -> Result<()> { - self.imp().insert_text(pos as usize, text) + pub fn insert_text(&self, offset: i32, text: &str) -> Result<()> { + self.imp().insert_text(offset as usize, text) } pub fn delete_range(&self, start_pos: i32, end_pos: i32) -> Result<()> {