From 779579d38fcd04a82d4577b79da5b775b80b0f03 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Sun, 22 Jun 2025 20:49:11 +0100 Subject: [PATCH] Add mergeTextWithHistory function --- examples/website/index.html | 55 ++++++++++--- examples/website/script.js | 39 ++++++--- examples/website/style.css | 79 ++++++++++--------- scripts/dev-website.sh | 2 + src/lib.rs | 5 +- src/operation_transformation.rs | 23 ++++-- src/operation_transformation/edited_text.rs | 50 ++++++++++-- src/operation_transformation/operation.rs | 35 +++++--- .../utils/cook_operations.rs | 13 ++- src/utils.rs | 1 + src/utils/history.rs | 15 ++++ src/utils/side.rs | 4 + src/utils/string_builder.rs | 15 +++- src/wasm.rs | 2 +- src/wasm/lib.rs | 14 +++- src/wasm/{cursor.rs => types.rs} | 23 ++++++ tests/examples/deletes.yml | 8 +- tests/web.rs | 2 +- 18 files changed, 285 insertions(+), 100 deletions(-) create mode 100644 src/utils/history.rs rename src/wasm/{cursor.rs => types.rs} (81%) diff --git a/examples/website/index.html b/examples/website/index.html index f4e8ed6..fdfc1b2 100644 --- a/examples/website/index.html +++ b/examples/website/index.html @@ -29,26 +29,53 @@
-
+
-
- +
+
-
- +
+
- - -
- - +
+ +
@@ -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::*;