From af274eac79cef1b7fe1b115d0378ab5d0f548d6c Mon Sep 17 00:00:00 2001 From: Zixuan Chen Date: Wed, 7 Aug 2024 23:42:15 +0800 Subject: [PATCH] refactor: add linear mode for text --- crates/delta/src/delta_rope.rs | 25 + crates/delta/src/delta_rope/compose.rs | 18 +- .../src/container/richtext/richtext_state.rs | 13 +- .../src/container/richtext/tracker.rs | 2 +- crates/loro-internal/src/diff_calc.rs | 493 ++++++++++++------ crates/loro-internal/src/event.rs | 6 +- .../loro-internal/src/state/richtext_state.rs | 400 +++++++------- 7 files changed, 604 insertions(+), 353 deletions(-) diff --git a/crates/delta/src/delta_rope.rs b/crates/delta/src/delta_rope.rs index 03de7932..3b8e5439 100644 --- a/crates/delta/src/delta_rope.rs +++ b/crates/delta/src/delta_rope.rs @@ -433,6 +433,31 @@ impl DeltaRope { .insert_many_by_cursor(Some(pos.cursor), values.into_iter()); } + pub fn insert_value(&mut self, pos: usize, value: V, attr: Attr) { + if self.len() < pos { + self.push_retain(pos - self.len(), Default::default()); + } + + if pos == self.len() { + self.tree.push(DeltaItem::Replace { + value, + attr, + delete: 0, + }); + return; + } + + let pos = self.tree.query::(&pos).unwrap(); + self.tree.insert_by_path( + pos.cursor, + DeltaItem::Replace { + value, + attr, + delete: 0, + }, + ); + } + fn update_attr_in_range(&mut self, range: Range, attr: &Attr) { if range.start == range.end || self.is_empty() { return; diff --git a/crates/delta/src/delta_rope/compose.rs b/crates/delta/src/delta_rope/compose.rs index b934bb09..7cf09670 100644 --- a/crates/delta/src/delta_rope/compose.rs +++ b/crates/delta/src/delta_rope/compose.rs @@ -59,6 +59,17 @@ impl DeltaRope { // trace!("Composed {:#?}", &self); } + pub fn delete(&mut self, mut index: usize, len: usize) { + self._compose_replace( + DeltaReplace { + value: &Default::default(), + attr: &Default::default(), + delete: len, + }, + &mut index, + ); + } + fn _compose_replace( &mut self, delta_replace_item @ DeltaReplace { @@ -70,8 +81,11 @@ impl DeltaRope { ) { let mut should_insert = this_value.rle_len() > 0; let mut left_del_len = delete; - if delete > 0 { - assert!(*index < self.len()); + if *index > self.len() { + self.push_retain(*index - self.len(), Attr::default()); + } + + if delete > 0 && *index != self.len() { let range = *index..(*index + left_del_len).min(self.len()); let from = self.tree.query::(&range.start).unwrap(); let to = self.tree.query::(&range.end).unwrap(); diff --git a/crates/loro-internal/src/container/richtext/richtext_state.rs b/crates/loro-internal/src/container/richtext/richtext_state.rs index 9302a2c1..9a8d5e2e 100644 --- a/crates/loro-internal/src/container/richtext/richtext_state.rs +++ b/crates/loro-internal/src/container/richtext/richtext_state.rs @@ -397,11 +397,21 @@ pub(crate) enum RichtextStateChunk { }, } +impl Default for RichtextStateChunk { + fn default() -> Self { + Self::new_empty() + } +} + impl RichtextStateChunk { pub fn new_text(s: BytesSlice, id: IdFull) -> Self { Self::Text(TextChunk::new(s, id)) } + pub fn new_empty() -> Self { + Self::Text(TextChunk::new_empty()) + } + pub fn new_style(style: Arc, anchor_type: AnchorType) -> Self { Self::Style { style, anchor_type } } @@ -450,6 +460,8 @@ impl RichtextStateChunk { } } +impl loro_delta::delta_trait::DeltaValue for RichtextStateChunk {} + impl DeltaValue for RichtextStateChunk { fn value_extend(&mut self, other: Self) -> Result<(), Self> { Err(other) @@ -1065,7 +1077,6 @@ mod cursor_cache { RichtextTreeTrait, }; use generic_btree::{rle::HasLength, BTree, Cursor, LeafIndex}; - #[derive(Debug, Clone)] struct CursorCacheItem { diff --git a/crates/loro-internal/src/container/richtext/tracker.rs b/crates/loro-internal/src/container/richtext/tracker.rs index 6173274e..05969830 100644 --- a/crates/loro-internal/src/container/richtext/tracker.rs +++ b/crates/loro-internal/src/container/richtext/tracker.rs @@ -598,7 +598,7 @@ impl Tracker { self._checkout(from, false); self._checkout(to, true); // self.id_to_cursor.diagnose(); - // tracing::trace!("Trace::diff {:#?}, ", &self); + tracing::trace!("Trace::diff {:#?}, ", &self); self.rope.get_diff() } diff --git a/crates/loro-internal/src/diff_calc.rs b/crates/loro-internal/src/diff_calc.rs index 4eea0270..fd17a9f6 100644 --- a/crates/loro-internal/src/diff_calc.rs +++ b/crates/loro-internal/src/diff_calc.rs @@ -15,6 +15,7 @@ use loro_common::{ CompactIdLp, ContainerID, Counter, HasCounterSpan, HasIdSpan, IdFull, IdLp, IdSpan, LoroValue, PeerID, ID, }; +use loro_delta::DeltaRope; use smallvec::SmallVec; use tracing::{instrument, trace, warn}; @@ -161,6 +162,7 @@ impl DiffCalculator { if *has_all { use_persisted_shortcut = true; + trace!("use persisted shortcut"); } } } @@ -184,6 +186,7 @@ impl DiffCalculator { tracing::debug!("LCA: {:?} mode={:?}", &lca, diff_mode); let mut started_set = FxHashSet::default(); for (change, start_counter, vv) in iter { + let end_counter = *merged.get(&change.id.peer).unwrap(); if let DiffCalculatorRetainMode::Persist { has_all, last_vv } = &mut self.retain_mode { @@ -197,7 +200,7 @@ impl DiffCalculator { ); } - last_vv.extend_to_include_end_id(change.id_end()); + last_vv.extend_to_include_end_id(ID::new(change.id.peer, end_counter)); } } @@ -206,9 +209,8 @@ impl DiffCalculator { .binary_search_by(|op| op.ctr_last().cmp(&start_counter)) .unwrap_or_else(|e| e); let mut visited = FxHashSet::default(); - let end_counter = merged.get(&change.id.peer).unwrap(); for mut op in &change.ops.vec()[iter_start..] { - if op.counter >= *end_counter { + if op.counter >= end_counter { break; } @@ -226,7 +228,7 @@ impl DiffCalculator { continue; } - if op.counter < start_counter || op.ctr_end() > *end_counter { + if op.counter < start_counter || op.ctr_end() > end_counter { stack_sliced_op = Some(op.slice( (start_counter as usize).saturating_sub(op.counter as usize), op.atom_len().min((end_counter - op.counter) as usize), @@ -391,7 +393,7 @@ impl DiffCalculator { .or_insert_with(|| match idx.get_type() { crate::ContainerType::Text => ( depth, - ContainerDiffCalculator::Richtext(RichtextDiffCalculator::default()), + ContainerDiffCalculator::Richtext(RichtextDiffCalculator::new()), ), crate::ContainerType::Map => ( depth, @@ -764,19 +766,45 @@ impl DiffCalculatorTrait for ListDiffCalculator { } } -#[derive(Debug, Default)] +#[derive(Debug)] pub(crate) struct RichtextDiffCalculator { - start_vv: VersionVector, - tracker: Box, - styles: Vec, + mode: Box, +} + +#[derive(Debug)] +enum RichtextCalcMode { + Crdt { + tracker: Box, + styles: Vec, + start_vv: VersionVector, + }, + Linear { + diff: DeltaRope, + last_style_start: Option<(Arc, u32)>, + }, } impl RichtextDiffCalculator { + pub fn new() -> Self { + Self { + mode: Box::new(RichtextCalcMode::Crdt { + tracker: Box::new(RichtextTracker::new_with_unknown()), + styles: Vec::new(), + start_vv: VersionVector::new(), + }), + } + } + /// This should be called after calc_diff /// /// TODO: Refactor, this can be simplified pub fn get_id_latest_pos(&self, id: ID) -> Option { - self.tracker.get_target_id_latest_index_at_new_version(id) + match &*self.mode { + RichtextCalcMode::Crdt { tracker, .. } => { + tracker.get_target_id_latest_index_at_new_version(id) + } + RichtextCalcMode::Linear { .. } => unreachable!(), + } } } @@ -787,13 +815,42 @@ impl DiffCalculatorTrait for RichtextDiffCalculator { vv: &crate::VersionVector, mode: DiffMode, ) { - if !vv.includes_vv(&self.start_vv) || !self.tracker.all_vv().includes_vv(vv) { - self.tracker = Box::new(RichtextTracker::new_with_unknown()); - self.styles.clear(); - self.start_vv = vv.clone(); + match mode { + DiffMode::Linear => { + self.mode = Box::new(RichtextCalcMode::Linear { + diff: DeltaRope::new(), + last_style_start: None, + }); + } + _ => { + if !matches!(&*self.mode, RichtextCalcMode::Crdt { .. }) { + self.mode = Box::new(RichtextCalcMode::Crdt { + tracker: Box::new(RichtextTracker::new_with_unknown()), + styles: Vec::new(), + start_vv: vv.clone(), + }); + + return; + } + } } - self.tracker.checkout(vv); + match &mut *self.mode { + RichtextCalcMode::Crdt { + tracker, + styles, + start_vv, + } => { + if !vv.includes_vv(start_vv) || tracker.all_vv().includes_vv(vv) { + *tracker = Box::new(RichtextTracker::new_with_unknown()); + styles.clear(); + *start_vv = vv.clone(); + } + + tracker.checkout(vv); + } + RichtextCalcMode::Linear { .. } => {} + } } fn apply_change( @@ -802,102 +859,178 @@ impl DiffCalculatorTrait for RichtextDiffCalculator { op: crate::op::RichOp, vv: Option<&crate::VersionVector>, ) { - if let Some(vv) = vv { - self.tracker.checkout(vv); - } - match &op.raw_op().content { - crate::op::InnerContent::List(l) => match l { - InnerListOp::Insert { .. } | InnerListOp::Move { .. } | InnerListOp::Set { .. } => { - unreachable!() - } - crate::container::list::list_op::InnerListOp::InsertText { - slice: _, - unicode_start, - unicode_len: len, - pos, - } => { - self.tracker.insert( - op.id_full(), - *pos as usize, - RichtextChunk::new_text(*unicode_start..*unicode_start + *len), - ); - } - crate::container::list::list_op::InnerListOp::Delete(del) => { - self.tracker.delete( - op.id_start(), - del.id_start, - del.start() as usize, - del.atom_len(), - del.is_reversed(), - ); - } - crate::container::list::list_op::InnerListOp::StyleStart { - start, - end, - key, - info, - value, - } => { - debug_assert!(start < end, "start: {}, end: {}", start, end); - let style_id = self.styles.len(); - self.styles.push(StyleOp { - lamport: op.lamport(), - peer: op.peer, - cnt: op.id_start().counter, - key: key.clone(), - value: value.clone(), - info: *info, - }); - self.tracker.insert( - op.id_full(), - *start as usize, - RichtextChunk::new_style_anchor(style_id as u32, AnchorType::Start), - ); - } - crate::container::list::list_op::InnerListOp::StyleEnd => { - let id = op.id(); - // PERF: this can be sped up by caching the last style op - let start_op = oplog.get_op(op.id().inc(-1)).unwrap(); - let InnerListOp::StyleStart { - start: _, + trace!("apply_change: {:?}", op.id()); + match &mut *self.mode { + RichtextCalcMode::Linear { + diff, + last_style_start, + } => match &op.raw_op().content { + crate::op::InnerContent::List(l) => match l { + InnerListOp::Insert { .. } + | InnerListOp::Move { .. } + | InnerListOp::Set { .. } => { + unreachable!() + } + crate::container::list::list_op::InnerListOp::InsertText { + slice: _, + unicode_start, + unicode_len: len, + pos, + } => { + let s = oplog.arena.slice_by_unicode( + *unicode_start as usize..(*unicode_start + *len) as usize, + ); + diff.insert_value( + *pos as usize, + RichtextStateChunk::new_text(s, op.id_full()), + (), + ); + } + crate::container::list::list_op::InnerListOp::Delete(del) => { + diff.delete(del.start() as usize, del.atom_len()); + } + crate::container::list::list_op::InnerListOp::StyleStart { + start, end, key, - value, info, - } = start_op.content.as_list().unwrap() - else { - unreachable!() - }; - let style_id = match self.styles.last() { - Some(last) if last.peer == id.peer && last.cnt == id.counter - 1 => { - self.styles.len() - 1 + value, + } => { + debug_assert!(start < end, "start: {}, end: {}", start, end); + let style_op = Arc::new(StyleOp { + lamport: op.lamport(), + peer: op.peer, + cnt: op.id_start().counter, + key: key.clone(), + value: value.clone(), + info: *info, + }); + + *last_style_start = Some((style_op.clone(), *end)); + diff.insert_value( + *start as usize, + RichtextStateChunk::new_style(style_op, AnchorType::Start), + (), + ); + } + crate::container::list::list_op::InnerListOp::StyleEnd => { + let (style_op, pos) = last_style_start.take().unwrap(); + assert_eq!(style_op.peer, op.peer); + assert_eq!(style_op.cnt, op.id_start().counter - 1); + diff.insert_value( + pos as usize + 1, + RichtextStateChunk::new_style(style_op, AnchorType::End), + (), + ); + } + }, + _ => unreachable!(), + }, + RichtextCalcMode::Crdt { + tracker, + styles, + start_vv, + } => { + if let Some(vv) = vv { + tracker.checkout(vv); + } + match &op.raw_op().content { + crate::op::InnerContent::List(l) => match l { + InnerListOp::Insert { .. } + | InnerListOp::Move { .. } + | InnerListOp::Set { .. } => { + unreachable!() } - _ => { - self.styles.push(StyleOp { - lamport: op.lamport() - 1, - peer: id.peer, - cnt: id.counter - 1, + crate::container::list::list_op::InnerListOp::InsertText { + slice: _, + unicode_start, + unicode_len: len, + pos, + } => { + tracker.insert( + op.id_full(), + *pos as usize, + RichtextChunk::new_text(*unicode_start..*unicode_start + *len), + ); + } + crate::container::list::list_op::InnerListOp::Delete(del) => { + tracker.delete( + op.id_start(), + del.id_start, + del.start() as usize, + del.atom_len(), + del.is_reversed(), + ); + } + crate::container::list::list_op::InnerListOp::StyleStart { + start, + end, + key, + info, + value, + } => { + debug_assert!(start < end, "start: {}, end: {}", start, end); + let style_id = styles.len(); + styles.push(StyleOp { + lamport: op.lamport(), + peer: op.peer, + cnt: op.id_start().counter, key: key.clone(), value: value.clone(), info: *info, }); - self.styles.len() - 1 + tracker.insert( + op.id_full(), + *start as usize, + RichtextChunk::new_style_anchor(style_id as u32, AnchorType::Start), + ); } - }; - self.tracker.insert( - op.id_full(), - // need to shift 1 because we insert the start style anchor before this pos - *end as usize + 1, - RichtextChunk::new_style_anchor(style_id as u32, AnchorType::End), - ); + crate::container::list::list_op::InnerListOp::StyleEnd => { + let id = op.id(); + // PERF: this can be sped up by caching the last style op + let start_op = oplog.get_op(op.id().inc(-1)).unwrap(); + let InnerListOp::StyleStart { + start: _, + end, + key, + value, + info, + } = start_op.content.as_list().unwrap() + else { + unreachable!() + }; + let style_id = match styles.last() { + Some(last) + if last.peer == id.peer && last.cnt == id.counter - 1 => + { + styles.len() - 1 + } + _ => { + styles.push(StyleOp { + lamport: op.lamport() - 1, + peer: id.peer, + cnt: id.counter - 1, + key: key.clone(), + value: value.clone(), + info: *info, + }); + styles.len() - 1 + } + }; + tracker.insert( + op.id_full(), + // need to shift 1 because we insert the start style anchor before this pos + *end as usize + 1, + RichtextChunk::new_style_anchor(style_id as u32, AnchorType::End), + ); + } + }, + _ => unreachable!(), } - }, - _ => unreachable!(), + } } } - fn finish_this_round(&mut self) {} - fn calculate_diff( &mut self, oplog: &OpLog, @@ -905,81 +1038,117 @@ impl DiffCalculatorTrait for RichtextDiffCalculator { to: &crate::VersionVector, _: impl FnMut(&ContainerID), ) -> (InternalDiff, DiffMode) { - // TODO: PERF: It can be linearized in certain cases - tracing::debug!("CalcDiff {:?} {:?}", from, to); - let mut delta = Delta::new(); - for item in self.tracker.diff(from, to) { - match item { - CrdtRopeDelta::Retain(len) => { - delta = delta.retain(len); - } - CrdtRopeDelta::Insert { - chunk: value, - id, - lamport, - } => match value.value() { - RichtextChunkValue::Text(text) => { - delta = delta.insert(RichtextStateChunk::Text( - // PERF: can be speedup by acquiring lock on arena - TextChunk::new( - oplog - .arena - .slice_by_unicode(text.start as usize..text.end as usize), - IdFull::new(id.peer, id.counter, lamport.unwrap()), - ), - )); - } - RichtextChunkValue::StyleAnchor { id, anchor_type } => { - delta = delta.insert(RichtextStateChunk::Style { - style: Arc::new(self.styles[id as usize].clone()), - anchor_type, - }); - } - RichtextChunkValue::Unknown(len) => { - // assert not unknown id - assert_ne!(id.peer, PeerID::MAX); - let mut acc_len = 0; - for rich_op in oplog.iter_ops(IdSpan::new( - id.peer, - id.counter, - id.counter + len as Counter, - )) { - acc_len += rich_op.content_len(); - let op = rich_op.op(); - let lamport = rich_op.lamport(); - let content = op.content.as_list().unwrap(); - match content { + match &mut *self.mode { + RichtextCalcMode::Linear { diff, .. } => { + trace!("end with linear mode"); + ( + InternalDiff::RichtextRaw(std::mem::take(diff)), + DiffMode::Linear, + ) + } + RichtextCalcMode::Crdt { + tracker, styles, .. + } => { + trace!("end with crdt mode"); + tracing::debug!("CalcDiff {:?} {:?}", from, to); + trace!("tracker version vector = {:?}", tracker.all_vv()); + let mut delta = DeltaRope::new(); + for item in tracker.diff(from, to) { + match item { + CrdtRopeDelta::Retain(len) => { + delta.push_retain(len, ()); + } + CrdtRopeDelta::Insert { + chunk: value, + id, + lamport, + } => match value.value() { + RichtextChunkValue::Text(text) => { + delta.push_insert( + RichtextStateChunk::Text( + // PERF: can be speedup by acquiring lock on arena + TextChunk::new( + oplog.arena.slice_by_unicode( + text.start as usize..text.end as usize, + ), + IdFull::new(id.peer, id.counter, lamport.unwrap()), + ), + ), + (), + ); + } + RichtextChunkValue::StyleAnchor { id, anchor_type } => { + delta.push_insert( + RichtextStateChunk::Style { + style: Arc::new(styles[id as usize].clone()), + anchor_type, + }, + (), + ); + } + RichtextChunkValue::Unknown(len) => { + // assert not unknown id + assert_ne!(id.peer, PeerID::MAX); + let mut acc_len = 0; + for rich_op in oplog.iter_ops(IdSpan::new( + id.peer, + id.counter, + id.counter + len as Counter, + )) { + acc_len += rich_op.content_len(); + let op = rich_op.op(); + let lamport = rich_op.lamport(); + let content = op.content.as_list().unwrap(); + match content { crate::container::list::list_op::InnerListOp::InsertText { slice, .. } => { - delta = delta.insert(RichtextStateChunk::Text(TextChunk::new( - slice.clone(), - IdFull::new(id.peer, op.counter, lamport), - ))); + delta.push_insert( + RichtextStateChunk::Text(TextChunk::new( + slice.clone(), + IdFull::new(id.peer, op.counter, lamport), + )), + (), + ); } _ => unreachable!("{:?}", content), } - } + } - debug_assert_eq!(acc_len, len as usize); + debug_assert_eq!(acc_len, len as usize); + } + RichtextChunkValue::MoveAnchor => unreachable!(), + }, + CrdtRopeDelta::Delete(len) => { + delta.push_delete(len); + } } - RichtextChunkValue::MoveAnchor => unreachable!(), - }, - CrdtRopeDelta::Delete(len) => { - delta = delta.delete(len); } + + (InternalDiff::RichtextRaw(delta), DiffMode::Checkout) } } + } - (InternalDiff::RichtextRaw(delta), DiffMode::Checkout) + fn finish_this_round(&mut self) { + match &mut *self.mode { + RichtextCalcMode::Crdt { .. } => {} + RichtextCalcMode::Linear { + diff, + last_style_start, + } => { + *diff = DeltaRope::new(); + last_style_start.take(); + } + } } } #[derive(Debug)] pub(crate) struct MovableListDiffCalculator { container_idx: ContainerIdx, - list: ListDiffCalculator, + list: Box, changed_elements: FxHashMap, current_mode: DiffMode, } @@ -1284,3 +1453,19 @@ impl MovableListDiffCalculator { } } } + +#[test] +fn test_size() { + let text = RichtextDiffCalculator::new(); + let size = std::mem::size_of_val(&text); + assert!(size < 50, "RichtextDiffCalculator size: {}", size); + let list = MovableListDiffCalculator::new(ContainerIdx::from_index_and_type( + 0, + loro_common::ContainerType::MovableList, + )); + let size = std::mem::size_of_val(&list); + assert!(size < 50, "MovableListDiffCalculator size: {}", size); + let calc = ContainerDiffCalculator::Richtext(text); + let size = std::mem::size_of_val(&calc); + assert!(size < 50, "ContainerDiffCalculator size: {}", size); +} diff --git a/crates/loro-internal/src/event.rs b/crates/loro-internal/src/event.rs index 28c81d24..af2869d5 100644 --- a/crates/loro-internal/src/event.rs +++ b/crates/loro-internal/src/event.rs @@ -234,7 +234,7 @@ impl DiffVariant { pub(crate) enum InternalDiff { ListRaw(Delta), /// This always uses entity indexes. - RichtextRaw(Delta), + RichtextRaw(DeltaRope), Map(MapDelta), Tree(TreeDelta), MovableList(MovableListInnerDelta), @@ -356,7 +356,9 @@ impl InternalDiff { Ok(InternalDiff::ListRaw(a.compose(b))) } (InternalDiff::RichtextRaw(a), InternalDiff::RichtextRaw(b)) => { - Ok(InternalDiff::RichtextRaw(a.compose(b))) + let mut ans = a.clone(); + ans.compose(&b); + Ok(InternalDiff::RichtextRaw(ans)) } (InternalDiff::Map(a), InternalDiff::Map(b)) => Ok(InternalDiff::Map(a.compose(b))), (InternalDiff::Tree(a), InternalDiff::Tree(b)) => Ok(InternalDiff::Tree(a.compose(b))), diff --git a/crates/loro-internal/src/state/richtext_state.rs b/crates/loro-internal/src/state/richtext_state.rs index 3572819d..92b0b8cf 100644 --- a/crates/loro-internal/src/state/richtext_state.rs +++ b/crates/loro-internal/src/state/richtext_state.rs @@ -7,6 +7,7 @@ use fxhash::{FxHashMap, FxHashSet}; use generic_btree::rle::HasLength; use loro_common::{ContainerID, InternalString, LoroError, LoroResult, LoroValue, ID}; use loro_delta::DeltaRopeBuilder; +use tracing::trace; use crate::{ arena::SharedArena, @@ -282,6 +283,7 @@ impl ContainerState for RichtextState { unreachable!() }; + trace!("diff = {:#?}, mode = {:?}", richtext, _ctx.mode); // tracing::info!("Self state = {:#?}", &self); // PERF: compose delta let mut ans: TextDiff = TextDiff::new(); @@ -290,156 +292,167 @@ impl ContainerState for RichtextState { let mut entity_index = 0; let mut event_index = 0; let mut new_style_deltas: Vec = Vec::new(); - for span in richtext.vec.iter() { + for span in richtext.iter() { match span { - crate::delta::DeltaItem::Retain { retain: len, .. } => { + loro_delta::DeltaItem::Retain { len, .. } => { entity_index += len; } - crate::delta::DeltaItem::Insert { insert: value, .. } => { - match value { - RichtextStateChunk::Text(s) => { - let (pos, styles) = self.state.get_mut().insert_elem_at_entity_index( - entity_index, - RichtextStateChunk::Text(s.clone()), - ); - let insert_styles = styles.clone().into(); + loro_delta::DeltaItem::Replace { value, delete, .. } => { + if *delete > 0 { + // Deletions + let mut deleted_style_keys: FxHashSet = + FxHashSet::default(); + let DrainInfo { + start_event_index: start, + end_event_index: end, + affected_style_range, + } = self.state.get_mut().drain_by_entity_index( + entity_index, + *delete, + Some(&mut |c| match c { + RichtextStateChunk::Style { + style, + anchor_type: AnchorType::Start, + } => { + deleted_style_keys.insert(style.key.clone()); + } + RichtextStateChunk::Style { + style, + anchor_type: AnchorType::End, + } => { + deleted_style_keys.insert(style.key.clone()); + } + _ => {} + }), + ); - if pos > event_index { - ans.push_retain(pos - event_index, Default::default()); - } - event_index = pos + s.event_len() as usize; - ans.push_insert(StringSlice::from(s.bytes().clone()), insert_styles); + if start > event_index { + ans.push_retain(start - event_index, Default::default()); + event_index = start; } - RichtextStateChunk::Style { anchor_type, style } => { - let (new_event_index, _) = - self.state.get_mut().insert_elem_at_entity_index( - entity_index, - RichtextStateChunk::Style { - style: style.clone(), - anchor_type: *anchor_type, - }, - ); - if new_event_index > event_index { - ans.push_retain(new_event_index - event_index, Default::default()); - // inserting style anchor will not affect event_index's positions - event_index = new_event_index; + if let Some((entity_range, event_range)) = affected_style_range { + let mut delta: TextDiff = DeltaRopeBuilder::new() + .retain(event_range.start, Default::default()) + .build(); + let mut entity_len_sum = 0; + let expected_sum = entity_range.len(); + + for IterRangeItem { + event_len, + chunk, + styles, + entity_len, + .. + } in self.state.get_mut().iter_range(entity_range) + { + entity_len_sum += entity_len; + match chunk { + RichtextStateChunk::Text(_) => { + let mut style_meta: StyleMeta = styles.into(); + for key in deleted_style_keys.iter() { + if !style_meta.contains_key(key) { + style_meta.insert( + key.clone(), + StyleMetaItem { + lamport: 0, + peer: 0, + value: LoroValue::Null, + }, + ) + } + } + delta.push_retain(event_len, style_meta); + } + RichtextStateChunk::Style { .. } => {} + } } - match anchor_type { - AnchorType::Start => { - style_starts.insert( - style.clone(), - Pos { - entity_index, - event_index: new_event_index, + debug_assert_eq!(entity_len_sum, expected_sum); + delta.chop(); + style_delta.compose(&delta); + } + + ans.push_delete(end - start); + } + + if value.rle_len() > 0 { + // Insertions + match value { + RichtextStateChunk::Text(s) => { + let (pos, styles) = + self.state.get_mut().insert_elem_at_entity_index( + entity_index, + RichtextStateChunk::Text(s.clone()), + ); + let insert_styles = styles.clone().into(); + + if pos > event_index { + ans.push_retain(pos - event_index, Default::default()); + } + event_index = pos + s.event_len() as usize; + ans.push_insert( + StringSlice::from(s.bytes().clone()), + insert_styles, + ); + } + RichtextStateChunk::Style { anchor_type, style } => { + let (new_event_index, _) = + self.state.get_mut().insert_elem_at_entity_index( + entity_index, + RichtextStateChunk::Style { + style: style.clone(), + anchor_type: *anchor_type, }, ); + + if new_event_index > event_index { + ans.push_retain( + new_event_index - event_index, + Default::default(), + ); + // inserting style anchor will not affect event_index's positions + event_index = new_event_index; } - AnchorType::End => { - // get the pair of style anchor. now we can annotate the range - let Pos { - entity_index: start_entity_index, - event_index: start_event_index, - } = self.get_style_start(&mut style_starts, style); - let mut delta: TextDiff = DeltaRopeBuilder::new() - .retain(start_event_index, Default::default()) - .build(); - // we need to + 1 because we also need to annotate the end anchor - let event = - self.state.get_mut().annotate_style_range_with_event( - start_entity_index..entity_index + 1, + + match anchor_type { + AnchorType::Start => { + style_starts.insert( style.clone(), + Pos { + entity_index, + event_index: new_event_index, + }, ); - for (s, l) in event { - delta.push_retain(l, s); } - - delta.chop(); - new_style_deltas.push(delta); - } - } - } - } - entity_index += value.rle_len(); - } - crate::delta::DeltaItem::Delete { - delete: len, - attributes: _, - } => { - let mut deleted_style_keys: FxHashSet = FxHashSet::default(); - let DrainInfo { - start_event_index: start, - end_event_index: end, - affected_style_range, - } = self.state.get_mut().drain_by_entity_index( - entity_index, - *len, - Some(&mut |c| match c { - RichtextStateChunk::Style { - style, - anchor_type: AnchorType::Start, - } => { - deleted_style_keys.insert(style.key.clone()); - } - RichtextStateChunk::Style { - style, - anchor_type: AnchorType::End, - } => { - deleted_style_keys.insert(style.key.clone()); - } - _ => {} - }), - ); - - if start > event_index { - ans.push_retain(start - event_index, Default::default()); - event_index = start; - } - - if let Some((entity_range, event_range)) = affected_style_range { - let mut delta: TextDiff = DeltaRopeBuilder::new() - .retain(event_range.start, Default::default()) - .build(); - let mut entity_len_sum = 0; - let expected_sum = entity_range.len(); - - for IterRangeItem { - event_len, - chunk, - styles, - entity_len, - .. - } in self.state.get_mut().iter_range(entity_range) - { - entity_len_sum += entity_len; - match chunk { - RichtextStateChunk::Text(_) => { - let mut style_meta: StyleMeta = styles.into(); - for key in deleted_style_keys.iter() { - if !style_meta.contains_key(key) { - style_meta.insert( - key.clone(), - StyleMetaItem { - lamport: 0, - peer: 0, - value: LoroValue::Null, - }, - ) + AnchorType::End => { + // get the pair of style anchor. now we can annotate the range + let Pos { + entity_index: start_entity_index, + event_index: start_event_index, + } = self.get_style_start(&mut style_starts, style); + let mut delta: TextDiff = DeltaRopeBuilder::new() + .retain(start_event_index, Default::default()) + .build(); + // we need to + 1 because we also need to annotate the end anchor + let event = + self.state.get_mut().annotate_style_range_with_event( + start_entity_index..entity_index + 1, + style.clone(), + ); + for (s, l) in event { + delta.push_retain(l, s); } + + delta.chop(); + new_style_deltas.push(delta); } - delta.push_retain(event_len, style_meta); } - RichtextStateChunk::Style { .. } => {} } } - debug_assert_eq!(entity_len_sum, expected_sum); - delta.chop(); - style_delta.compose(&delta); + entity_index += value.rle_len(); } - - ans.push_delete(end - start); } } } @@ -458,80 +471,81 @@ impl ContainerState for RichtextState { unreachable!() }; + trace!("diff = {:#?}", richtext); let mut style_starts: FxHashMap, usize> = FxHashMap::default(); let mut entity_index = 0; - for span in richtext.vec.iter() { + for span in richtext.iter() { match span { - crate::delta::DeltaItem::Retain { - retain: len, - attributes: _, - } => { + loro_delta::DeltaItem::Retain { len, .. } => { entity_index += len; } - crate::delta::DeltaItem::Insert { - insert: value, - attributes: _, + loro_delta::DeltaItem::Replace { + value, + attr, + delete, } => { - match value { - RichtextStateChunk::Text(s) => { - self.state.get_mut().insert_elem_at_entity_index( - entity_index, - RichtextStateChunk::Text(s.clone()), - ); - } - RichtextStateChunk::Style { style, anchor_type } => { - self.state.get_mut().insert_elem_at_entity_index( - entity_index, - RichtextStateChunk::Style { - style: style.clone(), - anchor_type: *anchor_type, - }, - ); - - if *anchor_type == AnchorType::Start { - style_starts.insert(style.clone(), entity_index); - } else { - let start_pos = match style_starts.get(style) { - Some(x) => *x, - None => { - // This should be rare, so it should be fine to scan - let mut start_entity_index = 0; - for c in self.state.get_mut().iter_chunk() { - match c { - RichtextStateChunk::Style { - style: s, - anchor_type: AnchorType::Start, - } if style == s => { - break; - } - RichtextStateChunk::Text(t) => { - start_entity_index += t.unicode_len() as usize; - } - RichtextStateChunk::Style { .. } => { - start_entity_index += 1; - } - } - } - start_entity_index - } - }; - // we need to + 1 because we also need to annotate the end anchor - self.state.get_mut().annotate_style_range( - start_pos..entity_index + 1, - style.clone(), + if *delete > 0 { + // Deletions + self.state + .get_mut() + .drain_by_entity_index(entity_index, *delete, None); + } + if value.rle_len() > 0 { + // Insertions + match value { + RichtextStateChunk::Text(s) => { + self.state.get_mut().insert_elem_at_entity_index( + entity_index, + RichtextStateChunk::Text(s.clone()), ); } + RichtextStateChunk::Style { style, anchor_type } => { + self.state.get_mut().insert_elem_at_entity_index( + entity_index, + RichtextStateChunk::Style { + style: style.clone(), + anchor_type: *anchor_type, + }, + ); + + if *anchor_type == AnchorType::Start { + style_starts.insert(style.clone(), entity_index); + } else { + let start_pos = match style_starts.get(style) { + Some(x) => *x, + None => { + // This should be rare, so it should be fine to scan + let mut start_entity_index = 0; + for c in self.state.get_mut().iter_chunk() { + match c { + RichtextStateChunk::Style { + style: s, + anchor_type: AnchorType::Start, + } if style == s => { + break; + } + RichtextStateChunk::Text(t) => { + start_entity_index += + t.unicode_len() as usize; + } + RichtextStateChunk::Style { .. } => { + start_entity_index += 1; + } + } + } + start_entity_index + } + }; + // we need to + 1 because we also need to annotate the end anchor + self.state.get_mut().annotate_style_range( + start_pos..entity_index + 1, + style.clone(), + ); + } + } } + entity_index += value.rle_len(); } - entity_index += value.rle_len(); - } - crate::delta::DeltaItem::Delete { - delete: len, - attributes: _, - } => { - self.state - .get_mut() - .drain_by_entity_index(entity_index, *len, None); } } }