diff --git a/src/operation_transformation/edited_text.rs b/src/operation_transformation/edited_text.rs index d5e9dd9..2484036 100644 --- a/src/operation_transformation/edited_text.rs +++ b/src/operation_transformation/edited_text.rs @@ -1,4 +1,4 @@ -use std::fmt::Debug; +use std::{cmp::Ordering, fmt::Debug}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -81,10 +81,19 @@ where /// applied. The operations must not overlap. fn new( text: &'a str, - operations: Vec>, + mut operations: Vec>, operation_sides: Vec, mut cursors: Vec, ) -> Self { + // Inserts are anchored after the parent text consumed by earlier operations. + let mut parent_index = 0; + for operation in &mut operations { + if let Operation::Insert { order, .. } = operation { + *order = parent_index; + } else { + parent_index += operation.len(); + } + } cursors.sort_by_key(|cursor| cursor.char_index); Self { @@ -121,8 +130,7 @@ where /// /// # Panics /// - /// Panics if there's an integer overflow (in isize) when calculating new - /// cursor positions. + /// Panics in debug builds if the instances have different original texts. #[must_use] #[allow(clippy::too_many_lines)] pub fn merge(self, other: Self) -> Self { @@ -131,6 +139,16 @@ where "`EditedText`-s must be derived from the same text to be mergable" ); + // Identical current operations can have different continuations (for + // example, Markdown whitespace followed by a newline). Break those ties. + let stream_priority = self + .operations + .iter() + .zip(&other.operations) + .map(|(left, right)| left.cmp_priority(0, right, 0)) + .find(|order| !order.is_eq()) + .unwrap_or_else(|| self.operations.len().cmp(&other.operations.len())); + let mut merged_cursors = Vec::with_capacity(self.cursors.len() + other.cursors.len()); let mut left_cursors = self.cursors.into_iter().peekable(); let mut right_cursors = other.cursors.into_iter().peekable(); @@ -154,10 +172,12 @@ where let mut last_right_op = None; loop { - let (side, operation) = match (maybe_left_op.as_ref(), maybe_right_op.as_ref()) { + let (side, mut operation) = match (maybe_left_op.as_ref(), maybe_right_op.as_ref()) { (Some(left_op), Some(right_op)) => { - if left_op.cmp_priority(seen_left_length, right_op, seen_right_length) - == std::cmp::Ordering::Less + if left_op + .cmp_priority(seen_left_length, right_op, seen_right_length) + .then(stream_priority) + == Ordering::Less { (Side::Left, maybe_left_op.take().unwrap()) } else { @@ -170,6 +190,13 @@ where (None, None) => break, }; + // Process a broad retain only up to the other input's next boundary. + let remainder = match side { + Side::Left => maybe_right_op.as_ref(), + Side::Right => maybe_left_op.as_ref(), + } + .and_then(|next| operation.split_retain_before(next)); + let is_advancing_operation = matches!( operation, Operation::Insert { .. } | Operation::Equal { .. } @@ -180,6 +207,7 @@ where Side::Left => &mut last_right_op, Side::Right => &mut last_left_op, }; + let previous_is_equal = matches!(previous_operation, Some(Operation::Equal { .. })); let (result, insertion_overlap) = operation.merge_operations_consuming_insert(previous_operation.as_mut()); @@ -188,34 +216,30 @@ where Side::Left => (&mut left_cursors, seen_left_length), Side::Right => (&mut right_cursors, seen_right_length), }; - let merged_length_signed = - isize::try_from(merged_length).expect("merged_length must fit in isize"); - let seen_length_signed = - isize::try_from(seen_length).expect("seen_length must fit in isize"); - let op_len_signed = isize::try_from(op.len()).expect("op.len() must fit in isize"); - let original_length_signed = - isize::try_from(original_length).expect("original_length must fit in isize"); - - let shift = merged_length_signed - seen_length_signed + op_len_signed - - original_length_signed; - let deduplicated_prefix = insertion_overlap.map(|overlap| { - let start = merged_length - overlap.offset_from_end; - start..start + overlap.length - }); - let deduplicated_prefix_end = seen_length + original_length - op.len(); + let removed_prefix_length = original_length - op.len(); + let prefix_position = match insertion_overlap { + Some(overlap) => { + let start = merged_length - overlap.offset_from_end; + start..start + overlap.length + } + None if previous_is_equal => { + merged_length - removed_prefix_length..merged_length + } + // Deleted text collapses to the current output boundary. + _ => merged_length..merged_length, + }; while let Some(cursor) = cursors.next_if(|cursor| cursor.char_index <= seen_length + original_length) { // Shared text stays at its matched location; the remaining suffix // is appended at the current end of the merged text. - let index = match deduplicated_prefix.as_ref() { - Some(prefix) if cursor.char_index <= deduplicated_prefix_end => { - let offset = cursor.char_index.saturating_sub(seen_length); - // Normalized tokens can have different original lengths. - prefix.start + offset.min(prefix.len()) - } - _ => cursor.char_index.saturating_add_signed(shift), + let offset = cursor.char_index.saturating_sub(seen_length); + let index = if offset <= removed_prefix_length { + // Normalized tokens can have different original lengths. + prefix_position.start + offset.min(prefix_position.len()) + } else { + merged_length + offset - removed_prefix_length }; merged_cursors.push(cursor.with_index(index)); } @@ -227,7 +251,7 @@ where seen_left_length += original_length; } - maybe_left_op = left_iter.next(); + maybe_left_op = remainder.or_else(|| left_iter.next()); Self::remember_operation(&mut last_left_op, &result); } Side::Right => { @@ -235,7 +259,7 @@ where seen_right_length += original_length; } - maybe_right_op = right_iter.next(); + maybe_right_op = remainder.or_else(|| right_iter.next()); Self::remember_operation(&mut last_right_op, &result); } } diff --git a/src/operation_transformation/operation.rs b/src/operation_transformation/operation.rs index 2c4b7ee..9c950c9 100644 --- a/src/operation_transformation/operation.rs +++ b/src/operation_transformation/operation.rs @@ -111,6 +111,43 @@ where } } + /// Split a retain where the next operation starts or ends, returning its tail. + pub(super) fn split_retain_before(&mut self, next: &Self) -> Option { + let Self::Equal { order, length, .. } = self else { + return None; + }; + + let boundary = if next.order() > *order { + next.order() + } else if !matches!(next, Self::Insert { .. }) { + next.order() + next.len() + } else { + return None; + }; + + if boundary <= *order || boundary >= *order + *length { + return None; + } + let tail = Self::create_equal(boundary, *order + *length - boundary); + let prefix_length = boundary - *order; + *length = prefix_length; + + #[cfg(debug_assertions)] + let tail = if let Self::Equal { + text: Some(text), .. + } = self + { + let byte_index = text.char_indices().nth(prefix_length).unwrap().0; + let suffix = text.split_off(byte_index); + text.shrink_to_fit(); + Self::create_equal_with_text(boundary, suffix) + } else { + tail + }; + + Some(tail) + } + fn type_priority(&self) -> u8 { match self { Operation::Delete { .. } => 1,