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

@ -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<OrderedOperation<T>>,
@ -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);

View file

@ -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<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>
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<T> Display for Operation<T>
@ -362,9 +391,11 @@ mod tests {
#[test]
#[should_panic]
fn test_shifting_error() {
insta::assert_debug_snapshot!(Operation::create_insert(1, vec!["hi".into()])
insta::assert_debug_snapshot!(
Operation::create_insert(1, vec!["hi".into()])
.unwrap()
.with_shifted_index(-2));
.with_shifted_index(-2)
);
}
#[test]