Fix avoid rich text apply diff err when time travel (#256)

* fix: avoid enter invalid richtext state

* fix: only include the style when the doc contains both style start and style end

* fix: iter_range err in richtext state

* fix: richtext state iter range

* fix: iter range err

* fix: iter range

* chore: rm log

* fix: iter range

* fix: get affected range

* fix: return err if given checkout target is invalid
This commit is contained in:
Zixuan Chen 2024-01-21 19:51:27 +08:00 committed by GitHub
parent bbea78b9bf
commit 9e57ccbc00
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 572 additions and 132 deletions

View file

@ -12,6 +12,7 @@
"idspan",
"insta",
"Itertools",
"lamports",
"Leeeon",
"LOGSTORE",
"napi",

View file

@ -42,6 +42,8 @@ pub enum LoroError {
StyleConfigMissing(InternalString),
#[error("Unknown Error ({0})")]
Unknown(Box<str>),
#[error("The given ID ({0}) is not contained by the doc")]
InvalidFrontierIdNotFound(ID),
}
#[derive(Error, Debug)]

View file

@ -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<usize>) -> Range<usize> {
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<Range<usize>> {
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::<EntityQuery>(&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<usize>,
) -> Option<impl Iterator<Item = &style_range_map::Elem>> {
self.style_ranges.as_ref().map(|x| x.iter_range(range))
) -> impl Iterator<Item = IterRangeItem<'_>> + '_ {
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::<EntityQuery>(&start).unwrap();
let end = self.tree.query::<EntityQuery>(&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<usize>, Range<usize>)>,
}
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};

View file

@ -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::<LengthFinder>(&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<usize>,
) -> impl Iterator<Item = &Elem> + '_ {
) -> impl Iterator<Item = ElemSlice<'_, Elem>> + '_ {
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::<LengthFinder>(&start).unwrap();
let end = self.tree.query::<LengthFinder>(&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<StyleOp>, 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<StyleOp>,
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<usize>) {

View file

@ -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>;

View file

@ -199,7 +199,7 @@ pub(crate) struct IterReturn<'a, T> {
pub slice: Range<i32>,
}
impl<'a, T: DagNode, D: Dag<Node = T>> DagCausalIter<'a, D> {
impl<'a, T: DagNode, D: Dag<Node = T> + Debug> DagCausalIter<'a, D> {
pub fn new(dag: &'a D, from: Frontiers, target: IdSpanVector) -> Self {
let mut out_degrees: FxHashMap<ID, usize> = FxHashMap::default();
let mut succ: BTreeMap<ID, Vec<ID>> = BTreeMap::default();
@ -213,7 +213,7 @@ impl<'a, T: DagNode, D: Dag<Node = T>> 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<Node = T>> 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
}

View file

@ -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),
},
],
);
}
}

View file

@ -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(

View file

@ -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 {

View file

@ -91,7 +91,7 @@ pub(crate) trait ContainerState: Clone {
state: &Weak<Mutex<DocState>>,
);
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<T: ContainerState> ContainerState for Box<T> {
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) {

View file

@ -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!(),

View file

@ -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() {

View file

@ -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<RichtextStateLoader, InnerState>,
}
struct Pos {
entity_index: usize,
event_index: usize,
}
impl RichtextState {
#[inline]
pub fn new(idx: ContainerIdx, config: Arc<RwLock<StyleConfigMap>>) -> Self {
@ -71,6 +76,42 @@ impl RichtextState {
LazyLoad::Dst(d) => d.diagnose(),
}
}
fn get_style_start(
&mut self,
style_starts: &mut FxHashMap<Arc<StyleOp>, Pos>,
style: &Arc<StyleOp>,
) -> 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<StringSlice, StyleMeta> = Delta::new();
let mut style_delta: Delta<StringSlice, StyleMeta> = Delta::new();
struct Pos {
entity_index: usize,
event_index: usize,
}
let mut style_starts: FxHashMap<Arc<StyleOp>, 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<StringSlice, StyleMeta> =
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<StringSlice, StyleMeta> =
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<InternalString> = 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<StringSlice, StyleMeta> =
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<StringSlice, StyleMeta> =
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 {

View file

@ -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;

View file

@ -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<TextDelta> = 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<TextDelta> = 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]