reconcile/src/operation_transformation/utils/elongate_operations.rs

126 lines
4.5 KiB
Rust

use core::iter;
use std::fmt::Debug;
use crate::raw_operation::RawOperation;
/// Elongates the operations by merging adjacent insertions and deletions that
/// can be joined. This makes the subsequent merging of operations more
/// intuitive.
pub fn elongate_operations<I, T>(raw_operations: I) -> Vec<RawOperation<T>>
where
I: IntoIterator<Item = RawOperation<T>>,
T: PartialEq + Clone + Debug,
{
// This might look bad, but this makes sense. The inserts and deltes can be
// interleaved, such as: IDIDID and we need to turn this into IIIDDD.
// So we need to keep track of both the last insert and delete operations, not
// just the last one.
let mut maybe_previous_insert: Option<RawOperation<T>> = None;
let mut maybe_previous_delete: Option<RawOperation<T>> = None;
// We don't elongate `equals` as they're needed to maintain cursor positions
// when merging against deletes.
let mut result: Vec<RawOperation<T>> = raw_operations
.into_iter()
.flat_map(|next| match next {
RawOperation::Insert(..) => match maybe_previous_insert.take() {
Some(prev) if prev.is_right_joinable() && next.is_left_joinable() => {
maybe_previous_insert = Some(prev.join(next));
Box::new(iter::empty()) as Box<dyn Iterator<Item = RawOperation<T>>>
}
prev => {
maybe_previous_insert = Some(next);
Box::new(prev.into_iter())
}
},
RawOperation::Delete(..) => match maybe_previous_delete.take() {
Some(prev) if prev.is_right_joinable() && next.is_left_joinable() => {
maybe_previous_delete = Some(prev.join(next));
Box::new(iter::empty()) as Box<dyn Iterator<Item = RawOperation<T>>>
}
prev => {
maybe_previous_delete = Some(next);
Box::new(prev.into_iter())
}
},
RawOperation::Equal(..) => Box::new(
maybe_previous_delete
.take()
.into_iter()
.chain(maybe_previous_insert.take())
.chain(iter::once(next)),
) as Box<dyn Iterator<Item = RawOperation<T>>>,
})
.collect();
if let Some(prev) = maybe_previous_delete {
result.push(prev);
}
if let Some(prev) = maybe_previous_insert {
result.push(prev);
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tokenizer::token::Token;
// Helper constructors for cleaner tests
fn ins(texts: &[&str]) -> RawOperation<String> {
RawOperation::Insert(texts.iter().map(|t| Token::from(*t)).collect())
}
fn del(texts: &[&str]) -> RawOperation<String> {
RawOperation::Delete(texts.iter().map(|t| Token::from(*t)).collect())
}
fn ins_custom(text: &str, lj: bool, rj: bool) -> RawOperation<String> {
RawOperation::Insert(vec![Token::new(text.to_owned(), text.to_owned(), lj, rj)])
}
#[test]
fn merges_adjacent_joinable_inserts() {
let ops = vec![ins(&["a"]), ins(&["b"]), ins(&["c"])];
let result = elongate_operations(ops);
assert_eq!(result.len(), 1);
match &result[0] {
RawOperation::Insert(tokens) => {
let originals: String = tokens
.iter()
.map(crate::tokenizer::token::Token::original)
.collect();
assert_eq!(originals, "abc");
}
_ => panic!("Expected single Insert operation"),
}
}
#[test]
fn does_not_merge_when_not_joinable() {
let ops = vec![
ins_custom("a", true, false), // not right-joinable
ins_custom("b", true, true), // left-joinable but previous isn't right-joinable
];
let result = elongate_operations(ops);
assert_eq!(
result.len(),
2,
"Operations should remain separate when not joinable"
);
}
#[test]
fn merges_interleaved_insert_delete_sequences() {
// Pattern IDID -> II DD
let ops = vec![ins(&["i1"]), del(&["d1"]), ins(&["i2"]), del(&["d2"])];
let result = elongate_operations(ops);
assert_eq!(result.len(), 2);
assert!(matches!(result[0], RawOperation::Delete(_)));
assert!(matches!(result[1], RawOperation::Insert(_)));
}
}