Fix chained merges and cursor positioning
This commit is contained in:
parent
4d6aae467f
commit
de63f2160f
2 changed files with 92 additions and 31 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
use std::fmt::Debug;
|
use std::{cmp::Ordering, fmt::Debug};
|
||||||
|
|
||||||
#[cfg(feature = "serde")]
|
#[cfg(feature = "serde")]
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
@ -81,10 +81,19 @@ where
|
||||||
/// applied. The operations must not overlap.
|
/// applied. The operations must not overlap.
|
||||||
fn new(
|
fn new(
|
||||||
text: &'a str,
|
text: &'a str,
|
||||||
operations: Vec<Operation<T>>,
|
mut operations: Vec<Operation<T>>,
|
||||||
operation_sides: Vec<Side>,
|
operation_sides: Vec<Side>,
|
||||||
mut cursors: Vec<CursorPosition>,
|
mut cursors: Vec<CursorPosition>,
|
||||||
) -> Self {
|
) -> 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);
|
cursors.sort_by_key(|cursor| cursor.char_index);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -121,8 +130,7 @@ where
|
||||||
///
|
///
|
||||||
/// # Panics
|
/// # Panics
|
||||||
///
|
///
|
||||||
/// Panics if there's an integer overflow (in isize) when calculating new
|
/// Panics in debug builds if the instances have different original texts.
|
||||||
/// cursor positions.
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
#[allow(clippy::too_many_lines)]
|
#[allow(clippy::too_many_lines)]
|
||||||
pub fn merge(self, other: Self) -> Self {
|
pub fn merge(self, other: Self) -> Self {
|
||||||
|
|
@ -131,6 +139,16 @@ where
|
||||||
"`EditedText`-s must be derived from the same text to be mergable"
|
"`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 merged_cursors = Vec::with_capacity(self.cursors.len() + other.cursors.len());
|
||||||
let mut left_cursors = self.cursors.into_iter().peekable();
|
let mut left_cursors = self.cursors.into_iter().peekable();
|
||||||
let mut right_cursors = other.cursors.into_iter().peekable();
|
let mut right_cursors = other.cursors.into_iter().peekable();
|
||||||
|
|
@ -154,10 +172,12 @@ where
|
||||||
let mut last_right_op = None;
|
let mut last_right_op = None;
|
||||||
|
|
||||||
loop {
|
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)) => {
|
(Some(left_op), Some(right_op)) => {
|
||||||
if left_op.cmp_priority(seen_left_length, right_op, seen_right_length)
|
if left_op
|
||||||
== std::cmp::Ordering::Less
|
.cmp_priority(seen_left_length, right_op, seen_right_length)
|
||||||
|
.then(stream_priority)
|
||||||
|
== Ordering::Less
|
||||||
{
|
{
|
||||||
(Side::Left, maybe_left_op.take().unwrap())
|
(Side::Left, maybe_left_op.take().unwrap())
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -170,6 +190,13 @@ where
|
||||||
(None, None) => break,
|
(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!(
|
let is_advancing_operation = matches!(
|
||||||
operation,
|
operation,
|
||||||
Operation::Insert { .. } | Operation::Equal { .. }
|
Operation::Insert { .. } | Operation::Equal { .. }
|
||||||
|
|
@ -180,6 +207,7 @@ where
|
||||||
Side::Left => &mut last_right_op,
|
Side::Left => &mut last_right_op,
|
||||||
Side::Right => &mut last_left_op,
|
Side::Right => &mut last_left_op,
|
||||||
};
|
};
|
||||||
|
let previous_is_equal = matches!(previous_operation, Some(Operation::Equal { .. }));
|
||||||
let (result, insertion_overlap) =
|
let (result, insertion_overlap) =
|
||||||
operation.merge_operations_consuming_insert(previous_operation.as_mut());
|
operation.merge_operations_consuming_insert(previous_operation.as_mut());
|
||||||
|
|
||||||
|
|
@ -188,34 +216,30 @@ where
|
||||||
Side::Left => (&mut left_cursors, seen_left_length),
|
Side::Left => (&mut left_cursors, seen_left_length),
|
||||||
Side::Right => (&mut right_cursors, seen_right_length),
|
Side::Right => (&mut right_cursors, seen_right_length),
|
||||||
};
|
};
|
||||||
let merged_length_signed =
|
let removed_prefix_length = original_length - op.len();
|
||||||
isize::try_from(merged_length).expect("merged_length must fit in isize");
|
let prefix_position = match insertion_overlap {
|
||||||
let seen_length_signed =
|
Some(overlap) => {
|
||||||
isize::try_from(seen_length).expect("seen_length must fit in isize");
|
let start = merged_length - overlap.offset_from_end;
|
||||||
let op_len_signed = isize::try_from(op.len()).expect("op.len() must fit in isize");
|
start..start + overlap.length
|
||||||
let original_length_signed =
|
}
|
||||||
isize::try_from(original_length).expect("original_length must fit in isize");
|
None if previous_is_equal => {
|
||||||
|
merged_length - removed_prefix_length..merged_length
|
||||||
let shift = merged_length_signed - seen_length_signed + op_len_signed
|
}
|
||||||
- original_length_signed;
|
// Deleted text collapses to the current output boundary.
|
||||||
let deduplicated_prefix = insertion_overlap.map(|overlap| {
|
_ => merged_length..merged_length,
|
||||||
let start = merged_length - overlap.offset_from_end;
|
};
|
||||||
start..start + overlap.length
|
|
||||||
});
|
|
||||||
let deduplicated_prefix_end = seen_length + original_length - op.len();
|
|
||||||
|
|
||||||
while let Some(cursor) =
|
while let Some(cursor) =
|
||||||
cursors.next_if(|cursor| cursor.char_index <= seen_length + original_length)
|
cursors.next_if(|cursor| cursor.char_index <= seen_length + original_length)
|
||||||
{
|
{
|
||||||
// Shared text stays at its matched location; the remaining suffix
|
// Shared text stays at its matched location; the remaining suffix
|
||||||
// is appended at the current end of the merged text.
|
// is appended at the current end of the merged text.
|
||||||
let index = match deduplicated_prefix.as_ref() {
|
let offset = cursor.char_index.saturating_sub(seen_length);
|
||||||
Some(prefix) if cursor.char_index <= deduplicated_prefix_end => {
|
let index = if offset <= removed_prefix_length {
|
||||||
let offset = cursor.char_index.saturating_sub(seen_length);
|
// Normalized tokens can have different original lengths.
|
||||||
// Normalized tokens can have different original lengths.
|
prefix_position.start + offset.min(prefix_position.len())
|
||||||
prefix.start + offset.min(prefix.len())
|
} else {
|
||||||
}
|
merged_length + offset - removed_prefix_length
|
||||||
_ => cursor.char_index.saturating_add_signed(shift),
|
|
||||||
};
|
};
|
||||||
merged_cursors.push(cursor.with_index(index));
|
merged_cursors.push(cursor.with_index(index));
|
||||||
}
|
}
|
||||||
|
|
@ -227,7 +251,7 @@ where
|
||||||
seen_left_length += original_length;
|
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);
|
Self::remember_operation(&mut last_left_op, &result);
|
||||||
}
|
}
|
||||||
Side::Right => {
|
Side::Right => {
|
||||||
|
|
@ -235,7 +259,7 @@ where
|
||||||
seen_right_length += original_length;
|
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);
|
Self::remember_operation(&mut last_right_op, &result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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<Self> {
|
||||||
|
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 {
|
fn type_priority(&self) -> u8 {
|
||||||
match self {
|
match self {
|
||||||
Operation::Delete { .. } => 1,
|
Operation::Delete { .. } => 1,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue