diff --git a/crates/loro-internal/src/container/list/list_op.rs b/crates/loro-internal/src/container/list/list_op.rs index 5ba932d1..bfe4f3a7 100644 --- a/crates/loro-internal/src/container/list/list_op.rs +++ b/crates/loro-internal/src/container/list/list_op.rs @@ -140,6 +140,9 @@ impl HasId for DeleteSpanWithId { } impl Mergable for DeleteSpanWithId { + /// If two spans are mergeable, their ids should be continuous. + /// LHS's end id should be equal to RHS's start id. + /// But their spans may be in a reversed order. fn is_mergable(&self, rhs: &Self, _conf: &()) -> bool where Self: Sized, diff --git a/crates/loro-internal/src/container/richtext/fugue_span.rs b/crates/loro-internal/src/container/richtext/fugue_span.rs index 18b960fb..17da4633 100644 --- a/crates/loro-internal/src/container/richtext/fugue_span.rs +++ b/crates/loro-internal/src/container/richtext/fugue_span.rs @@ -4,7 +4,7 @@ use generic_btree::rle::{HasLength, Mergeable, Sliceable}; use loro_common::{CompactId, Counter, HasId, IdFull, IdSpan, Lamport, ID}; use serde::{Deserialize, Serialize}; -use super::AnchorType; +use super::{tracker::UNKNOWN_PEER_ID, AnchorType}; #[derive(Clone, PartialEq, Eq, Copy, Serialize, Deserialize)] pub(crate) struct RichtextChunk { @@ -171,6 +171,11 @@ impl Sliceable for RichtextChunk { #[derive(Debug, Clone, PartialEq, Eq, Copy)] pub(super) struct FugueSpan { pub id: IdFull, + /// Sometimes, the id of the span is just a placeholder. + /// This field is used to track the real id of the span. + /// We know the real id when deletion happens because we + /// have the start_id info for each deletion. + pub real_id: Option, /// The status at the current version pub status: Status, /// The status at the `new` version. @@ -218,6 +223,7 @@ impl Sliceable for FugueSpan { fn _slice(&self, range: Range) -> Self { Self { id: self.id.inc(range.start as Counter), + real_id: self.real_id.map(|x| x.inc(range.start as Counter)), status: self.status, diff_status: self.diff_status, origin_left: if range.start == 0 { @@ -257,6 +263,11 @@ impl Mergeable for FugueSpan { == self.id.counter + self.content.len() as Counter - 1 && self.origin_right == rhs.origin_right && self.content.can_merge(&rhs.content) + && ((self.real_id.is_none() && rhs.real_id.is_none()) + || (self.real_id.is_some() + && rhs.real_id.is_some() + && self.real_id.unwrap().inc(self.content.len() as i32) + == rhs.real_id.unwrap())) } fn merge_right(&mut self, rhs: &Self) { @@ -265,6 +276,7 @@ impl Mergeable for FugueSpan { fn merge_left(&mut self, left: &Self) { self.id = left.id; + self.real_id = left.real_id; self.origin_left = left.origin_left; self.content.merge_left(&left.content); } @@ -275,6 +287,11 @@ impl FugueSpan { pub fn new(id: IdFull, content: RichtextChunk) -> Self { Self { id, + real_id: if id.peer == UNKNOWN_PEER_ID { + None + } else { + Some(id.id()) + }, status: Status::default(), diff_status: None, origin_left: None, diff --git a/crates/loro-internal/src/container/richtext/richtext_state.rs b/crates/loro-internal/src/container/richtext/richtext_state.rs index 688725ae..2ce82a79 100644 --- a/crates/loro-internal/src/container/richtext/richtext_state.rs +++ b/crates/loro-internal/src/container/richtext/richtext_state.rs @@ -1670,14 +1670,18 @@ impl RichtextState { match span.elem { RichtextStateChunk::Text(s) => { let event_len = s.entity_range_to_event_range(start..end).len(); + let id = s.id().inc(start as i32); match ans.last_mut() { - Some(last) if last.entity_end == entity_index => { + Some(last) + if last.entity_end == entity_index + && last.id_start.inc(last.event_len as i32) == id => + { last.entity_end += len; last.event_len += event_len; } _ => { ans.push(EntityRangeInfo { - id_start: s.id(), + id_start: id, entity_start: entity_index, entity_end: entity_index + len, event_len, @@ -2317,7 +2321,7 @@ mod test { let ranges = self .state .get_text_entity_ranges(pos, len, PosType::Unicode); - for range in ranges { + for range in ranges.into_iter().rev() { self.state.drain_by_entity_index( range.entity_start, range.entity_end - range.entity_start, @@ -2427,6 +2431,8 @@ mod test { fn delete_text() { let mut wrapper = SimpleWrapper::default(); wrapper.insert(0, "Hello World!"); + assert_eq!(wrapper.state.len_unicode(), 12); + assert_eq!(wrapper.state.len_entity(), 12); wrapper.delete(0, 5); assert_eq!( wrapper.state.get_richtext_value().to_json_value(), @@ -2437,8 +2443,12 @@ mod test { ]) ); + assert_eq!(wrapper.state.len_unicode(), 7); + assert_eq!(wrapper.state.len_entity(), 7); wrapper.delete(1, 1); + assert_eq!(wrapper.state.len_unicode(), 6); + assert_eq!(wrapper.state.len_entity(), 6); assert_eq!( wrapper.state.get_richtext_value().to_json_value(), json!([ @@ -2449,6 +2459,8 @@ mod test { ); wrapper.delete(5, 1); + assert_eq!(wrapper.state.len_unicode(), 5); + assert_eq!(wrapper.state.len_entity(), 5); assert_eq!( wrapper.state.get_richtext_value().to_json_value(), json!([ @@ -2459,6 +2471,7 @@ mod test { ); wrapper.delete(0, 5); + assert_eq!(wrapper.state.len_unicode(), 0); assert_eq!( wrapper.state.get_richtext_value().to_json_value(), json!([]) diff --git a/crates/loro-internal/src/container/richtext/tracker.rs b/crates/loro-internal/src/container/richtext/tracker.rs index 114a3de0..958f77d8 100644 --- a/crates/loro-internal/src/container/richtext/tracker.rs +++ b/crates/loro-internal/src/container/richtext/tracker.rs @@ -29,7 +29,7 @@ impl Default for Tracker { } } -const UNKNOWN_PEER_ID: PeerID = u64::MAX; +pub(super) const UNKNOWN_PEER_ID: PeerID = u64::MAX; impl Tracker { pub fn new_with_unknown() -> Self { let mut this = Self { @@ -42,6 +42,7 @@ impl Tracker { let result = this.rope.tree.push(FugueSpan { content: RichtextChunk::new_unknown(u32::MAX / 4), id: IdFull::new(UNKNOWN_PEER_ID, 0, 0), + real_id: None, status: Status::default(), diff_status: None, origin_left: None, @@ -115,6 +116,11 @@ impl Tracker { FugueSpan { content, id: op_id, + real_id: if op_id.peer == UNKNOWN_PEER_ID { + None + } else { + Some(op_id.id()) + }, status: Status::default(), diff_status: None, origin_left: None, @@ -149,7 +155,14 @@ impl Tracker { /// If `reverse` is true, the deletion happens from the end of the range to the start. /// So the first op is the one that deletes element at `pos+len-1`, the last op /// is the one that deletes element at `pos`. - pub(crate) fn delete(&mut self, mut op_id: ID, pos: usize, mut len: usize, reverse: bool) { + pub(crate) fn delete( + &mut self, + mut op_id: ID, + target_start_id: ID, + pos: usize, + mut len: usize, + reverse: bool, + ) { // debug_log::group!("Tracker Delete"); // debug_log::debug_dbg!(&op_id, pos, len, reverse); // debug_log::debug_dbg!(&self); @@ -185,17 +198,15 @@ impl Tracker { // debug_log::debug_log!("after forwarding pos={} len={}", pos, len); // debug_log::debug_dbg!(&self); let mut ans = Vec::new(); - let split = self.rope.delete(pos, len, |span| { - let mut id_span = span.id_span(); - if reverse { - id_span.reverse(); - } - ans.push(id_span); - }); - - if reverse { - ans.reverse(); - } + let split = self + .rope + .delete(target_start_id, pos, len, reverse, &mut |span| { + let mut id_span = span.id_span(); + if reverse { + id_span.reverse(); + } + ans.push(id_span); + }); let mut cur_id = op_id; for id_span in ans { @@ -206,7 +217,9 @@ impl Tracker { } debug_assert_eq!(cur_id.counter - op_id.counter, len as Counter); - self.update_insert_by_split(&split.arr); + for s in split { + self.update_insert_by_split(&s.arr); + } let end_id = op_id.inc(len as Counter); self.current_vv.extend_to_include_end_id(end_id); @@ -397,7 +410,7 @@ mod test { fn test_retreat_and_forward_delete() { let mut t = Tracker::new(); t.insert(IdFull::new(1, 0, 0), 0, RichtextChunk::new_text(0..10)); - t.delete(ID::new(2, 0), 0, 10, true); + t.delete(ID::new(2, 0), ID::NONE_ID, 0, 10, true); t.checkout(&vv!(1 => 10, 2=>5)); assert_eq!(t.rope.len(), 5); t.checkout(&vv!(1 => 10, 2=>0)); @@ -412,7 +425,7 @@ mod test { fn test_checkout_in_doc_with_del_span() { let mut t = Tracker::new(); t.insert(IdFull::new(1, 0, 0), 0, RichtextChunk::new_text(0..10)); - t.delete(ID::new(2, 0), 0, 10, false); + t.delete(ID::new(2, 0), ID::NONE_ID, 0, 10, false); t.checkout(&vv!(1 => 10, 2=>4)); let v: Vec = t.rope.tree().iter().copied().collect(); assert_eq!(v.len(), 2); @@ -421,18 +434,4 @@ mod test { assert!(v[1].is_activated()); assert_eq!(v[1].rle_len(), 6); } - - #[test] - fn test_checkout_in_doc_with_reversed_del_span() { - let mut t = Tracker::new(); - t.insert(IdFull::new(1, 0, 0), 0, RichtextChunk::new_text(0..10)); - t.delete(ID::new(2, 0), 0, 10, true); - t.checkout(&vv!(1 => 10, 2=>4)); - let v: Vec = t.rope.tree().iter().copied().collect(); - assert_eq!(v.len(), 2); - assert!(v[0].is_activated()); - assert_eq!(v[0].rle_len(), 6); - assert!(!v[1].is_activated()); - assert_eq!(v[1].rle_len(), 4); - } } diff --git a/crates/loro-internal/src/container/richtext/tracker/crdt_rope.rs b/crates/loro-internal/src/container/richtext/tracker/crdt_rope.rs index b118ce2e..204b9d5a 100644 --- a/crates/loro-internal/src/container/richtext/tracker/crdt_rope.rs +++ b/crates/loro-internal/src/container/richtext/tracker/crdt_rope.rs @@ -5,11 +5,13 @@ use generic_btree::{ BTree, BTreeTrait, Cursor, FindResult, LeafIndex, Query, SplittedLeaves, }; use itertools::Itertools; -use loro_common::{Counter, HasCounter, HasCounterSpan, HasIdSpan, IdFull, IdSpan, ID}; -use smallvec::SmallVec; +use loro_common::{Counter, HasCounter, HasCounterSpan, HasIdSpan, IdSpan, Lamport, ID}; +use smallvec::{smallvec, SmallVec}; use crate::container::richtext::{fugue_span::DiffStatus, FugueSpan, RichtextChunk, Status}; +use super::UNKNOWN_PEER_ID; + #[derive(Debug, Default, Clone)] pub(super) struct CrdtRope { pub(super) tree: BTree, @@ -226,16 +228,39 @@ impl CrdtRope { } } + /// - If reversed is true, the deletion will be done in reversed order. + /// But the start_id always refers to the first delete op's id. + /// - If reversed is true, the returned `SplittedLeaves` will be in reversed order. pub(super) fn delete( &mut self, + mut start_id: ID, pos: usize, len: usize, - mut notify_deleted_span: impl FnMut(FugueSpan), - ) -> SplittedLeaves { + reversed: bool, + notify_deleted_span: &mut dyn FnMut(&FugueSpan), + ) -> SmallVec<[SplittedLeaves; 1]> { if len == 0 { return Default::default(); } + if reversed && len > 1 { + let mut ans = SmallVec::with_capacity(len); + for i in (0..len).rev() { + let a = self.delete( + start_id.inc((len - i - 1) as i32), + pos + i, + 1, + false, + notify_deleted_span, + ); + + ans.extend(a); + } + + return ans; + } + + debug_log::debug_dbg!(&start_id); let start = self .tree .query::(&(pos as i32)) @@ -249,25 +274,35 @@ impl CrdtRope { let (a, b) = elem.update_with_split(start.offset..start.offset + len, |elem| { assert!(elem.is_activated()); debug_assert_eq!(len, elem.rle_len()); - notify_deleted_span(*elem); + notify_deleted_span(elem); elem.status.delete_times += 1; + if elem.real_id.is_none() { + elem.real_id = Some(start_id); + } + + start_id = start_id.inc(elem.rle_len() as i32); }); (true, a, b) }); // debug_log::debug_dbg!(&splitted); - return splitted; + return smallvec![splitted]; } let end = self .tree .query::(&((pos + len) as i32)) .unwrap(); - self.tree.update(start..end.cursor(), &mut |elem| { + smallvec![self.tree.update(start..end.cursor(), &mut |elem| { if elem.is_activated() { - notify_deleted_span(*elem); + notify_deleted_span(elem); elem.status.delete_times += 1; + if elem.real_id.is_none() { + elem.real_id = Some(start_id); + } + + start_id = start_id.inc(elem.rle_len() as i32); Some(Cache { len: -(elem.rle_len() as i32), changed_num: 0, @@ -275,7 +310,7 @@ impl CrdtRope { } else { None } - }) + })] } #[allow(unused)] @@ -359,7 +394,12 @@ impl CrdtRope { DiffStatus::Created => { let rt = Some(CrdtRopeDelta::Insert { chunk: elem.content, - id: elem.id, + id: elem.real_id.unwrap(), + lamport: if elem.id.peer == UNKNOWN_PEER_ID { + None + } else { + Some(elem.id.lamport) + }, }); if index > last_pos { next = rt; @@ -410,7 +450,17 @@ impl CrdtRope { #[derive(Debug, Clone, PartialEq, Eq, Copy)] pub(crate) enum CrdtRopeDelta { Retain(usize), - Insert { chunk: RichtextChunk, id: IdFull }, + Insert { + chunk: RichtextChunk, + id: ID, + /// This is a optional field, because we may not know the correct lamport + /// for chunk id with UNKNOWN_PEER_ID. + /// + /// This case happens when the chunk is created by default placeholder and + /// the deletion happens that marks the chunk with its start_id. But it doesn't + /// know the correct lamport for the chunk. + lamport: Option, + }, Delete(usize), } @@ -621,7 +671,7 @@ impl LeafUpdate { mod test { use std::ops::Range; - use loro_common::{CompactId, Counter, PeerID, ID}; + use loro_common::{CompactId, Counter, IdFull, PeerID, ID}; use crate::container::richtext::RichtextChunk; @@ -735,7 +785,7 @@ mod test { let mut rope = CrdtRope::new(); rope.insert(0, span(0, 0..10), |_| panic!()); assert_eq!(rope.len(), 10); - rope.delete(5, 2, |_| {}); + rope.delete(ID::NONE_ID, 5, 2, false, &mut |_| {}); assert_eq!(rope.len(), 8); let fugue = rope.insert(6, span(1, 10..20), |_| panic!()).content; assert_eq!(fugue.origin_left, Some(CompactId::new(0, 7))); @@ -827,7 +877,8 @@ mod test { CrdtRopeDelta::Retain(2), CrdtRopeDelta::Insert { chunk: RichtextChunk::new_text(10..13), - id: IdFull::new(1, 0, 0) + id: ID::new(1, 0), + lamport: Some(0) } ], vec, @@ -851,7 +902,8 @@ mod test { assert_eq!( vec![CrdtRopeDelta::Insert { chunk: RichtextChunk::new_text(2..10), - id: IdFull::new(0, 2, 2) + id: ID::new(0, 2), + lamport: Some(2) }], vec, ); diff --git a/crates/loro-internal/src/diff_calc.rs b/crates/loro-internal/src/diff_calc.rs index b0b9c95c..50a5c0dc 100644 --- a/crates/loro-internal/src/diff_calc.rs +++ b/crates/loro-internal/src/diff_calc.rs @@ -5,7 +5,9 @@ use itertools::Itertools; use enum_dispatch::enum_dispatch; use fxhash::{FxHashMap, FxHashSet}; -use loro_common::{ContainerID, HasCounterSpan, HasIdSpan, LoroValue, PeerID, ID}; +use loro_common::{ + ContainerID, Counter, HasCounterSpan, HasIdSpan, IdFull, IdSpan, LoroValue, PeerID, ID, +}; use crate::{ change::Lamport, @@ -82,25 +84,9 @@ impl DiffCalculator { } let affected_set = if !self.has_all { // if we don't have all the ops, we need to calculate the diff by tracing back - let mut after = after; - let mut before = before; let mut merged = before.clone(); - let mut before_frontiers = before_frontiers; - let mut after_frontiers = after_frontiers; merged.merge(after); - let empty_vv: VersionVector = Default::default(); - if !after.includes_vv(before) { - // If after is not after before, we need to calculate the diff from the beginning - // - // This is required because of [MapDiffCalculator]. It can be removed with - // a better data structure. See #114. - before = &empty_vv; - after = &merged; - before_frontiers = None; - after_frontiers = None; - self.has_all = true; - self.last_vv = Default::default(); - } else if before.is_empty() { + if before.is_empty() { self.has_all = true; self.last_vv = Default::default(); } @@ -361,7 +347,7 @@ impl DiffCalculatorTrait for MapDiffCalculator { op: crate::op::RichOp, _vv: Option<&crate::VersionVector>, ) { - let map = op.op().content.as_map().unwrap(); + let map = op.raw_op().content.as_map().unwrap(); self.changed_key.insert(map.key.clone()); } @@ -492,6 +478,7 @@ impl DiffCalculatorTrait for ListDiffCalculator { 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(), @@ -519,7 +506,11 @@ impl DiffCalculatorTrait for ListDiffCalculator { CrdtRopeDelta::Retain(len) => { delta = delta.retain(len); } - CrdtRopeDelta::Insert { chunk: value, id } => match value.value() { + CrdtRopeDelta::Insert { + chunk: value, + id, + lamport, + } => match value.value() { RichtextChunkValue::Text(range) => { for i in range.clone() { let v = oplog.arena.get_value(i as usize); @@ -529,11 +520,39 @@ impl DiffCalculatorTrait for ListDiffCalculator { } delta = delta.insert(SliceRanges { ranges: smallvec::smallvec![SliceRange(range)], - id, + id: IdFull::new(id.peer, id.counter, lamport.unwrap()), }); } RichtextChunkValue::StyleAnchor { .. } => unreachable!(), - RichtextChunkValue::Unknown(_) => unreachable!(), + 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().as_insert().unwrap(); + let range = content.0.clone(); + for i in content.0 .0.clone() { + let v = oplog.arena.get_value(i as usize); + if let Some(LoroValue::Container(c)) = &v { + on_new_container(c); + } + } + + delta = delta.insert(SliceRanges { + ranges: smallvec::smallvec![range], + id: IdFull::new(id.peer, op.counter, lamport), + }); + } + + debug_assert_eq!(acc_len, len as usize); + } }, CrdtRopeDelta::Delete(len) => { delta = delta.delete(len); @@ -572,7 +591,7 @@ impl DiffCalculatorTrait for RichtextDiffCalculator { if let Some(vv) = vv { self.tracker.checkout(vv); } - match &op.op().content { + match &op.raw_op().content { crate::op::InnerContent::List(l) => match l { crate::container::list::list_op::InnerListOp::Insert { .. } => { unreachable!() @@ -592,6 +611,7 @@ impl DiffCalculatorTrait for RichtextDiffCalculator { 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(), @@ -648,7 +668,11 @@ impl DiffCalculatorTrait for RichtextDiffCalculator { CrdtRopeDelta::Retain(len) => { delta = delta.retain(len); } - CrdtRopeDelta::Insert { chunk: value, id } => match value.value() { + 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 @@ -656,7 +680,7 @@ impl DiffCalculatorTrait for RichtextDiffCalculator { oplog .arena .slice_by_unicode(text.start as usize..text.end as usize), - id, + IdFull::new(id.peer, id.counter, lamport.unwrap()), ), )); } @@ -666,7 +690,36 @@ impl DiffCalculatorTrait for RichtextDiffCalculator { anchor_type, }); } - RichtextChunkValue::Unknown(_) => unreachable!(), + RichtextChunkValue::Unknown(len) => { + // assert not unknown id + assert_ne!(id.peer, PeerID::MAX); + debug_log::debug_dbg!(oplog.changes(), id, len); + 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), + ))); + } + _ => unreachable!("{:?}", content), + } + } + + debug_assert_eq!(acc_len, len as usize); + } }, CrdtRopeDelta::Delete(len) => { delta = delta.delete(len); diff --git a/crates/loro-internal/src/encoding/encode_reordered.rs b/crates/loro-internal/src/encoding/encode_reordered.rs index a15e7370..fde40e53 100644 --- a/crates/loro-internal/src/encoding/encode_reordered.rs +++ b/crates/loro-internal/src/encoding/encode_reordered.rs @@ -2132,6 +2132,7 @@ mod value { leb128::read::signed(&mut self.raw).map_err(|_| LoroError::DecodeDataCorruptionError) } + #[allow(unused)] pub fn read_i32(&mut self) -> LoroResult { leb128::read::signed(&mut self.raw) .map(|x| x as i32) diff --git a/crates/loro-internal/src/fuzz/recursive_refactored.rs b/crates/loro-internal/src/fuzz/recursive_refactored.rs index 341ef120..7bb9784b 100644 --- a/crates/loro-internal/src/fuzz/recursive_refactored.rs +++ b/crates/loro-internal/src/fuzz/recursive_refactored.rs @@ -4547,6 +4547,72 @@ mod failed_tests { ) } + #[test] + fn fuzz_18() { + test_multi_sites( + 5, + &mut [ + List { + site: 1, + container_idx: 0, + key: 0, + value: I32(1), + }, + List { + site: 1, + container_idx: 0, + key: 0, + value: Container(C::List), + }, + List { + site: 1, + container_idx: 0, + key: 0, + value: Null, + }, + List { + site: 1, + container_idx: 0, + key: 0, + value: Null, + }, + ], + ) + } + + #[test] + fn fuzz_19() { + test_multi_sites( + 5, + &mut [ + List { + site: 1, + container_idx: 0, + key: 0, + value: I32(2), + }, + List { + site: 1, + container_idx: 0, + key: 0, + value: I32(1), + }, + List { + site: 1, + container_idx: 0, + key: 1, + value: Null, + }, + List { + site: 1, + container_idx: 0, + key: 0, + value: Null, + }, + ], + ) + } + #[test] fn to_minify() { minify_error(5, vec![], test_multi_sites, normalize) diff --git a/crates/loro-internal/src/group.rs b/crates/loro-internal/src/group.rs index b854e4ed..b73dd04c 100644 --- a/crates/loro-internal/src/group.rs +++ b/crates/loro-internal/src/group.rs @@ -6,7 +6,7 @@ use std::{ use enum_as_inner::EnumAsInner; use enum_dispatch::enum_dispatch; use fxhash::FxHashMap; -use loro_common::{Counter, HasId, InternalString, LoroValue, PeerID, ID}; +use loro_common::{Counter, HasId, HasLamport, InternalString, LoroValue, PeerID, ID}; use crate::{ change::{Change, Lamport}, @@ -128,15 +128,15 @@ impl MapOpGroup { impl OpGroupTrait for MapOpGroup { fn insert(&mut self, op: &RichOp) { - let key = match &op.op.content { + let key = match &op.raw_op().content { InnerContent::Map(map) => map.key.clone(), _ => unreachable!(), }; let entry = self.ops.entry(key).or_default(); entry.insert(GroupedMapOpInfo { - value: op.op.content.as_map().unwrap().value.clone(), - counter: op.op.counter, - lamport: op.lamport, + value: op.raw_op().content.as_map().unwrap().value.clone(), + counter: op.raw_op().counter, + lamport: op.lamport(), peer: op.peer, }); } @@ -183,13 +183,13 @@ pub(crate) struct TreeOpGroup { impl OpGroupTrait for TreeOpGroup { fn insert(&mut self, op: &RichOp) { - let tree_op = op.op.content.as_tree().unwrap(); + let tree_op = op.raw_op().content.as_tree().unwrap(); let target = tree_op.target; let parent = tree_op.parent; - let entry = self.ops.entry(op.lamport).or_default(); + let entry = self.ops.entry(op.lamport()).or_default(); entry.insert(GroupedTreeOpInfo { value: TreeOp { target, parent }, - counter: op.op.counter, + counter: op.raw_op().counter, peer: op.peer, }); } diff --git a/crates/loro-internal/src/op.rs b/crates/loro-internal/src/op.rs index 8dc90aa7..559cf879 100644 --- a/crates/loro-internal/src/op.rs +++ b/crates/loro-internal/src/op.rs @@ -6,7 +6,7 @@ use crate::{ }; use crate::{delta::DeltaValue, LoroValue}; use enum_as_inner::EnumAsInner; -use loro_common::{IdFull, IdSpan}; +use loro_common::{CounterSpan, IdFull, IdSpan}; use rle::{HasIndex, HasLength, Mergable, Sliceable}; use serde::{ser::SerializeSeq, Deserialize, Serialize}; use smallvec::SmallVec; @@ -84,9 +84,9 @@ impl RawOp<'_> { /// RichOp includes lamport and timestamp info, which is used for conflict resolution. #[derive(Debug, Clone)] pub struct RichOp<'a> { - pub op: &'a Op, + op: &'a Op, pub peer: PeerID, - pub lamport: Lamport, + lamport: Lamport, pub timestamp: Timestamp, pub start: usize, pub end: usize, @@ -264,11 +264,32 @@ impl<'a> RichOp<'a> { } } - pub fn get_sliced(&self) -> Op { - self.op.slice(self.start, self.end) + pub fn new_by_cnt_range(change: &Change, span: CounterSpan, op: &'a Op) -> Option { + let op_index_in_change = op.counter - change.id.counter; + let op_slice_start = (span.start - op.counter).clamp(0, op.atom_len() as i32); + let op_slice_end = (span.end - op.counter).clamp(0, op.atom_len() as i32); + if op_slice_start == op_slice_end { + return None; + } + Some(RichOp { + op, + peer: change.id.peer, + lamport: change.lamport + op_index_in_change as Lamport, + timestamp: change.timestamp, + start: op_slice_start as usize, + end: op_slice_end as usize, + }) } - pub fn op(&self) -> &Op { + pub fn op(&self) -> Cow<'_, Op> { + if self.start == 0 && self.end == self.op.content_len() { + Cow::Borrowed(self.op) + } else { + Cow::Owned(self.op.slice(self.start, self.end)) + } + } + + pub fn raw_op(&self) -> &Op { self.op } diff --git a/crates/loro-internal/src/oplog.rs b/crates/loro-internal/src/oplog.rs index 4a3dd8aa..97b51e11 100644 --- a/crates/loro-internal/src/oplog.rs +++ b/crates/loro-internal/src/oplog.rs @@ -7,12 +7,6 @@ use std::cmp::Ordering; use std::mem::take; use std::rc::Rc; -use fxhash::FxHashMap; -use loro_common::{HasCounter, HasId}; -use rle::{HasLength, RleCollection, RlePush, RleVec, Sliceable}; -use smallvec::SmallVec; -// use tabled::measurment::Percent; - use crate::change::{get_sys_timestamp, Change, Lamport, Timestamp}; use crate::configure::Configure; use crate::container::list::list_op; @@ -21,10 +15,14 @@ use crate::encoding::ParsedHeaderAndBody; use crate::encoding::{decode_oplog, encode_oplog, EncodeMode}; use crate::group::OpGroups; use crate::id::{Counter, PeerID, ID}; -use crate::op::{ListSlice, RawOpContent, RemoteOp}; +use crate::op::{ListSlice, RawOpContent, RemoteOp, RichOp}; use crate::span::{HasCounterSpan, HasIdSpan, HasLamportSpan}; use crate::version::{Frontiers, ImVersionVector, VersionVector}; use crate::LoroError; +use fxhash::FxHashMap; +use loro_common::{HasCounter, HasId, IdSpan}; +use rle::{HasLength, RleCollection, RlePush, RleVec, Sliceable}; +use smallvec::SmallVec; type ClientChanges = FxHashMap>; pub use self::dag::FrontiersNotIncluded; @@ -465,6 +463,34 @@ impl OpLog { .map(|c| c.lamport + (id.counter - c.id.counter) as Lamport) } + pub(crate) fn iter_ops(&self, id_span: IdSpan) -> impl Iterator + '_ { + self.changes + .get(&id_span.client_id) + .map(move |changes| { + let len = changes.len(); + let start = changes + .get_by_atom_index(id_span.counter.start) + .map(|x| x.merged_index) + .unwrap_or(len); + let mut end = changes + .get_by_atom_index(id_span.counter.end) + .map(|x| x.merged_index) + .unwrap_or(len); + if end < changes.len() { + end += 1; + } + + changes[start..end].iter().flat_map(move |c| { + // TODO: PERF can be optimized + c.ops() + .iter() + .filter_map(move |op| RichOp::new_by_cnt_range(c, id_span.counter, op)) + }) + }) + .into_iter() + .flatten() + } + pub(crate) fn get_max_lamport_at(&self, id: ID) -> Lamport { self.get_change_at(id) .map(|c| { diff --git a/crates/loro/tests/loro_rust_test.rs b/crates/loro/tests/loro_rust_test.rs index e47a3d7a..e6206c02 100644 --- a/crates/loro/tests/loro_rust_test.rs +++ b/crates/loro/tests/loro_rust_test.rs @@ -1,7 +1,55 @@ use std::{cmp::Ordering, sync::Arc}; -use loro::{FrontiersNotIncluded, LoroDoc}; +use loro::{FrontiersNotIncluded, LoroDoc, LoroError, ToJson}; use loro_internal::{delta::DeltaItem, handler::TextDelta, id::ID, DiffEvent, LoroResult}; +use serde_json::json; + +#[test] +fn list_checkout() -> Result<(), LoroError> { + let mut doc = LoroDoc::new(); + doc.get_list("list") + .insert_container(0, loro::ContainerType::Map)?; + doc.commit(); + let f0 = doc.state_frontiers(); + doc.get_list("list") + .insert_container(0, loro::ContainerType::Text)?; + doc.commit(); + let f1 = doc.state_frontiers(); + doc.get_list("list").delete(1, 1)?; + doc.commit(); + let f2 = doc.state_frontiers(); + doc.get_list("list").delete(0, 1)?; + doc.commit(); + doc.checkout(&f1)?; + assert_eq!( + doc.get_deep_value().to_json_value(), + json!({ + "list": ["", {}] + }) + ); + doc.checkout(&f2)?; + assert_eq!( + doc.get_deep_value().to_json_value(), + json!({ + "list": [""] + }) + ); + doc.checkout(&f0)?; + assert_eq!( + doc.get_deep_value().to_json_value(), + json!({ + "list": [{}] + }) + ); + doc.checkout(&f1)?; + assert_eq!( + doc.get_deep_value().to_json_value(), + json!({ + "list": ["", {}] + }) + ); + Ok(()) +} #[test] fn timestamp() {