Fix deduplication of consecutive insertions
This commit is contained in:
parent
8a98d29a3b
commit
e8221e03c7
3 changed files with 77 additions and 7 deletions
|
|
@ -95,6 +95,25 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn remember_operation(previous: &mut Option<Operation<T>>, operation: &Operation<T>) {
|
||||||
|
// Consecutive insertions can all contribute to a later overlap match.
|
||||||
|
// Preserve their unconsumed tokens even when the latest result is empty.
|
||||||
|
if let (
|
||||||
|
Some(Operation::Insert {
|
||||||
|
order: previous_order,
|
||||||
|
text: previous_text,
|
||||||
|
}),
|
||||||
|
Operation::Insert { order, text },
|
||||||
|
) = (previous.as_mut(), operation)
|
||||||
|
&& previous_order == order
|
||||||
|
{
|
||||||
|
previous_text.extend_from_slice(text);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
*previous = Some(operation.clone());
|
||||||
|
}
|
||||||
|
|
||||||
/// Merge two `EditedText` instances. The two instances must be derived
|
/// Merge two `EditedText` instances. The two instances must be derived
|
||||||
/// from the same original text. The operations are merged using the
|
/// from the same original text. The operations are merged using the
|
||||||
/// principles of Operational Transformation. The cursors are updated
|
/// principles of Operational Transformation. The cursors are updated
|
||||||
|
|
@ -209,7 +228,7 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
maybe_left_op = left_iter.next();
|
maybe_left_op = left_iter.next();
|
||||||
last_left_op = Some(result.clone());
|
Self::remember_operation(&mut last_left_op, &result);
|
||||||
}
|
}
|
||||||
Side::Right => {
|
Side::Right => {
|
||||||
if is_advancing_operation {
|
if is_advancing_operation {
|
||||||
|
|
@ -217,7 +236,7 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
maybe_right_op = right_iter.next();
|
maybe_right_op = right_iter.next();
|
||||||
last_right_op = Some(result.clone());
|
Self::remember_operation(&mut last_right_op, &result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,13 @@ where
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Character bounds of a matched prefix within the cached insertion.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) struct InsertionOverlap {
|
||||||
|
pub offset_from_end: usize,
|
||||||
|
pub length: usize,
|
||||||
|
}
|
||||||
|
|
||||||
impl<T> Operation<T>
|
impl<T> Operation<T>
|
||||||
where
|
where
|
||||||
T: PartialEq + Clone + Debug,
|
T: PartialEq + Clone + Debug,
|
||||||
|
|
@ -221,10 +228,12 @@ where
|
||||||
|
|
||||||
/// Merges operations while consuming the matched part of a cached insertion.
|
/// Merges operations while consuming the matched part of a cached insertion.
|
||||||
/// Keeps its suffix for later matches until new text is emitted after it.
|
/// Keeps its suffix for later matches until new text is emitted after it.
|
||||||
|
/// Also returns the matched prefix's position within the cached insertion,
|
||||||
|
/// for positioning cursors in deduplicated text.
|
||||||
pub(crate) fn merge_operations_consuming_insert(
|
pub(crate) fn merge_operations_consuming_insert(
|
||||||
self,
|
self,
|
||||||
previous_operation: Option<&mut Self>,
|
previous_operation: Option<&mut Self>,
|
||||||
) -> Self {
|
) -> (Self, Option<InsertionOverlap>) {
|
||||||
match (self, previous_operation) {
|
match (self, previous_operation) {
|
||||||
(
|
(
|
||||||
Operation::Insert { order, text },
|
Operation::Insert { order, text },
|
||||||
|
|
@ -234,16 +243,35 @@ where
|
||||||
}),
|
}),
|
||||||
) => {
|
) => {
|
||||||
let overlap = find_longest_prefix_contained_within(previous_inserted_text, &text);
|
let overlap = find_longest_prefix_contained_within(previous_inserted_text, &text);
|
||||||
|
let insertion_overlap = (!overlap.is_empty()).then(|| {
|
||||||
|
let length = previous_inserted_text[overlap.clone()]
|
||||||
|
.iter()
|
||||||
|
.map(Token::get_original_length)
|
||||||
|
.sum::<usize>();
|
||||||
|
let trailing_length = previous_inserted_text[overlap.end..]
|
||||||
|
.iter()
|
||||||
|
.map(Token::get_original_length)
|
||||||
|
.sum::<usize>();
|
||||||
|
|
||||||
|
InsertionOverlap {
|
||||||
|
offset_from_end: length + trailing_length,
|
||||||
|
length,
|
||||||
|
}
|
||||||
|
});
|
||||||
if overlap.len() == text.len() {
|
if overlap.len() == text.len() {
|
||||||
previous_inserted_text.drain(..overlap.end);
|
previous_inserted_text.drain(..overlap.end);
|
||||||
} else {
|
} else {
|
||||||
previous_inserted_text.clear();
|
previous_inserted_text.clear();
|
||||||
}
|
}
|
||||||
Operation::create_insert(order, text[overlap.len()..].to_vec())
|
(
|
||||||
}
|
Operation::create_insert(order, text[overlap.len()..].to_vec()),
|
||||||
(operation, previous_operation) => {
|
insertion_overlap,
|
||||||
operation.merge_operations(previous_operation.as_deref())
|
)
|
||||||
}
|
}
|
||||||
|
(operation, previous_operation) => (
|
||||||
|
operation.merge_operations(previous_operation.as_deref()),
|
||||||
|
None,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -79,3 +79,26 @@ fn identical_edits_preserve_original_spelling(parent: &str, updated: &str) {
|
||||||
updated,
|
updated,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test_case("a", "\n\n", "a\n\n", "\n\n"; "deleted_prefix")]
|
||||||
|
#[test_case("a b", "\n\n", "b\n\n", "\n\n"; "partially_deleted_prefix")]
|
||||||
|
fn consecutive_markdown_insertions_preserve_text_in_both_orders(
|
||||||
|
parent: &str,
|
||||||
|
left: &str,
|
||||||
|
right: &str,
|
||||||
|
expected: &str,
|
||||||
|
) {
|
||||||
|
for (left, right) in [(left, right), (right, left)] {
|
||||||
|
assert_eq!(
|
||||||
|
reconcile(
|
||||||
|
parent,
|
||||||
|
&left.into(),
|
||||||
|
&right.into(),
|
||||||
|
&*BuiltinTokenizer::Markdown
|
||||||
|
)
|
||||||
|
.apply()
|
||||||
|
.text(),
|
||||||
|
expected,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue