Expose undiff to JS

This commit is contained in:
Andras Schmelczer 2025-11-16 15:02:31 +00:00
commit ee6277c13f
5 changed files with 64 additions and 21 deletions

View file

@ -4,7 +4,8 @@ import {
TextWithCursors as wasmTextWithCursors, TextWithCursors as wasmTextWithCursors,
SpanWithHistory as wasmSpanWithHistory, SpanWithHistory as wasmSpanWithHistory,
reconcileWithHistory as wasmReconcileWithHistory, reconcileWithHistory as wasmReconcileWithHistory,
getCompactDiff as wasmGetCompactDiff, diff as wasmDiff,
undiff as wasmUndiff,
initSync, initSync,
} from 'reconcile-text'; } from 'reconcile-text';
@ -182,7 +183,8 @@ export function reconcile(
/** /**
* Generates a compact diff representation between an original and changed text. * Generates a compact diff representation between an original and changed text.
* *
* These can be parsed and unpacked using Rust crate's EditedText::from_changes. * These can be parsed and unpacked using the `undiff` function or the Rust crate's EditedText::from_diff.
* Cursor positions are omitted from the diff result.
* *
* This function computes the differences between two versions of text and returns * This function computes the differences between two versions of text and returns
* a compact representation of those changes. * a compact representation of those changes.
@ -192,7 +194,7 @@ export function reconcile(
* @param tokenizer - The tokenisation strategy, which is the same as used in `reconcile`. * @param tokenizer - The tokenisation strategy, which is the same as used in `reconcile`.
* @returns An array representing the compact diff, with inserts as strings and deletes as negative integers. * @returns An array representing the compact diff, with inserts as strings and deletes as negative integers.
*/ */
export function getCompactDiff( export function diff(
original: string, original: string,
changed: string | TextWithOptionalCursors, changed: string | TextWithOptionalCursors,
tokenizer: BuiltinTokenizer = 'Word' tokenizer: BuiltinTokenizer = 'Word'
@ -205,13 +207,38 @@ export function getCompactDiff(
const changedWasm = toWasmTextWithCursors(changed); const changedWasm = toWasmTextWithCursors(changed);
const result = wasmGetCompactDiff(original, changedWasm, tokenizer); const result = wasmDiff(original, changedWasm, tokenizer);
changedWasm.free(); changedWasm.free();
return result; return result;
} }
/**
* Applies a compact diff to an original text to reconstruct the changed version.
*
* This function takes an original text and a compact diff representation (as produced
* by the `diff` function) and reconstructs the modified text.
*
* @param original - The original/base version of the text
* @param diff - The compact diff array representing changes (inserts as strings, deletes as negative integers)
* @param tokenizer - The tokenisation strategy, which is the same as used in `reconcile`.
* @returns The reconstructed changed text as a string.
*/
export function undiff(
original: string,
diff: Array<number | string>,
tokenizer: BuiltinTokenizer = 'Word'
): string {
init();
if (!BUILTIN_TOKENIZERS.includes(tokenizer)) {
throw new Error(UNSUPPORTED_TOKENIZER_ERROR);
}
return wasmUndiff(original, diff, tokenizer);
}
/** /**
* Merges three versions of text and returns detailed provenance information. * Merges three versions of text and returns detailed provenance information.
* *

View file

@ -172,7 +172,7 @@
//! &changes.into() //! &changes.into()
//! ); //! );
//! //!
//! let serialized = serde_yaml::to_string(&result.to_changes()).unwrap(); //! let serialized = serde_yaml::to_string(&result.to_diff()).unwrap();
//! assert_eq!( //! assert_eq!(
//! serialized, //! serialized,
//! concat!( //! concat!(
@ -183,7 +183,7 @@
//! ); //! );
//! //!
//! let deserialized = serde_yaml::from_str(&serialized).unwrap(); //! let deserialized = serde_yaml::from_str(&serialized).unwrap();
//! let reconstructed = EditedText::from_changes( //! let reconstructed = EditedText::from_diff(
//! original, //! original,
//! deserialized, //! deserialized,
//! &*BuiltinTokenizer::Word //! &*BuiltinTokenizer::Word

View file

@ -3,7 +3,7 @@ use core::str;
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
use crate::{BuiltinTokenizer, CursorPosition, SpanWithHistory, TextWithCursors}; use crate::{BuiltinTokenizer, CursorPosition, EditedText, SpanWithHistory, TextWithCursors};
#[global_allocator] #[global_allocator]
static ALLOC: wee_alloc::WeeAlloc<'_> = wee_alloc::WeeAlloc::INIT; static ALLOC: wee_alloc::WeeAlloc<'_> = wee_alloc::WeeAlloc::INIT;
@ -32,6 +32,7 @@ pub fn reconcile_with_history(
tokenizer: BuiltinTokenizer, tokenizer: BuiltinTokenizer,
) -> TextWithCursorsAndHistory { ) -> TextWithCursorsAndHistory {
set_panic_hook(); set_panic_hook();
let reconciled = crate::reconcile(parent, left, right, &*tokenizer); let reconciled = crate::reconcile(parent, left, right, &*tokenizer);
let text_with_cursors = reconciled.apply(); let text_with_cursors = reconciled.apply();
@ -80,22 +81,37 @@ pub fn generic_reconcile(
/// WASM wrapper around getting a compact diff representation of two texts as a /// WASM wrapper around getting a compact diff representation of two texts as a
/// list of numbers and strings. /// list of numbers and strings.
#[wasm_bindgen(js_name = getCompactDiff)] #[wasm_bindgen(js_name = diff)]
#[must_use] #[must_use]
pub fn get_compact_diff( pub fn diff(parent: &str, changed: &TextWithCursors, tokenizer: BuiltinTokenizer) -> Vec<JsValue> {
parent: &str,
changed: &TextWithCursors,
tokenizer: BuiltinTokenizer,
) -> Vec<JsValue> {
set_panic_hook(); set_panic_hook();
let edited_text = crate::EditedText::from_strings_with_tokenizer(parent, changed, &*tokenizer);
let edited_text = EditedText::from_strings_with_tokenizer(parent, changed, &*tokenizer);
edited_text edited_text
.to_changes() .to_diff()
.into_iter() .into_iter()
.map(std::convert::Into::into) .map(std::convert::Into::into)
.collect() .collect()
} }
/// Inverse of `diff`, applies a compact diff representation to a parent text
#[wasm_bindgen(js_name = undiff)]
#[must_use]
pub fn undiff(parent: &str, diff: Vec<JsValue>, tokenizer: BuiltinTokenizer) -> String {
set_panic_hook();
EditedText::from_diff(
parent,
diff.into_iter()
.map(|js_value| js_value.try_into())
.collect::<Result<_, _>>()
.expect("Invalid diff format"),
&*tokenizer,
)
.apply()
.text()
}
fn set_panic_hook() { fn set_panic_hook() {
// https://github.com/rustwasm/console_error_panic_hook#readme // https://github.com/rustwasm/console_error_panic_hook#readme
#[cfg(feature = "console_error_panic_hook")] #[cfg(feature = "console_error_panic_hook")]

View file

@ -50,16 +50,16 @@ fn test_document_one_way_with_serialisation() {
); );
let serialised_left = let serialised_left =
serde_yaml::from_str(&serde_yaml::to_string(&left_operations.to_changes()).unwrap()) serde_yaml::from_str(&serde_yaml::to_string(&left_operations.to_diff()).unwrap())
.unwrap(); .unwrap();
let serialised_right = let serialised_right =
serde_yaml::from_str(&serde_yaml::to_string(&right_operations.to_changes()).unwrap()) serde_yaml::from_str(&serde_yaml::to_string(&right_operations.to_diff()).unwrap())
.unwrap(); .unwrap();
let restored_left_operations = let restored_left_operations =
EditedText::from_changes(&parent, serialised_left, &*BuiltinTokenizer::Word); EditedText::from_diff(&parent, serialised_left, &*BuiltinTokenizer::Word);
let restored_right_operations = let restored_right_operations =
EditedText::from_changes(&parent, serialised_right, &*BuiltinTokenizer::Word); EditedText::from_diff(&parent, serialised_right, &*BuiltinTokenizer::Word);
doc.assert_eq_without_cursors( doc.assert_eq_without_cursors(
&restored_left_operations &restored_left_operations

View file

@ -56,11 +56,11 @@ fn test_merge_binary() {
} }
#[wasm_bindgen_test] // JsValue isn't supported outside of wasm #[wasm_bindgen_test] // JsValue isn't supported outside of wasm
fn test_get_compact_diff() { fn test_diff() {
let parent = "hello "; let parent = "hello ";
let changed = "world"; let changed = "world";
let result = get_compact_diff(parent, &changed.into(), BuiltinTokenizer::Word); let result = diff(parent, &changed.into(), BuiltinTokenizer::Word);
assert_eq!(result.len(), 2); assert_eq!(result.len(), 2);
let first: i64 = result[0].clone().try_into().unwrap(); let first: i64 = result[0].clone().try_into().unwrap();