Add diff applying error & improve CI (#32)
* Use stable rust * Add From impls * Revert to nightly * Improve dev env & CI setup * Update lock * Add thiserror * Add diff error * Fix tests * Lint * Rename NumberOrString * Format * Fix lint script
This commit is contained in:
parent
e03b9147df
commit
88d48afce3
17 changed files with 195 additions and 1192 deletions
29
.github/workflows/check.yml
vendored
29
.github/workflows/check.yml
vendored
|
|
@ -37,20 +37,8 @@ jobs:
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-cargo-
|
${{ runner.os }}-cargo-
|
||||||
|
|
||||||
- name: Setup rust
|
|
||||||
run: |
|
|
||||||
which wasm-pack || cargo install wasm-pack
|
|
||||||
which cargo-machete || cargo install cargo-machete
|
|
||||||
|
|
||||||
- name: Build wasm
|
|
||||||
run: |
|
|
||||||
wasm-pack build --target web --features wasm
|
|
||||||
|
|
||||||
- name: Lint
|
- name: Lint
|
||||||
run: |
|
run: scripts/lint.sh
|
||||||
cargo clippy --all-targets --all-features
|
|
||||||
cargo fmt --all -- --check
|
|
||||||
cargo machete
|
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: scripts/test.sh
|
run: scripts/test.sh
|
||||||
|
|
@ -117,19 +105,8 @@ jobs:
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-npm-
|
${{ runner.os }}-npm-
|
||||||
|
|
||||||
- name: Setup rust
|
- name: Build website
|
||||||
run: |
|
run: scripts/build-website.sh
|
||||||
which wasm-pack || cargo install wasm-pack
|
|
||||||
|
|
||||||
- name: Build wasm
|
|
||||||
run: |
|
|
||||||
wasm-pack build --target web --features wasm
|
|
||||||
|
|
||||||
- name: Build reconcile-js
|
|
||||||
run: |
|
|
||||||
cd reconcile-js
|
|
||||||
npm ci
|
|
||||||
npm run build
|
|
||||||
|
|
||||||
- name: Publish reconcile-js to NPM
|
- name: Publish reconcile-js to NPM
|
||||||
run: |
|
run: |
|
||||||
|
|
|
||||||
21
Cargo.lock
generated
21
Cargo.lock
generated
|
|
@ -245,6 +245,7 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_yaml",
|
"serde_yaml",
|
||||||
"test-case",
|
"test-case",
|
||||||
|
"thiserror",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"wasm-bindgen-test",
|
"wasm-bindgen-test",
|
||||||
"wee_alloc",
|
"wee_alloc",
|
||||||
|
|
@ -383,6 +384,26 @@ dependencies = [
|
||||||
"test-case-core",
|
"test-case-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror"
|
||||||
|
version = "2.0.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror-impl"
|
||||||
|
version = "2.0.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.22"
|
version = "1.0.22"
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ path = "examples/merge-file.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde = { version = "1.0.219", optional = true, features = ["derive"] }
|
serde = { version = "1.0.219", optional = true, features = ["derive"] }
|
||||||
|
thiserror = "2.0.17"
|
||||||
|
|
||||||
wasm-bindgen = { version = "0.2.99", optional = true }
|
wasm-bindgen = { version = "0.2.99", optional = true }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -125,14 +125,10 @@ Contributions are welcome!
|
||||||
|
|
||||||
#### Rust toolchain
|
#### Rust toolchain
|
||||||
|
|
||||||
1. Install [rustup](https://rustup.rs):
|
Install [rustup](https://rustup.rs):
|
||||||
```bash
|
```bash
|
||||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||||
```
|
```
|
||||||
2. Install additional tools:
|
|
||||||
```bash
|
|
||||||
cargo install wasm-pack cargo-insta cargo-edit
|
|
||||||
```
|
|
||||||
|
|
||||||
### Scripts
|
### Scripts
|
||||||
|
|
||||||
|
|
|
||||||
1124
reconcile-js/package-lock.json
generated
1124
reconcile-js/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -2,7 +2,9 @@
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
which wasm-pack || cargo install wasm-pack
|
||||||
wasm-pack build --target web --features wasm
|
wasm-pack build --target web --features wasm
|
||||||
|
|
||||||
cd reconcile-js
|
cd reconcile-js
|
||||||
npm ci
|
npm ci
|
||||||
npm run build
|
npm run build
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,12 @@ else
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Bumping versions"
|
echo "Bumping versions"
|
||||||
|
|
||||||
|
which cargo-set-version || cargo install cargo-edit
|
||||||
cargo set-version --bump $1
|
cargo set-version --bump $1
|
||||||
|
|
||||||
|
which wasm-pack || cargo install wasm-pack
|
||||||
|
|
||||||
wasm-pack build --target web --features wasm
|
wasm-pack build --target web --features wasm
|
||||||
|
|
||||||
cd reconcile-js
|
cd reconcile-js
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,18 @@
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
which cargo-machete || cargo install cargo-machete
|
||||||
|
cargo machete
|
||||||
|
|
||||||
cargo clippy --all-targets --all-features --fix --allow-dirty --allow-staged
|
cargo clippy --all-targets --all-features --fix --allow-dirty --allow-staged
|
||||||
cargo fmt --all
|
cargo fmt --all
|
||||||
|
|
||||||
cd reconcile-js
|
cd reconcile-js
|
||||||
|
npm ci
|
||||||
npm run format
|
npm run format
|
||||||
|
|
||||||
cd ../examples/website
|
cd ../examples/website
|
||||||
|
npm ci
|
||||||
npm run format
|
npm run format
|
||||||
|
|
||||||
echo "Success!"
|
echo "Success!"
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,15 @@
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
which cargo-insta || cargo install cargo-insta
|
||||||
|
which wasm-pack || cargo install wasm-pack
|
||||||
|
|
||||||
|
node_version=$(node --version | cut -d'.' -f1 | tr -d 'v')
|
||||||
|
if [ "$node_version" != "22" ]; then
|
||||||
|
echo "Error: Node.js version 22 is required, but found version $node_version"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
wasm-pack build --target web --features wasm,console_error_panic_hook
|
wasm-pack build --target web --features wasm,console_error_panic_hook
|
||||||
cargo test --verbose --features serde -- --include-ignored
|
cargo test --verbose --features serde -- --include-ignored
|
||||||
|
|
||||||
|
|
@ -13,7 +22,8 @@ cargo test --features all
|
||||||
wasm-pack test --node --features wasm,console_error_panic_hook
|
wasm-pack test --node --features wasm,console_error_panic_hook
|
||||||
|
|
||||||
cd reconcile-js
|
cd reconcile-js
|
||||||
npm install
|
npm ci
|
||||||
|
npm run build
|
||||||
npm run test
|
npm run test
|
||||||
cd -
|
cd -
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -187,7 +187,7 @@
|
||||||
//! original,
|
//! original,
|
||||||
//! deserialized,
|
//! deserialized,
|
||||||
//! &*BuiltinTokenizer::Word
|
//! &*BuiltinTokenizer::Word
|
||||||
//! );
|
//! ).unwrap();
|
||||||
//! assert_eq!(
|
//! assert_eq!(
|
||||||
//! reconstructed.apply().text(),
|
//! reconstructed.apply().text(),
|
||||||
//! "Merging text is easy with reconcile!"
|
//! "Merging text is easy with reconcile!"
|
||||||
|
|
@ -215,11 +215,11 @@ mod tokenizer;
|
||||||
mod types;
|
mod types;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
pub use operation_transformation::{EditedText, reconcile};
|
pub use operation_transformation::{DiffError, EditedText, reconcile};
|
||||||
pub use tokenizer::{BuiltinTokenizer, Tokenizer, token::Token};
|
pub use tokenizer::{BuiltinTokenizer, Tokenizer, token::Token};
|
||||||
pub use types::{
|
pub use types::{
|
||||||
cursor_position::CursorPosition, history::History, number_or_string::NumberOrString,
|
cursor_position::CursorPosition, history::History, number_or_text::NumberOrText, side::Side,
|
||||||
side::Side, span_with_history::SpanWithHistory, text_with_cursors::TextWithCursors,
|
span_with_history::SpanWithHistory, text_with_cursors::TextWithCursors,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "wasm")]
|
#[cfg(feature = "wasm")]
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
|
mod diff_error;
|
||||||
mod edited_text;
|
mod edited_text;
|
||||||
mod operation;
|
mod operation;
|
||||||
mod utils;
|
mod utils;
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
|
|
||||||
|
pub use diff_error::DiffError;
|
||||||
pub use edited_text::EditedText;
|
pub use edited_text::EditedText;
|
||||||
pub use operation::Operation;
|
pub use operation::Operation;
|
||||||
|
|
||||||
|
|
|
||||||
19
src/operation_transformation/diff_error.rs
Normal file
19
src/operation_transformation/diff_error.rs
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
/// Error type for invalid diff operations
|
||||||
|
#[derive(Error, Debug, Clone, PartialEq)]
|
||||||
|
pub enum DiffError {
|
||||||
|
/// The diff references a range that exceeds the original text length
|
||||||
|
#[error(
|
||||||
|
"Invalid diff: attempting to access {requested} characters starting at position \
|
||||||
|
{position}, but original text only has {available} characters remaining"
|
||||||
|
)]
|
||||||
|
LengthExceedsOriginal {
|
||||||
|
/// The position where the operation starts
|
||||||
|
position: usize,
|
||||||
|
/// The number of characters requested
|
||||||
|
requested: usize,
|
||||||
|
/// The number of characters available from the position
|
||||||
|
available: usize,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
@ -6,13 +6,13 @@ use serde::{Deserialize, Serialize};
|
||||||
use crate::{
|
use crate::{
|
||||||
BuiltinTokenizer, CursorPosition, TextWithCursors,
|
BuiltinTokenizer, CursorPosition, TextWithCursors,
|
||||||
operation_transformation::{
|
operation_transformation::{
|
||||||
Operation,
|
DiffError, Operation,
|
||||||
utils::{cook_operations::cook_operations, elongate_operations::elongate_operations},
|
utils::{cook_operations::cook_operations, elongate_operations::elongate_operations},
|
||||||
},
|
},
|
||||||
raw_operation::RawOperation,
|
raw_operation::RawOperation,
|
||||||
tokenizer::Tokenizer,
|
tokenizer::Tokenizer,
|
||||||
types::{
|
types::{
|
||||||
history::History, number_or_string::NumberOrString, side::Side,
|
history::History, number_or_text::NumberOrText, side::Side,
|
||||||
span_with_history::SpanWithHistory,
|
span_with_history::SpanWithHistory,
|
||||||
},
|
},
|
||||||
utils::string_builder::StringBuilder,
|
utils::string_builder::StringBuilder,
|
||||||
|
|
@ -366,8 +366,8 @@ where
|
||||||
///
|
///
|
||||||
/// Panics if there's an integer overflow in i64.
|
/// Panics if there's an integer overflow in i64.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn to_diff(&self) -> Vec<NumberOrString> {
|
pub fn to_diff(&self) -> Vec<NumberOrText> {
|
||||||
let mut result: Vec<NumberOrString> = Vec::with_capacity(self.operations.len());
|
let mut result: Vec<NumberOrText> = Vec::with_capacity(self.operations.len());
|
||||||
let mut previous_equal: Option<usize> = None;
|
let mut previous_equal: Option<usize> = None;
|
||||||
|
|
||||||
for operation in &self.operations {
|
for operation in &self.operations {
|
||||||
|
|
@ -382,7 +382,7 @@ where
|
||||||
|
|
||||||
Operation::Insert { text, .. } => {
|
Operation::Insert { text, .. } => {
|
||||||
if let Some(prev_length) = previous_equal {
|
if let Some(prev_length) = previous_equal {
|
||||||
result.push(NumberOrString::Number(
|
result.push(NumberOrText::Number(
|
||||||
i64::try_from(prev_length).expect("prev_length must fit in i64"),
|
i64::try_from(prev_length).expect("prev_length must fit in i64"),
|
||||||
));
|
));
|
||||||
previous_equal = None;
|
previous_equal = None;
|
||||||
|
|
@ -392,7 +392,7 @@ where
|
||||||
.iter()
|
.iter()
|
||||||
.map(super::super::tokenizer::token::Token::original)
|
.map(super::super::tokenizer::token::Token::original)
|
||||||
.collect();
|
.collect();
|
||||||
result.push(NumberOrString::Text(text));
|
result.push(NumberOrText::Text(text));
|
||||||
}
|
}
|
||||||
|
|
||||||
Operation::Delete {
|
Operation::Delete {
|
||||||
|
|
@ -400,7 +400,7 @@ where
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
if let Some(prev_length) = previous_equal {
|
if let Some(prev_length) = previous_equal {
|
||||||
result.push(NumberOrString::Number(
|
result.push(NumberOrText::Number(
|
||||||
i64::try_from(prev_length).expect("prev_length must fit in i64"),
|
i64::try_from(prev_length).expect("prev_length must fit in i64"),
|
||||||
));
|
));
|
||||||
previous_equal = None;
|
previous_equal = None;
|
||||||
|
|
@ -408,13 +408,13 @@ where
|
||||||
|
|
||||||
let count = i64::try_from(*deleted_character_count)
|
let count = i64::try_from(*deleted_character_count)
|
||||||
.expect("deleted_character_count must fit in i64");
|
.expect("deleted_character_count must fit in i64");
|
||||||
result.push(NumberOrString::Number(-count));
|
result.push(NumberOrText::Number(-count));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(prev_length) = previous_equal {
|
if let Some(prev_length) = previous_equal {
|
||||||
result.push(NumberOrString::Number(
|
result.push(NumberOrText::Number(
|
||||||
i64::try_from(prev_length).expect("prev_length must fit in i64"),
|
i64::try_from(prev_length).expect("prev_length must fit in i64"),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
@ -424,23 +424,38 @@ where
|
||||||
|
|
||||||
/// Deserialize an `EditedText` from a change list and the original text.
|
/// Deserialize an `EditedText` from a change list and the original text.
|
||||||
///
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns `DiffError::LengthExceedsOriginal` if the diff references a
|
||||||
|
/// range that exceeds the original text length.
|
||||||
|
///
|
||||||
/// # Panics
|
/// # Panics
|
||||||
///
|
///
|
||||||
/// Panics if there's an integer overflow in i64.
|
/// Panics if there's an integer overflow in i64.
|
||||||
#[must_use]
|
|
||||||
pub fn from_diff(
|
pub fn from_diff(
|
||||||
original_text: &'a str,
|
original_text: &'a str,
|
||||||
diff: Vec<NumberOrString>,
|
diff: Vec<NumberOrText>,
|
||||||
tokenizer: &Tokenizer<T>,
|
tokenizer: &Tokenizer<T>,
|
||||||
) -> EditedText<'a, T> {
|
) -> Result<EditedText<'a, T>, DiffError> {
|
||||||
let mut operations: Vec<Operation<T>> = Vec::with_capacity(diff.len());
|
let mut operations: Vec<Operation<T>> = Vec::with_capacity(diff.len());
|
||||||
let mut order = 0;
|
let mut order = 0;
|
||||||
|
|
||||||
for item in diff {
|
for item in diff {
|
||||||
match item {
|
match item {
|
||||||
NumberOrString::Number(length) => {
|
NumberOrText::Number(length) => {
|
||||||
if length >= 0 {
|
if length >= 0 {
|
||||||
let length = usize::try_from(length).expect("length must fit in usize");
|
let length = usize::try_from(length).expect("length must fit in usize");
|
||||||
|
|
||||||
|
// Validate that the range doesn't exceed the original text
|
||||||
|
let text_length = original_text.chars().count();
|
||||||
|
if order + length > text_length {
|
||||||
|
return Err(DiffError::LengthExceedsOriginal {
|
||||||
|
position: order,
|
||||||
|
requested: length,
|
||||||
|
available: text_length.saturating_sub(order),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let original_characters: String =
|
let original_characters: String =
|
||||||
original_text.chars().skip(order).take(length).collect();
|
original_text.chars().skip(order).take(length).collect();
|
||||||
|
|
||||||
|
|
@ -453,11 +468,22 @@ where
|
||||||
} else {
|
} else {
|
||||||
let length =
|
let length =
|
||||||
usize::try_from(-length).expect("negative length must fit in usize");
|
usize::try_from(-length).expect("negative length must fit in usize");
|
||||||
|
|
||||||
|
// Validate that the delete range doesn't exceed the original text
|
||||||
|
let text_length = original_text.chars().count();
|
||||||
|
if order + length > text_length {
|
||||||
|
return Err(DiffError::LengthExceedsOriginal {
|
||||||
|
position: order,
|
||||||
|
requested: length,
|
||||||
|
available: text_length.saturating_sub(order),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
operations.push(Operation::create_delete(order, length));
|
operations.push(Operation::create_delete(order, length));
|
||||||
order += length;
|
order += length;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
NumberOrString::Text(text) => {
|
NumberOrText::Text(text) => {
|
||||||
let tokens = tokenizer(&text);
|
let tokens = tokenizer(&text);
|
||||||
operations.push(Operation::create_insert(order, tokens));
|
operations.push(Operation::create_insert(order, tokens));
|
||||||
}
|
}
|
||||||
|
|
@ -465,12 +491,12 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
let operation_count = operations.len();
|
let operation_count = operations.len();
|
||||||
EditedText::new(
|
Ok(EditedText::new(
|
||||||
original_text,
|
original_text,
|
||||||
operations,
|
operations,
|
||||||
vec![Side::Left; operation_count],
|
vec![Side::Left; operation_count],
|
||||||
vec![],
|
vec![],
|
||||||
)
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -520,6 +546,49 @@ mod tests {
|
||||||
assert_eq!(operations.apply().text(), expected);
|
assert_eq!(operations.apply().text(), expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_from_diff_length_exceeds_original() {
|
||||||
|
let result = EditedText::from_diff(
|
||||||
|
"hello",
|
||||||
|
vec![
|
||||||
|
10.into(), // too large equal span - should error
|
||||||
|
" world".into(),
|
||||||
|
],
|
||||||
|
&*BuiltinTokenizer::Word,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
match result {
|
||||||
|
Err(DiffError::LengthExceedsOriginal {
|
||||||
|
position,
|
||||||
|
requested,
|
||||||
|
available,
|
||||||
|
}) => {
|
||||||
|
assert_eq!(position, 0);
|
||||||
|
assert_eq!(requested, 10);
|
||||||
|
assert_eq!(available, 5);
|
||||||
|
}
|
||||||
|
_ => panic!("Expected LengthExceedsOriginal error"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_from_diff_valid() {
|
||||||
|
let edited_text = EditedText::from_diff(
|
||||||
|
"hello",
|
||||||
|
vec![
|
||||||
|
5.into(), // exact length
|
||||||
|
" world".into(),
|
||||||
|
],
|
||||||
|
&*BuiltinTokenizer::Word,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let content = edited_text.apply().text();
|
||||||
|
|
||||||
|
assert_eq!(content, "hello world");
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "serde")]
|
#[cfg(feature = "serde")]
|
||||||
#[test]
|
#[test]
|
||||||
fn test_changes_deserialisation() {
|
fn test_changes_deserialisation() {
|
||||||
|
|
@ -542,7 +611,7 @@ mod tests {
|
||||||
|
|
||||||
let changes = edited_text.to_diff();
|
let changes = edited_text.to_diff();
|
||||||
let deserialized_edited_text =
|
let deserialized_edited_text =
|
||||||
EditedText::from_diff(original, changes, &*BuiltinTokenizer::Word);
|
EditedText::from_diff(original, changes, &*BuiltinTokenizer::Word).unwrap();
|
||||||
|
|
||||||
assert_eq!(deserialized_edited_text.apply().text(), updated);
|
assert_eq!(deserialized_edited_text.apply().text(), updated);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
pub mod cursor_position;
|
pub mod cursor_position;
|
||||||
pub mod history;
|
pub mod history;
|
||||||
pub mod number_or_string;
|
pub mod number_or_text;
|
||||||
pub mod side;
|
pub mod side;
|
||||||
pub mod span_with_history;
|
pub mod span_with_history;
|
||||||
pub mod text_with_cursors;
|
pub mod text_with_cursors;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use std::fmt::Debug;
|
use std::{borrow::Cow, fmt::Debug};
|
||||||
|
|
||||||
#[cfg(feature = "serde")]
|
#[cfg(feature = "serde")]
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
@ -12,18 +12,18 @@ const INTEGRAL_LIMIT: f64 = (1u64 << 53) as f64;
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
#[cfg_attr(feature = "serde", serde(untagged))]
|
#[cfg_attr(feature = "serde", serde(untagged))]
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum NumberOrString {
|
pub enum NumberOrText {
|
||||||
Number(i64),
|
Number(i64),
|
||||||
Text(String),
|
Text(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "wasm")]
|
#[cfg(feature = "wasm")]
|
||||||
impl TryFrom<JsValue> for NumberOrString {
|
impl TryFrom<JsValue> for NumberOrText {
|
||||||
type Error = DeserialisationError;
|
type Error = DeserialisationError;
|
||||||
|
|
||||||
fn try_from(value: JsValue) -> Result<Self, Self::Error> {
|
fn try_from(value: JsValue) -> Result<Self, Self::Error> {
|
||||||
if let Ok(num) = value.clone().try_into() {
|
if let Ok(num) = value.clone().try_into() {
|
||||||
return Ok(NumberOrString::Number(num));
|
return Ok(NumberOrText::Number(num));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(num) = value.clone().as_f64() {
|
if let Some(num) = value.clone().as_f64() {
|
||||||
|
|
@ -34,11 +34,11 @@ impl TryFrom<JsValue> for NumberOrString {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::cast_possible_truncation)]
|
#[allow(clippy::cast_possible_truncation)]
|
||||||
return Ok(NumberOrString::Number(num.round() as i64));
|
return Ok(NumberOrText::Number(num.round() as i64));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(text) = value.try_into() {
|
if let Ok(text) = value.try_into() {
|
||||||
return Ok(NumberOrString::Text(text));
|
return Ok(NumberOrText::Text(text));
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(DeserialisationError::new(
|
Err(DeserialisationError::new(
|
||||||
|
|
@ -48,15 +48,31 @@ impl TryFrom<JsValue> for NumberOrString {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "wasm")]
|
#[cfg(feature = "wasm")]
|
||||||
impl From<NumberOrString> for JsValue {
|
impl From<NumberOrText> for JsValue {
|
||||||
fn from(value: NumberOrString) -> Self {
|
fn from(value: NumberOrText) -> Self {
|
||||||
match value {
|
match value {
|
||||||
NumberOrString::Number(num) => JsValue::from(num),
|
NumberOrText::Number(num) => JsValue::from(num),
|
||||||
NumberOrString::Text(text) => JsValue::from(text),
|
NumberOrText::Text(text) => JsValue::from(text),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<i64> for NumberOrText {
|
||||||
|
fn from(value: i64) -> Self { NumberOrText::Number(value) }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for NumberOrText {
|
||||||
|
fn from(value: String) -> Self { NumberOrText::Text(value) }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for NumberOrText {
|
||||||
|
fn from(value: &str) -> Self { NumberOrText::Text(value.to_owned()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<Cow<'a, str>> for NumberOrText {
|
||||||
|
fn from(value: Cow<'a, str>) -> Self { NumberOrText::Text(value.into_owned()) }
|
||||||
|
}
|
||||||
|
|
||||||
/// Error type for deserialisation failures
|
/// Error type for deserialisation failures
|
||||||
#[cfg(feature = "wasm")]
|
#[cfg(feature = "wasm")]
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
@ -105,16 +105,17 @@ pub fn diff(parent: &str, changed: &TextWithCursors, tokenizer: BuiltinTokenizer
|
||||||
pub fn undiff(parent: &str, diff: Vec<JsValue>, tokenizer: BuiltinTokenizer) -> String {
|
pub fn undiff(parent: &str, diff: Vec<JsValue>, tokenizer: BuiltinTokenizer) -> String {
|
||||||
set_panic_hook();
|
set_panic_hook();
|
||||||
|
|
||||||
EditedText::from_diff(
|
match EditedText::from_diff(
|
||||||
parent,
|
parent,
|
||||||
diff.into_iter()
|
diff.into_iter()
|
||||||
.map(std::convert::TryInto::try_into)
|
.map(std::convert::TryInto::try_into)
|
||||||
.collect::<Result<_, _>>()
|
.collect::<Result<_, _>>()
|
||||||
.expect("Invalid diff format"),
|
.expect("Invalid diff format"),
|
||||||
&*tokenizer,
|
&*tokenizer,
|
||||||
)
|
) {
|
||||||
.apply()
|
Ok(edited_text) => edited_text.apply().text(),
|
||||||
.text()
|
Err(e) => panic!("{}", e),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_panic_hook() {
|
fn set_panic_hook() {
|
||||||
|
|
|
||||||
|
|
@ -57,9 +57,9 @@ fn test_document_one_way_with_serialisation() {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let restored_left_operations =
|
let restored_left_operations =
|
||||||
EditedText::from_diff(&parent, serialised_left, &*BuiltinTokenizer::Word);
|
EditedText::from_diff(&parent, serialised_left, &*BuiltinTokenizer::Word).unwrap();
|
||||||
let restored_right_operations =
|
let restored_right_operations =
|
||||||
EditedText::from_diff(&parent, serialised_right, &*BuiltinTokenizer::Word);
|
EditedText::from_diff(&parent, serialised_right, &*BuiltinTokenizer::Word).unwrap();
|
||||||
|
|
||||||
doc.assert_eq_without_cursors(
|
doc.assert_eq_without_cursors(
|
||||||
&restored_left_operations
|
&restored_left_operations
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue