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