Add diff error
This commit is contained in:
parent
e58370b2ca
commit
8e1690a0bf
3 changed files with 96 additions and 8 deletions
|
|
@ -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,7 +215,7 @@ 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_string::NumberOrString,
|
||||||
|
|
|
||||||
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,7 +6,7 @@ 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,
|
||||||
|
|
@ -424,15 +424,19 @@ 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<NumberOrString>,
|
||||||
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;
|
||||||
|
|
||||||
|
|
@ -441,6 +445,17 @@ where
|
||||||
NumberOrString::Number(length) => {
|
NumberOrString::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,6 +468,17 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue