-
-
+
+
+
@@ -64,9 +91,15 @@
width="24"
height="24"
viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ stroke-width="2"
+ stroke-linecap="round"
+ stroke-linejoin="round"
>
+
diff --git a/examples/website/script.js b/examples/website/script.js
index 4dcbaf9..dce83b0 100644
--- a/examples/website/script.js
+++ b/examples/website/script.js
@@ -1,10 +1,9 @@
-import init, { mergeText } from "./reconcile.js";
+import init, { mergeText, mergeTextWithHistory } from "./reconcile.js";
const originalTextArea = document.getElementById("original");
const leftTextArea = document.getElementById("left");
const rightTextArea = document.getElementById("right");
const mergedTextArea = document.getElementById("merged");
-const mergeButton = document.getElementById("merge-button");
const sampleTexts = [
"The quick brown fox jumps over the lazy dog.",
@@ -17,16 +16,35 @@ const sampleTexts = [
async function run() {
await init();
- mergeButton.addEventListener("click", () => {
- const original = originalTextArea.value;
- const left = leftTextArea.value;
- const right = rightTextArea.value;
-
- const result = mergeText(original, left, right);
- mergedTextArea.value = result;
- });
+ originalTextArea.addEventListener("input", updateMergedText);
+ leftTextArea.addEventListener("input", updateMergedText);
+ rightTextArea.addEventListener("input", updateMergedText);
loadSample();
+ updateMergedText();
+
+ // Put cursor at the end of the text in leftTextArea
+ leftTextArea.focus();
+ leftTextArea.selectionStart = leftTextArea.value.length;
+ leftTextArea.selectionEnd = leftTextArea.value.length;
+}
+
+function updateMergedText() {
+ const original = originalTextArea.value;
+ const left = leftTextArea.value;
+ const right = rightTextArea.value;
+
+ const results = mergeTextWithHistory(original, left, right);
+
+ mergedTextArea.innerHTML = "";
+
+ for (const result of results) {
+ const span = document.createElement("span");
+ span.className = result.history();
+ span.textContent = result.text();
+ mergedTextArea.appendChild(span);
+ result.free();
+ }
}
function loadSample() {
@@ -35,7 +53,6 @@ function loadSample() {
originalTextArea.value = text;
leftTextArea.value = text;
rightTextArea.value = text;
- mergedTextArea.value = "";
}
run();
diff --git a/examples/website/style.css b/examples/website/style.css
index 6324812..3de3fa3 100644
--- a/examples/website/style.css
+++ b/examples/website/style.css
@@ -1,6 +1,7 @@
* {
box-sizing: border-box;
margin: 0;
+ user-select: none;
}
html,
@@ -40,7 +41,7 @@ main {
flex: 1;
display: grid;
grid-template-rows: auto auto auto;
- grid-template-columns: 1fr auto 1fr;
+ grid-template-columns: 1fr 1fr;
gap: 20px;
justify-items: center;
align-items: center;
@@ -58,34 +59,8 @@ main {
}
.diamond-right {
- grid-column: 3;
- grid-row: 2;
-}
-
-#merge-button {
grid-column: 2;
grid-row: 2;
- padding: 12px 36px;
- border: none;
- border-radius: 8px;
- background: linear-gradient(90deg, #2451a6 0%, #3486eb 100%);
- color: #fff;
- font-size: 1.15rem;
- font-weight: 600;
- box-shadow: 0 2px 12px 0 rgba(36, 81, 166, 0.08);
- cursor: pointer;
- align-self: center;
- transition: transform 0.2s;
- margin: 0 32px;
-}
-
-#merge-button:hover {
- transform: scale(1.1);
-}
-
-.diamond-right {
- grid-column: 3;
- grid-row: 2;
}
.diamond-result {
@@ -93,10 +68,9 @@ main {
grid-row: 3;
display: flex;
align-items: center;
- pointer-events: none;
}
-.text-area {
+.text-area-card {
display: flex;
flex-direction: column;
align-items: center;
@@ -115,6 +89,15 @@ label {
color: #2451a6;
}
+.box {
+ width: 1ch;
+ height: 1ch;
+ border-radius: 50%;
+ margin-left: 6px;
+ display: inline-block;
+ transform: scale(1.5);
+}
+
textarea {
width: 100%;
border: none;
@@ -128,6 +111,29 @@ textarea {
height: 100%;
}
+#merged {
+ width: 100%;
+ text-align: left;
+ user-select: text;
+}
+
+.Left,
+.AddedFromLeft,
+.RemovedFromLeft {
+ background: #12d197;
+}
+
+.Right,
+.AddedFromRight,
+.RemovedFromRight {
+ background: #8575ed;
+}
+
+.RemovedFromLeft,
+.RemovedFromRight {
+ text-decoration: line-through;
+}
+
@media (max-width: 900px) {
main {
padding: 24px 2vw;
@@ -139,29 +145,25 @@ textarea {
grid-template-columns: 1fr;
grid-template-rows: auto auto auto auto auto;
}
+
+ main > * {
+ grid-column: 1;
+ }
+
.diamond-parent {
grid-row: 1;
- grid-column: 1;
}
.diamond-left {
grid-row: 2;
- grid-column: 1;
}
.diamond-right {
grid-row: 3;
- grid-column: 1;
- }
-
- #merge-button {
- grid-row: 4;
- grid-column: 1;
}
.diamond-result {
grid-row: 5;
- grid-column: 1;
}
}
@@ -178,6 +180,7 @@ footer {
.github-link > svg {
position: absolute;
+ color: #5a6272;
top: 50%;
right: 36px;
transform: translateY(-50%);
diff --git a/scripts/dev-website.sh b/scripts/dev-website.sh
index 284908b..b2b08ce 100755
--- a/scripts/dev-website.sh
+++ b/scripts/dev-website.sh
@@ -1,5 +1,7 @@
#!/bin/bash
+set -e
+
rm -rf pkg
wasm-pack build --target web --features wasm
diff --git a/src/lib.rs b/src/lib.rs
index f954c77..4ed5087 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,3 +1,5 @@
+#![feature(stmt_expr_attributes)]
+
mod diffs;
mod operation_transformation;
mod tokenizer;
@@ -5,9 +7,10 @@ mod utils;
pub use operation_transformation::{
CursorPosition, EditedText, TextWithCursors, reconcile, reconcile_with_cursors,
- reconcile_with_tokenizer,
+ reconcile_with_history, reconcile_with_tokenizer,
};
pub use tokenizer::{Tokenizer, token::Token, word_tokenizer::word_tokenizer};
+pub use utils::{history::History, side::Side};
#[cfg(feature = "wasm")]
pub mod wasm;
diff --git a/src/operation_transformation.rs b/src/operation_transformation.rs
index 777c2f0..7cd88d9 100644
--- a/src/operation_transformation.rs
+++ b/src/operation_transformation.rs
@@ -7,7 +7,10 @@ pub use cursor::{CursorPosition, TextWithCursors};
pub use edited_text::EditedText;
pub use operation::Operation;
-use crate::Tokenizer;
+use crate::{
+ Tokenizer,
+ utils::{history::History, side::Side},
+};
#[must_use]
pub fn reconcile(original: &str, left: &str, right: &str) -> String {
@@ -16,14 +19,22 @@ pub fn reconcile(original: &str, left: &str, right: &str) -> String {
.to_string()
}
+#[must_use]
+pub fn reconcile_with_history(original: &str, left: &str, right: &str) -> Vec<(History, String)> {
+ let left_operations = EditedText::from_strings(original, left.into(), Side::Left);
+ let right_operations = EditedText::from_strings(original, right.into(), Side::Right);
+
+ left_operations.merge(right_operations).apply_with_history()
+}
+
#[must_use]
pub fn reconcile_with_cursors<'a>(
original: &'a str,
left: TextWithCursors<'a>,
right: TextWithCursors<'a>,
) -> TextWithCursors<'static> {
- let left_operations = EditedText::from_strings(original, left);
- let right_operations = EditedText::from_strings(original, right);
+ let left_operations = EditedText::from_strings(original, left, Side::Left);
+ let right_operations = EditedText::from_strings(original, right, Side::Right);
let merged_operations = left_operations.merge(right_operations);
@@ -40,8 +51,10 @@ pub fn reconcile_with_tokenizer<'a, F, T>(
where
T: PartialEq + Clone + std::fmt::Debug,
{
- let left_operations = EditedText::from_strings_with_tokenizer(original, left, tokenizer);
- let right_operations = EditedText::from_strings_with_tokenizer(original, right, tokenizer);
+ let left_operations =
+ EditedText::from_strings_with_tokenizer(original, left, tokenizer, Side::Left);
+ let right_operations =
+ EditedText::from_strings_with_tokenizer(original, right, tokenizer, Side::Right);
let merged_operations = left_operations.merge(right_operations);
diff --git a/src/operation_transformation/edited_text.rs b/src/operation_transformation/edited_text.rs
index 4e9c7be..3e7f7be 100644
--- a/src/operation_transformation/edited_text.rs
+++ b/src/operation_transformation/edited_text.rs
@@ -8,7 +8,7 @@ use crate::{
cook_operations::cook_operations, elongate_operations::elongate_operations,
},
tokenizer::{Tokenizer, word_tokenizer::word_tokenizer},
- utils::{side::Side, string_builder::StringBuilder},
+ utils::{history::History, side::Side, string_builder::StringBuilder},
};
/// A text document and a sequence of operations that can be applied to the text
@@ -42,8 +42,8 @@ impl<'a> EditedText<'a, String> {
/// word tokenizer is used to tokenize the text which splits the text on
/// whitespaces.
#[must_use]
- pub fn from_strings(original: &'a str, updated: TextWithCursors<'a>) -> Self {
- Self::from_strings_with_tokenizer(original, updated, &word_tokenizer)
+ pub fn from_strings(original: &'a str, updated: TextWithCursors<'a>, side: Side) -> Self {
+ Self::from_strings_with_tokenizer(original, updated, &word_tokenizer, side)
}
}
@@ -60,6 +60,7 @@ where
original: &'a str,
updated: TextWithCursors<'a>,
tokenizer: &Tokenizer
,
+ side: Side,
) -> Self {
let original_tokens = (tokenizer)(original);
let updated_tokens = (tokenizer)(&updated.text);
@@ -68,7 +69,7 @@ where
Self::new(
original,
- cook_operations(elongate_operations(diff)).collect(),
+ cook_operations(elongate_operations(diff), side).collect(),
updated.cursors,
)
}
@@ -223,6 +224,39 @@ where
builder.build()
}
+
+ #[must_use]
+ pub fn apply_with_history(&self) -> Vec<(History, String)> {
+ let mut builder: StringBuilder<'_> = StringBuilder::new(self.text);
+
+ let mut history = Vec::with_capacity(self.operations.len());
+
+ for operation in &self.operations {
+ builder = operation.apply(builder);
+
+ match operation {
+ Operation::Equal { .. } => history.push((History::Unchanged, builder.take())),
+ Operation::Insert { side, .. } => match side {
+ Side::Left => history.push((History::AddedFromLeft, builder.take())),
+ Side::Right => history.push((History::AddedFromRight, builder.take())),
+ },
+ Operation::Delete {
+ deleted_character_count,
+ order,
+ side,
+ ..
+ } => {
+ let deleted = self.text[*order..*order + *deleted_character_count].to_string();
+ match side {
+ Side::Left => history.push((History::RemovedFromLeft, deleted)),
+ Side::Right => history.push((History::RemovedFromRight, deleted)),
+ }
+ }
+ }
+ }
+
+ history
+ }
}
#[cfg(test)]
@@ -237,7 +271,7 @@ mod tests {
let left = "hello world! How are you? Adam";
let right = "Hello, my friend! How are you doing? Albert";
- let operations = EditedText::from_strings(left, right.into());
+ let operations = EditedText::from_strings(left, right.into(), Side::Right);
insta::assert_debug_snapshot!(operations);
@@ -249,7 +283,7 @@ mod tests {
fn test_calculate_operations_with_no_diff() {
let text = "hello world!";
- let operations = EditedText::from_strings(text, text.into());
+ let operations = EditedText::from_strings(text, text.into(), Side::Right);
assert_debug_snapshot!(operations);
@@ -264,8 +298,8 @@ mod tests {
let right = "Hello world! How are you?";
let expected = "Hello world! How are you? I'm Andras.";
- let operations_1 = EditedText::from_strings(original, left.into());
- let operations_2 = EditedText::from_strings(original, right.into());
+ let operations_1 = EditedText::from_strings(original, left.into(), Side::Left);
+ let operations_2 = EditedText::from_strings(original, right.into(), Side::Right);
let operations = operations_1.merge(operations_2);
assert_eq!(operations.apply(), expected);
diff --git a/src/operation_transformation/operation.rs b/src/operation_transformation/operation.rs
index 6efaa91..ca2f128 100644
--- a/src/operation_transformation/operation.rs
+++ b/src/operation_transformation/operation.rs
@@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize};
use crate::{
Token,
utils::{
- find_longest_prefix_contained_within::find_longest_prefix_contained_within,
+ find_longest_prefix_contained_within::find_longest_prefix_contained_within, side::Side,
string_builder::StringBuilder,
},
};
@@ -27,11 +27,15 @@ where
},
Insert {
+ side: Side,
+
order: usize,
text: Vec>,
},
Delete {
+ side: Side,
+
order: usize,
deleted_character_count: usize,
@@ -68,14 +72,15 @@ where
}
/// Creates an insert operation with the given index and text.
- pub fn create_insert(order: usize, text: Vec>) -> Self {
- Operation::Insert { order, text }
+ pub fn create_insert(order: usize, text: Vec>, side: Side) -> Self {
+ Operation::Insert { side, order, text }
}
/// Creates a delete operation with the given index and number of
/// to-be-deleted characters.
- pub fn create_delete(order: usize, deleted_character_count: usize) -> Self {
+ pub fn create_delete(order: usize, deleted_character_count: usize, side: Side) -> Self {
Operation::Delete {
+ side,
order,
deleted_character_count,
@@ -84,8 +89,9 @@ where
}
}
- pub fn create_delete_with_text(order: usize, text: String) -> Self {
+ pub fn create_delete_with_text(order: usize, text: String, side: Side) -> Self {
Operation::Delete {
+ side,
order,
deleted_character_count: text.chars().count(),
@@ -200,7 +206,7 @@ where
match (operation, previous_operation) {
(
- Operation::Insert { order, text },
+ Operation::Insert { side, order, text },
Some(Operation::Insert {
text: previous_inserted_text,
..
@@ -212,11 +218,12 @@ where
let offset_in_tokens =
find_longest_prefix_contained_within(previous_inserted_text, &text);
- Operation::create_insert(order, text[offset_in_tokens..].to_vec())
+ Operation::create_insert(order, text[offset_in_tokens..].to_vec(), side)
}
(
Operation::Delete {
+ side,
order,
deleted_character_count,
@@ -240,19 +247,20 @@ where
#[cfg(debug_assertions)]
let updated_delete = deleted_text.as_ref().map_or_else(
- || Operation::create_delete(order + overlap, new_length),
+ || Operation::create_delete(order + overlap, new_length, side),
|text| {
Operation::create_delete_with_text(
order + overlap,
text.chars()
.skip(deleted_character_count - new_length)
.collect::(),
+ side,
)
},
);
#[cfg(not(debug_assertions))]
- let updated_delete = Operation::create_delete(order + overlap, new_length);
+ let updated_delete = Operation::create_delete(order + overlap, new_length, side);
updated_delete
}
@@ -334,6 +342,7 @@ where
#[cfg(debug_assertions)]
text,
+ ..
} => {
#[cfg(debug_assertions)]
write!(
@@ -349,7 +358,7 @@ where
Ok(())
}
- Operation::Insert { order, text } => {
+ Operation::Insert { order, text, .. } => {
write!(
f,
"",
@@ -365,6 +374,7 @@ where
#[cfg(debug_assertions)]
deleted_text,
+ ..
} => {
#[cfg(debug_assertions)]
write!(
@@ -404,7 +414,8 @@ mod tests {
#[test]
fn test_apply_delete_with_create() {
let builder = StringBuilder::new("hello world");
- let delete_operation = Operation::<()>::create_delete_with_text(0, "hello ".to_owned());
+ let delete_operation =
+ Operation::<()>::create_delete_with_text(0, "hello ".to_owned(), Side::Left);
let retain_operation = Operation::<()>::create_equal(6, 5);
let mut builder = delete_operation.apply(builder);
@@ -418,7 +429,7 @@ mod tests {
let builder = StringBuilder::new("hello");
let retain_operation = Operation::<()>::create_equal(0, 5);
- let insert_operation = Operation::create_insert(5, vec![" my friend".into()]);
+ let insert_operation = Operation::create_insert(5, vec![" my friend".into()], Side::Right);
let mut builder = retain_operation.apply(builder);
builder = insert_operation.apply(builder);
diff --git a/src/operation_transformation/utils/cook_operations.rs b/src/operation_transformation/utils/cook_operations.rs
index 7d5f85e..6ec3b43 100644
--- a/src/operation_transformation/utils/cook_operations.rs
+++ b/src/operation_transformation/utils/cook_operations.rs
@@ -1,8 +1,10 @@
-use crate::{diffs::raw_operation::RawOperation, operation_transformation::Operation};
+use crate::{
+ diffs::raw_operation::RawOperation, operation_transformation::Operation, utils::side::Side,
+};
/// Turn raw operations into ordered operations while keeping track of the
/// original token's indexes.
-pub fn cook_operations(raw_operations: I) -> impl Iterator- >
+pub fn cook_operations(raw_operations: I, side: Side) -> impl Iterator
- >
where
I: IntoIterator
- >,
T: PartialEq + Clone + std::fmt::Debug,
@@ -27,15 +29,18 @@ where
op
}
- RawOperation::Insert(tokens) => Operation::create_insert(original_text_index, tokens),
+ RawOperation::Insert(tokens) => {
+ Operation::create_insert(original_text_index, tokens, side)
+ }
RawOperation::Delete(..) => {
let op = if cfg!(debug_assertions) {
Operation::create_delete_with_text(
original_text_index,
raw_operation.get_original_text(),
+ side,
)
} else {
- Operation::create_delete(original_text_index, length)
+ Operation::create_delete(original_text_index, length, side)
};
original_text_index += length;
diff --git a/src/utils.rs b/src/utils.rs
index 91330ca..8d31e66 100644
--- a/src/utils.rs
+++ b/src/utils.rs
@@ -1,5 +1,6 @@
pub mod common_prefix_len;
pub mod common_suffix_len;
pub mod find_longest_prefix_contained_within;
+pub mod history;
pub mod side;
pub mod string_builder;
diff --git a/src/utils/history.rs b/src/utils/history.rs
new file mode 100644
index 0000000..91acd7a
--- /dev/null
+++ b/src/utils/history.rs
@@ -0,0 +1,15 @@
+#[cfg(feature = "serde")]
+use serde::{Deserialize, Serialize};
+#[cfg(feature = "wasm")]
+use wasm_bindgen::prelude::*;
+
+#[cfg_attr(feature = "wasm", wasm_bindgen)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum History {
+ Unchanged = "Unchanged",
+ AddedFromLeft = "AddedFromLeft",
+ AddedFromRight = "AddedFromRight",
+ RemovedFromLeft = "RemovedFromLeft",
+ RemovedFromRight = "RemovedFromRight",
+}
diff --git a/src/utils/side.rs b/src/utils/side.rs
index 825fa9e..54dba6f 100644
--- a/src/utils/side.rs
+++ b/src/utils/side.rs
@@ -1,5 +1,9 @@
use std::fmt::Display;
+#[cfg(feature = "serde")]
+use serde::{Deserialize, Serialize};
+
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Side {
Left,
diff --git a/src/utils/string_builder.rs b/src/utils/string_builder.rs
index af24d82..ee23fc6 100644
--- a/src/utils/string_builder.rs
+++ b/src/utils/string_builder.rs
@@ -35,7 +35,8 @@ impl StringBuilder<'_> {
self.original.nth(length - 1);
- if cfg!(debug_assertions) {
+ #[cfg(debug_assertions)]
+ {
self.remaining = self.remaining.chars().skip(length).collect();
}
}
@@ -44,20 +45,28 @@ impl StringBuilder<'_> {
pub fn retain(&mut self, length: usize) {
self.buffer.extend(self.original.by_ref().take(length));
- if cfg!(debug_assertions) {
+ #[cfg(debug_assertions)]
+ {
self.remaining = self.remaining.chars().skip(length).collect();
}
}
+ /// Returns the currently built buffer and clears it.
+ pub fn take(&mut self) -> String {
+ let result = self.buffer.clone();
+ self.buffer.clear();
+ result
+ }
+
/// Finish building the string after copying the remaining original string
/// since the last insertion or deletion.
pub fn build(self) -> String { self.buffer }
- #[cfg(debug_assertions)]
/// Get a slice of the remaining original string. The slice starts from
/// where the next delete/retain operation would start and is of length
/// `length`. The implementation is quite suboptimal but it's only used
/// for debugging.
+ #[cfg(debug_assertions)]
pub fn get_slice_from_remaining(&self, length: usize) -> String {
let result = self.remaining.chars().take(length).collect::();
diff --git a/src/wasm.rs b/src/wasm.rs
index 43f39f5..bf13f0a 100644
--- a/src/wasm.rs
+++ b/src/wasm.rs
@@ -1,2 +1,2 @@
-pub mod cursor;
pub mod lib;
+pub mod types;
diff --git a/src/wasm/lib.rs b/src/wasm/lib.rs
index 3188e1f..71f8afe 100644
--- a/src/wasm/lib.rs
+++ b/src/wasm/lib.rs
@@ -13,7 +13,7 @@ use core::str;
use wasm_bindgen::prelude::*;
-use crate::wasm::cursor::JsTextWithCursors;
+use crate::wasm::types::{JsTextWithCursors, JsTextWithHistory};
/// Merge two documents with a common parent. Relies on `reconcile::reconcile`
/// for texts and returns the right document as-is if either of the updated
@@ -58,6 +58,18 @@ pub fn merge_text(parent: &str, left: &str, right: &str) -> String {
crate::reconcile(parent, left, right)
}
+/// WASM wrapper around `crate::reconcile` for merging text.
+#[wasm_bindgen(js_name = mergeTextWithHistory)]
+#[must_use]
+pub fn merge_text_with_history(parent: &str, left: &str, right: &str) -> Vec {
+ set_panic_hook();
+
+ crate::reconcile_with_history(parent, left, right)
+ .into_iter()
+ .map(Into::into)
+ .collect()
+}
+
/// WASM wrapper around `reconcile::reconcile_with_cursors` for merging text.
#[wasm_bindgen(js_name = mergeTextWithCursors)]
#[must_use]
diff --git a/src/wasm/cursor.rs b/src/wasm/types.rs
similarity index 81%
rename from src/wasm/cursor.rs
rename to src/wasm/types.rs
index 9e72095..d2aa486 100644
--- a/src/wasm/cursor.rs
+++ b/src/wasm/types.rs
@@ -1,5 +1,7 @@
use wasm_bindgen::prelude::*;
+use crate::History;
+
/// Wrapper type to expose `TextWithCursors` to JS.
#[wasm_bindgen]
#[derive(Debug, Clone, PartialEq)]
@@ -86,3 +88,24 @@ impl From for JsCursorPosition {
}
}
}
+
+/// Wrapper type to expose `(History, String)` to JS.
+#[wasm_bindgen]
+#[derive(Debug, Clone, PartialEq)]
+pub struct JsTextWithHistory {
+ history: History,
+ text: String,
+}
+
+impl From<(History, String)> for JsTextWithHistory {
+ fn from((history, text): (History, String)) -> Self { JsTextWithHistory { history, text } }
+}
+
+#[wasm_bindgen]
+impl JsTextWithHistory {
+ #[must_use]
+ pub fn history(&self) -> History { self.history }
+
+ #[must_use]
+ pub fn text(&self) -> String { self.text.clone() }
+}
diff --git a/tests/examples/deletes.yml b/tests/examples/deletes.yml
index 3476e95..9aece4c 100644
--- a/tests/examples/deletes.yml
+++ b/tests/examples/deletes.yml
@@ -25,10 +25,10 @@ right: long with big and small
expected: long small
---
-parent: long run of text where one barely has no changes but has cursors
-left: long| run of tex|t where one barely has no |changes but has |cursors
-right: long run one barely has no changes cursors
-expected: long| ru|n one barely has no |changes |cursors
+parent: long run of text where one barely has changes but has cursors
+left: long| run of tex|t where one barely has |changes but has |cursors
+right: long run one barely has changes cursors
+expected: long| ru|n one barely has |changes |cursors
---
parent: long text where the cursor has to be clamped after delete
diff --git a/tests/web.rs b/tests/web.rs
index a992051..80da0f7 100644
--- a/tests/web.rs
+++ b/tests/web.rs
@@ -1,8 +1,8 @@
#![cfg(feature = "wasm")]
use reconcile::wasm::{
- cursor::{JsCursorPosition, JsTextWithCursors},
lib::{is_binary, is_file_type_mergable, merge, merge_text, merge_text_with_cursors},
+ types::{JsCursorPosition, JsTextWithCursors},
};
use wasm_bindgen_test::*;