Add deterministic ordering

This commit is contained in:
Andras Schmelczer 2025-03-02 15:10:15 +00:00
parent d7ae0a781d
commit 667b324a88
No known key found for this signature in database
GPG key ID: FC8F2C3D3D1A718C
3 changed files with 47 additions and 13 deletions

View file

@ -150,16 +150,16 @@ mod test {
test_merge_both_ways( test_merge_both_ways(
"hi ", "hi ",
"hi there ", "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 // The prefix of the 2nd appears on the 1st so it shouldn't get duplicated
test_merge_both_ways( test_merge_both_ways(
"hi ", "hi ",
"hi there you ", "hi there you ",
"hi there my friend", "hi there my friend ",
"hi there you my friend", "hi there you my friend ",
); );
} }

View file

@ -25,7 +25,7 @@ use crate::{
#[derive(Debug, Clone, PartialEq, Default)] #[derive(Debug, Clone, PartialEq, Default)]
pub struct EditedText<'a, T> pub struct EditedText<'a, T>
where where
T: PartialEq + Clone, T: PartialEq + Clone + std::fmt::Debug,
{ {
text: &'a str, text: &'a str,
operations: Vec<OrderedOperation<T>>, operations: Vec<OrderedOperation<T>>,
@ -46,7 +46,7 @@ impl<'a> EditedText<'a, String> {
impl<'a, T> EditedText<'a, T> impl<'a, T> EditedText<'a, T>
where where
T: PartialEq + Clone, T: PartialEq + Clone + std::fmt::Debug,
{ {
/// Create an `EditedText` from the given original (old) and updated (new) /// Create an `EditedText` from the given original (old) and updated (new)
/// strings. The returned `EditedText` represents the changes from the /// strings. The returned `EditedText` represents the changes from the
@ -207,9 +207,12 @@ where
|(operation, _)| { |(operation, _)| {
( (
operation.order, 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. // inserts can be merged with other inserts and deletes with deletes.
usize::from(matches!(operation.operation, Operation::Delete { .. })), 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 original = "hello world! ...";
let left = "Hello world! I'm Andras."; let left = "Hello world! I'm Andras.";
let right = "Hello world! How are you?"; 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_1 = EditedText::from_strings(original, left);
let operations_2 = EditedText::from_strings(original, right); let operations_2 = EditedText::from_strings(original, right);

View file

@ -2,18 +2,18 @@ use core::{
fmt::{Debug, Display}, fmt::{Debug, Display},
ops::Range, ops::Range,
}; };
use std::cmp::min; use std::hash::{DefaultHasher, Hash, Hasher};
#[cfg(feature = "serde")] #[cfg(feature = "serde")]
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::merge_context::MergeContext; use super::merge_context::MergeContext;
use crate::{ use crate::{
Token,
utils::{ utils::{
find_longest_prefix_contained_within::find_longest_prefix_contained_within, find_longest_prefix_contained_within::find_longest_prefix_contained_within,
string_builder::StringBuilder, string_builder::StringBuilder,
}, },
Token,
}; };
/// Represents a change that can be applied to a text document. /// Represents a change that can be applied to a text document.
@ -39,6 +39,28 @@ where
}, },
} }
impl<T> Hash for Operation<T>
where
T: PartialEq + Clone + std::fmt::Debug,
{
fn hash<H: Hasher>(&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<T> Operation<T> impl<T> Operation<T>
where where
T: PartialEq + Clone + std::fmt::Debug, 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<T> Display for Operation<T> impl<T> Display for Operation<T>
@ -362,9 +391,11 @@ mod tests {
#[test] #[test]
#[should_panic] #[should_panic]
fn test_shifting_error() { fn test_shifting_error() {
insta::assert_debug_snapshot!(Operation::create_insert(1, vec!["hi".into()]) insta::assert_debug_snapshot!(
.unwrap() Operation::create_insert(1, vec!["hi".into()])
.with_shifted_index(-2)); .unwrap()
.with_shifted_index(-2)
);
} }
#[test] #[test]