diff --git a/.vscode/settings.json b/.vscode/settings.json index 78240293..7c58563b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -12,6 +12,7 @@ "idspan", "insta", "Itertools", + "lamports", "Leeeon", "LOGSTORE", "napi", diff --git a/crates/loro-common/src/error.rs b/crates/loro-common/src/error.rs index 9672887b..2b0a972f 100644 --- a/crates/loro-common/src/error.rs +++ b/crates/loro-common/src/error.rs @@ -42,6 +42,8 @@ pub enum LoroError { StyleConfigMissing(InternalString), #[error("Unknown Error ({0})")] Unknown(Box), + #[error("The given ID ({0}) is not contained by the doc")] + InvalidFrontierIdNotFound(ID), } #[derive(Error, Debug)] diff --git a/crates/loro-internal/src/container/richtext/richtext_state.rs b/crates/loro-internal/src/container/richtext/richtext_state.rs index d4df8562..cb6a3eb2 100644 --- a/crates/loro-internal/src/container/richtext/richtext_state.rs +++ b/crates/loro-internal/src/container/richtext/richtext_state.rs @@ -8,7 +8,7 @@ use loro_common::{IdSpan, LoroValue, ID}; use serde::{ser::SerializeStruct, Serialize}; use std::{ fmt::{Display, Formatter}, - ops::RangeBounds, + ops::{Bound, RangeBounds}, }; use std::{ ops::{Add, AddAssign, Range, Sub}, @@ -35,7 +35,7 @@ use self::{ use super::{ query_by_len::{IndexQuery, QueryByLen}, - style_range_map::{self, IterAnchorItem, StyleRangeMap, Styles}, + style_range_map::{IterAnchorItem, StyleRangeMap, Styles}, AnchorType, RichtextSpan, StyleOp, }; @@ -399,6 +399,17 @@ impl RichtextStateChunk { }, } } + + pub fn entity_range_to_event_range(&self, range: Range) -> Range { + match self { + RichtextStateChunk::Text(t) => t.entity_range_to_event_range(range), + RichtextStateChunk::Style { .. } => { + assert_eq!(range.start, 0); + assert_eq!(range.end, 1); + 0..1 + } + } + } } impl DeltaValue for RichtextStateChunk { @@ -1638,12 +1649,10 @@ impl RichtextState { break; } - debug_log::debug_dbg!(start, end, &span.elem); let len = end - start; match span.elem { RichtextStateChunk::Text(s) => { let event_len = s.entity_range_to_event_range(start..end).len(); - debug_log::debug_dbg!(event_len); match ans.last_mut() { Some(last) if last.entity_end == entity_index => { last.entity_end += len; @@ -1676,7 +1685,7 @@ impl RichtextState { pos: usize, len: usize, mut f: Option<&mut dyn FnMut(RichtextStateChunk)>, - ) -> (usize, usize) { + ) -> DrainInfo { assert!( pos + len <= self.len_entity(), "pos: {}, len: {}, self.len(): {}", @@ -1698,6 +1707,8 @@ impl RichtextState { struct StyleRangeUpdater<'a> { style_ranges: Option<&'a mut StyleRangeMap>, current_index: usize, + start: usize, + end: usize, } impl<'a> StyleRangeUpdater<'a> { @@ -1707,9 +1718,12 @@ impl RichtextState { self.current_index += t.unicode_len() as usize; } RichtextStateChunk::Style { style, anchor_type } => { - if matches!(anchor_type, AnchorType::Start) { + if matches!(anchor_type, AnchorType::End) { + self.end = self.end.max(self.current_index); if let Some(s) = self.style_ranges.as_mut() { - s.remove_style(style, self.current_index); + let start = + s.remove_style_scanning_backward(style, self.current_index); + self.start = self.start.min(start); } } @@ -1722,6 +1736,22 @@ impl RichtextState { Self { style_ranges: style_ranges.map(|x| &mut **x), current_index: start_index, + end: 0, + start: usize::MAX, + } + } + + fn get_affected_range(&self, pos: usize) -> Option> { + if self.start == usize::MAX { + None + } else { + let start = self.start.min(pos); + let end = self.end.min(pos); + if start == end { + None + } else { + Some(start..end) + } } } } @@ -1758,10 +1788,22 @@ impl RichtextState { } }); + let affected_range = updater.get_affected_range(pos); if let Some(s) = self.style_ranges.as_mut() { s.delete(pos..pos + len); } - (start_f.event_index, start_f.event_index + event_len) + + DrainInfo { + start_event_index: start_f.event_index, + end_event_index: (start_f.event_index + event_len), + affected_style_range: affected_range.map(|entity_range| { + ( + entity_range.clone(), + self.entity_index_to_event_index(entity_range.start) + ..self.entity_index_to_event_index(entity_range.end), + ) + }), + } } else { let (end, end_f) = self .tree @@ -1774,13 +1816,30 @@ impl RichtextState { } } + let affected_range = updater.get_affected_range(pos); if let Some(s) = self.style_ranges.as_mut() { s.delete(pos..pos + len); } - (start_f.event_index, end_f.event_index) + + DrainInfo { + start_event_index: start_f.event_index, + end_event_index: end_f.event_index, + affected_style_range: affected_range.map(|entity_range| { + ( + entity_range.clone(), + self.entity_index_to_event_index(entity_range.start) + ..self.entity_index_to_event_index(entity_range.end), + ) + }), + } } } + fn entity_index_to_event_index(&self, index: usize) -> usize { + let cursor = self.tree.query::(&index).unwrap(); + self.cursor_to_event_index(cursor.cursor) + } + #[allow(unused)] pub(crate) fn check(&self) { self.tree.check(); @@ -2031,14 +2090,121 @@ impl RichtextState { } /// Iter style ranges in the given range in entity index - pub(crate) fn iter_style_range( + pub(crate) fn iter_range( &self, range: impl RangeBounds, - ) -> Option> { - self.style_ranges.as_ref().map(|x| x.iter_range(range)) + ) -> impl Iterator> + '_ { + let start = match range.start_bound() { + Bound::Included(x) => *x, + Bound::Excluded(x) => x + 1, + Bound::Unbounded => 0, + }; + let end = match range.end_bound() { + Bound::Included(x) => x + 1, + Bound::Excluded(x) => *x, + Bound::Unbounded => self.len_entity(), + }; + assert!(end > start); + assert!(end <= self.len_entity()); + // debug_log::debug_dbg!(start, end); + // debug_log::debug_dbg!(&self.tree); + let mut style_iter = self + .style_ranges + .as_ref() + .map(|x| x.iter_range(range)) + .into_iter() + .flatten(); + + let start = self.tree.query::(&start).unwrap(); + let end = self.tree.query::(&end).unwrap(); + let mut content_iter = self.tree.iter_range(start.cursor..end.cursor); + let mut style_left_len = usize::MAX; + let mut cur_style = style_iter + .next() + .map(|x| { + style_left_len = x.elem.len - x.start.unwrap_or(0); + &x.elem.styles + }) + .unwrap_or(&*EMPTY_STYLES); + let mut chunk = content_iter.next(); + let mut offset = 0; + let mut chunk_left_len = chunk + .as_ref() + .map(|x| { + let len = x.elem.rle_len(); + offset = x.start.unwrap_or(0); + x.end.map(|v| v.min(len)).unwrap_or(len) - offset + }) + .unwrap_or(0); + std::iter::from_fn(move || { + if chunk_left_len == 0 { + chunk = content_iter.next(); + chunk_left_len = chunk + .as_ref() + .map(|x| { + let len = x.elem.rle_len(); + x.end.map(|v| v.min(len)).unwrap_or(len) + }) + .unwrap_or(0); + offset = 0; + } + + let iter_chunk = chunk.as_ref()?; + // debug_log::debug_dbg!(&iter_chunk, &chunk, offset, chunk_left_len); + let styles = cur_style; + let iter_len; + let event_range; + if chunk_left_len >= style_left_len { + iter_len = style_left_len; + event_range = iter_chunk + .elem + .entity_range_to_event_range(offset..offset + iter_len); + chunk_left_len -= style_left_len; + offset += style_left_len; + style_left_len = 0; + } else { + iter_len = chunk_left_len; + event_range = iter_chunk + .elem + .entity_range_to_event_range(offset..offset + iter_len); + style_left_len -= chunk_left_len; + chunk_left_len = 0; + } + + if style_left_len == 0 { + cur_style = style_iter + .next() + .map(|x| { + style_left_len = x.elem.len; + &x.elem.styles + }) + .unwrap_or(&*EMPTY_STYLES); + } + + Some(IterRangeItem { + chunk: iter_chunk.elem, + styles, + entity_len: iter_len, + event_len: event_range.len(), + }) + }) } } +pub(crate) struct DrainInfo { + pub start_event_index: usize, + pub end_event_index: usize, + // entity range, event range + pub affected_style_range: Option<(Range, Range)>, +} + +pub(crate) struct IterRangeItem<'a> { + pub(crate) chunk: &'a RichtextStateChunk, + pub(crate) styles: &'a Styles, + pub(crate) entity_len: usize, + pub(crate) event_len: usize, +} + use converter::ContinuousIndexConverter; mod converter { use generic_btree::{rle::HasLength, Cursor}; diff --git a/crates/loro-internal/src/container/richtext/style_range_map.rs b/crates/loro-internal/src/container/richtext/style_range_map.rs index f6046d1d..165c81e0 100644 --- a/crates/loro-internal/src/container/richtext/style_range_map.rs +++ b/crates/loro-internal/src/container/richtext/style_range_map.rs @@ -11,7 +11,7 @@ use std::{ use fxhash::FxHashMap; use generic_btree::{ rle::{HasLength, Mergeable, Sliceable}, - BTree, BTreeTrait, LengthFinder, UseLengthFinder, + BTree, BTreeTrait, ElemSlice, LengthFinder, UseLengthFinder, }; use once_cell::sync::Lazy; @@ -295,16 +295,17 @@ impl StyleRangeMap { }) } - pub fn update_styles_from( + /// Update the styles from `pos` to the start of the document. + fn update_styles_scanning_backward( &mut self, pos: usize, - mut f: impl FnMut(&mut Styles) -> ControlFlow<()>, + mut f: impl FnMut(&mut Elem) -> ControlFlow<()>, ) { let mut cursor = self.tree.query::(&pos).map(|x| x.cursor); while let Some(inner_cursor) = cursor { - cursor = self.tree.next_elem(inner_cursor); + cursor = self.tree.prev_elem(inner_cursor); let node = self.tree.get_elem_mut(inner_cursor.leaf).unwrap(); - match f(&mut node.styles) { + match f(node) { ControlFlow::Continue(_) => {} ControlFlow::Break(_) => { break; @@ -316,7 +317,7 @@ impl StyleRangeMap { pub(crate) fn iter_range( &self, range: impl RangeBounds, - ) -> impl Iterator + '_ { + ) -> impl Iterator> + '_ { let start = match range.start_bound() { std::ops::Bound::Included(x) => *x, std::ops::Bound::Excluded(x) => *x + 1, @@ -331,9 +332,7 @@ impl StyleRangeMap { let start = self.tree.query::(&start).unwrap(); let end = self.tree.query::(&end).unwrap(); - self.tree - .iter_range(start.cursor..end.cursor) - .map(|x| x.elem) + self.tree.iter_range(start.cursor..end.cursor) } /// Return the expected style anchors with their indexes. @@ -384,8 +383,16 @@ impl StyleRangeMap { vec.into_iter() } - pub fn remove_style(&mut self, to_remove: &Arc, start_index: usize) { - self.update_styles_from(start_index, |styles| { + /// Remove the style scanning backward, return the start_entity_index + pub fn remove_style_scanning_backward( + &mut self, + to_remove: &Arc, + last_index: usize, + ) -> usize { + let mut removed_len = 0; + self.update_styles_scanning_backward(last_index, |elem| { + removed_len += elem.len; + let styles = &mut elem.styles; let key = to_remove.get_style_key(); let mut has_removed = false; if let Some(value) = styles.get_mut(&key) { @@ -401,6 +408,8 @@ impl StyleRangeMap { ControlFlow::Break(()) } }); + + last_index + 1 - removed_len } pub fn delete(&mut self, range: Range) { diff --git a/crates/loro-internal/src/dag.rs b/crates/loro-internal/src/dag.rs index 94a2dd38..58e2f98a 100644 --- a/crates/loro-internal/src/dag.rs +++ b/crates/loro-internal/src/dag.rs @@ -46,7 +46,7 @@ pub(crate) trait DagNode: HasLamport + HasId + HasLength + Debug + Sliceable { /// /// We have following invariance in DAG /// - All deps' lamports are smaller than current node's lamport -pub(crate) trait Dag { +pub(crate) trait Dag: Debug { type Node: DagNode; fn get(&self, id: ID) -> Option<&Self::Node>; diff --git a/crates/loro-internal/src/dag/iter.rs b/crates/loro-internal/src/dag/iter.rs index 3dbd0e18..beb11074 100644 --- a/crates/loro-internal/src/dag/iter.rs +++ b/crates/loro-internal/src/dag/iter.rs @@ -199,7 +199,7 @@ pub(crate) struct IterReturn<'a, T> { pub slice: Range, } -impl<'a, T: DagNode, D: Dag> DagCausalIter<'a, D> { +impl<'a, T: DagNode, D: Dag + Debug> DagCausalIter<'a, D> { pub fn new(dag: &'a D, from: Frontiers, target: IdSpanVector) -> Self { let mut out_degrees: FxHashMap = FxHashMap::default(); let mut succ: BTreeMap> = BTreeMap::default(); @@ -213,7 +213,7 @@ impl<'a, T: DagNode, D: Dag> DagCausalIter<'a, D> { } // traverse all nodes, calculate the out_degrees - // if out_degree is 0, then it can be iterate directly + // if out_degree is 0, then it can be iterated directly while let Some(id) = q.pop() { let client = id.peer; let node = dag.get(id).unwrap(); @@ -223,11 +223,12 @@ impl<'a, T: DagNode, D: Dag> DagCausalIter<'a, D> { deps.iter() .filter(|&dep| { if let Some(span) = target.get(&dep.peer) { - let ans = dep.counter >= span.min() && dep.counter <= span.max(); - if ans { + let included_in_target = + dep.counter >= span.min() && dep.counter <= span.max(); + if included_in_target { succ.entry(*dep).or_default().push(id); } - ans + included_in_target } else { false } diff --git a/crates/loro-internal/src/fuzz/richtext.rs b/crates/loro-internal/src/fuzz/richtext.rs index e337d32b..c989de38 100644 --- a/crates/loro-internal/src/fuzz/richtext.rs +++ b/crates/loro-internal/src/fuzz/richtext.rs @@ -137,7 +137,7 @@ impl Actor { debug_log::debug_log!("delta {:?}", text_deltas); text_h.apply_delta_with_txn(&mut txn, &text_deltas).unwrap(); - // println!("after {:?}\n", text_h.get_richtext_value()); + // debug_log::debug_log!("after {:?}\n", text_h.get_richtext_value()); } else { debug_dbg!(&event.container); unreachable!() @@ -1003,4 +1003,141 @@ mod failed_tests { ], ); } + + #[test] + fn checkout_err() { + test_multi_sites( + 5, + &mut [ + RichText { + site: 1, + pos: 72057594977517568, + value: 0, + action: RichTextAction::Insert, + }, + RichText { + site: 1, + pos: 279268526740791, + value: 18446744069419041023, + action: RichTextAction::Insert, + }, + RichText { + site: 1, + pos: 278391190192126, + value: 18446744070572146943, + action: RichTextAction::Mark(6196952189613637631), + }, + RichText { + site: 251, + pos: 863599313408753663, + value: 458499228937131, + action: RichTextAction::Mark(72308159810888675), + }, + ], + ) + } + + #[test] + fn checkout_err_2() { + test_multi_sites( + 3, + &mut [ + RichText { + site: 1, + pos: 0, + value: 14497138626449185274, + action: RichTextAction::Insert, + }, + RichText { + site: 1, + pos: 5, + value: 10, + action: RichTextAction::Mark(8536327904765227054), + }, + RichText { + site: 1, + pos: 14, + value: 6, + action: RichTextAction::Mark(13562224825669899), + }, + Checkout { + site: 1, + to: 522133279, + }, + ], + ) + } + + #[test] + fn checkout_err_3() { + test_multi_sites( + 5, + &mut [ + RichText { + site: 25, + pos: 18446490194317148160, + value: 18446744073709551615, + action: RichTextAction::Mark(18446744073709551615), + }, + SyncAll, + RichText { + site: 25, + pos: 48378530044185, + value: 9910452455013810176, + action: RichTextAction::Insert, + }, + RichText { + site: 4, + pos: 359156590005978116, + value: 72057576757069051, + action: RichTextAction::Insert, + }, + RichText { + site: 60, + pos: 289360692308608004, + value: 359156590005978116, + action: RichTextAction::Mark(289360751431254011), + }, + RichText { + site: 4, + pos: 18446744073709551364, + value: 18446744073709551615, + action: RichTextAction::Mark(18446744069482020863), + }, + ], + ) + } + + #[test] + fn iter_range_err() { + test_multi_sites( + 5, + &mut [ + RichText { + site: 1, + pos: 939589632, + value: 256, + action: RichTextAction::Insert, + }, + RichText { + site: 1, + pos: 279268526740791, + value: 18446744069419041023, + action: RichTextAction::Insert, + }, + RichText { + site: 1, + pos: 278383768546103, + value: 18446744069419041023, + action: RichTextAction::Mark(6196952189613637631), + }, + RichText { + site: 251, + pos: 863599313408753663, + value: 458499228937131, + action: RichTextAction::Mark(2378151169024582627), + }, + ], + ); + } } diff --git a/crates/loro-internal/src/loro.rs b/crates/loro-internal/src/loro.rs index acb60b3a..e54ca6a7 100644 --- a/crates/loro-internal/src/loro.rs +++ b/crates/loro-internal/src/loro.rs @@ -17,6 +17,7 @@ use crate::{ change::Timestamp, configure::Configure, container::{idx::ContainerIdx, richtext::config::StyleConfigMap, IntoContainerId}, + dag::DagUtils, encoding::{ decode_snapshot, export_snapshot, parse_header_and_body, EncodeMode, ParsedHeaderAndBody, }, @@ -664,6 +665,11 @@ impl LoroDoc { let mut state = self.state.lock().unwrap(); self.detached.store(true, Release); let mut calc = self.diff_calculator.lock().unwrap(); + for &f in frontiers.iter() { + if !oplog.dag.contains(f) { + return Err(LoroError::InvalidFrontierIdNotFound(f)); + } + } let before = &oplog.dag.frontiers_to_vv(&state.frontiers).unwrap(); let Some(after) = &oplog.dag.frontiers_to_vv(frontiers) else { return Err(LoroError::NotFoundError( diff --git a/crates/loro-internal/src/oplog/dag.rs b/crates/loro-internal/src/oplog/dag.rs index 4d7509ff..96e3abcf 100644 --- a/crates/loro-internal/src/oplog/dag.rs +++ b/crates/loro-internal/src/oplog/dag.rs @@ -5,6 +5,7 @@ use crate::dag::{Dag, DagNode}; use crate::id::{Counter, ID}; use crate::span::{HasId, HasLamport}; use crate::version::{Frontiers, ImVersionVector, VersionVector}; +use loro_common::HasIdSpan; use rle::{HasIndex, HasLength, Mergable, RleCollection, Sliceable}; use super::{AppDag, AppDagNode}; @@ -93,9 +94,15 @@ impl Dag for AppDag { peer: client_id, counter, } = id; - self.map - .get(&client_id) - .and_then(|rle| rle.get_by_atom_index(counter).map(|x| x.element)) + self.map.get(&client_id).and_then(|rle| { + rle.get_by_atom_index(counter).and_then(|x| { + if x.element.contains_id(id) { + Some(x.element) + } else { + None + } + }) + }) } fn vv(&self) -> VersionVector { diff --git a/crates/loro-internal/src/state.rs b/crates/loro-internal/src/state.rs index 7f2e5515..c0f57b4b 100644 --- a/crates/loro-internal/src/state.rs +++ b/crates/loro-internal/src/state.rs @@ -91,7 +91,7 @@ pub(crate) trait ContainerState: Clone { state: &Weak>, ); - fn apply_op(&mut self, raw_op: &RawOp, op: &Op) -> LoroResult<()>; + fn apply_local_op(&mut self, raw_op: &RawOp, op: &Op) -> LoroResult<()>; /// Convert a state to a diff, such that an empty state will be transformed into the same as this state when it's applied. fn to_diff( &mut self, @@ -154,8 +154,8 @@ impl ContainerState for Box { self.as_mut().apply_diff(diff, arena, txn, state) } - fn apply_op(&mut self, raw_op: &RawOp, op: &Op) -> LoroResult<()> { - self.as_mut().apply_op(raw_op, op) + fn apply_local_op(&mut self, raw_op: &RawOp, op: &Op) -> LoroResult<()> { + self.as_mut().apply_local_op(raw_op, op) } #[doc = r" Convert a state to a diff, such that an empty state will be transformed into the same as this state when it's applied."] @@ -479,7 +479,7 @@ impl DocState { if self.in_txn { self.changed_idx_in_txn.insert(op.container); } - state.apply_op(raw_op, op) + state.apply_local_op(raw_op, op) } pub(crate) fn start_txn(&mut self, origin: InternalString, local: bool) { diff --git a/crates/loro-internal/src/state/list_state.rs b/crates/loro-internal/src/state/list_state.rs index 2d6f3b74..99cb96b3 100644 --- a/crates/loro-internal/src/state/list_state.rs +++ b/crates/loro-internal/src/state/list_state.rs @@ -379,7 +379,7 @@ impl ContainerState for ListState { } } - fn apply_op(&mut self, op: &RawOp, _: &Op) -> LoroResult<()> { + fn apply_local_op(&mut self, op: &RawOp, _: &Op) -> LoroResult<()> { match &op.content { RawOpContent::Map(_) => unreachable!(), RawOpContent::Tree(_) => unreachable!(), diff --git a/crates/loro-internal/src/state/map_state.rs b/crates/loro-internal/src/state/map_state.rs index 5350babc..2b60ba23 100644 --- a/crates/loro-internal/src/state/map_state.rs +++ b/crates/loro-internal/src/state/map_state.rs @@ -79,7 +79,7 @@ impl ContainerState for MapState { self.apply_diff_and_convert(diff, arena, txn, state); } - fn apply_op(&mut self, op: &RawOp, _: &Op) -> LoroResult<()> { + fn apply_local_op(&mut self, op: &RawOp, _: &Op) -> LoroResult<()> { match &op.content { RawOpContent::Map(MapSet { key, value }) => { if value.is_none() { diff --git a/crates/loro-internal/src/state/richtext_state.rs b/crates/loro-internal/src/state/richtext_state.rs index 1ffd6219..4580b854 100644 --- a/crates/loro-internal/src/state/richtext_state.rs +++ b/crates/loro-internal/src/state/richtext_state.rs @@ -3,9 +3,9 @@ use std::{ sync::{Arc, Mutex, RwLock, Weak}, }; -use fxhash::FxHashMap; +use fxhash::{FxHashMap, FxHashSet}; use generic_btree::rle::HasLength; -use loro_common::{ContainerID, LoroResult, LoroValue, ID}; +use loro_common::{ContainerID, InternalString, LoroResult, LoroValue, ID}; use crate::{ arena::SharedArena, @@ -13,7 +13,7 @@ use crate::{ idx::ContainerIdx, richtext::{ config::StyleConfigMap, - richtext_state::{EntityRangeInfo, PosType}, + richtext_state::{DrainInfo, EntityRangeInfo, IterRangeItem, PosType}, AnchorType, RichtextState as InnerState, StyleOp, Styles, }, }, @@ -38,6 +38,11 @@ pub struct RichtextState { pub(crate) state: LazyLoad, } +struct Pos { + entity_index: usize, + event_index: usize, +} + impl RichtextState { #[inline] pub fn new(idx: ContainerIdx, config: Arc>) -> Self { @@ -71,6 +76,42 @@ impl RichtextState { LazyLoad::Dst(d) => d.diagnose(), } } + + fn get_style_start( + &mut self, + style_starts: &mut FxHashMap, Pos>, + style: &Arc, + ) -> Pos { + match style_starts.remove(style) { + Some(x) => x, + None => { + // this should happen rarely, so it should be fine to scan + let mut pos = Pos { + entity_index: 0, + event_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) => { + pos.entity_index += t.unicode_len() as usize; + pos.event_index += t.event_len() as usize; + } + RichtextStateChunk::Style { .. } => { + pos.entity_index += 1; + } + } + } + pos + } + } + } } impl Clone for RichtextState { @@ -118,10 +159,6 @@ impl ContainerState for RichtextState { // PERF: compose delta let mut ans: Delta = Delta::new(); let mut style_delta: Delta = Delta::new(); - struct Pos { - entity_index: usize, - event_index: usize, - } let mut style_starts: FxHashMap, Pos> = FxHashMap::default(); let mut entity_index = 0; @@ -165,34 +202,37 @@ impl ContainerState for RichtextState { event_index = new_event_index; } - if *anchor_type == AnchorType::Start { - style_starts.insert( - style.clone(), - Pos { - entity_index, - event_index: new_event_index, - }, - ); - } else { - // get the pair of style anchor. now we can annotate the range - let Pos { - entity_index: start_entity_index, - event_index: start_event_index, - } = style_starts.remove(style).unwrap(); - - let mut delta: Delta = - Delta::new().retain(start_event_index); - // 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 = delta.retain_with_meta(l, s); + match anchor_type { + AnchorType::Start => { + style_starts.insert( + style.clone(), + Pos { + entity_index, + 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: Delta = + Delta::new().retain(start_event_index); + // 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 = delta.retain_with_meta(l, s); + } - delta = delta.chop(); - style_delta = style_delta.compose(delta); + delta = delta.chop(); + style_delta = style_delta.compose(delta); + } } } } @@ -202,14 +242,28 @@ impl ContainerState for RichtextState { delete: len, attributes: _, } => { - let mut deleted_style_chunks = Vec::new(); - let (start, end) = self.state.get_mut().drain_by_entity_index( + 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| { - if matches!(c, RichtextStateChunk::Style { .. }) { - deleted_style_chunks.push(c); + 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()); + } + _ => {} }), ); @@ -218,60 +272,46 @@ impl ContainerState for RichtextState { event_index = start; } - for chunk in deleted_style_chunks { - if let RichtextStateChunk::Style { style, anchor_type } = chunk { - match anchor_type { - AnchorType::Start => { - style_starts.insert( - style, - Pos { - entity_index, - event_index, - }, - ); - } - AnchorType::End => { - let Pos { - entity_index: start_entity_index, - event_index: start_event_index, - } = style_starts.remove(&style).unwrap(); - if event_index == start_event_index { - debug_assert_eq!(start_entity_index, entity_index); - // deleted by this batch, can be ignored - continue; - } - - // Otherwise, we need to calculate the new styles with the key between the ranges - let mut delta: Delta = - Delta::new().retain(start_event_index); - if let Some(iter) = self - .state - .get_mut() - .iter_style_range(start_entity_index..entity_index) - { - for style_range in iter { - let mut style_meta: StyleMeta = - (&style_range.styles).into(); - if !style_meta.contains_key(&style.key) { - style_meta.insert( - style.key.clone(), - StyleMetaItem { - lamport: 0, - peer: 0, - value: LoroValue::Null, - }, - ) - } - delta = - delta.retain_with_meta(style_range.len, style_meta); + if let Some((entity_range, event_range)) = affected_style_range { + let mut delta: Delta = + Delta::new().retain(event_range.start); + let mut entity_len_sum = 0; + let expected_sum = entity_range.len(); + // debug_log::debug_dbg!(&entity_range); + for IterRangeItem { + event_len, + chunk, + styles, + entity_len, + .. + } in self.state.get_mut().iter_range(entity_range) + { + // debug_log::debug_dbg!(&chunk, entity_len); + 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 = delta.chop(); - style_delta = style_delta.compose(delta); + delta = delta.retain_with_meta(event_len, style_meta); } + RichtextStateChunk::Style { .. } => {} } } + + debug_assert_eq!(entity_len_sum, expected_sum); + delta = delta.chop(); + style_delta = style_delta.compose(delta); } ans = ans.delete(end - start); @@ -280,7 +320,6 @@ impl ContainerState for RichtextState { } // self.check_consistency_between_content_and_style_ranges(); - debug_assert!(style_starts.is_empty(), "Styles should always be paired"); let ans = ans.compose(style_delta); Diff::Text(ans) } @@ -329,11 +368,33 @@ impl ContainerState for RichtextState { if *anchor_type == AnchorType::Start { style_starts.insert(style.clone(), entity_index); } else { - let start_pos = - style_starts.get(style).expect("Style start not found"); + 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, + start_pos..entity_index + 1, style.clone(), ); } @@ -355,7 +416,7 @@ impl ContainerState for RichtextState { // self.check_consistency_between_content_and_style_ranges() } - fn apply_op(&mut self, r_op: &RawOp, op: &Op) -> LoroResult<()> { + fn apply_local_op(&mut self, r_op: &RawOp, op: &Op) -> LoroResult<()> { match &op.content { crate::op::InnerContent::List(l) => match l { list_op::InnerListOp::Insert { slice: _, pos: _ } => { @@ -387,6 +448,15 @@ impl ContainerState for RichtextState { value, info, } => { + // Behavior here is a little different from apply_diff. + // + // When apply_diff, we only do the mark when we have included both + // StyleStart and StyleEnd. + // + // When applying local op, we can do the mark when we have StyleStart. + // We can assume StyleStart and StyleEnd are always appear in a pair + // for apply_local_op. (Because for local behavior, when we mark, + // we always create a pair of style ops.) self.state.get_mut().mark_with_entity_index( *start as usize..*end as usize, Arc::new(StyleOp { diff --git a/crates/loro-internal/src/state/tree_state.rs b/crates/loro-internal/src/state/tree_state.rs index f6629de1..d772e40f 100644 --- a/crates/loro-internal/src/state/tree_state.rs +++ b/crates/loro-internal/src/state/tree_state.rs @@ -280,7 +280,7 @@ impl ContainerState for TreeState { self.apply_diff_and_convert(diff, arena, txn, state); } - fn apply_op(&mut self, raw_op: &RawOp, _op: &crate::op::Op) -> LoroResult<()> { + fn apply_local_op(&mut self, raw_op: &RawOp, _op: &crate::op::Op) -> LoroResult<()> { match raw_op.content { crate::op::RawOpContent::Tree(tree) => { let TreeOp { target, parent, .. } = tree; diff --git a/crates/loro/tests/loro_rust_test.rs b/crates/loro/tests/loro_rust_test.rs index 5f8c6f98..9d17d92c 100644 --- a/crates/loro/tests/loro_rust_test.rs +++ b/crates/loro/tests/loro_rust_test.rs @@ -30,6 +30,40 @@ fn get_change_at_lamport() { }) } +#[test] +fn time_travel() { + let mut doc = LoroDoc::new(); + let doc2 = LoroDoc::new(); + let text = doc.get_text("text"); + let text2 = doc2.get_text("text"); + doc.subscribe( + &text.id(), + Arc::new(move |x| { + let Some(text) = x.container.diff.as_text() else { + return; + }; + + let delta: Vec = text.iter().map(|x| x.into()).collect(); + dbg!(&delta); + text2.apply_delta(&delta).unwrap(); + }), + ); + + let text2 = doc2.get_text("text"); + text.insert(0, "[14497138626449185274] ").unwrap(); + doc.commit(); + text.mark(5..15, "link", true).unwrap(); + doc.commit(); + let f = doc.state_frontiers(); + text.mark(14..20, "bold", true).unwrap(); + doc.commit(); + assert_eq!(text.to_delta(), text2.to_delta()); + doc.checkout(&f).unwrap(); + assert_eq!(text.to_delta(), text2.to_delta()); + doc.attach(); + assert_eq!(text.to_delta(), text2.to_delta()); +} + #[test] fn travel_back_should_remove_styles() { let mut doc = LoroDoc::new(); @@ -44,6 +78,7 @@ fn travel_back_should_remove_styles() { }; let delta: Vec = text.iter().map(|x| x.into()).collect(); + // dbg!(&delta); text2.apply_delta(&delta).unwrap(); }), ); @@ -52,11 +87,17 @@ fn travel_back_should_remove_styles() { text.insert(0, "Hello world!").unwrap(); doc.commit(); let f = doc.state_frontiers(); + let mut f1 = f.clone(); + f1[0].counter += 1; text.mark(0..5, "bold", true).unwrap(); doc.commit(); + let f2 = doc.state_frontiers(); assert_eq!(text.to_delta(), text2.to_delta()); + doc.checkout(&f1).unwrap(); // checkout to the middle of the start anchor op and the end anchor op doc.checkout(&f).unwrap(); assert_eq!(text.to_delta(), text2.to_delta()); + doc.checkout(&f2).unwrap(); + assert_eq!(text.to_delta(), text2.to_delta()); } #[test]