loro/crates/delta/src/text_delta.rs
Zixuan Chen 9d4f7aa8a3
DeltaRope (#327)
* feat: delta rope support init

* perf: use generic-btree v0.9.0

* refactor: improve readability and maintainability

* fix: fix several issues about composing

* fix: a few more issue about composing deletions

* test: rich text

* fix: cover more edge cases

* refactor: use deltarope for list event

* refactor: replace text delta with DeltaRope

* fix: list fuzz err

* fix: safety issue on insert_many

* chore: refine impl of text delta

* refactor: use Replace instead of insert+del in DeltaItem (#330)

* refactor: use Replace instead of insert+del in DeltaItem

* fix: each deltaitem should have non-zero rle_len
Updated generic-btree dependency to version 0.10.3 and refactored DeltaItem and DeltaRope implementations in loro-delta. Refine compose impl

* fix: update generic-btree to fix the update leaf issue

* chore: lockfile

* chore: clippy fix

* refactor: make composing easier to understand

* refactor: simplify the impl of composing
2024-04-24 13:53:26 +08:00

175 lines
4.8 KiB
Rust

use crate::{
delta_trait::{DeltaAttr, DeltaValue},
DeltaItem, DeltaRope,
};
use arrayvec::ArrayString;
use generic_btree::rle::{HasLength, Mergeable, Sliceable, TryInsert};
#[cfg(test)]
const MAX_STRING_SIZE: usize = 8;
#[cfg(not(test))]
const MAX_STRING_SIZE: usize = 128;
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct TextChunk(ArrayString<MAX_STRING_SIZE>);
pub type TextDelta<Attr = ()> = DeltaRope<TextChunk, Attr>;
impl<Attr: DeltaAttr> TextDelta<Attr> {
pub fn insert_str(&mut self, index: usize, s: &str) {
if s.is_empty() || index == self.len() {
self.push_str_insert(s);
return;
}
self.insert_values(
index,
TextChunk::from_long_str(s).map(|chunk| DeltaItem::Replace {
value: chunk,
attr: Default::default(),
delete: 0,
}),
);
}
pub fn push_str_insert(&mut self, s: &str) -> &mut Self {
self.push_str_insert_with_attr(s, Default::default())
}
pub fn push_str_insert_with_attr(&mut self, s: &str, attr: Attr) -> &mut Self {
if s.is_empty() {
return self;
}
if s.len() <= MAX_STRING_SIZE {
self.push_insert(TextChunk(ArrayString::from(s).unwrap()), attr);
return self;
}
let mut split_end = 128;
let mut split_start = 0;
while split_end != s.len() {
while !s.is_char_boundary(split_end) {
split_end -= 1;
}
let chunk = TextChunk(ArrayString::from(&s[split_start..split_end]).unwrap());
self.push_insert(chunk, attr.clone());
split_start = split_end;
split_end = (split_end + 128).min(s.len());
}
self
}
pub fn try_to_string(&self) -> Option<String> {
let mut ans = String::with_capacity(self.len());
for item in self.iter() {
match item {
crate::DeltaItem::Retain { .. } => return None,
crate::DeltaItem::Replace { value, .. } => {
ans.push_str(&value.0);
}
}
}
Some(ans)
}
}
impl TextChunk {
pub(crate) fn try_insert(&mut self, pos: usize, s: &str) -> Result<(), ()> {
if self.0.len() + s.len() > MAX_STRING_SIZE {
return Err(());
}
assert!(self.0.is_char_boundary(pos));
let new_len = self.0.len() + s.len();
unsafe {
let ptr = self.0.as_mut_ptr().add(pos);
ptr.copy_to(ptr.add(s.len()), self.0.len() - pos);
ptr.copy_from_nonoverlapping(s.as_ptr(), s.len());
self.0.set_len(new_len);
}
Ok(())
}
pub fn try_from_str(s: &str) -> Option<Self> {
Some(TextChunk(ArrayString::from(s).ok()?))
}
pub fn from_long_str(s: &str) -> impl Iterator<Item = Self> + '_ {
let mut text_iter = s.chars();
std::iter::from_fn(move || {
let mut chunk = Self::default();
for c in text_iter.by_ref() {
let mut bytes = [0, 0, 0, 0];
chunk.0.push_str(c.encode_utf8(&mut bytes));
if chunk.0.is_full() {
break;
}
}
if chunk.0.is_empty() {
return None;
}
Some(chunk)
})
}
}
impl HasLength for TextChunk {
fn rle_len(&self) -> usize {
self.0.len()
}
}
impl Mergeable for TextChunk {
fn can_merge(&self, rhs: &Self) -> bool {
MAX_STRING_SIZE >= self.0.len() + rhs.0.len()
}
fn merge_right(&mut self, rhs: &Self) {
self.0.push_str(&rhs.0)
}
fn merge_left(&mut self, left: &Self) {
let ptr = self.0.as_mut_ptr();
// Safety: `self.0` is a valid `ArrayString` and `left.0` is a valid `ArrayString`.
unsafe {
ptr.copy_to(ptr.add(left.0.len()), self.0.len());
ptr.copy_from_nonoverlapping(left.0.as_ptr(), left.0.len());
self.0.set_len(self.0.len() + left.0.len());
}
}
}
impl Sliceable for TextChunk {
fn _slice(&self, range: std::ops::Range<usize>) -> Self {
let mut new = ArrayString::new();
new.push_str(&self.0.as_str()[range]);
TextChunk(new)
}
fn split(&mut self, pos: usize) -> Self {
let mut right = ArrayString::new();
right.push_str(&self.0.as_str()[pos..]);
self.0.truncate(pos);
TextChunk(right)
}
}
impl TryInsert for TextChunk {
fn try_insert(&mut self, pos: usize, elem: Self) -> Result<(), Self>
where
Self: Sized,
{
match self.try_insert(pos, elem.0.as_str()) {
Ok(_) => Ok(()),
Err(_) => Err(elem),
}
}
}
impl DeltaValue for TextChunk {}