diff --git a/src/operation_transformation.rs b/src/operation_transformation.rs index 10bda6d..a2ac1c5 100644 --- a/src/operation_transformation.rs +++ b/src/operation_transformation.rs @@ -3,13 +3,10 @@ mod operation; mod utils; use std::fmt::Debug; -pub use edited_text::EditedText; +pub use edited_text::{ChangeSet, EditedText}; pub use operation::Operation; -use crate::{ - Tokenizer, - types::{side::Side, text_with_cursors::TextWithCursors}, -}; +use crate::{Tokenizer, types::text_with_cursors::TextWithCursors}; /// Given an `original` document and two concurrent edits to it, /// return a document containing all changes from both `left` @@ -48,10 +45,8 @@ pub fn reconcile<'a, T>( where T: PartialEq + Clone + Debug, { - let left_operations = - EditedText::from_strings_with_tokenizer(original, left, tokenizer, Side::Left); - let right_operations = - EditedText::from_strings_with_tokenizer(original, right, tokenizer, Side::Right); + let left_operations = EditedText::from_strings_with_tokenizer(original, left, tokenizer); + let right_operations = EditedText::from_strings_with_tokenizer(original, right, tokenizer); left_operations.merge(right_operations) } diff --git a/src/operation_transformation/edited_text.rs b/src/operation_transformation/edited_text.rs index 174cfaa..3894aae 100644 --- a/src/operation_transformation/edited_text.rs +++ b/src/operation_transformation/edited_text.rs @@ -1,4 +1,4 @@ -use std::fmt::Debug; +use std::{fmt::Debug, vec}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -35,9 +35,35 @@ where { text: &'a str, operations: Vec>, + operation_sides: Vec, cursors: Vec, } +/// A serializable representation of the changes made to a text document +/// without the original text. +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Debug, Clone, PartialEq, Default)] +pub struct ChangeSet +where + T: PartialEq + Clone + Debug, +{ + operations: Vec>, + cursors: Vec, +} + +impl<'a, T> ChangeSet +where + T: PartialEq + Clone + Debug, +{ + #[must_use] + pub fn new(operations: Vec>, cursors: Vec) -> Self { + Self { + operations, + cursors, + } + } +} + impl<'a> EditedText<'a, String> { /// Create an `EditedText` from the given original (old) and updated (new) /// strings. The returned `EditedText` represents the changes from the @@ -46,8 +72,8 @@ impl<'a> EditedText<'a, String> { /// word tokenizer is used to tokenize the text which splits the text on /// whitespaces. #[must_use] - pub fn from_strings(original: &'a str, updated: &TextWithCursors, side: Side) -> Self { - Self::from_strings_with_tokenizer(original, updated, &*BuiltinTokenizer::Word, side) + pub fn from_strings(original: &'a str, updated: &TextWithCursors) -> Self { + Self::from_strings_with_tokenizer(original, updated, &*BuiltinTokenizer::Word) } } @@ -64,16 +90,18 @@ where original: &'a str, updated: &TextWithCursors, tokenizer: &Tokenizer, - side: Side, ) -> Self { let original_tokens = (tokenizer)(original); let updated_tokens = (tokenizer)(&updated.text()); let diff: Vec> = RawOperation::vec_from(&original_tokens, &updated_tokens); + let operations: Vec> = cook_operations(elongate_operations(diff)).collect(); + let operation_count = operations.len(); Self::new( original, - cook_operations(elongate_operations(diff), side).collect(), + operations, + vec![Side::Left; operation_count], updated.cursors(), ) } @@ -81,12 +109,18 @@ where /// Create a new `EditedText` with the given operations. /// The operations must be in the order in which they are meant to be /// applied. The operations must not overlap. - fn new(text: &'a str, operations: Vec>, mut cursors: Vec) -> Self { + fn new( + text: &'a str, + operations: Vec>, + operation_sides: Vec, + mut cursors: Vec, + ) -> Self { cursors.sort_by_key(|cursor| cursor.char_index); Self { text, operations, + operation_sides, cursors, } } @@ -109,6 +143,8 @@ where let mut merged_operations: Vec> = Vec::with_capacity(self.operations.len() + other.operations.len()); + let mut merged_operation_sides: Vec = + Vec::with_capacity(self.operations.len() + other.operations.len()); let mut left_iter = self.operations.into_iter(); let mut right_iter = other.operations.into_iter(); @@ -149,7 +185,7 @@ where ); let original_length = operation.len(); - let result = match side { + let (side, result) = match side { Side::Left => { let result = operation.merge_operations(&mut last_other_op); @@ -181,7 +217,7 @@ where maybe_left_op = left_iter.next(); last_left_op = Some(result.clone()); - result + (Side::Left, result) } Side::Right => { let result = operation.merge_operations(&mut last_other_op); @@ -214,7 +250,7 @@ where maybe_right_op = right_iter.next(); last_right_op = Some(result.clone()); - result + (Side::Right, result) } }; @@ -227,13 +263,21 @@ where } merged_operations.push(result); + merged_operation_sides.push(side); } for cursor in left_cursors.chain(right_cursors) { merged_cursors.push(cursor.with_index(merged_length)); } - Self::new(self.text, merged_operations, merged_cursors) + debug_assert_eq!(merged_operations.len(), merged_operation_sides.len()); + + Self::new( + self.text, + merged_operations, + merged_operation_sides, + merged_cursors, + ) } /// Apply the operations to the text and return the resulting text. @@ -288,14 +332,14 @@ where let mut history = Vec::with_capacity(self.operations.len()); - for operation in &self.operations { + for (operation, side) in self.operations.iter().zip(self.operation_sides.iter()) { builder = operation.apply(builder); match operation { Operation::Equal { .. } => { history.push(SpanWithHistory::new(builder.take(), History::Unchanged)); } - Operation::Insert { side, .. } => match side { + Operation::Insert { .. } => match side { Side::Left => { history.push(SpanWithHistory::new(builder.take(), History::AddedFromLeft)); } @@ -307,7 +351,6 @@ where Operation::Delete { deleted_character_count, order, - side, .. } => { let deleted = self.text[*order..*order + *deleted_character_count].to_string(); @@ -325,6 +368,29 @@ where history } + + /// Serialize the `EditedText` as a `ChangeSet`, which contains only + /// the operations and cursor positions, without the original text. + /// This is useful for sending changes over the network if there's + /// a clear consensus on the original text. + #[must_use] + pub fn serialise_as_change_set(&self) -> ChangeSet { + ChangeSet::new(self.operations.clone(), self.cursors.clone()) + } + + /// Deserialize an `EditedText` from a `ChangeSet` and the original text. + /// This is useful for reconstructing the `EditedText` on the receiving + /// end after sending only the `ChangeSet` over the network. + #[must_use] + pub fn from_change_set(text: &'a str, change_set: ChangeSet) -> EditedText<'a, T> { + let operation_count = change_set.operations.len(); + EditedText::new( + text, + change_set.operations, + vec![Side::Left; operation_count], + change_set.cursors, + ) + } } #[cfg(test)] @@ -339,7 +405,7 @@ mod tests { let left = "hello world! How are you? Adam"; let right = "Hello, my friend! How are you doing? Albert"; - let operations = EditedText::from_strings(left, &right.into(), Side::Right); + let operations = EditedText::from_strings(left, &right.into()); insta::assert_debug_snapshot!(operations); @@ -351,7 +417,7 @@ mod tests { fn test_calculate_operations_with_no_diff() { let text = "hello world!"; - let operations = EditedText::from_strings(text, &text.into(), Side::Right); + let operations = EditedText::from_strings(text, &text.into()); assert_debug_snapshot!(operations); @@ -366,8 +432,8 @@ mod tests { let right = "Hello world! How are you?"; let expected = "Hello world! How are you? I'm Andras."; - let operations_1 = EditedText::from_strings(original, &left.into(), Side::Left); - let operations_2 = EditedText::from_strings(original, &right.into(), Side::Right); + let operations_1 = EditedText::from_strings(original, &left.into()); + let operations_2 = EditedText::from_strings(original, &right.into()); let operations = operations_1.merge(operations_2); assert_eq!(operations.apply().text(), expected); diff --git a/src/operation_transformation/operation.rs b/src/operation_transformation/operation.rs index 1c3060c..7a8f92a 100644 --- a/src/operation_transformation/operation.rs +++ b/src/operation_transformation/operation.rs @@ -4,7 +4,7 @@ use core::fmt::{Debug, Display}; use serde::{Deserialize, Serialize}; use crate::{ - Side, Token, + Token, utils::{ find_longest_prefix_contained_within::find_longest_prefix_contained_within, string_builder::StringBuilder, @@ -23,23 +23,21 @@ where length: usize, #[cfg(debug_assertions)] + #[cfg_attr(feature = "serde", serde(skip_serializing))] text: Option, }, Insert { - side: Side, - order: usize, text: Vec>, }, Delete { - side: Side, - order: usize, deleted_character_count: usize, #[cfg(debug_assertions)] + #[cfg_attr(feature = "serde", serde(skip_serializing))] deleted_text: Option, }, } @@ -72,15 +70,14 @@ where } /// Creates an insert operation with the given index and text. - pub fn create_insert(order: usize, text: Vec>, side: Side) -> Self { - Operation::Insert { side, order, text } + pub fn create_insert(order: usize, text: Vec>) -> Self { + Operation::Insert { order, text } } /// Creates a delete operation with the given index and number of /// to-be-deleted characters. - pub fn create_delete(order: usize, deleted_character_count: usize, side: Side) -> Self { + pub fn create_delete(order: usize, deleted_character_count: usize) -> Self { Operation::Delete { - side, order, deleted_character_count, @@ -89,9 +86,8 @@ where } } - pub fn create_delete_with_text(order: usize, text: String, side: Side) -> Self { + pub fn create_delete_with_text(order: usize, text: String) -> Self { Operation::Delete { - side, order, deleted_character_count: text.chars().count(), @@ -206,7 +202,7 @@ where match (operation, previous_operation) { ( - Operation::Insert { side, order, text }, + Operation::Insert { order, text }, Some(Operation::Insert { text: previous_inserted_text, .. @@ -218,12 +214,11 @@ where let offset_in_tokens = find_longest_prefix_contained_within(previous_inserted_text, &text); - Operation::create_insert(order, text[offset_in_tokens..].to_vec(), side) + Operation::create_insert(order, text[offset_in_tokens..].to_vec()) } ( Operation::Delete { - side, order, deleted_character_count, @@ -247,20 +242,19 @@ where #[cfg(debug_assertions)] let updated_delete = deleted_text.as_ref().map_or_else( - || Operation::create_delete(order + overlap, new_length, side), + || Operation::create_delete(order + overlap, new_length), |text| { Operation::create_delete_with_text( order + overlap, text.chars() .skip(deleted_character_count - new_length) .collect::(), - side, ) }, ); #[cfg(not(debug_assertions))] - let updated_delete = Operation::create_delete(order + overlap, new_length, side); + let updated_delete = Operation::create_delete(order + overlap, new_length); updated_delete } @@ -405,8 +399,7 @@ mod tests { #[test] fn test_apply_delete_with_create() { let builder = StringBuilder::new("hello world"); - let delete_operation = - Operation::<()>::create_delete_with_text(0, "hello ".to_owned(), Side::Left); + let delete_operation = Operation::<()>::create_delete_with_text(0, "hello ".to_owned()); let retain_operation = Operation::<()>::create_equal(6, 5); let mut builder = delete_operation.apply(builder); @@ -420,7 +413,7 @@ mod tests { let builder = StringBuilder::new("hello"); let retain_operation = Operation::<()>::create_equal(0, 5); - let insert_operation = Operation::create_insert(5, vec![" my friend".into()], Side::Right); + let insert_operation = Operation::create_insert(5, vec![" my friend".into()]); let mut builder = retain_operation.apply(builder); builder = insert_operation.apply(builder); diff --git a/src/operation_transformation/snapshots/reconcile_text__operation_transformation__edited_text__tests__calculate_operations.snap b/src/operation_transformation/snapshots/reconcile_text__operation_transformation__edited_text__tests__calculate_operations.snap index abbabbd..0096f0e 100644 --- a/src/operation_transformation/snapshots/reconcile_text__operation_transformation__edited_text__tests__calculate_operations.snap +++ b/src/operation_transformation/snapshots/reconcile_text__operation_transformation__edited_text__tests__calculate_operations.snap @@ -1,7 +1,6 @@ --- source: src/operation_transformation/edited_text.rs expression: operations -snapshot_kind: text --- EditedText { text: "hello world! How are you? Adam", @@ -15,5 +14,15 @@ EditedText { , , ], + operation_sides: [ + Left, + Left, + Left, + Left, + Left, + Left, + Left, + Left, + ], cursors: [], } diff --git a/src/operation_transformation/snapshots/reconcile_text__operation_transformation__edited_text__tests__calculate_operations_with_no_diff.snap b/src/operation_transformation/snapshots/reconcile_text__operation_transformation__edited_text__tests__calculate_operations_with_no_diff.snap index 275a552..cf6a674 100644 --- a/src/operation_transformation/snapshots/reconcile_text__operation_transformation__edited_text__tests__calculate_operations_with_no_diff.snap +++ b/src/operation_transformation/snapshots/reconcile_text__operation_transformation__edited_text__tests__calculate_operations_with_no_diff.snap @@ -1,7 +1,6 @@ --- source: src/operation_transformation/edited_text.rs expression: operations -snapshot_kind: text --- EditedText { text: "hello world!", @@ -10,5 +9,10 @@ EditedText { , , ], + operation_sides: [ + Left, + Left, + Left, + ], cursors: [], } diff --git a/src/operation_transformation/utils/cook_operations.rs b/src/operation_transformation/utils/cook_operations.rs index 0b188cc..2f1d0ac 100644 --- a/src/operation_transformation/utils/cook_operations.rs +++ b/src/operation_transformation/utils/cook_operations.rs @@ -1,10 +1,10 @@ use std::fmt::Debug; -use crate::{operation_transformation::Operation, raw_operation::RawOperation, types::side::Side}; +use crate::{operation_transformation::Operation, raw_operation::RawOperation}; /// Turn raw operations into ordered operations while keeping track of the /// original token's indexes. -pub fn cook_operations(raw_operations: I, side: Side) -> impl Iterator> +pub fn cook_operations(raw_operations: I) -> impl Iterator> where I: IntoIterator>, T: PartialEq + Clone + Debug, @@ -29,18 +29,15 @@ where op } - RawOperation::Insert(tokens) => { - Operation::create_insert(original_text_index, tokens, side) - } + RawOperation::Insert(tokens) => Operation::create_insert(original_text_index, tokens), RawOperation::Delete(..) => { let op = if cfg!(debug_assertions) { Operation::create_delete_with_text( original_text_index, raw_operation.get_original_text(), - side, ) } else { - Operation::create_delete(original_text_index, length, side) + Operation::create_delete(original_text_index, length) }; original_text_index += length;