diff --git a/backend/reconcile/src/operation_transformation.rs b/backend/reconcile/src/operation_transformation.rs index 1f34fa12..3f83197a 100644 --- a/backend/reconcile/src/operation_transformation.rs +++ b/backend/reconcile/src/operation_transformation.rs @@ -150,16 +150,16 @@ mod test { test_merge_both_ways( "hi ", "hi there ", - "hi there my friend", - "hi there my friend", + "hi there my friend ", + "hi there my friend ", ); // The prefix of the 2nd appears on the 1st so it shouldn't get duplicated test_merge_both_ways( "hi ", "hi there you ", - "hi there my friend", - "hi there you my friend", + "hi there my friend ", + "hi there you my friend ", ); } diff --git a/backend/reconcile/src/operation_transformation/edited_text.rs b/backend/reconcile/src/operation_transformation/edited_text.rs index 32bda1b2..4052485c 100644 --- a/backend/reconcile/src/operation_transformation/edited_text.rs +++ b/backend/reconcile/src/operation_transformation/edited_text.rs @@ -25,7 +25,7 @@ use crate::{ #[derive(Debug, Clone, PartialEq, Default)] pub struct EditedText<'a, T> where - T: PartialEq + Clone, + T: PartialEq + Clone + std::fmt::Debug, { text: &'a str, operations: Vec>, @@ -46,7 +46,7 @@ impl<'a> EditedText<'a, String> { impl<'a, T> EditedText<'a, T> where - T: PartialEq + Clone, + T: PartialEq + Clone + std::fmt::Debug, { /// Create an `EditedText` from the given original (old) and updated (new) /// strings. The returned `EditedText` represents the changes from the @@ -207,9 +207,12 @@ where |(operation, _)| { ( operation.order, - // Operations on left and right must come in the same order so that + // Operations on the left and right must come in the same order so that // inserts can be merged with other inserts and deletes with deletes. usize::from(matches!(operation.operation, Operation::Delete { .. })), + // Make sure that the ordering is deterministic regardless which text + // is left or right. + operation.operation.get_hash(), ) }, ) @@ -282,7 +285,7 @@ mod tests { let original = "hello world! ..."; let left = "Hello world! I'm Andras."; let right = "Hello world! How are you?"; - let expected = "Hello world! I'm Andras.How are you?"; + let expected = "Hello world! How are you?I'm Andras."; let operations_1 = EditedText::from_strings(original, left); let operations_2 = EditedText::from_strings(original, right); diff --git a/backend/reconcile/src/operation_transformation/operation.rs b/backend/reconcile/src/operation_transformation/operation.rs index 5de0141b..a985ad7b 100644 --- a/backend/reconcile/src/operation_transformation/operation.rs +++ b/backend/reconcile/src/operation_transformation/operation.rs @@ -2,18 +2,18 @@ use core::{ fmt::{Debug, Display}, ops::Range, }; -use std::cmp::min; +use std::hash::{DefaultHasher, Hash, Hasher}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; use super::merge_context::MergeContext; use crate::{ + Token, utils::{ find_longest_prefix_contained_within::find_longest_prefix_contained_within, string_builder::StringBuilder, }, - Token, }; /// Represents a change that can be applied to a text document. @@ -39,6 +39,28 @@ where }, } +impl Hash for Operation +where + T: PartialEq + Clone + std::fmt::Debug, +{ + fn hash(&self, state: &mut H) { + match self { + Operation::Insert { index, text } => { + index.hash(state); + text.iter().for_each(|token| token.original().hash(state)); + } + Operation::Delete { + index, + deleted_character_count, + .. + } => { + index.hash(state); + deleted_character_count.hash(state); + } + }; + } +} + impl Operation where T: PartialEq + Clone + std::fmt::Debug, @@ -300,6 +322,13 @@ where } } } + + /// Gets the hash of the operation based on the indexes and original text. + pub fn get_hash(&self) -> u64 { + let mut hasher = DefaultHasher::new(); + self.hash(&mut hasher); + hasher.finish() + } } impl Display for Operation @@ -362,9 +391,11 @@ mod tests { #[test] #[should_panic] fn test_shifting_error() { - insta::assert_debug_snapshot!(Operation::create_insert(1, vec!["hi".into()]) - .unwrap() - .with_shifted_index(-2)); + insta::assert_debug_snapshot!( + Operation::create_insert(1, vec!["hi".into()]) + .unwrap() + .with_shifted_index(-2) + ); } #[test]