From 3a952ff45bba80d6503463481b723e036372ac30 Mon Sep 17 00:00:00 2001 From: alexjg Date: Tue, 30 Jan 2024 09:18:26 +0000 Subject: [PATCH] Add Text::update (#44) Problem: sometimes when modifying text you are not able to capture the edits to the text field as they happen. For example, if the text is in a file and you just get notified when the file has changed then you have no way of knowing which text was inserted and deleted. The `Text` API requires that you express all changes as `splice` calls, so the user is forced to figure out how to turn the new value into a set of `splice` calls, which can be tricky. Solution: add `Text::update`, which performs an LCS diff to figure out a minimal set of `splice` calls to perform internally. --- Cargo.toml | 4 +- autosurgeon-derive/tests/text.rs | 31 ++++++++++++++ autosurgeon/Cargo.toml | 2 +- autosurgeon/src/text.rs | 73 ++++++++++++++++++++++++++++++++ 4 files changed, 107 insertions(+), 3 deletions(-) create mode 100644 autosurgeon-derive/tests/text.rs diff --git a/Cargo.toml b/Cargo.toml index 2f75f22..e4da2e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,5 +13,5 @@ repository = "https://github.com/automerge/autosurgeon" license = "MIT" [workspace.dependencies] -automerge = "0.5.0" -automerge-test = "0.4.0" +automerge = "0.5" +automerge-test = "0.4" diff --git a/autosurgeon-derive/tests/text.rs b/autosurgeon-derive/tests/text.rs new file mode 100644 index 0000000..6e2694c --- /dev/null +++ b/autosurgeon-derive/tests/text.rs @@ -0,0 +1,31 @@ +use autosurgeon::Text; +use autosurgeon::{Hydrate, Reconcile}; + +#[derive(Hydrate, Reconcile)] +struct TextDoc { + content: Text, +} + +#[test] +fn diff_generates_splices() { + let start = TextDoc { + content: Text::with_value("some value"), + }; + + let mut doc = automerge::AutoCommit::new(); + autosurgeon::reconcile(&mut doc, &start).unwrap(); + let mut doc2 = doc.fork(); + + let mut start2 = autosurgeon::hydrate::<_, TextDoc>(&doc).unwrap(); + start2.content.update("some day"); + autosurgeon::reconcile(&mut doc, &start2).unwrap(); + + let mut start3 = autosurgeon::hydrate::<_, TextDoc>(&doc2).unwrap(); + start3.content.update("another value"); + autosurgeon::reconcile(&mut doc2, &start3).unwrap(); + + doc.merge(&mut doc2).unwrap(); + + let start3 = autosurgeon::hydrate::<_, TextDoc>(&doc).unwrap(); + assert_eq!(start3.content.as_str(), "another day"); +} diff --git a/autosurgeon/Cargo.toml b/autosurgeon/Cargo.toml index 692dcfe..e07cf66 100644 --- a/autosurgeon/Cargo.toml +++ b/autosurgeon/Cargo.toml @@ -12,7 +12,7 @@ license = { workspace = true } [dependencies] automerge = { workspace = true } autosurgeon-derive = { path = "../autosurgeon-derive", version = "0.8.0" } -similar = "2.2.1" +similar = { version = "2.2.1", features = ["unicode"] } thiserror = "1.0.37" uuid = { version = "1.2.2", optional = true } diff --git a/autosurgeon/src/text.rs b/autosurgeon/src/text.rs index 1bee11e..dcf14c0 100644 --- a/autosurgeon/src/text.rs +++ b/autosurgeon/src/text.rs @@ -119,6 +119,79 @@ impl Text { } } + /// Update the value of the text field by diffing it with a new string + /// + /// This is useful if you can't capture the edits to a text field as they happen (i.e. the + /// insertion and deletion events) but instead you just get given the new value of the field. + /// This method will diff the new value with the current value and convert the diff into a set + /// of edits which are applied to the text field. This will produce more confusing merge + /// results than capturing the edits directly, but sometimes it's all you can do. + /// + /// ## Example + /// + /// ```rust + /// # use autosurgeon::{Hydrate, Reconcile, Text}; + /// #[derive(Hydrate, Reconcile)] + /// struct TextDoc { + /// content: Text, + /// } + /// + /// let start = TextDoc { + /// content: Text::with_value("some value"), + /// }; + /// + /// // Create the initial document + /// let mut doc = automerge::AutoCommit::new(); + /// autosurgeon::reconcile(&mut doc, &start).unwrap(); + /// + /// // Fork the document so we can make concurrent changes + /// let mut doc2 = doc.fork(); + /// + /// // On one fork replace 'value' with 'day' + /// let mut start2 = autosurgeon::hydrate::<_, TextDoc>(&doc).unwrap(); + /// // Note the use of `update` to replace the entire content instead of `splice` + /// start2.content.update("some day"); + /// autosurgeon::reconcile(&mut doc, &start2).unwrap(); + /// + /// // On the other fork replace 'some' with 'another' + /// let mut start3 = autosurgeon::hydrate::<_, TextDoc>(&doc2).unwrap(); + /// start3.content.update("another value"); + /// autosurgeon::reconcile(&mut doc2, &start3).unwrap(); + /// + /// // Merge the two forks + /// doc.merge(&mut doc2).unwrap(); + /// + /// // The result is 'another day' + /// let start3 = autosurgeon::hydrate::<_, TextDoc>(&doc).unwrap(); + /// assert_eq!(start3.content.as_str(), "another day"); + /// ``` + pub fn update>(&mut self, new_value: S) { + match &mut self.0 { + State::Fresh(v) => *v = new_value.as_ref().to_string(), + State::Rehydrated { value, .. } => { + let mut idx = 0; + let old = value.clone(); + for change in similar::TextDiff::from_graphemes(old.as_str(), new_value.as_ref()) + .iter_all_changes() + { + match change.tag() { + similar::ChangeTag::Delete => { + let len = change.value().len(); + self.splice(idx, len as isize, ""); + } + similar::ChangeTag::Insert => { + self.splice(idx, 0, change.value()); + idx += change.value().len(); + } + similar::ChangeTag::Equal => { + idx += change.value().len(); + } + } + } + } + } + } + pub fn as_str(&self) -> &str { match &self.0 { State::Fresh(v) => v,