Add diffing & apply
This commit is contained in:
parent
e712164e89
commit
6d81033338
2 changed files with 269 additions and 0 deletions
143
backend/sync_lib/src/operations/operation.rs
Normal file
143
backend/sync_lib/src/operations/operation.rs
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
use std::cmp::Ordering;
|
||||||
|
|
||||||
|
use ropey::Rope;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use similar::{Change, ChangeTag};
|
||||||
|
|
||||||
|
use crate::errors::SyncLibError;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
|
pub enum Operation {
|
||||||
|
Insert {
|
||||||
|
index: u64,
|
||||||
|
text: String,
|
||||||
|
},
|
||||||
|
Delete {
|
||||||
|
index: u64,
|
||||||
|
deleted_character_count: u64,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Operation {
|
||||||
|
pub fn new(tag: ChangeTag, index: u64, text: &str) -> Self {
|
||||||
|
match tag {
|
||||||
|
ChangeTag::Insert => Operation::Insert {
|
||||||
|
index,
|
||||||
|
text: text.to_string(),
|
||||||
|
},
|
||||||
|
ChangeTag::Delete => Operation::Delete {
|
||||||
|
index,
|
||||||
|
deleted_character_count: text.chars().count() as u64,
|
||||||
|
},
|
||||||
|
_ => panic!("Only insertion and deletions are supported"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn apply<'a>(&self, rope_text: &'a mut Rope) -> Result<&'a mut Rope, SyncLibError> {
|
||||||
|
let index: usize = self.index() as usize;
|
||||||
|
match self {
|
||||||
|
Operation::Insert { text, .. } => rope_text.try_insert(index, &text).map_err(|err| {
|
||||||
|
SyncLibError::OperationApplicationError(format!("Failed to insert text: {}", err))
|
||||||
|
}),
|
||||||
|
Operation::Delete {
|
||||||
|
deleted_character_count,
|
||||||
|
..
|
||||||
|
} => rope_text
|
||||||
|
.try_remove(index..index + *deleted_character_count as usize)
|
||||||
|
.map_err(|err| {
|
||||||
|
SyncLibError::OperationApplicationError(format!(
|
||||||
|
"Failed to remove text: {}",
|
||||||
|
err
|
||||||
|
))
|
||||||
|
}),
|
||||||
|
}?;
|
||||||
|
|
||||||
|
Ok(rope_text)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn index(&self) -> u64 {
|
||||||
|
match self {
|
||||||
|
Operation::Insert { index, .. } => *index,
|
||||||
|
Operation::Delete { index, .. } => *index,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_index(&self, index: u64) -> Self {
|
||||||
|
match self {
|
||||||
|
Operation::Insert { text, .. } => Operation::Insert {
|
||||||
|
index,
|
||||||
|
text: text.clone(),
|
||||||
|
},
|
||||||
|
Operation::Delete {
|
||||||
|
deleted_character_count,
|
||||||
|
..
|
||||||
|
} => Operation::Delete {
|
||||||
|
index,
|
||||||
|
deleted_character_count: *deleted_character_count,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_shifted_index(&self, offset: i64) -> Result<Self, SyncLibError> {
|
||||||
|
let new_index: i64 = self.index() as i64 + offset;
|
||||||
|
let new_index: u64 = new_index
|
||||||
|
.try_into()
|
||||||
|
.map_err(|err| SyncLibError::OperationShiftingError(format!("{}", err)))?;
|
||||||
|
Ok(self.with_index(new_index))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for Operation {
|
||||||
|
fn cmp(&self, other: &Self) -> Ordering {
|
||||||
|
let result = self.index().cmp(&other.index());
|
||||||
|
if result == Ordering::Equal {
|
||||||
|
match (self, other) {
|
||||||
|
(Operation::Insert { .. }, Operation::Delete { .. }) => Ordering::Greater,
|
||||||
|
(Operation::Delete { .. }, Operation::Insert { .. }) => Ordering::Less,
|
||||||
|
_ => Ordering::Equal,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for Operation {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
|
Some(self.cmp(&other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_apply_delete() -> Result<(), SyncLibError> {
|
||||||
|
let mut rope = Rope::from_str("hello world");
|
||||||
|
let operation = Operation::Delete {
|
||||||
|
index: 5,
|
||||||
|
deleted_character_count: 6,
|
||||||
|
};
|
||||||
|
|
||||||
|
operation.apply(&mut rope)?;
|
||||||
|
|
||||||
|
assert_eq!(rope.to_string(), "hello");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_apply_insert() -> Result<(), SyncLibError> {
|
||||||
|
let mut rope = Rope::from_str("hello");
|
||||||
|
let operation = Operation::Insert {
|
||||||
|
index: 5,
|
||||||
|
text: " my friend".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
operation.apply(&mut rope)?;
|
||||||
|
|
||||||
|
assert_eq!(rope.to_string(), "hello my friend");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
126
backend/sync_lib/src/operations/operation_sequence.rs
Normal file
126
backend/sync_lib/src/operations/operation_sequence.rs
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
use super::{operation, Operation};
|
||||||
|
use crate::errors::SyncLibError;
|
||||||
|
use log::info;
|
||||||
|
use ropey::Rope;
|
||||||
|
use similar::utils::diff_graphemes;
|
||||||
|
use similar::{utils::TextDiffRemapper, ChangeTag, TextDiff};
|
||||||
|
use similar::{Algorithm, DiffableStrRef};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct OperationSequence {
|
||||||
|
operations: Vec<Operation>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OperationSequence {
|
||||||
|
pub fn new(mut operations: Vec<Operation>) -> Self {
|
||||||
|
operations.sort();
|
||||||
|
|
||||||
|
Self { operations }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn try_from_string_diff(
|
||||||
|
left: &str,
|
||||||
|
right: &str,
|
||||||
|
diff_ratio_threshold: f32,
|
||||||
|
) -> Result<Self, SyncLibError> {
|
||||||
|
let diff = TextDiff::configure()
|
||||||
|
.algorithm(Algorithm::Myers)
|
||||||
|
.diff_words(left, right);
|
||||||
|
|
||||||
|
let diff_ratio = 1.0 - diff.ratio();
|
||||||
|
if diff_ratio > diff_ratio_threshold {
|
||||||
|
return Err(SyncLibError::DiffTooLarge {
|
||||||
|
diff_ratio,
|
||||||
|
diff_ratio_limit: diff_ratio_threshold as f32,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let remapper = TextDiffRemapper::from_text_diff(&diff, left, right);
|
||||||
|
|
||||||
|
let mut index = 0;
|
||||||
|
let operations = diff
|
||||||
|
.ops()
|
||||||
|
.iter()
|
||||||
|
.flat_map(move |x| remapper.iter_slices(x))
|
||||||
|
.map(|(tag, text)| match tag {
|
||||||
|
ChangeTag::Equal => {
|
||||||
|
println!("Equal: {}", text);
|
||||||
|
index += text.chars().count();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
ChangeTag::Insert => {
|
||||||
|
println!("Insert: {}", text);
|
||||||
|
let result = Some(Operation::new(tag, index as u64, text));
|
||||||
|
index += text.chars().count();
|
||||||
|
result
|
||||||
|
}
|
||||||
|
ChangeTag::Delete => {
|
||||||
|
println!("Delete: {}", text);
|
||||||
|
Some(Operation::new(tag, index as u64, text))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.flat_map(Option::into_iter)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
Ok(Self::new(operations))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply<'a>(&self, rope_text: &'a mut Rope) -> Result<&'a mut Rope, SyncLibError> {
|
||||||
|
for operation in &self.operations {
|
||||||
|
println!("Applying operation: {:?}", operation);
|
||||||
|
operation.apply(rope_text)?;
|
||||||
|
println!("Text after operation: {}", rope_text);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(rope_text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_calculate_operations() -> Result<(), SyncLibError> {
|
||||||
|
let left = "hello world! How are you? Adam";
|
||||||
|
let right = "Hello, my friend! How are you doing? Albert";
|
||||||
|
|
||||||
|
let operations = OperationSequence::try_from_string_diff(left, right, 0.6)?;
|
||||||
|
|
||||||
|
insta::assert_debug_snapshot!(operations);
|
||||||
|
|
||||||
|
let mut left = Rope::from_str(left);
|
||||||
|
let new_right = operations.apply(&mut left)?;
|
||||||
|
|
||||||
|
assert_eq!(new_right.to_string(), right);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_calculate_operations_with_large_diff() {
|
||||||
|
let left = "hello world! How are you? Adam";
|
||||||
|
let right = "Hello, my friend! How are you doing? Albert";
|
||||||
|
|
||||||
|
let result = OperationSequence::try_from_string_diff(left, right, 0.1);
|
||||||
|
|
||||||
|
insta::assert_debug_snapshot!(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_calculate_operations_with_no_diff() -> Result<(), SyncLibError> {
|
||||||
|
let left = "hello world!";
|
||||||
|
let right = "hello world!";
|
||||||
|
|
||||||
|
let operations = OperationSequence::try_from_string_diff(left, right, 0.0)?;
|
||||||
|
|
||||||
|
assert_eq!(operations.operations.len(), 0);
|
||||||
|
|
||||||
|
let mut left = Rope::from_str(left);
|
||||||
|
let new_right = operations.apply(&mut left)?;
|
||||||
|
|
||||||
|
assert_eq!(new_right.to_string(), right);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue