From 4c77a0360eee856f45d96cc3e64659ce86f71ad6 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Sun, 16 Nov 2025 12:29:30 +0000 Subject: [PATCH] Don't depend on serde for wasm --- Cargo.lock | 20 --- Cargo.toml | 5 +- reconcile-js/src/index.ts | 9 +- src/lib.rs | 24 +-- src/operation_transformation.rs | 1 - src/operation_transformation/edited_text.rs | 136 ++++++++++---- src/types.rs | 2 +- src/types/change_set.rs | 189 -------------------- src/types/number_or_string.rs | 74 ++++++++ src/utils.rs | 1 - src/utils/string_or_nothing.rs | 26 --- src/wasm.rs | 49 +++-- tests/test.rs | 23 ++- tests/wasm.rs | 6 +- 14 files changed, 251 insertions(+), 314 deletions(-) delete mode 100644 src/types/change_set.rs create mode 100644 src/types/number_or_string.rs delete mode 100644 src/utils/string_or_nothing.rs diff --git a/Cargo.lock b/Cargo.lock index b739f33..5e187e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -124,12 +124,6 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" -[[package]] -name = "memchr" -version = "2.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" - [[package]] name = "memory_units" version = "0.4.0" @@ -188,7 +182,6 @@ dependencies = [ "insta", "pretty_assertions", "serde", - "serde_json", "serde_yaml", "test-case", "wasm-bindgen", @@ -247,19 +240,6 @@ dependencies = [ "syn", ] -[[package]] -name = "serde_json" -version = "1.0.145" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" -dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", - "serde_core", -] - [[package]] name = "serde_yaml" version = "0.9.34+deprecated" diff --git a/Cargo.toml b/Cargo.toml index f960633..74820a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,6 @@ path = "examples/merge-file.rs" serde = { version = "1.0.219", optional = true, features = ["derive"] } wasm-bindgen = { version = "0.2.99", optional = true } -serde_json = { version = "1.0.145", optional = true } # The `console_error_panic_hook` crate provides better debugging of panics by # logging them with `console.error`. This is great for development, but requires @@ -37,9 +36,9 @@ wee_alloc = { version = "0.4.2", optional = true } [features] default = [] serde = [ "dep:serde" ] -wasm = [ "dep:wasm-bindgen", "dep:wee_alloc", "dep:serde_json", "serde" ] +wasm = [ "dep:wasm-bindgen", "dep:wee_alloc" ] console_error_panic_hook = [ "dep:console_error_panic_hook" ] -all = [ "wasm", "console_error_panic_hook" ] +all = [ "wasm", "console_error_panic_hook", "serde" ] [dev-dependencies] insta = "1.43.2" diff --git a/reconcile-js/src/index.ts b/reconcile-js/src/index.ts index aa07532..14da817 100644 --- a/reconcile-js/src/index.ts +++ b/reconcile-js/src/index.ts @@ -182,22 +182,21 @@ export function reconcile( /** * Generates a compact diff representation between an original and changed text. * - * These can be parsed and unpacked using Rust crate's EditedText::from_change_set. + * These can be parsed and unpacked using Rust crate's EditedText::from_changes. * * This function computes the differences between two versions of text and returns - * a compact string representation of those changes. The returned format is - * serialised JSON. + * a compact representation of those changes. * * @param original - The original/base version of the text * @param changed - The modified version of the text (either string or TextWithCursors with cursor positions) * @param tokenizer - The tokenisation strategy, which is the same as used in `reconcile`. - * @returns A compact string representation of the diff between original and changed text + * @returns An array representing the compact diff, with inserts as strings and deletes as negative integers. */ export function getCompactDiff( original: string, changed: string | TextWithOptionalCursors, tokenizer: BuiltinTokenizer = 'Word' -): string { +): Array { init(); if (!BUILTIN_TOKENIZERS.includes(tokenizer)) { diff --git a/src/lib.rs b/src/lib.rs index a760a95..990febe 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -157,6 +157,8 @@ //! original text, making the size only depends on the changes made. //! //! ```rust +//! # #[cfg(feature = "serde")] +//! # { //! use reconcile_text::{EditedText, BuiltinTokenizer}; //! use serde_yaml; //! use pretty_assertions::assert_eq; @@ -170,20 +172,18 @@ //! &changes.into() //! ); //! -//! let serialized = serde_yaml::to_string(&result.to_change_set()).unwrap(); +//! let serialized = serde_yaml::to_string(&result.to_changes()).unwrap(); //! assert_eq!( //! serialized, //! concat!( -//! "operations:\n", //! "- 15\n", //! "- -6\n", -//! "- ' easy with reconcile!'\n", -//! "cursors: []\n" +//! "- ' easy with reconcile!'\n" //! ) //! ); //! //! let deserialized = serde_yaml::from_str(&serialized).unwrap(); -//! let reconstructed = EditedText::from_change_set( +//! let reconstructed = EditedText::from_changes( //! original, //! deserialized, //! &*BuiltinTokenizer::Word @@ -192,13 +192,17 @@ //! reconstructed.apply().text(), //! "Merging text is easy with reconcile!" //! ); +//! # } //! ``` //! //! ## Error handling //! //! The library is designed to be robust and will always produce a result, even -//! in edge cases. However, be aware that extremely large diffs may have -//! performance implications. +//! for edge cases. +//! +//! ## Performance +//! +//! Be aware that extremely large diffs may have performance implications. //! //! ## Algorithm overview //! @@ -211,11 +215,11 @@ mod tokenizer; mod types; mod utils; -pub use operation_transformation::{ChangeSet, EditedText, reconcile}; +pub use operation_transformation::{EditedText, reconcile}; pub use tokenizer::{BuiltinTokenizer, Tokenizer, token::Token}; pub use types::{ - cursor_position::CursorPosition, history::History, side::Side, - span_with_history::SpanWithHistory, text_with_cursors::TextWithCursors, + cursor_position::CursorPosition, history::History, number_or_string::NumberOrString, + side::Side, span_with_history::SpanWithHistory, text_with_cursors::TextWithCursors, }; #[cfg(feature = "wasm")] diff --git a/src/operation_transformation.rs b/src/operation_transformation.rs index 01eb9b0..85e3995 100644 --- a/src/operation_transformation.rs +++ b/src/operation_transformation.rs @@ -5,7 +5,6 @@ use std::fmt::Debug; pub use edited_text::EditedText; pub use operation::Operation; -pub use transport::ChangeSet; use crate::{Tokenizer, types::text_with_cursors::TextWithCursors}; diff --git a/src/operation_transformation/edited_text.rs b/src/operation_transformation/edited_text.rs index a6465fe..0d3a4c1 100644 --- a/src/operation_transformation/edited_text.rs +++ b/src/operation_transformation/edited_text.rs @@ -4,15 +4,17 @@ use std::{fmt::Debug, vec}; use serde::{Deserialize, Serialize}; use crate::{ - BuiltinTokenizer, ChangeSet, CursorPosition, TextWithCursors, + BuiltinTokenizer, CursorPosition, TextWithCursors, operation_transformation::{ Operation, - transport::SimpleOperation, utils::{cook_operations::cook_operations, elongate_operations::elongate_operations}, }, raw_operation::RawOperation, tokenizer::Tokenizer, - types::{history::History, side::Side, span_with_history::SpanWithHistory}, + types::{ + history::History, number_or_string::NumberOrString, side::Side, + span_with_history::SpanWithHistory, + }, utils::string_builder::StringBuilder, }; @@ -345,34 +347,105 @@ where history } - /// Serialize the `EditedText` as a `ChangeSet`, which contains only - /// the operations and cursor positions, but without the original text. - /// This is useful for sending changes over the network if there's - /// a clear consensus on the original text. + /// Convert the `EditedText` into a terse representation ready for + /// serialization. The result omits cursor positions and the original text. + /// This is useful for sending text diffs over the network if there's a + /// clear consensus on the original text. + /// + /// Inserts are represented as strings, deletes as negative integers, + /// and equal spans as positive integers. #[must_use] - pub fn to_change_set(&self) -> ChangeSet { - ChangeSet::new( - SimpleOperation::from_operations(&self.operations), - self.cursors.clone(), - ) + pub fn to_changes(&self) -> Vec { + let mut result: Vec = Vec::with_capacity(self.operations.len()); + let mut previous_equal: Option = None; + + for operation in &self.operations { + match operation { + Operation::Equal { length, .. } => { + if let Some(prev_length) = previous_equal { + previous_equal = Some(prev_length + *length); + } else { + previous_equal = Some(*length); + } + } + + Operation::Insert { text, .. } => { + if let Some(prev_length) = previous_equal { + result.push(NumberOrString::Number(prev_length as i64)); + previous_equal = None; + } + + let text: String = text + .iter() + .map(super::super::tokenizer::token::Token::original) + .collect(); + result.push(NumberOrString::Text(text)); + } + + Operation::Delete { + deleted_character_count, + .. + } => { + if let Some(prev_length) = previous_equal { + result.push(NumberOrString::Number(prev_length as i64)); + previous_equal = None; + } + + result.push(NumberOrString::Number(-(*deleted_character_count as i64))); + } + } + } + + if let Some(prev_length) = previous_equal { + result.push(NumberOrString::Number(prev_length as i64)); + } + + result } - /// Deserialize an `EditedText` from a `ChangeSet` and the original text. - /// This is useful for reconstructing the `EditedText` on the receiving - /// end after sending only the `ChangeSet` over the network. + /// Deserialize an `EditedText` from a change list and the original text. #[must_use] - pub fn from_change_set( - text: &'a str, - change_set: ChangeSet, + pub fn from_changes( + original_text: &'a str, + simple_operations: Vec, tokenizer: &Tokenizer, ) -> EditedText<'a, T> { - let operations = SimpleOperation::to_operations(change_set.operations, text, tokenizer); + let mut operations: Vec> = Vec::with_capacity(simple_operations.len()); + let mut order = 0; + + for simple_operation in simple_operations { + match simple_operation { + NumberOrString::Number(length) => { + if length >= 0 { + let length = length as usize; + let original_characters: String = + original_text.chars().skip(order).take(length).collect(); + + let original_tokens = tokenizer(&original_characters); + for token in original_tokens { + operations + .push(Operation::create_equal(order, token.get_original_length())); + order += token.get_original_length(); + } + } else { + let length = -length as usize; + operations.push(Operation::create_delete(order, length)); + order += length; + } + } + NumberOrString::Text(text) => { + let tokens = tokenizer(&text); + operations.push(Operation::create_insert(order, tokens)); + } + } + } + let operation_count = operations.len(); EditedText::new( - text, + original_text, operations, vec![Side::Left; operation_count], - change_set.cursors, + vec![], ) } } @@ -423,34 +496,29 @@ mod tests { assert_eq!(operations.apply().text(), expected); } + #[cfg(feature = "serde")] #[test] - fn test_change_set_deserialisation() { + fn test_changes_deserialisation() { let original = "Merging text is hard!"; let changes = "Merging text is easy with reconcile!"; let result = EditedText::from_strings(original, &changes.into()); - let serialized = serde_yaml::to_string(&result.to_change_set()).unwrap(); - - let expected = concat!( - "operations:\n", - "- 15\n", - "- -6\n", - "- ' easy with reconcile!'\n", - "cursors: []\n" - ); + let serialized = serde_yaml::to_string(&result.to_changes()).unwrap(); + let expected = concat!("- 15\n", "- -6\n", "- ' easy with reconcile!'\n",); assert_eq!(serialized, expected); } + #[cfg(feature = "serde")] #[test] - fn test_change_set_serialization() { + fn test_changes_serialization() { let original = "The quick brown fox jumps over the lazy dog."; let updated = "The quick red fox jumped over the very lazy dog!"; let edited_text = EditedText::from_strings(original, &updated.into()); - let change_set = edited_text.to_change_set(); + let changes = edited_text.to_changes(); let deserialized_edited_text = - EditedText::from_change_set(original, change_set, &*BuiltinTokenizer::Word); + EditedText::from_changes(original, changes, &*BuiltinTokenizer::Word); assert_eq!(deserialized_edited_text.apply().text(), updated); } diff --git a/src/types.rs b/src/types.rs index 8980b0e..b5c2f7c 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,6 +1,6 @@ -pub mod change_set; pub mod cursor_position; pub mod history; +pub mod number_or_string; pub mod side; pub mod span_with_history; pub mod text_with_cursors; diff --git a/src/types/change_set.rs b/src/types/change_set.rs deleted file mode 100644 index 1d56697..0000000 --- a/src/types/change_set.rs +++ /dev/null @@ -1,189 +0,0 @@ -use std::fmt::Debug; - -#[cfg(feature = "serde")] -use serde::{ - Deserialize, Serialize, - de::{self, Deserializer, Visitor}, - ser::Serializer, -}; -#[cfg(feature = "wasm")] -use wasm_bindgen::prelude::*; - -use crate::{Tokenizer, operation_transformation::Operation}; - -/// A serializable representation of the changes made to a text document -/// without the original text. -#[derive(Clone, PartialEq, Eq, Debug)] -enum SimpleOperation { - Equal { length: usize }, - Insert { text: String }, - Delete { length: usize }, -} - -impl SimpleOperation { - pub fn from_operations(operation: &Vec>) -> Vec - where - T: PartialEq + Clone + Debug, - { - let mut result: Vec = Vec::with_capacity(operation.len()); - let mut previous_equal: Option = None; - - for operation in operation { - match operation { - Operation::Equal { length, .. } => { - if let Some(prev_length) = previous_equal { - previous_equal = Some(prev_length + *length); - } else { - previous_equal = Some(*length); - } - } - - Operation::Insert { text, .. } => { - if let Some(prev_length) = previous_equal { - result.push(SimpleOperation::Equal { - length: prev_length, - }); - previous_equal = None; - } - - let text: String = text - .iter() - .map(super::super::tokenizer::token::Token::original) - .collect(); - result.push(SimpleOperation::Insert { text }); - } - - Operation::Delete { - deleted_character_count, - .. - } => { - if let Some(prev_length) = previous_equal { - result.push(SimpleOperation::Equal { - length: prev_length, - }); - previous_equal = None; - } - - result.push(SimpleOperation::Delete { - length: *deleted_character_count, - }); - } - } - } - - if let Some(prev_length) = previous_equal { - result.push(SimpleOperation::Equal { - length: prev_length, - }); - } - - result - } - - // This is similar to `crate::operation_transformation::utils::cook_operations` - pub fn to_operations( - simple_operations: Vec, - original_text: &str, - tokenizer: &Tokenizer, - ) -> Vec> - where - T: PartialEq + Clone + Debug, - { - let mut operations: Vec> = Vec::with_capacity(simple_operations.len()); - let mut order = 0; - - for simple_operation in simple_operations { - match simple_operation { - SimpleOperation::Equal { length } => { - let original_characters: String = - original_text.chars().skip(order).take(length).collect(); - - let original_tokens = tokenizer(&original_characters); - for token in original_tokens { - operations - .push(Operation::create_equal(order, token.get_original_length())); - order += token.get_original_length(); - } - } - - SimpleOperation::Insert { text } => { - let tokens = tokenizer(&text); - operations.push(Operation::create_insert(order, tokens)); - } - - SimpleOperation::Delete { length } => { - operations.push(Operation::create_delete(order, length)); - order += length; - } - } - } - - operations - } -} - -#[cfg(feature = "serde")] -impl Serialize for SimpleOperation { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - // neat idea from https://github.com/spebern/operational-transform-rs/blob/9faa17f0a2b282ac2e09dbb2d29fdaf2ae0bbb4a/operational-transform/src/serde.rs#L14 - match self { - SimpleOperation::Equal { length } => serializer.serialize_u64(*length as u64), - SimpleOperation::Insert { text } => serializer.serialize_str(text), - SimpleOperation::Delete { length } => { - serializer.serialize_i64(-(i64::try_from(*length).unwrap_or(i64::MAX))) - } - } - } -} - -#[cfg(feature = "serde")] -impl<'de> Deserialize<'de> for SimpleOperation { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - use std::fmt; - - struct OperationVisitor; - - impl Visitor<'_> for OperationVisitor { - type Value = SimpleOperation; - - fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str("an integer between -2^63 and 2^64-1 or a string") - } - - fn visit_u64(self, value: u64) -> Result - where - E: de::Error, - { - Ok(SimpleOperation::Equal { - length: usize::try_from(value).unwrap_or(usize::MAX), - }) - } - - fn visit_i64(self, value: i64) -> Result - where - E: de::Error, - { - Ok(SimpleOperation::Delete { - length: usize::try_from(-value).unwrap_or(usize::MAX), - }) - } - - fn visit_str(self, value: &str) -> Result - where - E: de::Error, - { - Ok(SimpleOperation::Insert { - text: value.to_owned(), - }) - } - } - - deserializer.deserialize_any(OperationVisitor) - } -} diff --git a/src/types/number_or_string.rs b/src/types/number_or_string.rs new file mode 100644 index 0000000..7272a60 --- /dev/null +++ b/src/types/number_or_string.rs @@ -0,0 +1,74 @@ +use std::fmt::Debug; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +#[cfg(feature = "wasm")] +use wasm_bindgen::prelude::*; + +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(untagged))] +#[derive(Debug, Clone, PartialEq)] +pub enum NumberOrString { + Number(i64), + Text(String), +} + +#[cfg(feature = "wasm")] +impl TryFrom for NumberOrString { + type Error = DeserialisationError; + + fn try_from(value: JsValue) -> Result { + if let Ok(num) = value.clone().try_into() { + return Ok(NumberOrString::Number(num)); + } + + if let Ok(text) = value.try_into() { + return Ok(NumberOrString::Text(text)); + } + + Err(DeserialisationError::new( + "Could not parse JsValue as either number or string", + )) + } +} + +#[cfg(feature = "wasm")] +impl From for JsValue { + fn from(value: NumberOrString) -> Self { + match value { + NumberOrString::Number(num) => JsValue::from(num), + NumberOrString::Text(text) => JsValue::from(text), + } + } +} + +/// Error type for deserialisation failures +#[cfg(feature = "wasm")] +#[derive(Debug, Clone)] +pub struct DeserialisationError { + pub message: String, +} + +#[cfg(feature = "wasm")] +impl DeserialisationError { + pub fn new(message: impl Into) -> Self { + Self { + message: message.into(), + } + } +} + +#[cfg(feature = "wasm")] +impl std::fmt::Display for DeserialisationError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Deserialisation error: {}", self.message) + } +} + +#[cfg(feature = "wasm")] +impl std::error::Error for DeserialisationError {} + +#[cfg(feature = "wasm")] +impl From for JsValue { + fn from(error: DeserialisationError) -> Self { JsValue::from_str(&error.message) } +} diff --git a/src/utils.rs b/src/utils.rs index e6966c6..2e05a70 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -3,4 +3,3 @@ pub mod common_suffix_len; pub mod find_longest_prefix_contained_within; pub mod myers_diff; pub mod string_builder; -pub mod string_or_nothing; diff --git a/src/utils/string_or_nothing.rs b/src/utils/string_or_nothing.rs deleted file mode 100644 index 1ca7d2b..0000000 --- a/src/utils/string_or_nothing.rs +++ /dev/null @@ -1,26 +0,0 @@ -/// Determine if the given data is a binary or a text file's content. -/// -/// Returns the UTF8 parsed string if it's a text, or `None` if it's likely -/// binary. -#[must_use] -pub fn string_or_nothing(data: &[u8]) -> Option { - if data.contains(&0) { - // Even though the NUL character is valid in UTF-8, it's highly suspicious in - // human-readable text. - return None; - } - - std::str::from_utf8(data).map(|s| s.to_string()).ok() -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_string_or_nothing() { - assert_eq!(string_or_nothing(&[0, 159, 146, 150]), None); - assert_eq!(string_or_nothing(&[0, 12]), None); - assert_eq!(string_or_nothing(b"hello"), Some("hello".into())); - } -} diff --git a/src/wasm.rs b/src/wasm.rs index e2e83f3..54d48e2 100644 --- a/src/wasm.rs +++ b/src/wasm.rs @@ -3,10 +3,7 @@ use core::str; use wasm_bindgen::prelude::*; -use crate::{ - BuiltinTokenizer, CursorPosition, SpanWithHistory, TextWithCursors, - utils::string_or_nothing::string_or_nothing, -}; +use crate::{BuiltinTokenizer, CursorPosition, SpanWithHistory, TextWithCursors}; #[global_allocator] static ALLOC: wee_alloc::WeeAlloc<'_> = wee_alloc::WeeAlloc::INIT; @@ -81,23 +78,22 @@ pub fn generic_reconcile( } } -/// WASM wrapper around getting a compact diff representation as a JSON string -/// -/// # Panics -/// -/// If serialization to JSON fails which should not happen +/// WASM wrapper around getting a compact diff representation of two texts as a +/// list of numbers and strings. #[wasm_bindgen(js_name = getCompactDiff)] #[must_use] pub fn get_compact_diff( parent: &str, changed: &TextWithCursors, tokenizer: BuiltinTokenizer, -) -> String { +) -> Vec { set_panic_hook(); let edited_text = crate::EditedText::from_strings_with_tokenizer(parent, changed, &*tokenizer); - let change_set = edited_text.to_change_set(); - - serde_json::to_string(&change_set).expect("Failed to serialize change set") + edited_text + .to_changes() + .into_iter() + .map(std::convert::Into::into) + .collect() } fn set_panic_hook() { @@ -125,3 +121,30 @@ impl TextWithCursorsAndHistory { #[must_use] pub fn history(&self) -> Vec { self.history.clone() } } + +/// Returns the UTF8 parsed string if it's a text, or `None` if it's likely +/// binary. +#[must_use] +pub fn string_or_nothing(data: &[u8]) -> Option { + if data.contains(&0) { + // Even though the NUL character is valid in UTF-8, it's highly suspicious in + // human-readable text. + return None; + } + + std::str::from_utf8(data) + .map(std::borrow::ToOwned::to_owned) + .ok() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_string_or_nothing() { + assert_eq!(string_or_nothing(&[0, 159, 146, 150]), None); + assert_eq!(string_or_nothing(&[0, 12]), None); + assert_eq!(string_or_nothing(b"hello"), Some("hello".into())); + } +} diff --git a/tests/test.rs b/tests/test.rs index e8fae7d..63cdfa2 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -34,8 +34,9 @@ fn test_document_one_way_with_cursors() { } } +#[cfg(feature = "serde")] #[test] -fn test_document_one_way_with_cursors_and_serialisation() { +fn test_document_one_way_with_serialisation() { for doc in &get_all_documents() { let parent = doc.parent(); let left_operations = @@ -47,19 +48,23 @@ fn test_document_one_way_with_cursors_and_serialisation() { ); let serialised_left = - serde_yaml::from_str(&serde_yaml::to_string(&left_operations.to_change_set()).unwrap()) + serde_yaml::from_str(&serde_yaml::to_string(&left_operations.to_changes()).unwrap()) + .unwrap(); + let serialised_right = + serde_yaml::from_str(&serde_yaml::to_string(&right_operations.to_changes()).unwrap()) .unwrap(); - let serialised_right = serde_yaml::from_str( - &serde_yaml::to_string(&right_operations.to_change_set()).unwrap(), - ) - .unwrap(); let restored_left_operations = - EditedText::from_change_set(&parent, serialised_left, &*BuiltinTokenizer::Word); + EditedText::from_changes(&parent, serialised_left, &*BuiltinTokenizer::Word); let restored_right_operations = - EditedText::from_change_set(&parent, serialised_right, &*BuiltinTokenizer::Word); + EditedText::from_changes(&parent, serialised_right, &*BuiltinTokenizer::Word); - doc.assert_eq(&restored_left_operations.merge(restored_right_operations)); + doc.assert_eq_without_cursors( + &restored_left_operations + .merge(restored_right_operations) + .apply() + .text(), + ); } } diff --git a/tests/wasm.rs b/tests/wasm.rs index 03a0f1a..652b516 100644 --- a/tests/wasm.rs +++ b/tests/wasm.rs @@ -55,10 +55,12 @@ fn test_merge_binary() { ); } -#[wasm_bindgen_test(unsupported = test)] +#[wasm_bindgen_test] // JsValue isn't supported outside of wasm fn test_get_compact_diff() { let parent = "hello "; let changed = "world"; let result = get_compact_diff(parent, &changed.into(), BuiltinTokenizer::Word); - assert_eq!(result, "{\"operations\":[-6,\"world\"],\"cursors\":[]}"); + assert_eq!(result.len(), 2); + assert_eq!(result[0].as_f64().unwrap(), -6.0); + assert_eq!(result[1].as_string().unwrap(), "world"); }