Add cursor moving #19
3 changed files with 123 additions and 51 deletions
Implement cursor merging
commit
680486c4b5
|
|
@ -3,7 +3,7 @@ use core::iter;
|
||||||
#[cfg(feature = "serde")]
|
#[cfg(feature = "serde")]
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use super::Operation;
|
use super::{CursorPosition, Operation, TextWithCursors};
|
||||||
use crate::{
|
use crate::{
|
||||||
diffs::{myers::diff, raw_operation::RawOperation},
|
diffs::{myers::diff, raw_operation::RawOperation},
|
||||||
operation_transformation::merge_context::MergeContext,
|
operation_transformation::merge_context::MergeContext,
|
||||||
|
|
@ -29,6 +29,7 @@ where
|
||||||
{
|
{
|
||||||
text: &'a str,
|
text: &'a str,
|
||||||
operations: Vec<OrderedOperation<T>>,
|
operations: Vec<OrderedOperation<T>>,
|
||||||
|
pub(crate) cursors: Vec<CursorPosition>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> EditedText<'a, String> {
|
impl<'a> EditedText<'a, String> {
|
||||||
|
|
@ -39,7 +40,7 @@ impl<'a> EditedText<'a, String> {
|
||||||
/// word tokenizer is used to tokenize the text which splits the text on
|
/// word tokenizer is used to tokenize the text which splits the text on
|
||||||
/// whitespaces.
|
/// whitespaces.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn from_strings(original: &'a str, updated: &str) -> Self {
|
pub fn from_strings(original: &'a str, updated: TextWithCursors<'a>) -> Self {
|
||||||
Self::from_strings_with_tokenizer(original, updated, &word_tokenizer)
|
Self::from_strings_with_tokenizer(original, updated, &word_tokenizer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -55,17 +56,18 @@ where
|
||||||
/// function is used to tokenize the text.
|
/// function is used to tokenize the text.
|
||||||
pub fn from_strings_with_tokenizer(
|
pub fn from_strings_with_tokenizer(
|
||||||
original: &'a str,
|
original: &'a str,
|
||||||
updated: &str,
|
updated: TextWithCursors<'a>,
|
||||||
tokenizer: &Tokenizer<T>,
|
tokenizer: &Tokenizer<T>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let original_tokens = (tokenizer)(original);
|
let original_tokens = (tokenizer)(original);
|
||||||
let updated_tokens = (tokenizer)(updated);
|
let updated_tokens = (tokenizer)(&updated.text);
|
||||||
|
|
||||||
let diff: Vec<RawOperation<T>> = diff(&original_tokens, &updated_tokens);
|
let diff: Vec<RawOperation<T>> = diff(&original_tokens, &updated_tokens);
|
||||||
|
|
||||||
Self::new(
|
Self::new(
|
||||||
original,
|
original,
|
||||||
Self::cook_operations(Self::elongate_operations(diff)).collect(),
|
Self::cook_operations(Self::elongate_operations(diff)).collect(),
|
||||||
|
updated.cursors,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -170,7 +172,11 @@ where
|
||||||
/// Create a new `EditedText` with the given operations.
|
/// Create a new `EditedText` with the given operations.
|
||||||
/// The operations must be in the order in which they are meant to be
|
/// The operations must be in the order in which they are meant to be
|
||||||
/// applied. The operations must not overlap.
|
/// applied. The operations must not overlap.
|
||||||
fn new(text: &'a str, operations: Vec<OrderedOperation<T>>) -> Self {
|
fn new(
|
||||||
|
text: &'a str,
|
||||||
|
operations: Vec<OrderedOperation<T>>,
|
||||||
|
mut cursors: Vec<CursorPosition>,
|
||||||
|
) -> Self {
|
||||||
operations
|
operations
|
||||||
.iter()
|
.iter()
|
||||||
.zip(operations.iter().skip(1))
|
.zip(operations.iter().skip(1))
|
||||||
|
|
@ -183,7 +189,13 @@ where
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
Self { text, operations }
|
cursors.sort_by_key(|cursor| cursor.char_index);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
text,
|
||||||
|
operations,
|
||||||
|
cursors,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
|
|
@ -196,50 +208,110 @@ where
|
||||||
let mut left_merge_context = MergeContext::default();
|
let mut left_merge_context = MergeContext::default();
|
||||||
let mut right_merge_context = MergeContext::default();
|
let mut right_merge_context = MergeContext::default();
|
||||||
|
|
||||||
Self::new(
|
let mut merged_cursors = Vec::with_capacity(self.cursors.len() + other.cursors.len());
|
||||||
self.text,
|
let mut left_cursors = self.cursors.iter().peekable();
|
||||||
self.operations
|
let mut right_cursors = other.cursors.iter().peekable();
|
||||||
.into_iter()
|
|
||||||
.map(|op| (op, Side::Left))
|
let merged_operations = self
|
||||||
.merge_sorted_by_key(
|
.operations
|
||||||
other.operations.into_iter().map(|op| (op, Side::Right)),
|
.into_iter()
|
||||||
|(operation, _)| {
|
// The current text is always the left; the other operation is the right side.
|
||||||
(
|
.map(|op| (op, Side::Left))
|
||||||
operation.order,
|
.merge_sorted_by_key(
|
||||||
// Operations on the left and right must come in the same order so that
|
other.operations.into_iter().map(|op| (op, Side::Right)),
|
||||||
// inserts can be merged with other inserts and deletes with deletes.
|
|(operation, _)| {
|
||||||
usize::from(matches!(operation.operation, Operation::Delete { .. })),
|
(
|
||||||
// Make sure that the ordering is deterministic regardless which text
|
operation.order,
|
||||||
// is left or right.
|
// Operations on the left and right must come in the same order so that
|
||||||
match &operation.operation {
|
// inserts can be merged with other inserts and deletes with deletes.
|
||||||
Operation::Insert { text, .. } => text
|
usize::from(matches!(operation.operation, Operation::Delete { .. })),
|
||||||
.iter()
|
// Make sure that the ordering is deterministic regardless which text
|
||||||
.map(super::super::tokenizer::token::Token::original)
|
// is left or right.
|
||||||
.collect::<String>(),
|
match &operation.operation {
|
||||||
Operation::Delete {
|
Operation::Insert { text, .. } => text
|
||||||
deleted_character_count,
|
.iter()
|
||||||
..
|
.map(super::super::tokenizer::token::Token::original)
|
||||||
} => deleted_character_count.to_string(),
|
.collect::<String>(),
|
||||||
},
|
Operation::Delete {
|
||||||
|
deleted_character_count,
|
||||||
|
..
|
||||||
|
} => deleted_character_count.to_string(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.flat_map(|(OrderedOperation { order, operation }, side)| {
|
||||||
|
match side {
|
||||||
|
Side::Left => {
|
||||||
|
while let Some(cursor) = left_cursors
|
||||||
|
.next_if(|cursor| cursor.char_index <= operation.start_index())
|
||||||
|
{
|
||||||
|
right_merge_context.consume_last_operation_if_it_is_too_behind(
|
||||||
|
cursor.char_index as i64,
|
||||||
|
);
|
||||||
|
merged_cursors.push(cursor.apply_merge_context(&right_merge_context));
|
||||||
|
}
|
||||||
|
|
||||||
|
while let Some(cursor) = right_cursors.next_if(|cursor| {
|
||||||
|
cursor.char_index as i64
|
||||||
|
<= operation.start_index() as i64 + right_merge_context.shift
|
||||||
|
- left_merge_context.shift
|
||||||
|
}) {
|
||||||
|
left_merge_context.consume_last_operation_if_it_is_too_behind(
|
||||||
|
cursor.char_index as i64,
|
||||||
|
);
|
||||||
|
merged_cursors.push(cursor.apply_merge_context(&left_merge_context));
|
||||||
|
}
|
||||||
|
|
||||||
|
operation.merge_operations_with_context(
|
||||||
|
&mut right_merge_context,
|
||||||
|
&mut left_merge_context,
|
||||||
)
|
)
|
||||||
},
|
|
||||||
)
|
|
||||||
.flat_map(|(OrderedOperation { order, operation }, side)| {
|
|
||||||
match side {
|
|
||||||
Side::Left => operation.merge_operations_with_context(
|
|
||||||
&mut right_merge_context,
|
|
||||||
&mut left_merge_context,
|
|
||||||
),
|
|
||||||
Side::Right => operation.merge_operations_with_context(
|
|
||||||
&mut left_merge_context,
|
|
||||||
&mut right_merge_context,
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
.map(|operation| OrderedOperation { order, operation })
|
Side::Right => {
|
||||||
.into_iter()
|
while let Some(cursor) = right_cursors
|
||||||
})
|
.next_if(|cursor| cursor.char_index <= operation.start_index())
|
||||||
.collect(),
|
{
|
||||||
)
|
left_merge_context.consume_last_operation_if_it_is_too_behind(
|
||||||
|
cursor.char_index as i64,
|
||||||
|
);
|
||||||
|
merged_cursors.push(cursor.apply_merge_context(&left_merge_context));
|
||||||
|
}
|
||||||
|
|
||||||
|
while let Some(cursor) = left_cursors.next_if(|cursor| {
|
||||||
|
cursor.char_index as i64
|
||||||
|
<= operation.start_index() as i64 + left_merge_context.shift
|
||||||
|
- right_merge_context.shift
|
||||||
|
}) {
|
||||||
|
right_merge_context.consume_last_operation_if_it_is_too_behind(
|
||||||
|
cursor.char_index as i64,
|
||||||
|
);
|
||||||
|
merged_cursors.push(cursor.apply_merge_context(&right_merge_context));
|
||||||
|
}
|
||||||
|
|
||||||
|
operation.merge_operations_with_context(
|
||||||
|
&mut left_merge_context,
|
||||||
|
&mut right_merge_context,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.map(|operation| OrderedOperation { order, operation })
|
||||||
|
.into_iter()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for cursor in left_cursors {
|
||||||
|
right_merge_context
|
||||||
|
.consume_last_operation_if_it_is_too_behind(cursor.char_index as i64);
|
||||||
|
merged_cursors.push(cursor.apply_merge_context(&right_merge_context));
|
||||||
|
}
|
||||||
|
|
||||||
|
for cursor in right_cursors {
|
||||||
|
left_merge_context.consume_last_operation_if_it_is_too_behind(cursor.char_index as i64);
|
||||||
|
merged_cursors.push(cursor.apply_merge_context(&left_merge_context));
|
||||||
|
}
|
||||||
|
|
||||||
|
Self::new(self.text, merged_operations, merged_cursors)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Apply the operations to the text and return the resulting text.
|
/// Apply the operations to the text and return the resulting text.
|
||||||
|
|
@ -268,7 +340,7 @@ mod tests {
|
||||||
let left = "hello world! How are you? Adam";
|
let left = "hello world! How are you? Adam";
|
||||||
let right = "Hello, my friend! How are you doing? Albert";
|
let right = "Hello, my friend! How are you doing? Albert";
|
||||||
|
|
||||||
let operations = EditedText::from_strings(left, right);
|
let operations = EditedText::from_strings(left, right.into());
|
||||||
|
|
||||||
insta::assert_debug_snapshot!(operations);
|
insta::assert_debug_snapshot!(operations);
|
||||||
|
|
||||||
|
|
@ -280,7 +352,7 @@ mod tests {
|
||||||
fn test_calculate_operations_with_no_diff() {
|
fn test_calculate_operations_with_no_diff() {
|
||||||
let text = "hello world!";
|
let text = "hello world!";
|
||||||
|
|
||||||
let operations = EditedText::from_strings(text, text);
|
let operations = EditedText::from_strings(text, text.into());
|
||||||
|
|
||||||
assert_eq!(operations.operations.len(), 0);
|
assert_eq!(operations.operations.len(), 0);
|
||||||
|
|
||||||
|
|
@ -296,8 +368,8 @@ mod tests {
|
||||||
let right = "Hello world! How are you?";
|
let right = "Hello world! How are you?";
|
||||||
let expected = "Hello world! How are you? I'm Andras.";
|
let expected = "Hello world! How are you? I'm Andras.";
|
||||||
|
|
||||||
let operations_1 = EditedText::from_strings(original, left);
|
let operations_1 = EditedText::from_strings(original, left.into());
|
||||||
let operations_2 = EditedText::from_strings(original, right);
|
let operations_2 = EditedText::from_strings(original, right.into());
|
||||||
|
|
||||||
let operations = operations_1.merge(operations_2);
|
let operations = operations_1.merge(operations_2);
|
||||||
assert_eq!(operations.apply(), expected);
|
assert_eq!(operations.apply(), expected);
|
||||||
|
|
|
||||||
|
|
@ -189,7 +189,7 @@ where
|
||||||
affecting_context: &mut MergeContext<T>,
|
affecting_context: &mut MergeContext<T>,
|
||||||
produced_context: &mut MergeContext<T>,
|
produced_context: &mut MergeContext<T>,
|
||||||
) -> Option<Operation<T>> {
|
) -> Option<Operation<T>> {
|
||||||
affecting_context.consume_last_operation_if_it_is_too_behind(&self);
|
affecting_context.consume_last_operation_if_it_is_too_behind(self.start_index() as i64);
|
||||||
let operation = self.with_shifted_index(affecting_context.shift);
|
let operation = self.with_shifted_index(affecting_context.shift);
|
||||||
|
|
||||||
match (operation, affecting_context.last_operation()) {
|
match (operation, affecting_context.last_operation()) {
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ fn test_with_examples() {
|
||||||
&doc.right().text,
|
&doc.right().text,
|
||||||
));
|
));
|
||||||
|
|
||||||
doc.assert_eq(reconcile_with_cursors(
|
doc.assert_eq(&reconcile_with_cursors(
|
||||||
&doc.parent(),
|
&doc.parent(),
|
||||||
doc.left(),
|
doc.left(),
|
||||||
doc.right(),
|
doc.right(),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue