diff --git a/src/operation_transformation/operation.rs b/src/operation_transformation/operation.rs index e194f9c..827da77 100644 --- a/src/operation_transformation/operation.rs +++ b/src/operation_transformation/operation.rs @@ -15,7 +15,7 @@ use crate::{ /// Represents a change that can be applied on a `StringBuilder`. #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Clone, PartialEq)] +#[derive(Clone)] pub enum Operation where T: PartialEq + Clone + std::fmt::Debug, @@ -42,6 +42,40 @@ where }, } +impl PartialEq for Operation +where + T: PartialEq + Clone + std::fmt::Debug, +{ + fn eq(&self, other: &Self) -> bool { + match (self, other) { + ( + Operation::Equal { length, .. }, + Operation::Equal { + length: other_length, + .. + }, + ) => length == other_length, + ( + Operation::Insert { text, .. }, + Operation::Insert { + text: other_text, .. + }, + ) => text == other_text, + ( + Operation::Delete { + deleted_character_count, + .. + }, + Operation::Delete { + deleted_character_count: other_deleted_character_count, + .. + }, + ) => deleted_character_count == other_deleted_character_count, + _ => false, + } + } +} + impl Operation where T: PartialEq + Clone + std::fmt::Debug, @@ -129,24 +163,28 @@ where Operation::Equal { #[cfg(debug_assertions)] text, + length, .. } => { #[cfg(debug_assertions)] debug_assert!( text.as_ref() .is_none_or(|text| builder.get_slice(self.range()) == *text), - "Text which is supposed to be equal does not match the text in the range" + "Text (`{}`) which is supposed to be equal does not match the text in the \ + range: `{}`", + text.as_ref().unwrap_or(&"".to_owned()), + builder.get_slice(self.range()) ); - return builder; + builder.retain(*length) + } + Operation::Insert { text, .. } => { + builder.insert(&text.iter().map(Token::original).collect::()) } - Operation::Insert { text, .. } => builder.insert( - self.start_index(), - &text.iter().map(Token::original).collect::(), - ), Operation::Delete { #[cfg(debug_assertions)] deleted_text, + deleted_character_count, .. } => { #[cfg(debug_assertions)] @@ -154,10 +192,12 @@ where deleted_text .as_ref() .is_none_or(|text| builder.get_slice(self.range()) == *text), - "Text to delete does not match the text in the range" + "Text to delete (`{}`) does not match the text in the range: {}", + deleted_text.as_ref().unwrap_or(&"".to_owned()), + builder.get_slice(self.range()) ); - builder.delete(self.range()); + builder.delete(*deleted_character_count) } } @@ -424,7 +464,7 @@ where f, "", text.as_ref() - .map(|text| format!("'{text}'")) + .map(|text| format!("'{}'", text.replace('\n', "\\n"))) .unwrap_or(format!("{length} characters")), index )?; @@ -438,7 +478,10 @@ where write!( f, "", - text.iter().map(Token::original).collect::(), + text.iter() + .map(Token::original) + .collect::() + .replace('\n', "\\n"), index ) } @@ -455,7 +498,7 @@ where "", deleted_text .as_ref() - .map(|text| format!("'{text}'")) + .map(|text| format!("'{}'", text.replace('\n', "\\n"))) .unwrap_or(format!("{deleted_character_count} characters")), index )?; @@ -498,16 +541,26 @@ mod tests { #[test] fn test_apply_delete_with_create() { let builder = StringBuilder::new("hello world"); - let operation = Operation::<()>::create_delete_with_text(5, " world".to_owned()).unwrap(); + let delete_operation = + Operation::<()>::create_delete_with_text(0, "hello ".to_owned()).unwrap(); + let retain_operation = Operation::<()>::create_equal(5, 5).unwrap(); - assert_eq!(operation.apply(builder).build(), "hello"); + let mut builder = delete_operation.apply(builder); + builder = retain_operation.apply(builder); + + assert_eq!(builder.build(), "world"); } #[test] fn test_apply_insert() { let builder = StringBuilder::new("hello"); - let operation = Operation::create_insert(5, vec![" my friend".into()]).unwrap(); - assert_eq!(operation.apply(builder).build(), "hello my friend"); + let retain_operation = Operation::<()>::create_equal(5, 5).unwrap(); + let insert_operation = Operation::create_insert(5, vec![" my friend".into()]).unwrap(); + + let mut builder = retain_operation.apply(builder); + builder = insert_operation.apply(builder); + + assert_eq!(builder.build(), "hello my friend"); } } diff --git a/src/utils/string_builder.rs b/src/utils/string_builder.rs index b19bcbb..8e37b79 100644 --- a/src/utils/string_builder.rs +++ b/src/utils/string_builder.rs @@ -1,78 +1,67 @@ use core::ops::Range; +use std::iter::Iterator; -/// A helper for building a string in order based on an original string and a -/// series of insertions and deletions applied to it. It is safe to use with -/// UTF-8 strings as all operations are based on character indices. -#[derive(Debug, Clone)] +/// A helper for building a string in-order based on an original string and a +/// series of insertions, deletions, and copies applied to it. It is safe to use +/// with UTF-8 strings as all operations are based on character indices. The +/// methods must be called in-order. pub struct StringBuilder<'a> { - original: &'a str, - last_old_char_index: usize, + original: Box + 'a>, buffer: String, + + #[cfg(debug_assertions)] + remaining: String, } impl StringBuilder<'_> { pub fn new(original: &str) -> StringBuilder<'_> { StringBuilder { - original, - last_old_char_index: 0, + original: Box::new(original.chars()), buffer: String::with_capacity(original.len()), + + #[cfg(debug_assertions)] + remaining: original.to_string(), } } - /// Insert a string at the given index after copying the original string up - /// to that index from the last insertion or deletion. - pub fn insert(&mut self, from: usize, text: &str) { - self.copy_until(from); - self.buffer.push_str(text); + /// Insert a string at the end of the built buffer. + pub fn insert(&mut self, text: &str) { self.buffer.push_str(text); } + + /// Skip copying `length` characters from the original string to the built + /// buffer. + pub fn delete(&mut self, length: usize) { + if length == 0 { + return; + } + + self.original.nth(length - 1); + + if cfg!(debug_assertions) { + self.remaining = self.remaining.chars().skip(length).collect(); + } } - /// Delete a string at the given index after copying the original string up - /// to that index from the last insertion or deletion. - pub fn delete(&mut self, range: core::ops::Range) { - self.copy_until(range.start); - self.last_old_char_index += range.len(); - } + /// Copy `length` characters from the original string to the built buffer. + pub fn retain(&mut self, length: usize) { + self.buffer.extend(self.original.by_ref().take(length)); - fn copy_until(&mut self, index: usize) { - let current_char_count = self.buffer.chars().count(); - debug_assert!( - index >= current_char_count, - "String builder only support building in order" - ); - - let jump = index - current_char_count; - - self.buffer.push_str( - &self - .original - .chars() - .skip(self.last_old_char_index) - .take(jump) - .collect::(), - ); - self.last_old_char_index += jump; + if cfg!(debug_assertions) { + self.remaining = self.remaining.chars().skip(length).collect(); + } } /// Finish building the string after copying the remaining original string /// since the last insertion or deletion. - pub fn build(mut self) -> String { - self.buffer.push_str( - &self - .original - .chars() - .skip(self.last_old_char_index) - .collect::(), - ); + pub fn build(self) -> String { self.buffer } - self.buffer - } - - #[allow(dead_code)] + #[cfg(debug_assertions)] + /// Get a slice of the built string and the remaining original string. + /// The implementation is quite suboptimal but it's only used for debugging. pub fn get_slice(&self, range: Range) -> String { let result = self .buffer .chars() - .chain(self.original.chars().skip(self.last_old_char_index)) + .chain(self.remaining.chars()) .skip(range.start) .take(range.end - range.start) .collect::(); @@ -85,6 +74,8 @@ impl StringBuilder<'_> { #[cfg(test)] mod tests { + use pretty_assertions::assert_eq; + use super::*; #[test] @@ -92,20 +83,74 @@ mod tests { let original = "aaa bbb ccc"; let mut builder = StringBuilder::new(original); - builder.insert(0, "ddd "); - builder.delete(4..8); - builder.insert(11, " eee"); + builder.insert("ddd"); + builder.delete(3); + builder.retain(8); + builder.insert(" eee"); assert_eq!(builder.build(), "ddd bbb ccc eee"); - } - #[test] - fn test_string_builder2() { let original = "abcde"; let mut builder = StringBuilder::new(original); - builder.delete(1..4); + builder.retain(1); + builder.delete(3); + builder.retain(1); assert_eq!(builder.build(), "ae"); } + + #[test] + fn test_empty_original() { + let original = ""; + let mut builder = StringBuilder::new(original); + + builder.insert("test"); + assert_eq!(builder.build(), "test"); + } + + #[test] + fn test_unicode_characters() { + let original = "こんにちは"; + let mut builder = StringBuilder::new(original); + + builder.retain(3); + builder.insert("世界, "); // Insert "World, " + builder.retain(2); + + assert_eq!(builder.build(), "こんに世界, ちは"); + } + + #[test] + fn test_get_slice() { + let original = "abcdef"; + let builder = StringBuilder::new(original); + + // Test getting a slice of the original string + assert_eq!(builder.get_slice(1..4), "bcd"); + + // Test getting a slice that includes both buffer and remaining original + let mut builder = StringBuilder::new(original); + builder.retain(2); // "ab" in buffer + assert_eq!(builder.get_slice(1..5), "bcde"); + } + + #[test] + fn test_retain_all() { + let original = "Hello, world!"; + let mut builder = StringBuilder::new(original); + + builder.retain(original.len()); + assert_eq!(builder.build(), original); + } + + #[test] + fn test_delete_all() { + let original = "Hello"; + let mut builder = StringBuilder::new(original); + + builder.delete(original.len()); + builder.insert("Hi"); + assert_eq!(builder.build(), "Hi"); + } }