Skip to content

Commit

Permalink
Add Text::update (#44)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
alexjg authored Jan 30, 2024
1 parent 4a1a033 commit 3a952ff
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 3 deletions.
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
31 changes: 31 additions & 0 deletions autosurgeon-derive/tests/text.rs
Original file line number Diff line number Diff line change
@@ -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");
}
2 changes: 1 addition & 1 deletion autosurgeon/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }

Expand Down
73 changes: 73 additions & 0 deletions autosurgeon/src/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<S: AsRef<str>>(&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,
Expand Down

0 comments on commit 3a952ff

Please sign in to comment.