1use std::{num::NonZeroU16, sync::Arc};
2
3#[cfg(feature = "counter")]
4mod counter;
5#[cfg(feature = "counter")]
6pub(crate) use counter::CounterDiffCalculator;
7pub(super) mod tree;
8mod unknown;
9use either::Either;
10use generic_btree::rle::{HasLength as _, Sliceable as _};
11use itertools::Itertools;
12
13use enum_dispatch::enum_dispatch;
14use loro_common::{
15 CompactIdLp, ContainerID, Counter, HasCounterSpan, IdFull, IdLp, IdSpan, LoroValue, PeerID, ID,
16};
17use loro_delta::DeltaRope;
18use rustc_hash::{FxHashMap, FxHashSet};
19use smallvec::SmallVec;
20use tracing::{info_span, instrument};
21
22use crate::{
23 change::Lamport,
24 container::{
25 idx::ContainerIdx,
26 list::list_op::InnerListOp,
27 richtext::{
28 richtext_state::{RichtextStateChunk, TextChunk},
29 AnchorType, CrdtRopeDelta, RichtextChunk, RichtextChunkValue, RichtextTracker, StyleOp,
30 },
31 },
32 cursor::AbsolutePosition,
33 delta::{
34 Delta, DeltaItem, DeltaValue, ElementDelta, MapDelta, MapValue, MovableListInnerDelta,
35 },
36 event::{DiffVariant, InternalDiff},
37 op::{InnerContent, RichOp, SliceRange, SliceWithId},
38 span::{HasId, HasLamport},
39 version::Frontiers,
40 InternalString, VersionVector,
41};
42
43use self::tree::TreeDiffCalculator;
44
45use self::unknown::UnknownDiffCalculator;
46
47use super::{
48 event::InternalContainerDiff,
49 oplog::{OpLog, ReplayBase},
50};
51
52#[derive(Debug)]
57pub struct DiffCalculator {
58 calculators: FxHashMap<ContainerIdx, (Option<NonZeroU16>, ContainerDiffCalculator)>,
62 retain_mode: DiffCalculatorRetainMode,
63}
64
65#[derive(Debug)]
66enum DiffCalculatorRetainMode {
67 Once { used: bool },
69 Persist,
71}
72
73#[derive(Debug, Clone, PartialEq, Eq, Copy)]
75pub(crate) enum DiffMode {
76 Checkout,
84 Import,
94 ImportGreaterUpdates,
100 Linear,
106}
107
108#[derive(Debug, Clone, Copy)]
109pub(crate) struct DiffCalcVersionInfo<'a> {
110 from_vv: &'a VersionVector,
111 to_vv: &'a VersionVector,
112 from_frontiers: &'a Frontiers,
113 to_frontiers: &'a Frontiers,
114 replay_base_vv: &'a VersionVector,
115 replay_base_is_critical: bool,
119}
120
121fn changed_containers_between(
122 oplog: &OpLog,
123 before: &VersionVector,
124 after: &VersionVector,
125) -> FxHashSet<ContainerIdx> {
126 let (retreat, forward) = before.diff_iter(after);
127 oplog.containers_in_spans(retreat.chain(forward))
128}
129
130impl DiffCalculator {
131 pub fn new(persist: bool) -> Self {
137 Self {
138 calculators: Default::default(),
139 retain_mode: if persist {
140 DiffCalculatorRetainMode::Persist
141 } else {
142 DiffCalculatorRetainMode::Once { used: false }
143 },
144 }
145 }
146
147 #[allow(unused)]
148 pub(crate) fn get_calc(&self, container: ContainerIdx) -> Option<&ContainerDiffCalculator> {
149 self.calculators.get(&container).map(|(_, c)| c)
150 }
151
152 pub(crate) fn calc_diff_internal(
158 &mut self,
159 oplog: &super::oplog::OpLog,
160 before: &crate::VersionVector,
161 before_frontiers: &Frontiers,
162 after: &crate::VersionVector,
163 after_frontiers: &Frontiers,
164 container_filter: Option<&dyn Fn(ContainerIdx) -> bool>,
165 ) -> (Vec<InternalContainerDiff>, DiffMode) {
166 if before == after {
167 return (Vec::new(), DiffMode::Linear);
168 }
169
170 let s = tracing::span!(tracing::Level::INFO, "DiffCalc", ?before, ?after,);
171 let _e = s.enter();
172
173 let mut merged = before.clone();
174 merged.merge(after);
175 let (
176 ReplayBase {
177 vv: replay_base,
178 diff_mode: origin_diff_mode,
179 is_critical: replay_base_is_critical,
180 concurrent_containers,
181 },
182 iter,
183 ) = oplog.iter_from_replay_base_causally(before, before_frontiers, after, after_frontiers);
184 let changed_containers =
190 (&replay_base != before).then(|| changed_containers_between(oplog, before, after));
191 let mut calc_mode = origin_diff_mode;
203 match &mut self.retain_mode {
204 DiffCalculatorRetainMode::Once { used } => {
205 if *used {
206 panic!("DiffCalculator with retain_mode Once can only be used once");
207 }
208 }
209 DiffCalculatorRetainMode::Persist => {
210 calc_mode = DiffMode::Checkout;
211 }
212 }
213
214 let affected_set = {
215 loro_common::debug!("replay_base: {:?} mode={:?}", &replay_base, calc_mode);
216 let mut started_set = FxHashSet::default();
217 for (change, (start_counter, end_counter), vv) in iter {
218 let iter_start = change
219 .ops
220 .binary_search_by(|op| op.ctr_last().cmp(&start_counter))
221 .unwrap_or_else(|e| e);
222 let mut visited = FxHashSet::default();
223 for mut op in &change.ops.vec()[iter_start..] {
224 if op.counter >= end_counter {
225 break;
226 }
227
228 let idx = op.container;
229 if changed_containers
230 .as_ref()
231 .is_some_and(|containers| !containers.contains(&idx))
232 {
233 continue;
234 }
235
236 if let Some(filter) = container_filter {
237 if !filter(idx) {
238 continue;
239 }
240 }
241
242 let stack_sliced_op;
245 if op.ctr_last() < start_counter {
246 continue;
247 }
248
249 if op.counter < start_counter || op.ctr_end() > end_counter {
250 stack_sliced_op = Some(op.slice(
251 (start_counter as usize).saturating_sub(op.counter as usize),
252 op.atom_len().min((end_counter - op.counter) as usize),
253 ));
254 op = stack_sliced_op.as_ref().unwrap();
255 }
256
257 let vv = &mut vv.borrow_mut();
258 vv.extend_to_include_end_id(ID::new(change.peer(), op.counter));
259 let container = op.container;
260 let depth = oplog.arena.get_depth(container);
261 let (old_depth, calculator) = self.get_or_create_calc(container, depth);
262 if *old_depth != depth {
265 *old_depth = depth;
266 }
267
268 if !started_set.contains(&op.container) {
269 started_set.insert(container);
270 let container_mode = if concurrent_containers
275 .as_ref()
276 .is_some_and(|set| set.contains(&container))
277 {
278 DiffMode::Import
279 } else {
280 calc_mode
281 };
282 calculator.start_tracking(oplog, &replay_base, container_mode);
283 }
284
285 if !vv.includes_vv(before)
291 && concurrent_containers
292 .as_ref()
293 .is_none_or(|set| set.contains(&container))
294 {
295 calculator.mark_source_not_in_op_context();
296 }
297
298 debug_assert!(
301 !replay_base_is_critical || vv.includes_vv(&replay_base),
302 "op {}@{} is concurrent with a replay base claimed critical",
303 op.counter,
304 change.peer(),
305 );
306
307 if calculator.ignores_ops_shared_by_both_versions() {
308 let op_id = ID::new(change.peer(), op.ctr_last());
309 if before.includes_id(op_id) && after.includes_id(op_id) {
310 continue;
311 }
312 }
313
314 if visited.contains(&op.container) {
315 calculator.apply_change(oplog, RichOp::new_by_change(&change, op), None);
317 } else {
318 calculator.apply_change(
319 oplog,
320 RichOp::new_by_change(&change, op),
321 Some(vv),
322 );
323 visited.insert(container);
324 }
325 }
326 }
327
328 Some(started_set)
329 };
330
331 let mut new_containers = FxHashSet::default();
334 let mut container_id_to_depth = FxHashMap::default();
335 let mut all: Vec<(Option<NonZeroU16>, ContainerIdx)> = if let Some(set) = affected_set {
336 set.into_iter()
338 .map(|x| {
339 let (depth, _) = self.calculators.get_mut(&x).unwrap();
340 (*depth, x)
341 })
342 .collect()
343 } else {
344 self.calculators
345 .iter_mut()
346 .map(|(x, (depth, _))| (*depth, *x))
347 .collect()
348 };
349 let mut ans = FxHashMap::default();
350 let info = DiffCalcVersionInfo {
351 from_vv: before,
352 to_vv: after,
353 from_frontiers: before_frontiers,
354 to_frontiers: after_frontiers,
355 replay_base_vv: &replay_base,
356 replay_base_is_critical,
357 };
358 while !all.is_empty() {
359 all.sort_by_key(|x| x.0);
361 for (_, container_idx) in std::mem::take(&mut all) {
362 if ans.contains_key(&container_idx) {
363 continue;
364 }
365 let (depth, calc) = self.calculators.get_mut(&container_idx).unwrap();
366 if depth.is_none() {
367 let d = oplog.arena.get_depth(container_idx);
368 if d != *depth {
369 *depth = d;
370 all.push((*depth, container_idx));
371 continue;
372 }
373 }
374 let id = oplog.arena.idx_to_id(container_idx).unwrap();
375 let bring_back = new_containers.remove(&id);
376
377 info_span!("CalcDiff", ?id).in_scope(|| {
378 let (diff, diff_mode) = calc.calculate_diff(container_idx, oplog, info, |c| {
379 new_containers.insert(c.clone());
380 container_id_to_depth
381 .insert(c.clone(), depth.and_then(|d| d.checked_add(1)));
382 oplog.arena.register_container(c);
383 });
384 calc.finish_this_round();
385 if !diff.is_empty() || bring_back {
386 ans.insert(
387 container_idx,
388 (
389 *depth,
390 InternalContainerDiff {
391 idx: container_idx,
392 bring_back,
393 diff: diff.into(),
394 diff_mode,
395 },
396 ),
397 );
398 }
399 });
400 }
401 }
402
403 while !new_containers.is_empty() {
404 for id in std::mem::take(&mut new_containers) {
405 let idx = oplog.arena.register_container(&id);
407 if ans.contains_key(&idx) {
408 continue;
409 }
410 let depth = container_id_to_depth.remove(&id).unwrap();
411 ans.insert(
412 idx,
413 (
414 depth,
415 InternalContainerDiff {
416 idx,
417 bring_back: true,
418 diff: DiffVariant::None,
419 diff_mode: DiffMode::Checkout,
420 },
421 ),
422 );
423 }
424 }
425
426 (
427 ans.into_values().map(|x| x.1).collect_vec(),
428 origin_diff_mode,
429 )
430 }
431
432 pub(crate) fn get_or_create_calc(
434 &mut self,
435 idx: ContainerIdx,
436 depth: Option<NonZeroU16>,
437 ) -> &mut (Option<NonZeroU16>, ContainerDiffCalculator) {
438 self.calculators
439 .entry(idx)
440 .or_insert_with(|| match idx.get_type() {
441 crate::ContainerType::Text => (
442 depth,
443 ContainerDiffCalculator::Richtext(RichtextDiffCalculator::new()),
444 ),
445 crate::ContainerType::Map => (
446 depth,
447 ContainerDiffCalculator::Map(MapDiffCalculator::new(idx)),
448 ),
449 crate::ContainerType::List => (
450 depth,
451 ContainerDiffCalculator::List(ListDiffCalculator::default()),
452 ),
453 crate::ContainerType::Tree => (
454 depth,
455 ContainerDiffCalculator::Tree(TreeDiffCalculator::new(idx)),
456 ),
457 crate::ContainerType::Unknown(_) => (
458 depth,
459 ContainerDiffCalculator::Unknown(unknown::UnknownDiffCalculator),
460 ),
461 crate::ContainerType::MovableList => (
462 depth,
463 ContainerDiffCalculator::MovableList(MovableListDiffCalculator::new(idx)),
464 ),
465 #[cfg(feature = "counter")]
466 crate::ContainerType::Counter => (
467 depth,
468 ContainerDiffCalculator::Counter(CounterDiffCalculator::new(idx)),
469 ),
470 })
471 }
472}
473
474#[enum_dispatch]
482pub(crate) trait DiffCalculatorTrait {
483 fn start_tracking(&mut self, oplog: &OpLog, vv: &crate::VersionVector, mode: DiffMode);
484 fn apply_change(
485 &mut self,
486 oplog: &OpLog,
487 op: crate::op::RichOp,
488 vv: Option<&crate::VersionVector>,
489 );
490 fn calculate_diff(
491 &mut self,
492 idx: ContainerIdx,
493 oplog: &OpLog,
494 info: DiffCalcVersionInfo,
495 on_new_container: impl FnMut(&ContainerID),
496 ) -> (InternalDiff, DiffMode);
497 fn finish_this_round(&mut self);
499}
500
501#[cfg(test)]
502thread_local! {
503 pub(crate) static FULL_TRACKER_REBUILD_COUNT: std::cell::Cell<u64> =
508 const { std::cell::Cell::new(0) };
509}
510
511#[enum_dispatch(DiffCalculatorTrait)]
512#[derive(Debug)]
513pub(crate) enum ContainerDiffCalculator {
514 Map(MapDiffCalculator),
515 List(ListDiffCalculator),
516 Richtext(RichtextDiffCalculator),
517 Tree(TreeDiffCalculator),
518 MovableList(MovableListDiffCalculator),
519 #[cfg(feature = "counter")]
520 Counter(counter::CounterDiffCalculator),
521 Unknown(UnknownDiffCalculator),
522}
523
524impl ContainerDiffCalculator {
525 fn ignores_ops_shared_by_both_versions(&self) -> bool {
533 matches!(self, Self::Map(_))
534 }
535
536 fn mark_source_not_in_op_context(&mut self) {
537 match self {
538 Self::Richtext(calc) => calc.mark_source_not_in_op_context(),
539 Self::List(calc) => calc.mark_source_not_in_op_context(),
540 Self::MovableList(calc) => calc.mark_source_not_in_op_context(),
541 _ => {}
542 }
543 }
544}
545
546trait RebuildOpVisitor {
547 fn visit(&mut self, vv: &VersionVector, op: RichOp<'_>);
548}
549
550#[cold]
551#[inline(never)]
552fn replay_container_ops_from_empty(
553 idx: ContainerIdx,
554 oplog: &OpLog,
555 vv: &VersionVector,
556 visitor: &mut dyn RebuildOpVisitor,
557) {
558 let empty_vv = VersionVector::default();
559 let empty_frontiers = Frontiers::default();
560 let target_frontiers = oplog.dag.vv_to_frontiers(vv);
561 let (_, iter) =
562 oplog.iter_from_replay_base_causally(&empty_vv, &empty_frontiers, vv, &target_frontiers);
563
564 for (change, (start_counter, end_counter), vv) in iter {
565 let iter_start = change
566 .ops
567 .binary_search_by(|op| op.ctr_last().cmp(&start_counter))
568 .unwrap_or_else(|e| e);
569 for mut op in &change.ops.vec()[iter_start..] {
570 if op.counter >= end_counter {
571 break;
572 }
573
574 if op.container != idx || op.ctr_last() < start_counter {
575 continue;
576 }
577
578 let stack_sliced_op;
579 if op.counter < start_counter || op.ctr_end() > end_counter {
580 stack_sliced_op = Some(op.slice(
581 (start_counter as usize).saturating_sub(op.counter as usize),
582 op.atom_len().min((end_counter - op.counter) as usize),
583 ));
584 op = stack_sliced_op.as_ref().unwrap();
585 }
586
587 let vv = &mut vv.borrow_mut();
588 vv.extend_to_include_end_id(ID::new(change.peer(), op.counter));
589 visitor.visit(vv, RichOp::new_by_change(&change, op));
590 }
591 }
592}
593
594#[derive(Debug)]
595pub(crate) struct MapDiffCalculator {
596 container_idx: ContainerIdx,
597 changed: FxHashMap<InternalString, Option<MapValue>>,
606 current_mode: DiffMode,
607}
608
609impl MapDiffCalculator {
610 pub(crate) fn new(container_idx: ContainerIdx) -> Self {
611 Self {
612 container_idx,
613 changed: Default::default(),
614 current_mode: DiffMode::Checkout,
615 }
616 }
617}
618
619impl DiffCalculatorTrait for MapDiffCalculator {
620 fn start_tracking(
621 &mut self,
622 _oplog: &crate::OpLog,
623 _vv: &crate::VersionVector,
624 mode: DiffMode,
625 ) {
626 self.changed.clear();
627 self.current_mode = mode;
628 }
629
630 fn apply_change(
631 &mut self,
632 _oplog: &crate::OpLog,
633 op: crate::op::RichOp,
634 _vv: Option<&crate::VersionVector>,
635 ) {
636 let map = op.raw_op().content.as_map().unwrap();
637 if matches!(self.current_mode, DiffMode::Checkout | DiffMode::Import) {
638 if !self.changed.contains_key(&map.key) {
641 self.changed.insert(map.key.clone(), None);
642 }
643 return;
644 }
645
646 let new_value = MapValue {
647 value: map.value.clone(),
648 peer: op.peer,
649 lamp: op.lamport(),
650 };
651 match self.changed.get(&map.key) {
652 Some(Some(old_value)) if old_value > &new_value => {}
653 _ => {
654 self.changed.insert(map.key.clone(), Some(new_value));
655 }
656 }
657 }
658
659 fn finish_this_round(&mut self) {
660 self.changed.clear();
661 self.current_mode = DiffMode::Checkout;
662 }
663
664 fn calculate_diff(
665 &mut self,
666 _idx: ContainerIdx,
667 oplog: &super::oplog::OpLog,
668 DiffCalcVersionInfo { from_vv, to_vv, .. }: DiffCalcVersionInfo,
669 mut on_new_container: impl FnMut(&ContainerID),
670 ) -> (InternalDiff, DiffMode) {
671 match self.current_mode {
672 DiffMode::Checkout | DiffMode::Import => oplog.with_history_cache(|h| {
673 let checkout_index = &h.get_checkout_index().map;
674 let mut changed = Vec::new();
675 let keys = std::mem::take(&mut self.changed);
676 let from_map = checkout_index.get_container_latest_op_at_vv_for_keys(
677 self.container_idx,
678 from_vv,
679 keys.keys().cloned(),
680 oplog,
681 );
682 let mut to_map = checkout_index.get_container_latest_op_at_vv_for_keys(
683 self.container_idx,
684 to_vv,
685 keys.into_keys(),
686 oplog,
687 );
688
689 for (k, peek_from) in from_map.iter() {
690 let peek_to = to_map.remove(k);
691 match peek_to {
692 None => changed.push((k.clone(), None)),
693 Some(b) => {
694 if peek_from.value != b.value {
695 changed.push((k.clone(), Some(b)))
696 }
697 }
698 }
699 }
700
701 for (k, peek_to) in to_map.into_iter() {
702 changed.push((k, Some(peek_to)));
703 }
704
705 let mut updated =
706 FxHashMap::with_capacity_and_hasher(changed.len(), Default::default());
707 for (key, value) in changed {
708 let value = value.map(|v| {
709 let value = v.value.clone();
710 if let Some(LoroValue::Container(c)) = &value {
711 on_new_container(c);
712 }
713
714 MapValue {
715 value,
716 lamp: v.lamport,
717 peer: v.peer,
718 }
719 });
720
721 updated.insert(key, value);
722 }
723
724 (InternalDiff::Map(MapDelta { updated }), DiffMode::Checkout)
725 }),
726 DiffMode::ImportGreaterUpdates | DiffMode::Linear => {
727 let changed = std::mem::take(&mut self.changed);
728 let mode = self.current_mode;
729 self.current_mode = DiffMode::Checkout;
732 (InternalDiff::Map(MapDelta { updated: changed }), mode)
733 }
734 }
735 }
736}
737
738use rle::{HasLength as _, Sliceable};
739
740#[derive(Default)]
741pub(crate) struct ListDiffCalculator {
742 start_vv: VersionVector,
743 tracker: Box<RichtextTracker>,
744 source_not_in_op_context: bool,
745}
746
747impl ListDiffCalculator {
748 pub(crate) fn get_id_latest_pos(&self, id: ID) -> Option<crate::cursor::AbsolutePosition> {
749 self.tracker.get_target_id_latest_index_at_new_version(id)
750 }
751
752 fn mark_source_not_in_op_context(&mut self) {
753 self.source_not_in_op_context = true;
754 }
755
756 #[inline(never)]
757 fn apply_op_to_tracker(tracker: &mut RichtextTracker, op: &crate::op::RichOp<'_>) {
758 match &op.op().content {
759 crate::op::InnerContent::List(l) => match l {
760 InnerListOp::Insert { slice, pos } => {
761 tracker.insert(op.id_full(), *pos, RichtextChunk::new_text(slice.0.clone()));
762 }
763 InnerListOp::Delete(del) => {
764 tracker.delete(
765 op.id_start(),
766 del.id_start,
767 del.start() as usize,
768 del.atom_len(),
769 del.is_reversed(),
770 );
771 }
772 _ => unreachable!(),
773 },
774 _ => unreachable!(),
775 }
776 }
777
778 #[cold]
779 #[inline(never)]
780 fn build_full_tracker(idx: ContainerIdx, oplog: &OpLog, vv: &VersionVector) -> RichtextTracker {
781 struct ListRebuildVisitor<'a> {
782 tracker: &'a mut RichtextTracker,
783 }
784
785 impl RebuildOpVisitor for ListRebuildVisitor<'_> {
786 fn visit(&mut self, vv: &VersionVector, op: RichOp<'_>) {
787 self.tracker.checkout(vv);
788 ListDiffCalculator::apply_op_to_tracker(self.tracker, &op);
789 }
790 }
791
792 let mut tracker = RichtextTracker::new_with_unknown();
793 let mut visitor = ListRebuildVisitor {
794 tracker: &mut tracker,
795 };
796 replay_container_ops_from_empty(idx, oplog, vv, &mut visitor);
797
798 tracker
799 }
800}
801
802impl MovableListDiffCalculator {
803 pub(crate) fn get_id_latest_pos(&self, id: ID) -> Option<crate::cursor::AbsolutePosition> {
804 self.list
805 .tracker
806 .get_target_id_latest_index_at_new_version(id)
807 }
808
809 fn mark_source_not_in_op_context(&mut self) {
810 self.list.mark_source_not_in_op_context();
811 }
812}
813
814impl std::fmt::Debug for ListDiffCalculator {
815 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
816 f.debug_struct("ListDiffCalculator")
817 .finish()
819 }
820}
821
822impl DiffCalculatorTrait for ListDiffCalculator {
823 fn start_tracking(&mut self, _oplog: &OpLog, vv: &crate::VersionVector, _mode: DiffMode) {
824 self.source_not_in_op_context = false;
825 if !vv.includes_vv(&self.start_vv) || !self.tracker.all_vv().includes_vv(vv) {
826 *self.tracker = RichtextTracker::new_with_unknown();
827 self.start_vv = vv.clone();
828 }
829
830 self.tracker.checkout(vv);
831 }
832
833 fn apply_change(
834 &mut self,
835 _oplog: &OpLog,
836 op: crate::op::RichOp,
837 vv: Option<&crate::VersionVector>,
838 ) {
839 if let Some(vv) = vv {
840 self.tracker.checkout(vv);
841 }
842
843 Self::apply_op_to_tracker(&mut self.tracker, &op);
844 }
845
846 fn finish_this_round(&mut self) {}
847
848 fn calculate_diff(
849 &mut self,
850 idx: ContainerIdx,
851 oplog: &OpLog,
852 info: DiffCalcVersionInfo,
853 mut on_new_container: impl FnMut(&ContainerID),
854 ) -> (InternalDiff, DiffMode) {
855 let mut delta = Delta::new();
856 let (mut retreat, _) = info.from_vv.diff_iter(info.to_vv);
857 let has_retreat = retreat.next().is_some();
858 let should_rebuild = matches!(idx.get_type(), crate::ContainerType::List) && {
859 let base_is_trustworthy = (info.replay_base_is_critical
869 && oplog.shallow_since_vv().is_empty())
870 || (info.replay_base_vv == info.from_vv && !self.source_not_in_op_context);
871 has_retreat || !base_is_trustworthy
872 };
873 let diff_items = if should_rebuild {
874 #[cfg(test)]
875 FULL_TRACKER_REBUILD_COUNT.with(|c| c.set(c.get() + 1));
876 let mut merged = info.from_vv.clone();
877 merged.merge(info.to_vv);
878 let mut full_tracker = Self::build_full_tracker(idx, oplog, &merged);
879 let diff_items = full_tracker.diff(info.from_vv, info.to_vv).collect_vec();
880 *self.tracker = full_tracker;
881 diff_items
882 } else {
883 self.tracker.diff(info.from_vv, info.to_vv).collect_vec()
884 };
885
886 for item in diff_items {
887 match item {
888 CrdtRopeDelta::Retain(len) => {
889 delta = delta.retain(len);
890 }
891 CrdtRopeDelta::Insert {
892 chunk: value,
893 id,
894 lamport,
895 } => match value.value() {
896 RichtextChunkValue::Text(range) => {
897 for i in range.clone() {
898 let v = oplog.arena.get_value(i as usize);
899 if let Some(LoroValue::Container(c)) = &v {
900 on_new_container(c);
901 }
902 }
903 delta = delta.insert(SliceWithId {
904 values: Either::Left(SliceRange(range)),
905 id: IdFull::new(id.peer, id.counter, lamport.unwrap()),
906 elem_id: None,
907 });
908 }
909 RichtextChunkValue::StyleAnchor { .. } => unreachable!(),
910 RichtextChunkValue::Unknown(len) => {
911 delta = handle_unknown(idx, id, oplog, len, &mut on_new_container, delta);
912 }
913 RichtextChunkValue::MoveAnchor => {
914 delta = handle_unknown(idx, id, oplog, 1, &mut on_new_container, delta);
915 }
916 },
917 CrdtRopeDelta::Delete(len) => {
918 delta = delta.delete(len);
919 }
920 }
921 }
922
923 fn handle_unknown(
927 idx: ContainerIdx,
928 mut id: ID,
929 oplog: &OpLog,
930 len: u32,
931 on_new_container: &mut dyn FnMut(&ContainerID),
932 mut delta: Delta<SliceWithId>,
933 ) -> Delta<SliceWithId> {
934 assert_ne!(id.peer, PeerID::MAX);
936 let mut acc_len = 0;
937 let end = id.counter + len as Counter;
938 let shallow_root = oplog.shallow_since_vv().get(&id.peer).copied().unwrap_or(0);
939 if id.counter < shallow_root {
940 let target_end = shallow_root.min(end);
942 delta = oplog.with_history_cache(|h| {
943 let chunks =
944 h.find_list_chunks_in(idx, IdSpan::new(id.peer, id.counter, target_end));
945 for c in chunks {
946 acc_len += c.length();
947 match &c.values {
948 Either::Left(_) => unreachable!(),
949 Either::Right(r) => {
950 if let LoroValue::Container(c) = r {
951 on_new_container(c)
952 }
953 }
954 }
955 delta = delta.insert(c);
956 }
957
958 delta
959 });
960 id.counter = shallow_root;
961 }
962
963 if id.counter < end {
964 for rich_op in oplog.iter_ops(IdSpan::new(id.peer, id.counter, end)) {
965 acc_len += rich_op.content_len();
966 let op = rich_op.op();
967 let lamport = rich_op.lamport();
968
969 if let InnerListOp::Insert { slice, pos: _ } = op.content.as_list().unwrap() {
970 let range = slice.clone();
971 for i in slice.0.clone() {
972 let v = oplog.arena.get_value(i as usize);
973 if let Some(LoroValue::Container(c)) = &v {
974 (on_new_container)(c);
975 }
976 }
977
978 delta = delta.insert(SliceWithId {
979 values: Either::Left(range),
980 id: IdFull::new(id.peer, op.counter, lamport),
981 elem_id: None,
982 });
983 } else if let InnerListOp::Move { elem_id, .. } = op.content.as_list().unwrap()
984 {
985 delta = delta.insert(SliceWithId {
986 values: Either::Right(LoroValue::Null),
989 id: IdFull::new(id.peer, op.counter, lamport),
990 elem_id: Some(elem_id.compact()),
991 });
992 }
993 }
994 }
995
996 debug_assert_eq!(acc_len, len as usize);
997 delta
998 }
999
1000 (InternalDiff::ListRaw(delta), DiffMode::Checkout)
1001 }
1002}
1003
1004#[derive(Debug)]
1005pub(crate) struct RichtextDiffCalculator {
1006 mode: Box<RichtextCalcMode>,
1007}
1008
1009#[derive(Debug)]
1010enum RichtextCalcMode {
1011 Crdt {
1012 tracker: Box<RichtextTracker>,
1013 styles: Vec<(StyleOp, usize)>,
1015 start_vv: VersionVector,
1016 source_not_in_op_context: bool,
1017 shallow_root_seeded: bool,
1018 },
1019 Linear {
1020 diff: DeltaRope<RichtextStateChunk, ()>,
1021 last_style_start: Option<(Arc<StyleOp>, u32)>,
1022 },
1023}
1024
1025impl RichtextDiffCalculator {
1026 pub fn new() -> Self {
1027 Self {
1028 mode: Box::new(RichtextCalcMode::Crdt {
1029 tracker: Box::new(RichtextTracker::new_with_unknown()),
1030 styles: Vec::new(),
1031 start_vv: VersionVector::new(),
1032 source_not_in_op_context: false,
1033 shallow_root_seeded: false,
1034 }),
1035 }
1036 }
1037
1038 fn mark_source_not_in_op_context(&mut self) {
1039 if let RichtextCalcMode::Crdt {
1040 source_not_in_op_context,
1041 ..
1042 } = &mut *self.mode
1043 {
1044 *source_not_in_op_context = true;
1045 }
1046 }
1047
1048 #[cfg(test)]
1051 pub(crate) fn tracked_style_ids(&self) -> Vec<ID> {
1052 match &*self.mode {
1053 RichtextCalcMode::Crdt { styles, .. } => {
1054 styles.iter().map(|(s, _)| ID::new(s.peer, s.cnt)).collect()
1055 }
1056 RichtextCalcMode::Linear { .. } => unreachable!(),
1057 }
1058 }
1059
1060 fn style_for_end_anchor(oplog: &OpLog, op: &RichOp) -> Option<(StyleOp, usize)> {
1061 let style_start_id = op.id().inc(-1);
1062 if let Some(start_op) = oplog.get_op_that_includes(style_start_id) {
1063 let InnerListOp::StyleStart {
1064 start: _,
1065 end,
1066 key,
1067 value,
1068 info,
1069 } = start_op.content.as_list().unwrap()
1070 else {
1071 unreachable!()
1072 };
1073
1074 Some((
1075 StyleOp {
1076 lamport: start_op.lamport(),
1077 peer: style_start_id.peer,
1078 cnt: style_start_id.counter,
1079 key: key.clone(),
1080 value: value.clone(),
1081 info: *info,
1082 },
1083 *end as usize,
1084 ))
1085 } else {
1086 oplog.with_history_cache(|history_cache| {
1087 history_cache
1088 .find_text_style_end_in_shallow_root(op.raw_op().container, style_start_id)
1089 })
1090 }
1091 }
1092
1093 fn shallow_delete_range_matches_tracker(
1094 tracker: &RichtextTracker,
1095 target_start: ID,
1096 pos: usize,
1097 len: usize,
1098 ) -> bool {
1099 let mut remaining = len;
1100 let mut pos = pos;
1101 let mut expected = target_start;
1102 while remaining > 0 {
1103 let Some((real_id, available)) = tracker.active_real_span_at(pos) else {
1104 return false;
1105 };
1106
1107 if real_id.peer != expected.peer || real_id.counter != expected.counter {
1108 return false;
1109 }
1110
1111 let take = remaining.min(available);
1112 expected = expected.inc(take as Counter);
1113 pos += take;
1114 remaining -= take;
1115 }
1116
1117 true
1118 }
1119
1120 fn shallow_clamped_tracker_pos(oplog: &OpLog, tracker: &RichtextTracker, pos: usize) -> usize {
1121 if oplog.shallow_since_vv().is_empty() {
1122 pos
1123 } else {
1124 pos.min(tracker.len())
1125 }
1126 }
1127
1128 pub fn get_id_latest_pos(&self, id: ID) -> Option<AbsolutePosition> {
1132 match &*self.mode {
1133 RichtextCalcMode::Crdt { tracker, .. } => {
1134 tracker.get_target_id_latest_index_at_new_version(id)
1135 }
1136 RichtextCalcMode::Linear { .. } => unreachable!(),
1137 }
1138 }
1139
1140 fn apply_crdt_op_to_tracker(
1141 oplog: &OpLog,
1142 tracker: &mut RichtextTracker,
1143 styles: &mut Vec<(StyleOp, usize)>,
1144 op: RichOp,
1145 ) {
1146 match &op.raw_op().content {
1147 crate::op::InnerContent::List(l) => match l {
1148 InnerListOp::Insert { .. } | InnerListOp::Move { .. } | InnerListOp::Set { .. } => {
1149 unreachable!()
1150 }
1151 InnerListOp::InsertText {
1152 slice: _,
1153 unicode_start,
1154 unicode_len: len,
1155 pos,
1156 } => {
1157 let pos = Self::shallow_clamped_tracker_pos(oplog, tracker, *pos as usize);
1158 tracker.insert(
1159 op.id_full(),
1160 pos,
1161 RichtextChunk::new_text(*unicode_start..*unicode_start + *len),
1162 );
1163 }
1164 InnerListOp::Delete(del) => {
1165 let is_shallow = !oplog.shallow_since_vv().is_empty();
1166 let pos = del.start() as usize;
1167 if is_shallow
1168 && !Self::shallow_delete_range_matches_tracker(
1169 tracker,
1170 del.id_start,
1171 pos,
1172 del.atom_len(),
1173 )
1174 {
1175 let atom_len = del.atom_len();
1176 let mut segments =
1177 tracker.active_segments_of_real_id_span(del.id_start, atom_len);
1178 if segments.is_empty() {
1179 return;
1180 }
1181
1182 segments.sort_unstable_by_key(|(_, pos, _)| *pos);
1183 if del.is_reversed() {
1184 for (target_id, pos, len) in segments.into_iter().rev() {
1185 let target_offset =
1186 (target_id.counter - del.id_start.counter) as usize;
1187 debug_assert!(target_offset + len <= atom_len);
1188 let op_offset = atom_len - target_offset - len;
1189 tracker.delete(
1190 op.id_start().inc(op_offset as Counter),
1191 target_id,
1192 pos,
1193 len,
1194 true,
1195 );
1196 }
1197 } else {
1198 let mut deleted_before = 0;
1199 for (target_id, pos, len) in segments {
1200 let target_offset =
1201 (target_id.counter - del.id_start.counter) as usize;
1202 debug_assert!(pos >= deleted_before);
1203 tracker.delete(
1204 op.id_start().inc(target_offset as Counter),
1205 target_id,
1206 pos.saturating_sub(deleted_before),
1207 len,
1208 false,
1209 );
1210 deleted_before += len;
1211 }
1212 }
1213
1214 return;
1215 }
1216
1217 tracker.delete(
1218 op.id_start(),
1219 del.id_start,
1220 pos,
1221 del.atom_len(),
1222 del.is_reversed(),
1223 );
1224 }
1225 InnerListOp::StyleStart {
1226 start,
1227 end,
1228 key,
1229 info,
1230 value,
1231 } => {
1232 debug_assert!(start < end, "start: {}, end: {}", start, end);
1233 let id = op.id_start();
1246 let existing_style_id = if tracker.all_vv().includes_id(id) {
1247 styles
1248 .iter()
1249 .rev()
1250 .position(|(s, _)| s.peer == id.peer && s.cnt == id.counter)
1251 .map(|pos| styles.len() - pos - 1)
1252 } else {
1253 None
1254 };
1255 let style_id = match existing_style_id {
1256 Some(style_id) => style_id,
1257 None => {
1258 styles.push((
1259 StyleOp {
1260 lamport: op.lamport(),
1261 peer: id.peer,
1262 cnt: id.counter,
1263 key: key.clone(),
1264 value: value.clone(),
1265 info: *info,
1266 },
1267 *end as usize,
1268 ));
1269 styles.len() - 1
1270 }
1271 };
1272 let start = Self::shallow_clamped_tracker_pos(oplog, tracker, *start as usize);
1273 tracker.insert(
1274 op.id_full(),
1275 start,
1276 RichtextChunk::new_style_anchor(style_id as u32, AnchorType::Start),
1277 );
1278 }
1279 InnerListOp::StyleEnd => {
1280 let id = op.id();
1281 if let Some(pos) = styles
1282 .iter()
1283 .rev()
1284 .position(|(op, _pos)| op.peer == id.peer && op.cnt == id.counter - 1)
1285 {
1286 let style_id = styles.len() - pos - 1;
1287 let (_start_op, end_pos) = &styles[style_id];
1288 tracker.insert(
1289 op.id_full(),
1290 (*end_pos + 1).min(tracker.len()),
1292 RichtextChunk::new_style_anchor(style_id as u32, AnchorType::End),
1293 );
1294 } else {
1295 let Some((style, end)) = Self::style_for_end_anchor(oplog, &op) else {
1296 panic!("Unhandled checkout case")
1297 };
1298 styles.push((style, end));
1299 let style_id = styles.len() - 1;
1300 tracker.insert(
1301 op.id_full(),
1302 (end + 1).min(tracker.len()),
1304 RichtextChunk::new_style_anchor(style_id as u32, AnchorType::End),
1305 );
1306 }
1307 }
1308 },
1309 _ => unreachable!(),
1310 }
1311 }
1312
1313 #[cold]
1314 #[inline(never)]
1315 fn seed_tracker_from_shallow_root(
1316 idx: ContainerIdx,
1317 oplog: &OpLog,
1318 tracker: &mut RichtextTracker,
1319 styles: &mut Vec<(StyleOp, usize)>,
1320 vv: &VersionVector,
1321 ) {
1322 if oplog.shallow_since_vv().is_empty() {
1323 return;
1324 }
1325
1326 *tracker = RichtextTracker::new_empty();
1327 styles.clear();
1328 let shallow_root_vv = oplog
1329 .dag
1330 .frontiers_to_vv(oplog.shallow_since_frontiers())
1331 .unwrap_or_else(|| oplog.shallow_since_vv().to_vv());
1332 let seed_vv = if vv.includes_vv(&shallow_root_vv) {
1333 &shallow_root_vv
1334 } else {
1335 vv
1336 };
1337
1338 #[derive(Debug, Clone, Copy)]
1339 struct SeedItem {
1340 order: usize,
1341 id: IdFull,
1342 content: RichtextChunk,
1343 }
1344
1345 struct Fenwick {
1346 tree: Vec<usize>,
1347 }
1348
1349 impl Fenwick {
1350 fn new(len: usize) -> Self {
1351 Self {
1352 tree: vec![0; len + 1],
1353 }
1354 }
1355
1356 fn add(&mut self, mut index: usize, value: usize) {
1357 index += 1;
1358 while index < self.tree.len() {
1359 self.tree[index] += value;
1360 index += index & index.wrapping_neg();
1361 }
1362 }
1363
1364 fn prefix_sum(&self, mut end: usize) -> usize {
1365 let mut sum = 0;
1366 while end > 0 {
1367 sum += self.tree[end];
1368 end -= end & end.wrapping_neg();
1369 }
1370
1371 sum
1372 }
1373 }
1374
1375 let chunks = oplog.with_history_cache(|h| h.find_text_chunks_in_shallow_root_order(idx));
1376 let mut pos = 0;
1377 let mut seed_items = Vec::new();
1378 let mut style_id_to_index = FxHashMap::default();
1379 for chunk in chunks {
1380 match chunk {
1381 RichtextStateChunk::Text(text) => {
1382 let id = text.id_full();
1383 let vv_end = seed_vv.get(&id.peer).copied().unwrap_or(0);
1384 if vv_end <= id.counter {
1385 continue;
1386 }
1387
1388 let end = vv_end.min(id.counter + text.unicode_len() as Counter);
1389 let len = (end - id.counter) as usize;
1390 if len == 0 {
1391 continue;
1392 }
1393
1394 seed_items.push(SeedItem {
1395 order: seed_items.len(),
1396 id,
1397 content: RichtextChunk::new_unknown(len as u32),
1398 });
1399 pos += len;
1400 }
1401 RichtextStateChunk::Style { style, anchor_type } => {
1402 let id = match anchor_type {
1403 AnchorType::Start => style.id(),
1404 AnchorType::End => style.id().inc(1),
1405 };
1406 if !seed_vv.includes_id(id) {
1407 continue;
1408 }
1409
1410 let style_id = if let Some(id) = style_id_to_index.get(&style.id()) {
1411 *id
1412 } else {
1413 let id = styles.len();
1414 styles.push((style.as_ref().clone(), pos));
1415 style_id_to_index.insert(style.id(), id);
1416 id
1417 };
1418
1419 if anchor_type == AnchorType::End {
1420 styles[style_id].1 = pos.saturating_sub(1);
1421 }
1422
1423 seed_items.push(SeedItem {
1424 order: seed_items.len(),
1425 id: IdFull::new(
1426 id.peer,
1427 id.counter,
1428 style.lamport + (id.counter - style.cnt) as u32,
1429 ),
1430 content: RichtextChunk::new_style_anchor(style_id as u32, anchor_type),
1431 });
1432 pos += 1;
1433 }
1434 }
1435 }
1436
1437 seed_items.sort_unstable_by_key(|item| (item.id.peer, item.id.counter));
1438 let mut seen_end_by_peer: FxHashMap<PeerID, Counter> = FxHashMap::default();
1439 let mut normalized_items = Vec::with_capacity(seed_items.len());
1440 for mut item in seed_items {
1441 let end = item.id.counter + item.content.len() as Counter;
1442 let seen_end = seen_end_by_peer.entry(item.id.peer).or_default();
1443 if end <= *seen_end {
1444 continue;
1445 }
1446
1447 if item.id.counter < *seen_end {
1448 let skip = (*seen_end - item.id.counter) as usize;
1449 item.id = item.id.inc(skip as Counter);
1450 item.content = item.content.slice(skip..item.content.len());
1451 }
1452
1453 *seen_end = end;
1454 normalized_items.push(item);
1455 }
1456
1457 let seed_items = normalized_items;
1458 let fenwick_len = seed_items
1459 .iter()
1460 .map(|item| item.order)
1461 .max()
1462 .map_or(0, |order| order + 1);
1463 let mut inserted = Fenwick::new(fenwick_len);
1464 for item in seed_items {
1465 let pos = inserted.prefix_sum(item.order);
1466 tracker.insert_seeded(pos, item.content, item.id);
1467 inserted.add(item.order, item.content.len());
1468 }
1469
1470 tracker.mark_shallow_root_applied(seed_vv);
1471 }
1472
1473 #[cold]
1474 #[inline(never)]
1475 fn build_full_crdt_tracker(
1476 idx: ContainerIdx,
1477 oplog: &OpLog,
1478 vv: &VersionVector,
1479 ) -> (RichtextTracker, Vec<(StyleOp, usize)>) {
1480 struct RichtextRebuildVisitor<'a> {
1481 oplog: &'a OpLog,
1482 tracker: &'a mut RichtextTracker,
1483 styles: &'a mut Vec<(StyleOp, usize)>,
1484 }
1485
1486 impl RebuildOpVisitor for RichtextRebuildVisitor<'_> {
1487 fn visit(&mut self, vv: &VersionVector, op: RichOp<'_>) {
1488 self.tracker.checkout(vv);
1489 RichtextDiffCalculator::apply_crdt_op_to_tracker(
1490 self.oplog,
1491 self.tracker,
1492 self.styles,
1493 op,
1494 );
1495 }
1496 }
1497
1498 let mut tracker = RichtextTracker::new_with_unknown();
1499 let mut styles = Vec::new();
1500 if !oplog.shallow_since_vv().is_empty() {
1501 Self::seed_tracker_from_shallow_root(idx, oplog, &mut tracker, &mut styles, vv);
1502 }
1503
1504 let mut visitor = RichtextRebuildVisitor {
1505 oplog,
1506 tracker: &mut tracker,
1507 styles: &mut styles,
1508 };
1509 replay_container_ops_from_empty(idx, oplog, vv, &mut visitor);
1510
1511 (tracker, styles)
1512 }
1513}
1514
1515impl DiffCalculatorTrait for RichtextDiffCalculator {
1516 fn start_tracking(
1517 &mut self,
1518 _oplog: &super::oplog::OpLog,
1519 vv: &crate::VersionVector,
1520 mode: DiffMode,
1521 ) {
1522 match mode {
1523 DiffMode::Linear => {
1524 *self.mode = RichtextCalcMode::Linear {
1525 diff: DeltaRope::new(),
1526 last_style_start: None,
1527 };
1528 }
1529 _ => {
1530 if !matches!(&*self.mode, RichtextCalcMode::Crdt { .. }) {
1531 unreachable!();
1532 }
1533 }
1534 }
1535
1536 match &mut *self.mode {
1537 RichtextCalcMode::Crdt {
1538 tracker,
1539 styles,
1540 start_vv,
1541 source_not_in_op_context,
1542 shallow_root_seeded,
1543 } => {
1544 *source_not_in_op_context = false;
1545 if !vv.includes_vv(start_vv) || !tracker.all_vv().includes_vv(vv) {
1546 **tracker = RichtextTracker::new_with_unknown();
1547 styles.clear();
1548 *start_vv = vv.clone();
1549 *shallow_root_seeded = false;
1550 }
1551
1552 tracker.checkout(vv);
1553 }
1554 RichtextCalcMode::Linear { .. } => {}
1555 }
1556 }
1557
1558 fn apply_change(
1559 &mut self,
1560 oplog: &super::oplog::OpLog,
1561 op: crate::op::RichOp,
1562 vv: Option<&crate::VersionVector>,
1563 ) {
1564 match &mut *self.mode {
1565 RichtextCalcMode::Linear {
1566 diff,
1567 last_style_start,
1568 } => match &op.raw_op().content {
1569 crate::op::InnerContent::List(l) => match l {
1570 InnerListOp::Insert { .. }
1571 | InnerListOp::Move { .. }
1572 | InnerListOp::Set { .. } => {
1573 unreachable!()
1574 }
1575 InnerListOp::InsertText {
1576 slice: _,
1577 unicode_start,
1578 unicode_len: len,
1579 pos,
1580 } => {
1581 let s = oplog.arena.slice_by_unicode(
1582 *unicode_start as usize..(*unicode_start + *len) as usize,
1583 );
1584 diff.insert_value(
1585 *pos as usize,
1586 RichtextStateChunk::new_text(s, op.id_full()),
1587 (),
1588 );
1589 }
1590 InnerListOp::Delete(del) => {
1591 diff.delete(del.start() as usize, del.atom_len());
1592 }
1593 InnerListOp::StyleStart {
1594 start,
1595 end,
1596 key,
1597 info,
1598 value,
1599 } => {
1600 debug_assert!(start < end, "start: {}, end: {}", start, end);
1601 let style_op = Arc::new(StyleOp {
1602 lamport: op.lamport(),
1603 peer: op.peer,
1604 cnt: op.id_start().counter,
1605 key: key.clone(),
1606 value: value.clone(),
1607 info: *info,
1608 });
1609
1610 *last_style_start = Some((style_op.clone(), *end));
1611 diff.insert_value(
1612 *start as usize,
1613 RichtextStateChunk::new_style(style_op, AnchorType::Start),
1614 (),
1615 );
1616 }
1617 InnerListOp::StyleEnd => {
1618 let (style_op, pos) = match last_style_start.take() {
1619 Some((style_op, pos)) => (style_op, pos),
1620 None => {
1621 let Some((style_op, pos)) = Self::style_for_end_anchor(oplog, &op)
1622 else {
1623 panic!("Unhandled checkout case")
1624 };
1625
1626 (Arc::new(style_op), pos as u32)
1627 }
1628 };
1629 assert_eq!(style_op.peer, op.peer);
1630 assert_eq!(style_op.cnt, op.id_start().counter - 1);
1631 diff.insert_value(
1632 pos as usize + 1,
1633 RichtextStateChunk::new_style(style_op, AnchorType::End),
1634 (),
1635 );
1636 }
1637 },
1638 _ => unreachable!(),
1639 },
1640 RichtextCalcMode::Crdt {
1641 tracker,
1642 styles,
1643 start_vv,
1644 source_not_in_op_context: _,
1645 shallow_root_seeded,
1646 } => {
1647 if !*shallow_root_seeded && !oplog.shallow_since_vv().is_empty() {
1648 Self::seed_tracker_from_shallow_root(
1649 op.raw_op().container,
1650 oplog,
1651 tracker,
1652 styles,
1653 start_vv,
1654 );
1655 *shallow_root_seeded = true;
1656 }
1657
1658 if let Some(vv) = vv {
1659 tracker.checkout(vv);
1660 }
1661 Self::apply_crdt_op_to_tracker(oplog, tracker, styles, op);
1662 }
1663 }
1664 }
1665
1666 fn calculate_diff(
1667 &mut self,
1668 idx: ContainerIdx,
1669 oplog: &OpLog,
1670 info: DiffCalcVersionInfo,
1671 _: impl FnMut(&ContainerID),
1672 ) -> (InternalDiff, DiffMode) {
1673 fn push_tracker_chunk(
1674 delta: &mut DeltaRope<RichtextStateChunk, ()>,
1675 idx: ContainerIdx,
1676 oplog: &OpLog,
1677 styles: &[(StyleOp, usize)],
1678 value: RichtextChunk,
1679 id: ID,
1680 lamport: Option<Lamport>,
1681 ) {
1682 match value.value() {
1683 RichtextChunkValue::Text(text) => {
1684 delta.push_insert(
1685 RichtextStateChunk::Text(
1686 TextChunk::new(
1688 oplog
1689 .arena
1690 .slice_by_unicode(text.start as usize..text.end as usize),
1691 IdFull::new(id.peer, id.counter, lamport.unwrap()),
1692 ),
1693 ),
1694 (),
1695 );
1696 }
1697 RichtextChunkValue::StyleAnchor { id, anchor_type } => {
1698 delta.push_insert(
1699 RichtextStateChunk::Style {
1700 style: Arc::new(styles[id as usize].0.clone()),
1701 anchor_type,
1702 },
1703 (),
1704 );
1705 }
1706 RichtextChunkValue::Unknown(len) => {
1707 assert_ne!(id.peer, PeerID::MAX);
1709 let mut id = id;
1710 let mut acc_len = 0;
1711 let end = id.counter + len as Counter;
1712 let shallow_root = oplog.shallow_since_vv().get(&id.peer).copied().unwrap_or(0);
1713 if id.counter < shallow_root {
1714 let target_end = shallow_root.min(end);
1716 oplog.with_history_cache(|h| {
1717 let chunks = h.find_text_chunks_in(
1718 idx,
1719 IdSpan::new(id.peer, id.counter, target_end),
1720 );
1721 for c in chunks {
1722 acc_len += c.rle_len();
1723 delta.push_insert(c, ());
1724 }
1725 });
1726 id.counter = shallow_root;
1727 }
1728
1729 if id.counter < end {
1730 for rich_op in oplog.iter_ops(IdSpan::new(id.peer, id.counter, end)) {
1731 acc_len += rich_op.content_len();
1732 let op = rich_op.op();
1733 let lamport = rich_op.lamport();
1734 let content = op.content.as_list().unwrap();
1735 match content {
1736 InnerListOp::InsertText { slice, .. } => {
1737 delta.push_insert(
1738 RichtextStateChunk::Text(TextChunk::new(
1739 slice.clone(),
1740 IdFull::new(id.peer, op.counter, lamport),
1741 )),
1742 (),
1743 );
1744 }
1745 _ => unreachable!("{:?}", content),
1746 }
1747 }
1748 }
1749
1750 debug_assert_eq!(acc_len, len as usize);
1751 }
1752 RichtextChunkValue::MoveAnchor => unreachable!(),
1753 }
1754 }
1755
1756 fn push_tracker_delta_item(
1757 delta: &mut DeltaRope<RichtextStateChunk, ()>,
1758 idx: ContainerIdx,
1759 oplog: &OpLog,
1760 styles: &[(StyleOp, usize)],
1761 item: CrdtRopeDelta,
1762 ) {
1763 match item {
1764 CrdtRopeDelta::Retain(len) => {
1765 delta.push_retain(len, ());
1766 }
1767 CrdtRopeDelta::Insert {
1768 chunk: value,
1769 id,
1770 lamport,
1771 } => push_tracker_chunk(delta, idx, oplog, styles, value, id, lamport),
1772 CrdtRopeDelta::Delete(len) => {
1773 delta.push_delete(len);
1774 }
1775 }
1776 }
1777
1778 match &mut *self.mode {
1779 RichtextCalcMode::Linear { diff, .. } => (
1780 InternalDiff::RichtextRaw(std::mem::take(diff)),
1781 DiffMode::Linear,
1782 ),
1783 RichtextCalcMode::Crdt {
1784 tracker,
1785 styles,
1786 source_not_in_op_context,
1787 ..
1788 } => {
1789 let (mut retreat, _) = info.from_vv.diff_iter(info.to_vv);
1790 let has_retreat = retreat.next().is_some();
1791 let base_is_trustworthy = info.replay_base_is_critical
1810 || (info.replay_base_vv == info.from_vv && !*source_not_in_op_context);
1811 let should_rebuild =
1812 has_retreat || !base_is_trustworthy || !oplog.shallow_since_vv().is_empty();
1813 if should_rebuild {
1814 #[cfg(test)]
1824 FULL_TRACKER_REBUILD_COUNT.with(|c| c.set(c.get() + 1));
1825 let mut merged = info.from_vv.clone();
1826 merged.merge(info.to_vv);
1827 let (mut full_tracker, full_styles) =
1828 Self::build_full_crdt_tracker(idx, oplog, &merged);
1829
1830 let mut delta = DeltaRope::new();
1831 for item in full_tracker.diff(info.from_vv, info.to_vv) {
1832 push_tracker_delta_item(&mut delta, idx, oplog, &full_styles, item);
1833 }
1834 **tracker = full_tracker;
1835 *styles = full_styles;
1836 return (InternalDiff::RichtextRaw(delta), DiffMode::Checkout);
1842 }
1843
1844 let mut delta = DeltaRope::new();
1845 for item in tracker.diff(info.from_vv, info.to_vv) {
1846 #[cfg(debug_assertions)]
1852 if let CrdtRopeDelta::Insert { chunk, .. } = &item {
1853 debug_assert!(
1854 !matches!(chunk.value(), RichtextChunkValue::Unknown(_)),
1855 "the opaque below-base span leaked into a richtext diff"
1856 );
1857 }
1858 push_tracker_delta_item(&mut delta, idx, oplog, styles, item);
1859 }
1860
1861 (InternalDiff::RichtextRaw(delta), DiffMode::Checkout)
1862 }
1863 }
1864 }
1865
1866 fn finish_this_round(&mut self) {
1867 match &mut *self.mode {
1868 RichtextCalcMode::Crdt { .. } => {}
1869 RichtextCalcMode::Linear {
1870 diff,
1871 last_style_start,
1872 } => {
1873 *diff = DeltaRope::new();
1874 last_style_start.take();
1875 }
1876 }
1877 }
1878}
1879
1880#[derive(Debug)]
1881pub(crate) struct MovableListDiffCalculator {
1882 list: Box<ListDiffCalculator>,
1883 inner: Box<MovableListInner>,
1884}
1885
1886#[derive(Debug)]
1887struct MovableListInner {
1888 changed_elements: FxHashMap<CompactIdLp, ElementDelta>,
1889 move_id_to_elem_id: FxHashMap<ID, IdLp>,
1890 current_mode: DiffMode,
1891}
1892
1893impl DiffCalculatorTrait for MovableListDiffCalculator {
1894 fn start_tracking(&mut self, _oplog: &OpLog, vv: &crate::VersionVector, mode: DiffMode) {
1895 self.list.source_not_in_op_context = false;
1896 if !vv.includes_vv(&self.list.start_vv) || !self.list.tracker.all_vv().includes_vv(vv) {
1897 *self.list.tracker = RichtextTracker::new_with_unknown();
1898 self.list.start_vv = vv.clone();
1899 }
1900
1901 self.list.tracker.checkout(vv);
1902 self.inner.current_mode = mode;
1903 }
1904
1905 fn apply_change(
1906 &mut self,
1907 oplog: &OpLog,
1908 op: crate::op::RichOp,
1909 vv: Option<&crate::VersionVector>,
1910 ) {
1911 let InnerContent::List(l) = &op.raw_op().content else {
1912 unreachable!()
1913 };
1914
1915 match l {
1920 InnerListOp::Insert { slice, pos: _ } => {
1921 let op_id = op.id_full().idlp();
1922 for i in 0..slice.atom_len() {
1923 let id = op_id.inc(i as Counter);
1924 let value = oplog.arena.get_value(slice.0.start as usize + i).unwrap();
1925
1926 self.inner.changed_elements.insert(
1927 id.compact(),
1928 ElementDelta {
1929 pos: Some(id),
1930 value: value.clone(),
1931 value_updated: true,
1932 value_id: Some(id),
1933 },
1934 );
1935 }
1936 }
1937 InnerListOp::Delete(_) => {}
1938 InnerListOp::Move { elem_id, .. } => {
1939 let idlp = IdLp::new(op.peer, op.lamport());
1940 match self.inner.changed_elements.get_mut(&elem_id.compact()) {
1941 Some(change) => {
1942 if change.pos.is_some() && change.pos.as_ref().unwrap() > &idlp {
1943 } else {
1944 change.pos = Some(idlp);
1945 }
1946 }
1947 None => {
1948 self.inner.changed_elements.insert(
1949 elem_id.compact(),
1950 ElementDelta {
1951 pos: Some(idlp),
1952 value: LoroValue::Null,
1953 value_updated: false,
1954 value_id: None,
1955 },
1956 );
1957 }
1958 }
1959 }
1960 InnerListOp::Set { elem_id, value } => {
1961 let idlp = IdLp::new(op.peer, op.lamport());
1962 match self.inner.changed_elements.get_mut(&elem_id.compact()) {
1963 Some(change) => {
1964 if change.value_id.is_some() && change.value_id.as_ref().unwrap() > &idlp {
1965 } else {
1966 change.value_id = Some(idlp);
1967 change.value = value.clone();
1968 }
1969 }
1970 None => {
1971 self.inner.changed_elements.insert(
1972 elem_id.compact(),
1973 ElementDelta {
1974 pos: None,
1975 value: value.clone(),
1976 value_updated: true,
1977 value_id: Some(idlp),
1978 },
1979 );
1980 }
1981 }
1982 }
1983
1984 InnerListOp::StyleStart { .. } => unreachable!(),
1985 InnerListOp::StyleEnd => unreachable!(),
1986 InnerListOp::InsertText { .. } => unreachable!(),
1987 }
1988
1989 let is_checkout = matches!(self.inner.current_mode, DiffMode::Checkout);
1990
1991 {
1992 if let Some(vv) = vv {
1994 self.list.tracker.checkout(vv);
1995 }
1996 Self::apply_op_to_tracker(
1997 &mut self.list.tracker,
1998 &mut self.inner.move_id_to_elem_id,
1999 oplog,
2000 &op,
2001 is_checkout,
2002 );
2003 };
2004 }
2005
2006 fn finish_this_round(&mut self) {
2007 self.list.finish_this_round();
2008 }
2009
2010 #[instrument(skip(self, oplog, on_new_container))]
2011 fn calculate_diff(
2012 &mut self,
2013 idx: ContainerIdx,
2014 oplog: &OpLog,
2015 info: DiffCalcVersionInfo,
2016 mut on_new_container: impl FnMut(&ContainerID),
2017 ) -> (InternalDiff, DiffMode) {
2018 let (mut retreat, _) = info.from_vv.diff_iter(info.to_vv);
2019 let has_retreat = retreat.next().is_some();
2020 if has_retreat || info.replay_base_vv != info.from_vv || self.list.source_not_in_op_context
2026 {
2027 let mut merged = info.from_vv.clone();
2028 merged.merge(info.to_vv);
2029 self.rebuild_full_tracker(idx, oplog, &merged);
2030 }
2031
2032 let (InternalDiff::ListRaw(list_diff), diff_mode) =
2033 self.list.calculate_diff(idx, oplog, info, |_| {})
2034 else {
2035 unreachable!()
2036 };
2037
2038 assert_eq!(diff_mode, DiffMode::Checkout);
2039 let is_checkout = matches!(
2040 self.inner.current_mode,
2041 DiffMode::Checkout | DiffMode::Import
2042 );
2043 let mut element_changes: FxHashMap<CompactIdLp, ElementDelta> = if is_checkout {
2044 FxHashMap::default()
2045 } else {
2046 std::mem::take(&mut self.inner.changed_elements)
2047 };
2048
2049 if is_checkout {
2050 for id in self.inner.changed_elements.keys() {
2051 element_changes.insert(*id, ElementDelta::placeholder());
2052 }
2053 }
2054
2055 let list_diff: Delta<SmallVec<[IdFull; 1]>, ()> = Delta {
2056 vec: list_diff
2057 .iter()
2058 .map(|x| match x {
2059 &DeltaItem::Retain { retain, .. } => DeltaItem::Retain {
2060 retain,
2061 attributes: (),
2062 },
2063 DeltaItem::Insert { insert, .. } => {
2064 let len = insert.length();
2065 let id = insert.id;
2066 let mut new_insert = SmallVec::with_capacity(len);
2067 for i in 0..len {
2068 let id = id.inc(i as i32);
2069 let elem_id =
2070 if let Some(e) = self.inner.move_id_to_elem_id.get(&id.id()) {
2071 e.compact()
2072 } else {
2073 insert.elem_id.unwrap_or_else(|| id.idlp().compact())
2074 };
2075 if is_checkout {
2076 element_changes.insert(elem_id, ElementDelta::placeholder());
2078 }
2079 new_insert.push(id);
2080 }
2081
2082 DeltaItem::Insert {
2083 insert: new_insert,
2084 attributes: (),
2085 }
2086 }
2087 &DeltaItem::Delete { delete, .. } => DeltaItem::Delete {
2088 delete,
2089 attributes: (),
2090 },
2091 })
2092 .collect(),
2093 };
2094
2095 if is_checkout {
2096 oplog.with_history_cache(|history_cache| {
2097 let checkout_index = &history_cache.get_checkout_index().movable_list;
2098 element_changes.retain(|id, change| {
2099 let id = id.to_id();
2100 let Some(pos) = checkout_index.last_pos(id, info.to_vv, Lamport::MAX, oplog)
2105 else {
2106 return false;
2107 };
2108 let value = checkout_index
2110 .last_value(id, info.to_vv, Lamport::MAX, oplog)
2111 .unwrap();
2112 let old_pos = checkout_index.last_pos(id, info.from_vv, Lamport::MAX, oplog);
2114 let old_value =
2116 checkout_index.last_value(id, info.from_vv, Lamport::MAX, oplog);
2117 if old_pos.is_none() && old_value.is_none() {
2118 if let LoroValue::Container(c) = &value.value {
2119 on_new_container(c);
2120 }
2121 *change = ElementDelta {
2122 pos: Some(pos.idlp()),
2123 value: value.value.clone(),
2124 value_id: Some(IdLp::new(value.peer, value.lamport)),
2125 value_updated: true,
2126 };
2127 } else {
2128 *change = ElementDelta {
2130 pos: Some(pos.idlp()),
2131 value: value.value.clone(),
2132 value_updated: old_value.unwrap().value != value.value,
2133 value_id: Some(IdLp::new(value.peer, value.lamport)),
2134 };
2135 }
2136
2137 true
2138 });
2139 });
2140 }
2141
2142 let diff = MovableListInnerDelta {
2143 list: list_diff,
2144 elements: element_changes,
2145 };
2146
2147 (InternalDiff::MovableList(diff), self.inner.current_mode)
2148 }
2149}
2150
2151impl MovableListDiffCalculator {
2152 fn new(_container: ContainerIdx) -> MovableListDiffCalculator {
2153 MovableListDiffCalculator {
2154 list: Default::default(),
2155 inner: Box::new(MovableListInner {
2156 changed_elements: Default::default(),
2157 current_mode: DiffMode::Checkout,
2158 move_id_to_elem_id: Default::default(),
2159 }),
2160 }
2161 }
2162
2163 fn apply_op_to_tracker(
2164 tracker: &mut RichtextTracker,
2165 move_id_to_elem_id: &mut FxHashMap<ID, IdLp>,
2166 oplog: &OpLog,
2167 op: &RichOp<'_>,
2168 is_checkout: bool,
2169 ) {
2170 let real_op = op.op();
2171 match &real_op.content {
2172 InnerContent::List(l) => match l {
2173 InnerListOp::Insert { .. } | InnerListOp::Delete(_) => {
2174 ListDiffCalculator::apply_op_to_tracker(tracker, op);
2175 }
2176 InnerListOp::Move { from, elem_id, to } => {
2177 move_id_to_elem_id.insert(op.id(), *elem_id);
2178 if !tracker.current_vv().includes_id(op.id()) {
2179 let last_pos = if is_checkout {
2180 oplog.with_history_cache(|h| {
2181 let list = &h.get_checkout_index().movable_list;
2182 list.last_pos(*elem_id, tracker.current_vv(), Lamport::MAX, oplog)
2183 .expect("moved element should have a visible source position")
2184 .id()
2185 })
2186 } else {
2187 ID::new(PeerID::MAX - 2, 0)
2190 };
2191 tracker.move_item(op.id_full(), last_pos, *from as usize, *to as usize);
2192 }
2193 }
2194 InnerListOp::Set { .. } => {}
2195 InnerListOp::InsertText { .. }
2196 | InnerListOp::StyleStart { .. }
2197 | InnerListOp::StyleEnd => unreachable!(),
2198 },
2199 _ => unreachable!(),
2200 }
2201 }
2202
2203 #[cold]
2204 #[inline(never)]
2205 fn rebuild_full_tracker(&mut self, idx: ContainerIdx, oplog: &OpLog, vv: &VersionVector) {
2206 struct MovableListRebuildVisitor<'a> {
2207 oplog: &'a OpLog,
2208 tracker: &'a mut RichtextTracker,
2209 move_id_to_elem_id: &'a mut FxHashMap<ID, IdLp>,
2210 }
2211
2212 impl RebuildOpVisitor for MovableListRebuildVisitor<'_> {
2213 fn visit(&mut self, vv: &VersionVector, op: RichOp<'_>) {
2214 self.tracker.checkout(vv);
2215 MovableListDiffCalculator::apply_op_to_tracker(
2216 self.tracker,
2217 self.move_id_to_elem_id,
2218 self.oplog,
2219 &op,
2220 true,
2221 );
2222 }
2223 }
2224
2225 let mut tracker = RichtextTracker::new_with_unknown();
2226 let mut move_id_to_elem_id = FxHashMap::default();
2227 let mut visitor = MovableListRebuildVisitor {
2228 oplog,
2229 tracker: &mut tracker,
2230 move_id_to_elem_id: &mut move_id_to_elem_id,
2231 };
2232 replay_container_ops_from_empty(idx, oplog, vv, &mut visitor);
2233
2234 *self.list.tracker = tracker;
2235 self.inner.move_id_to_elem_id = move_id_to_elem_id;
2236 }
2237}
2238
2239#[test]
2240fn test_size() {
2241 let text = RichtextDiffCalculator::new();
2242 let size = std::mem::size_of_val(&text);
2243 assert!(size < 50, "RichtextDiffCalculator size: {}", size);
2244 let list = MovableListDiffCalculator::new(ContainerIdx::from_index_and_type(
2245 0,
2246 loro_common::ContainerType::MovableList,
2247 ));
2248 let size = std::mem::size_of_val(&list);
2249 assert!(size < 50, "MovableListDiffCalculator size: {}", size);
2250 let calc = ContainerDiffCalculator::Richtext(text);
2251 let size = std::mem::size_of_val(&calc);
2252 assert!(size < 50, "ContainerDiffCalculator size: {}", size);
2253}
2254
2255#[cfg(test)]
2258fn movable_list_fixture() -> crate::LoroDoc {
2259 use crate::{LoroDoc, MapHandler};
2260
2261 let base = LoroDoc::new_auto_commit();
2262 base.set_peer_id(1).unwrap();
2263 let nested = base
2264 .get_movable_list("items")
2265 .push_container(MapHandler::new_detached())
2266 .unwrap();
2267 nested.insert("value", 0).unwrap();
2268 base.get_list("unrelated-list").push("a").unwrap();
2269 base.get_text("unrelated-text")
2270 .insert(0, "a", crate::cursor::PosType::Unicode)
2271 .unwrap();
2272 base.commit_then_renew();
2273 base
2274}
2275
2276#[cfg(test)]
2277fn nested_map(doc: &crate::LoroDoc) -> crate::MapHandler {
2278 use crate::handler::ValueOrHandler;
2279
2280 match doc.get_movable_list("items").get_(0).unwrap() {
2281 ValueOrHandler::Handler(handler) => handler.into_map().unwrap(),
2282 ValueOrHandler::Value(value) => panic!("expected nested map, got {value:?}"),
2283 }
2284}
2285
2286#[cfg(test)]
2289fn assert_single_map_value_diff(
2290 diffs: &[InternalContainerDiff],
2291 expected_idx: ContainerIdx,
2292 key: &str,
2293 expected: LoroValue,
2294) {
2295 assert_eq!(diffs.len(), 1);
2296 let diff = &diffs[0];
2297 assert_eq!(diff.idx, expected_idx);
2298 let DiffVariant::Internal(InternalDiff::Map(map_diff)) = &diff.diff else {
2299 panic!("expected an internal map diff, got {:?}", diff.diff);
2300 };
2301 let value = map_diff
2302 .updated
2303 .get(&InternalString::from(key))
2304 .unwrap_or_else(|| panic!("map diff has no entry for {key:?}: {map_diff:?}"))
2305 .as_ref()
2306 .and_then(|map_value| map_value.value.clone());
2307 assert_eq!(value, Some(expected));
2308}
2309
2310#[test]
2311fn causal_existing_peer_import_uses_current_version_as_replay_base() {
2312 use crate::{handler::HandlerTrait, loro::ExportMode};
2313
2314 let base = movable_list_fixture();
2315 let target = base.fork();
2316 let source = base.fork();
2317 let relay = base.fork();
2318 source.set_peer_id(1).unwrap();
2322 relay.set_peer_id(2).unwrap();
2323 relay.get_map("relay").insert("ready", true).unwrap();
2324 relay.commit_then_renew();
2325 let relay_updates = relay.export(ExportMode::updates(&base.oplog_vv())).unwrap();
2326 target.import(&relay_updates).unwrap();
2327 source.import(&relay_updates).unwrap();
2328
2329 let before = target.oplog_vv();
2330 let before_frontiers = target.oplog_frontiers();
2331 nested_map(&source).insert("value", 1).unwrap();
2332 source.commit_then_renew();
2333 let nested_update = source.export(ExportMode::updates(&before)).unwrap();
2334
2335 target.detach();
2337 target.import(&nested_update).unwrap();
2338 let after = target.oplog_vv();
2339 let after_frontiers = target.oplog_frontiers();
2340 let expected_idx = nested_map(&target).idx();
2341
2342 let oplog = target.oplog().lock();
2343 let (base, _) =
2344 oplog.iter_from_replay_base_causally(&before, &before_frontiers, &after, &after_frontiers);
2345 assert_eq!(base.vv, before);
2346 assert_eq!(base.diff_mode, DiffMode::ImportGreaterUpdates);
2347 assert_eq!(
2348 changed_containers_between(&oplog, &before, &after),
2349 [expected_idx].into_iter().collect()
2350 );
2351
2352 let mut calculator = DiffCalculator::new(false);
2353 let (diffs, _) = calculator.calc_diff_internal(
2354 &oplog,
2355 &before,
2356 &before_frontiers,
2357 &after,
2358 &after_frontiers,
2359 None,
2360 );
2361 assert_eq!(calculator.calculators.len(), 1);
2362 assert!(calculator.get_calc(expected_idx).is_some());
2363 assert_single_map_value_diff(&diffs, expected_idx, "value", 1.into());
2364}
2365
2366#[test]
2367fn conservative_replay_only_builds_calculators_for_changed_containers() {
2368 use crate::{handler::HandlerTrait, loro::ExportMode};
2369
2370 let base = movable_list_fixture();
2371 let target = base.fork();
2372 target.set_peer_id(2).unwrap();
2373 target.get_map("relay").insert("ready", true).unwrap();
2374 target.get_list("unrelated-list").push("b").unwrap();
2377 target.commit_then_renew();
2378
2379 let source = base.fork();
2380 source.set_peer_id(3).unwrap();
2381 nested_map(&source).insert("value", 1).unwrap();
2382 source.get_list("unrelated-list").push("c").unwrap();
2383 source.commit_then_renew();
2384
2385 let before = target.oplog_vv();
2386 let before_frontiers = target.oplog_frontiers();
2387 let nested_update = source.export(ExportMode::updates(&before)).unwrap();
2388
2389 target.detach();
2391 target.import(&nested_update).unwrap();
2392 let after = target.oplog_vv();
2393 let after_frontiers = target.oplog_frontiers();
2394 let expected_idx = nested_map(&target).idx();
2395 let list_idx = target.get_list("unrelated-list").idx();
2396
2397 let oplog = target.oplog().lock();
2398 let (base, _) =
2399 oplog.iter_from_replay_base_causally(&before, &before_frontiers, &after, &after_frontiers);
2400 assert_ne!(
2401 base.vv, before,
2402 "the fixture must exercise conservative replay"
2403 );
2404 assert_eq!(base.diff_mode, DiffMode::Import);
2405 assert_eq!(
2406 changed_containers_between(&oplog, &before, &after),
2407 [expected_idx, list_idx].into_iter().collect()
2408 );
2409
2410 let mut calculator = DiffCalculator::new(false);
2411 let (diffs, _) = calculator.calc_diff_internal(
2412 &oplog,
2413 &before,
2414 &before_frontiers,
2415 &after,
2416 &after_frontiers,
2417 None,
2418 );
2419 assert_eq!(calculator.calculators.len(), 2);
2420 assert!(calculator.get_calc(expected_idx).is_some());
2421 assert!(calculator.get_calc(list_idx).is_some());
2422 let map_diffs: Vec<_> = diffs
2423 .iter()
2424 .filter(|d| d.idx == expected_idx)
2425 .cloned()
2426 .collect();
2427 assert_single_map_value_diff(&map_diffs, expected_idx, "value", 1.into());
2428}
2429
2430#[test]
2431fn conservative_checkout_replay_filters_to_retreat_changed_containers() {
2432 use crate::{handler::HandlerTrait, loro::ExportMode};
2433
2434 let base = movable_list_fixture();
2435 let target = base.fork();
2436 target.set_peer_id(2).unwrap();
2437 target.get_map("relay").insert("ready", true).unwrap();
2438 target.commit_then_renew();
2439
2440 let source = base.fork();
2441 source.set_peer_id(3).unwrap();
2442 nested_map(&source).insert("value", 1).unwrap();
2443 source.commit_then_renew();
2444
2445 let after = target.oplog_vv();
2448 let after_frontiers = target.oplog_frontiers();
2449 let nested_update = source
2450 .export(ExportMode::updates(&base.oplog_vv()))
2451 .unwrap();
2452
2453 target.detach();
2455 target.import(&nested_update).unwrap();
2456 let before = target.oplog_vv();
2457 let before_frontiers = target.oplog_frontiers();
2458 let expected_idx = nested_map(&target).idx();
2459
2460 let oplog = target.oplog().lock();
2461 let (base, _) =
2462 oplog.iter_from_replay_base_causally(&before, &before_frontiers, &after, &after_frontiers);
2463 assert_ne!(
2464 base.vv, before,
2465 "the fixture must exercise conservative replay"
2466 );
2467 assert_eq!(base.diff_mode, DiffMode::Checkout);
2468 assert_eq!(
2469 changed_containers_between(&oplog, &before, &after),
2470 [expected_idx].into_iter().collect()
2471 );
2472
2473 let mut calculator = DiffCalculator::new(false);
2474 let (diffs, _) = calculator.calc_diff_internal(
2475 &oplog,
2476 &before,
2477 &before_frontiers,
2478 &after,
2479 &after_frontiers,
2480 None,
2481 );
2482 assert_eq!(calculator.calculators.len(), 1);
2483 assert!(calculator.get_calc(expected_idx).is_some());
2484 assert_single_map_value_diff(&diffs, expected_idx, "value", 0.into());
2486}
2487
2488#[test]
2495fn persisted_walk_does_not_duplicate_style_entries() {
2496 use crate::{cursor::PosType, handler::HandlerTrait, LoroDoc};
2497
2498 let a = LoroDoc::new_auto_commit();
2499 a.set_peer_id(1).unwrap();
2500 a.get_text("t")
2501 .insert(0, "seed text", PosType::Unicode)
2502 .unwrap();
2503 a.commit_then_renew();
2504 let marks = 5;
2505 let mut stops = vec![a.oplog_frontiers()];
2506 for round in 0..marks {
2507 let t = a.get_text("t");
2508 t.insert(0, "x", PosType::Unicode).unwrap();
2509 t.mark(0, 4, "bold", (round % 2 == 0).into(), PosType::Unicode)
2510 .unwrap();
2511 a.commit_then_renew();
2512 stops.push(a.oplog_frontiers());
2513 }
2514
2515 let oplog = a.oplog().lock();
2516 let idx = a.get_text("t").idx();
2517 let vv = |f: &Frontiers| oplog.dag.frontiers_to_vv(f).unwrap();
2518 let mut calc = DiffCalculator::new(true);
2519 let first = &stops[0];
2520 let last = stops.last().unwrap();
2521 calc.calc_diff_internal(&oplog, &vv(first), first, &vv(last), last, None);
2522 calc.calc_diff_internal(&oplog, &vv(last), last, &vv(first), first, None);
2523 for w in stops.windows(2) {
2524 calc.calc_diff_internal(&oplog, &vv(&w[0]), &w[0], &vv(&w[1]), &w[1], None);
2525 }
2526
2527 let (_, c) = calc.get_or_create_calc(idx, oplog.arena.get_depth(idx));
2528 let ContainerDiffCalculator::Richtext(text) = c else {
2529 panic!("expected a richtext calculator");
2530 };
2531 let ids = text.tracked_style_ids();
2532 assert_eq!(ids.len(), marks, "duplicate style entries: {ids:?}");
2533}