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 _;
11use itertools::Itertools;
12
13use enum_dispatch::enum_dispatch;
14use fxhash::{FxHashMap, FxHashSet};
15use loro_common::{
16 CompactIdLp, ContainerID, Counter, HasCounterSpan, IdFull, IdLp, IdSpan, LoroValue, PeerID, ID,
17};
18use loro_delta::DeltaRope;
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}
112
113impl DiffCalculator {
114 pub fn new(persist: bool) -> Self {
120 Self {
121 calculators: Default::default(),
122 retain_mode: if persist {
123 DiffCalculatorRetainMode::Persist
124 } else {
125 DiffCalculatorRetainMode::Once { used: false }
126 },
127 }
128 }
129
130 #[allow(unused)]
131 pub(crate) fn get_calc(&self, container: ContainerIdx) -> Option<&ContainerDiffCalculator> {
132 self.calculators.get(&container).map(|(_, c)| c)
133 }
134
135 pub(crate) fn calc_diff_internal(
141 &mut self,
142 oplog: &super::oplog::OpLog,
143 before: &crate::VersionVector,
144 before_frontiers: &Frontiers,
145 after: &crate::VersionVector,
146 after_frontiers: &Frontiers,
147 container_filter: Option<&dyn Fn(ContainerIdx) -> bool>,
148 ) -> (Vec<InternalContainerDiff>, DiffMode) {
149 if before == after {
150 return (Vec::new(), DiffMode::Linear);
151 }
152
153 let s = tracing::span!(tracing::Level::INFO, "DiffCalc", ?before, ?after,);
154 let _e = s.enter();
155
156 let mut merged = before.clone();
157 merged.merge(after);
158 let (lca, origin_diff_mode, iter) =
159 oplog.iter_from_lca_causally(before, before_frontiers, after, after_frontiers);
160 let mut diff_mode = origin_diff_mode;
161 match &mut self.retain_mode {
162 DiffCalculatorRetainMode::Once { used } => {
163 if *used {
164 panic!("DiffCalculator with retain_mode Once can only be used once");
165 }
166 }
167 DiffCalculatorRetainMode::Persist => {
168 diff_mode = DiffMode::Checkout;
169 }
170 }
171
172 let affected_set = {
173 tracing::debug!("LCA: {:?} mode={:?}", &lca, diff_mode);
174 let mut started_set = FxHashSet::default();
175 for (change, (start_counter, end_counter), vv) in iter {
176 let iter_start = change
177 .ops
178 .binary_search_by(|op| op.ctr_last().cmp(&start_counter))
179 .unwrap_or_else(|e| e);
180 let mut visited = FxHashSet::default();
181 for mut op in &change.ops.vec()[iter_start..] {
182 if op.counter >= end_counter {
183 break;
184 }
185
186 let idx = op.container;
187 if let Some(filter) = container_filter {
188 if !filter(idx) {
189 continue;
190 }
191 }
192
193 let stack_sliced_op;
196 if op.ctr_last() < start_counter {
197 continue;
198 }
199
200 if op.counter < start_counter || op.ctr_end() > end_counter {
201 stack_sliced_op = Some(op.slice(
202 (start_counter as usize).saturating_sub(op.counter as usize),
203 op.atom_len().min((end_counter - op.counter) as usize),
204 ));
205 op = stack_sliced_op.as_ref().unwrap();
206 }
207
208 let vv = &mut vv.borrow_mut();
209 vv.extend_to_include_end_id(ID::new(change.peer(), op.counter));
210 let container = op.container;
211 let depth = oplog.arena.get_depth(container);
212 let (old_depth, calculator) = self.get_or_create_calc(container, depth);
213 if *old_depth != depth {
216 *old_depth = depth;
217 }
218
219 if !started_set.contains(&op.container) {
220 started_set.insert(container);
221 calculator.start_tracking(oplog, &lca, diff_mode);
222 }
223
224 if visited.contains(&op.container) {
225 calculator.apply_change(oplog, RichOp::new_by_change(&change, op), None);
227 } else {
228 calculator.apply_change(
229 oplog,
230 RichOp::new_by_change(&change, op),
231 Some(vv),
232 );
233 visited.insert(container);
234 }
235 }
236 }
237
238 Some(started_set)
239 };
240
241 let mut new_containers = FxHashSet::default();
244 let mut container_id_to_depth = FxHashMap::default();
245 let mut all: Vec<(Option<NonZeroU16>, ContainerIdx)> = if let Some(set) = affected_set {
246 set.into_iter()
248 .map(|x| {
249 let (depth, _) = self.calculators.get_mut(&x).unwrap();
250 (*depth, x)
251 })
252 .collect()
253 } else {
254 self.calculators
255 .iter_mut()
256 .map(|(x, (depth, _))| (*depth, *x))
257 .collect()
258 };
259 let mut ans = FxHashMap::default();
260 let info = DiffCalcVersionInfo {
261 from_vv: before,
262 to_vv: after,
263 from_frontiers: before_frontiers,
264 to_frontiers: after_frontiers,
265 };
266 while !all.is_empty() {
267 all.sort_by_key(|x| x.0);
269 for (_, container_idx) in std::mem::take(&mut all) {
270 if ans.contains_key(&container_idx) {
271 continue;
272 }
273 let (depth, calc) = self.calculators.get_mut(&container_idx).unwrap();
274 if depth.is_none() {
275 let d = oplog.arena.get_depth(container_idx);
276 if d != *depth {
277 *depth = d;
278 all.push((*depth, container_idx));
279 continue;
280 }
281 }
282 let id = oplog.arena.idx_to_id(container_idx).unwrap();
283 let bring_back = new_containers.remove(&id);
284
285 info_span!("CalcDiff", ?id).in_scope(|| {
286 let (diff, diff_mode) = calc.calculate_diff(container_idx, oplog, info, |c| {
287 new_containers.insert(c.clone());
288 container_id_to_depth
289 .insert(c.clone(), depth.and_then(|d| d.checked_add(1)));
290 oplog.arena.register_container(c);
291 });
292 calc.finish_this_round();
293 if !diff.is_empty() || bring_back {
294 ans.insert(
295 container_idx,
296 (
297 *depth,
298 InternalContainerDiff {
299 idx: container_idx,
300 bring_back,
301 is_container_deleted: false,
302 diff: diff.into(),
303 diff_mode,
304 },
305 ),
306 );
307 }
308 });
309 }
310 }
311
312 while !new_containers.is_empty() {
313 for id in std::mem::take(&mut new_containers) {
314 let Some(idx) = oplog.arena.id_to_idx(&id) else {
315 continue;
316 };
317
318 if ans.contains_key(&idx) {
319 continue;
320 }
321 let depth = container_id_to_depth.remove(&id).unwrap();
322 ans.insert(
323 idx,
324 (
325 depth,
326 InternalContainerDiff {
327 idx,
328 bring_back: true,
329 is_container_deleted: false,
330 diff: DiffVariant::None,
331 diff_mode: DiffMode::Checkout,
332 },
333 ),
334 );
335 }
336 }
337
338 (
339 ans.into_values().map(|x| x.1).collect_vec(),
340 origin_diff_mode,
341 )
342 }
343
344 pub(crate) fn get_or_create_calc(
346 &mut self,
347 idx: ContainerIdx,
348 depth: Option<NonZeroU16>,
349 ) -> &mut (Option<NonZeroU16>, ContainerDiffCalculator) {
350 self.calculators
351 .entry(idx)
352 .or_insert_with(|| match idx.get_type() {
353 crate::ContainerType::Text => (
354 depth,
355 ContainerDiffCalculator::Richtext(RichtextDiffCalculator::new()),
356 ),
357 crate::ContainerType::Map => (
358 depth,
359 ContainerDiffCalculator::Map(MapDiffCalculator::new(idx)),
360 ),
361 crate::ContainerType::List => (
362 depth,
363 ContainerDiffCalculator::List(ListDiffCalculator::default()),
364 ),
365 crate::ContainerType::Tree => (
366 depth,
367 ContainerDiffCalculator::Tree(TreeDiffCalculator::new(idx)),
368 ),
369 crate::ContainerType::Unknown(_) => (
370 depth,
371 ContainerDiffCalculator::Unknown(unknown::UnknownDiffCalculator),
372 ),
373 crate::ContainerType::MovableList => (
374 depth,
375 ContainerDiffCalculator::MovableList(MovableListDiffCalculator::new(idx)),
376 ),
377 #[cfg(feature = "counter")]
378 crate::ContainerType::Counter => (
379 depth,
380 ContainerDiffCalculator::Counter(CounterDiffCalculator::new(idx)),
381 ),
382 })
383 }
384}
385
386#[enum_dispatch]
394pub(crate) trait DiffCalculatorTrait {
395 fn start_tracking(&mut self, oplog: &OpLog, vv: &crate::VersionVector, mode: DiffMode);
396 fn apply_change(
397 &mut self,
398 oplog: &OpLog,
399 op: crate::op::RichOp,
400 vv: Option<&crate::VersionVector>,
401 );
402 fn calculate_diff(
403 &mut self,
404 idx: ContainerIdx,
405 oplog: &OpLog,
406 info: DiffCalcVersionInfo,
407 on_new_container: impl FnMut(&ContainerID),
408 ) -> (InternalDiff, DiffMode);
409 fn finish_this_round(&mut self);
411}
412
413#[enum_dispatch(DiffCalculatorTrait)]
414#[derive(Debug)]
415pub(crate) enum ContainerDiffCalculator {
416 Map(MapDiffCalculator),
417 List(ListDiffCalculator),
418 Richtext(RichtextDiffCalculator),
419 Tree(TreeDiffCalculator),
420 MovableList(MovableListDiffCalculator),
421 #[cfg(feature = "counter")]
422 Counter(counter::CounterDiffCalculator),
423 Unknown(UnknownDiffCalculator),
424}
425
426#[derive(Debug)]
427pub(crate) struct MapDiffCalculator {
428 container_idx: ContainerIdx,
429 changed: FxHashMap<InternalString, Option<MapValue>>,
430 current_mode: DiffMode,
431}
432
433impl MapDiffCalculator {
434 pub(crate) fn new(container_idx: ContainerIdx) -> Self {
435 Self {
436 container_idx,
437 changed: Default::default(),
438 current_mode: DiffMode::Checkout,
439 }
440 }
441}
442
443impl DiffCalculatorTrait for MapDiffCalculator {
444 fn start_tracking(
445 &mut self,
446 _oplog: &crate::OpLog,
447 _vv: &crate::VersionVector,
448 mode: DiffMode,
449 ) {
450 self.changed.clear();
451 self.current_mode = mode;
452 }
453
454 fn apply_change(
455 &mut self,
456 _oplog: &crate::OpLog,
457 op: crate::op::RichOp,
458 _vv: Option<&crate::VersionVector>,
459 ) {
460 if matches!(self.current_mode, DiffMode::Checkout) {
461 return;
463 }
464
465 let map = op.raw_op().content.as_map().unwrap();
466 let new_value = MapValue {
467 value: map.value.clone(),
468 peer: op.peer,
469 lamp: op.lamport(),
470 };
471 match self.changed.get(&map.key) {
472 Some(Some(old_value)) if old_value > &new_value => {}
473 _ => {
474 self.changed.insert(map.key.clone(), Some(new_value));
475 }
476 }
477 }
478
479 fn finish_this_round(&mut self) {
480 self.changed.clear();
481 self.current_mode = DiffMode::Checkout;
482 }
483
484 fn calculate_diff(
485 &mut self,
486 _idx: ContainerIdx,
487 oplog: &super::oplog::OpLog,
488 DiffCalcVersionInfo { from_vv, to_vv, .. }: DiffCalcVersionInfo,
489 mut on_new_container: impl FnMut(&ContainerID),
490 ) -> (InternalDiff, DiffMode) {
491 match self.current_mode {
492 DiffMode::Checkout | DiffMode::Import => oplog.with_history_cache(|h| {
493 let checkout_index = &h.get_checkout_index().map;
494 let mut changed = Vec::new();
495 let from_map = checkout_index.get_container_latest_op_at_vv(
496 self.container_idx,
497 from_vv,
498 Lamport::MAX,
499 oplog,
500 );
501 let mut to_map = checkout_index.get_container_latest_op_at_vv(
502 self.container_idx,
503 to_vv,
504 Lamport::MAX,
505 oplog,
506 );
507
508 for (k, peek_from) in from_map.iter() {
509 let peek_to = to_map.remove(k);
510 match peek_to {
511 None => changed.push((k.clone(), None)),
512 Some(b) => {
513 if peek_from.value != b.value {
514 changed.push((k.clone(), Some(b)))
515 }
516 }
517 }
518 }
519
520 for (k, peek_to) in to_map.into_iter() {
521 changed.push((k, Some(peek_to)));
522 }
523
524 let mut updated =
525 FxHashMap::with_capacity_and_hasher(changed.len(), Default::default());
526 for (key, value) in changed {
527 let value = value.map(|v| {
528 let value = v.value.clone();
529 if let Some(LoroValue::Container(c)) = &value {
530 on_new_container(c);
531 }
532
533 MapValue {
534 value,
535 lamp: v.lamport,
536 peer: v.peer,
537 }
538 });
539
540 updated.insert(key, value);
541 }
542
543 (InternalDiff::Map(MapDelta { updated }), DiffMode::Checkout)
544 }),
545 DiffMode::ImportGreaterUpdates | DiffMode::Linear => {
546 let changed = std::mem::take(&mut self.changed);
547 let mode = self.current_mode;
548 self.current_mode = DiffMode::Checkout;
551 (InternalDiff::Map(MapDelta { updated: changed }), mode)
552 }
553 }
554 }
555}
556
557use rle::{HasLength as _, Sliceable};
558
559#[derive(Default)]
560pub(crate) struct ListDiffCalculator {
561 start_vv: VersionVector,
562 tracker: Box<RichtextTracker>,
563}
564
565impl ListDiffCalculator {
566 pub(crate) fn get_id_latest_pos(&self, id: ID) -> Option<crate::cursor::AbsolutePosition> {
567 self.tracker.get_target_id_latest_index_at_new_version(id)
568 }
569}
570
571impl MovableListDiffCalculator {
572 pub(crate) fn get_id_latest_pos(&self, id: ID) -> Option<crate::cursor::AbsolutePosition> {
573 self.list
574 .tracker
575 .get_target_id_latest_index_at_new_version(id)
576 }
577}
578
579impl std::fmt::Debug for ListDiffCalculator {
580 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
581 f.debug_struct("ListDiffCalculator")
582 .finish()
584 }
585}
586
587impl DiffCalculatorTrait for ListDiffCalculator {
588 fn start_tracking(&mut self, _oplog: &OpLog, vv: &crate::VersionVector, _mode: DiffMode) {
589 if !vv.includes_vv(&self.start_vv) || !self.tracker.all_vv().includes_vv(vv) {
590 self.tracker = Box::new(RichtextTracker::new_with_unknown());
591 self.start_vv = vv.clone();
592 }
593
594 self.tracker.checkout(vv);
595 }
596
597 fn apply_change(
598 &mut self,
599 _oplog: &OpLog,
600 op: crate::op::RichOp,
601 vv: Option<&crate::VersionVector>,
602 ) {
603 if let Some(vv) = vv {
604 self.tracker.checkout(vv);
605 }
606
607 match &op.op().content {
608 crate::op::InnerContent::List(l) => match l {
609 InnerListOp::Insert { slice, pos } => {
610 self.tracker.insert(
611 op.id_full(),
612 *pos,
613 RichtextChunk::new_text(slice.0.clone()),
614 );
615 }
616 InnerListOp::Delete(del) => {
617 self.tracker.delete(
618 op.id_start(),
619 del.id_start,
620 del.start() as usize,
621 del.atom_len(),
622 del.is_reversed(),
623 );
624 }
625 _ => unreachable!(),
626 },
627 _ => unreachable!(),
628 }
629 }
630
631 fn finish_this_round(&mut self) {}
632
633 fn calculate_diff(
634 &mut self,
635 idx: ContainerIdx,
636 oplog: &OpLog,
637 info: DiffCalcVersionInfo,
638 mut on_new_container: impl FnMut(&ContainerID),
639 ) -> (InternalDiff, DiffMode) {
640 let mut delta = Delta::new();
641 for item in self.tracker.diff(info.from_vv, info.to_vv) {
642 match item {
643 CrdtRopeDelta::Retain(len) => {
644 delta = delta.retain(len);
645 }
646 CrdtRopeDelta::Insert {
647 chunk: value,
648 id,
649 lamport,
650 } => match value.value() {
651 RichtextChunkValue::Text(range) => {
652 for i in range.clone() {
653 let v = oplog.arena.get_value(i as usize);
654 if let Some(LoroValue::Container(c)) = &v {
655 on_new_container(c);
656 }
657 }
658 delta = delta.insert(SliceWithId {
659 values: Either::Left(SliceRange(range)),
660 id: IdFull::new(id.peer, id.counter, lamport.unwrap()),
661 elem_id: None,
662 });
663 }
664 RichtextChunkValue::StyleAnchor { .. } => unreachable!(),
665 RichtextChunkValue::Unknown(len) => {
666 delta = handle_unknown(idx, id, oplog, len, &mut on_new_container, delta);
667 }
668 RichtextChunkValue::MoveAnchor => {
669 delta = handle_unknown(idx, id, oplog, 1, &mut on_new_container, delta);
670 }
671 },
672 CrdtRopeDelta::Delete(len) => {
673 delta = delta.delete(len);
674 }
675 }
676 }
677
678 fn handle_unknown(
682 idx: ContainerIdx,
683 mut id: ID,
684 oplog: &OpLog,
685 len: u32,
686 on_new_container: &mut dyn FnMut(&ContainerID),
687 mut delta: Delta<SliceWithId>,
688 ) -> Delta<SliceWithId> {
689 assert_ne!(id.peer, PeerID::MAX);
691 let mut acc_len = 0;
692 let end = id.counter + len as Counter;
693 let shallow_root = oplog.shallow_since_vv().get(&id.peer).copied().unwrap_or(0);
694 if id.counter < shallow_root {
695 let target_end = shallow_root.min(end);
697 delta = oplog.with_history_cache(|h| {
698 let chunks =
699 h.find_list_chunks_in(idx, IdSpan::new(id.peer, id.counter, target_end));
700 for c in chunks {
701 acc_len += c.length();
702 match &c.values {
703 Either::Left(_) => unreachable!(),
704 Either::Right(r) => {
705 if let LoroValue::Container(c) = r {
706 on_new_container(c)
707 }
708 }
709 }
710 delta = delta.insert(c);
711 }
712
713 delta
714 });
715 id.counter = shallow_root;
716 }
717
718 if id.counter < end {
719 for rich_op in oplog.iter_ops(IdSpan::new(id.peer, id.counter, end)) {
720 acc_len += rich_op.content_len();
721 let op = rich_op.op();
722 let lamport = rich_op.lamport();
723
724 if let InnerListOp::Insert { slice, pos: _ } = op.content.as_list().unwrap() {
725 let range = slice.clone();
726 for i in slice.0.clone() {
727 let v = oplog.arena.get_value(i as usize);
728 if let Some(LoroValue::Container(c)) = &v {
729 (on_new_container)(c);
730 }
731 }
732
733 delta = delta.insert(SliceWithId {
734 values: Either::Left(range),
735 id: IdFull::new(id.peer, op.counter, lamport),
736 elem_id: None,
737 });
738 } else if let InnerListOp::Move { elem_id, .. } = op.content.as_list().unwrap()
739 {
740 delta = delta.insert(SliceWithId {
741 values: Either::Right(LoroValue::Null),
744 id: IdFull::new(id.peer, op.counter, lamport),
745 elem_id: Some(elem_id.compact()),
746 });
747 }
748 }
749 }
750
751 debug_assert_eq!(acc_len, len as usize);
752 delta
753 }
754
755 (InternalDiff::ListRaw(delta), DiffMode::Checkout)
756 }
757}
758
759#[derive(Debug)]
760pub(crate) struct RichtextDiffCalculator {
761 mode: Box<RichtextCalcMode>,
762}
763
764#[derive(Debug)]
765enum RichtextCalcMode {
766 Crdt {
767 tracker: Box<RichtextTracker>,
768 styles: Vec<(StyleOp, usize)>,
770 start_vv: VersionVector,
771 },
772 Linear {
773 diff: DeltaRope<RichtextStateChunk, ()>,
774 last_style_start: Option<(Arc<StyleOp>, u32)>,
775 },
776}
777
778impl RichtextDiffCalculator {
779 pub fn new() -> Self {
780 Self {
781 mode: Box::new(RichtextCalcMode::Crdt {
782 tracker: Box::new(RichtextTracker::new_with_unknown()),
783 styles: Vec::new(),
784 start_vv: VersionVector::new(),
785 }),
786 }
787 }
788
789 pub fn get_id_latest_pos(&self, id: ID) -> Option<AbsolutePosition> {
793 match &*self.mode {
794 RichtextCalcMode::Crdt { tracker, .. } => {
795 tracker.get_target_id_latest_index_at_new_version(id)
796 }
797 RichtextCalcMode::Linear { .. } => unreachable!(),
798 }
799 }
800}
801
802impl DiffCalculatorTrait for RichtextDiffCalculator {
803 fn start_tracking(
804 &mut self,
805 _oplog: &super::oplog::OpLog,
806 vv: &crate::VersionVector,
807 mode: DiffMode,
808 ) {
809 match mode {
810 DiffMode::Linear => {
811 self.mode = Box::new(RichtextCalcMode::Linear {
812 diff: DeltaRope::new(),
813 last_style_start: None,
814 });
815 }
816 _ => {
817 if !matches!(&*self.mode, RichtextCalcMode::Crdt { .. }) {
818 unreachable!();
819 }
820 }
821 }
822
823 match &mut *self.mode {
824 RichtextCalcMode::Crdt {
825 tracker,
826 styles,
827 start_vv,
828 } => {
829 if !vv.includes_vv(start_vv) || !tracker.all_vv().includes_vv(vv) {
830 *tracker = Box::new(RichtextTracker::new_with_unknown());
831 styles.clear();
832 *start_vv = vv.clone();
833 }
834
835 tracker.checkout(vv);
836 }
837 RichtextCalcMode::Linear { .. } => {}
838 }
839 }
840
841 fn apply_change(
842 &mut self,
843 oplog: &super::oplog::OpLog,
844 op: crate::op::RichOp,
845 vv: Option<&crate::VersionVector>,
846 ) {
847 match &mut *self.mode {
848 RichtextCalcMode::Linear {
849 diff,
850 last_style_start,
851 } => match &op.raw_op().content {
852 crate::op::InnerContent::List(l) => match l {
853 InnerListOp::Insert { .. }
854 | InnerListOp::Move { .. }
855 | InnerListOp::Set { .. } => {
856 unreachable!()
857 }
858 InnerListOp::InsertText {
859 slice: _,
860 unicode_start,
861 unicode_len: len,
862 pos,
863 } => {
864 let s = oplog.arena.slice_by_unicode(
865 *unicode_start as usize..(*unicode_start + *len) as usize,
866 );
867 diff.insert_value(
868 *pos as usize,
869 RichtextStateChunk::new_text(s, op.id_full()),
870 (),
871 );
872 }
873 InnerListOp::Delete(del) => {
874 diff.delete(del.start() as usize, del.atom_len());
875 }
876 InnerListOp::StyleStart {
877 start,
878 end,
879 key,
880 info,
881 value,
882 } => {
883 debug_assert!(start < end, "start: {}, end: {}", start, end);
884 let style_op = Arc::new(StyleOp {
885 lamport: op.lamport(),
886 peer: op.peer,
887 cnt: op.id_start().counter,
888 key: key.clone(),
889 value: value.clone(),
890 info: *info,
891 });
892
893 *last_style_start = Some((style_op.clone(), *end));
894 diff.insert_value(
895 *start as usize,
896 RichtextStateChunk::new_style(style_op, AnchorType::Start),
897 (),
898 );
899 }
900 InnerListOp::StyleEnd => {
901 let (style_op, pos) = match last_style_start.take() {
902 Some((style_op, pos)) => (style_op, pos),
903 None => {
904 let Some(start_op) = oplog.get_op_that_includes(op.id().inc(-1))
905 else {
906 panic!("Unhandled checkout case")
907 };
908
909 let InnerListOp::StyleStart {
910 key,
911 value,
912 info,
913 end,
914 ..
915 } = start_op.content.as_list().unwrap()
916 else {
917 unreachable!()
918 };
919 let style_op = Arc::new(StyleOp {
920 lamport: op.lamport() - 1,
921 peer: op.peer,
922 cnt: op.id_start().counter - 1,
923 key: key.clone(),
924 value: value.clone(),
925 info: *info,
926 });
927
928 (style_op, *end)
929 }
930 };
931 assert_eq!(style_op.peer, op.peer);
932 assert_eq!(style_op.cnt, op.id_start().counter - 1);
933 diff.insert_value(
934 pos as usize + 1,
935 RichtextStateChunk::new_style(style_op, AnchorType::End),
936 (),
937 );
938 }
939 },
940 _ => unreachable!(),
941 },
942 RichtextCalcMode::Crdt {
943 tracker,
944 styles,
945 start_vv: _,
946 } => {
947 if let Some(vv) = vv {
948 tracker.checkout(vv);
949 }
950 match &op.raw_op().content {
951 crate::op::InnerContent::List(l) => match l {
952 InnerListOp::Insert { .. }
953 | InnerListOp::Move { .. }
954 | InnerListOp::Set { .. } => {
955 unreachable!()
956 }
957 InnerListOp::InsertText {
958 slice: _,
959 unicode_start,
960 unicode_len: len,
961 pos,
962 } => {
963 tracker.insert(
964 op.id_full(),
965 *pos as usize,
966 RichtextChunk::new_text(*unicode_start..*unicode_start + *len),
967 );
968 }
969 InnerListOp::Delete(del) => {
970 tracker.delete(
971 op.id_start(),
972 del.id_start,
973 del.start() as usize,
974 del.atom_len(),
975 del.is_reversed(),
976 );
977 }
978 InnerListOp::StyleStart {
979 start,
980 end,
981 key,
982 info,
983 value,
984 } => {
985 debug_assert!(start < end, "start: {}, end: {}", start, end);
986 let style_id = styles.len();
987 styles.push((
988 StyleOp {
989 lamport: op.lamport(),
990 peer: op.peer,
991 cnt: op.id_start().counter,
992 key: key.clone(),
993 value: value.clone(),
994 info: *info,
995 },
996 *end as usize,
997 ));
998 tracker.insert(
999 op.id_full(),
1000 *start as usize,
1001 RichtextChunk::new_style_anchor(style_id as u32, AnchorType::Start),
1002 );
1003 }
1004 InnerListOp::StyleEnd => {
1005 let id = op.id();
1006 if let Some(pos) = styles.iter().rev().position(|(op, _pos)| {
1007 op.peer == id.peer && op.cnt == id.counter - 1
1008 }) {
1009 let style_id = styles.len() - pos - 1;
1010 let (_start_op, end_pos) = &styles[style_id];
1011 tracker.insert(
1012 op.id_full(),
1013 *end_pos + 1,
1015 RichtextChunk::new_style_anchor(
1016 style_id as u32,
1017 AnchorType::End,
1018 ),
1019 );
1020 } else {
1021 let Some(start_op) = oplog.get_op_that_includes(op.id().inc(-1))
1022 else {
1023 unimplemented!("Unhandled checkout case")
1027 };
1028 let InnerListOp::StyleStart {
1029 start: _,
1030 end,
1031 key,
1032 value,
1033 info,
1034 } = start_op.content.as_list().unwrap()
1035 else {
1036 unreachable!()
1037 };
1038
1039 styles.push((
1040 StyleOp {
1041 lamport: op.lamport() - 1,
1042 peer: id.peer,
1043 cnt: id.counter - 1,
1044 key: key.clone(),
1045 value: value.clone(),
1046 info: *info,
1047 },
1048 *end as usize,
1049 ));
1050 let style_id = styles.len() - 1;
1051 tracker.insert(
1052 op.id_full(),
1053 *end as usize + 1,
1055 RichtextChunk::new_style_anchor(
1056 style_id as u32,
1057 AnchorType::End,
1058 ),
1059 );
1060 }
1061 }
1062 },
1063 _ => unreachable!(),
1064 }
1065 }
1066 }
1067 }
1068
1069 fn calculate_diff(
1070 &mut self,
1071 idx: ContainerIdx,
1072 oplog: &OpLog,
1073 info: DiffCalcVersionInfo,
1074 _: impl FnMut(&ContainerID),
1075 ) -> (InternalDiff, DiffMode) {
1076 match &mut *self.mode {
1077 RichtextCalcMode::Linear { diff, .. } => (
1078 InternalDiff::RichtextRaw(std::mem::take(diff)),
1079 DiffMode::Linear,
1080 ),
1081 RichtextCalcMode::Crdt {
1082 tracker, styles, ..
1083 } => {
1084 let mut delta = DeltaRope::new();
1085 for item in tracker.diff(info.from_vv, info.to_vv) {
1086 match item {
1087 CrdtRopeDelta::Retain(len) => {
1088 delta.push_retain(len, ());
1089 }
1090 CrdtRopeDelta::Insert {
1091 chunk: value,
1092 id,
1093 lamport,
1094 } => match value.value() {
1095 RichtextChunkValue::Text(text) => {
1096 delta.push_insert(
1097 RichtextStateChunk::Text(
1098 TextChunk::new(
1100 oplog.arena.slice_by_unicode(
1101 text.start as usize..text.end as usize,
1102 ),
1103 IdFull::new(id.peer, id.counter, lamport.unwrap()),
1104 ),
1105 ),
1106 (),
1107 );
1108 }
1109 RichtextChunkValue::StyleAnchor { id, anchor_type } => {
1110 delta.push_insert(
1111 RichtextStateChunk::Style {
1112 style: Arc::new(styles[id as usize].0.clone()),
1113 anchor_type,
1114 },
1115 (),
1116 );
1117 }
1118 RichtextChunkValue::Unknown(len) => {
1119 assert_ne!(id.peer, PeerID::MAX);
1121 let mut id = id;
1122 let mut acc_len = 0;
1123 let end = id.counter + len as Counter;
1124 let shallow_root =
1125 oplog.shallow_since_vv().get(&id.peer).copied().unwrap_or(0);
1126 if id.counter < shallow_root {
1127 let target_end = shallow_root.min(end);
1129 oplog.with_history_cache(|h| {
1130 let chunks = h.find_text_chunks_in(
1131 idx,
1132 IdSpan::new(id.peer, id.counter, target_end),
1133 );
1134 for c in chunks {
1135 acc_len += c.rle_len();
1136 delta.push_insert(c, ());
1137 }
1138 });
1139 id.counter = shallow_root;
1140 }
1141
1142 if id.counter < end {
1143 for rich_op in
1144 oplog.iter_ops(IdSpan::new(id.peer, id.counter, end))
1145 {
1146 acc_len += rich_op.content_len();
1147 let op = rich_op.op();
1148 let lamport = rich_op.lamport();
1149 let content = op.content.as_list().unwrap();
1150 match content {
1151 InnerListOp::InsertText { slice, .. } => {
1152 delta.push_insert(
1153 RichtextStateChunk::Text(TextChunk::new(
1154 slice.clone(),
1155 IdFull::new(id.peer, op.counter, lamport),
1156 )),
1157 (),
1158 );
1159 }
1160 _ => unreachable!("{:?}", content),
1161 }
1162 }
1163 }
1164
1165 debug_assert_eq!(acc_len, len as usize);
1166 }
1167 RichtextChunkValue::MoveAnchor => unreachable!(),
1168 },
1169 CrdtRopeDelta::Delete(len) => {
1170 delta.push_delete(len);
1171 }
1172 }
1173 }
1174
1175 (InternalDiff::RichtextRaw(delta), DiffMode::Checkout)
1176 }
1177 }
1178 }
1179
1180 fn finish_this_round(&mut self) {
1181 match &mut *self.mode {
1182 RichtextCalcMode::Crdt { .. } => {}
1183 RichtextCalcMode::Linear {
1184 diff,
1185 last_style_start,
1186 } => {
1187 *diff = DeltaRope::new();
1188 last_style_start.take();
1189 }
1190 }
1191 }
1192}
1193
1194#[derive(Debug)]
1195pub(crate) struct MovableListDiffCalculator {
1196 list: Box<ListDiffCalculator>,
1197 inner: Box<MovableListInner>,
1198}
1199
1200#[derive(Debug)]
1201struct MovableListInner {
1202 changed_elements: FxHashMap<CompactIdLp, ElementDelta>,
1203 move_id_to_elem_id: FxHashMap<ID, IdLp>,
1204 current_mode: DiffMode,
1205}
1206
1207impl DiffCalculatorTrait for MovableListDiffCalculator {
1208 fn start_tracking(&mut self, _oplog: &OpLog, vv: &crate::VersionVector, mode: DiffMode) {
1209 if !vv.includes_vv(&self.list.start_vv) || !self.list.tracker.all_vv().includes_vv(vv) {
1210 self.list.tracker = Box::new(RichtextTracker::new_with_unknown());
1211 self.list.start_vv = vv.clone();
1212 }
1213
1214 self.list.tracker.checkout(vv);
1215 self.inner.current_mode = mode;
1216 }
1217
1218 fn apply_change(
1219 &mut self,
1220 oplog: &OpLog,
1221 op: crate::op::RichOp,
1222 vv: Option<&crate::VersionVector>,
1223 ) {
1224 let InnerContent::List(l) = &op.raw_op().content else {
1225 unreachable!()
1226 };
1227
1228 match l {
1233 InnerListOp::Insert { slice, pos: _ } => {
1234 let op_id = op.id_full().idlp();
1235 for i in 0..slice.atom_len() {
1236 let id = op_id.inc(i as Counter);
1237 let value = oplog.arena.get_value(slice.0.start as usize + i).unwrap();
1238
1239 self.inner.changed_elements.insert(
1240 id.compact(),
1241 ElementDelta {
1242 pos: Some(id),
1243 value: value.clone(),
1244 value_updated: true,
1245 value_id: Some(id),
1246 },
1247 );
1248 }
1249 }
1250 InnerListOp::Delete(_) => {}
1251 InnerListOp::Move { elem_id, .. } => {
1252 let idlp = IdLp::new(op.peer, op.lamport());
1253 match self.inner.changed_elements.get_mut(&elem_id.compact()) {
1254 Some(change) => {
1255 if change.pos.is_some() && change.pos.as_ref().unwrap() > &idlp {
1256 } else {
1257 change.pos = Some(idlp);
1258 }
1259 }
1260 None => {
1261 self.inner.changed_elements.insert(
1262 elem_id.compact(),
1263 ElementDelta {
1264 pos: Some(idlp),
1265 value: LoroValue::Null,
1266 value_updated: false,
1267 value_id: None,
1268 },
1269 );
1270 }
1271 }
1272 }
1273 InnerListOp::Set { elem_id, value } => {
1274 let idlp = IdLp::new(op.peer, op.lamport());
1275 match self.inner.changed_elements.get_mut(&elem_id.compact()) {
1276 Some(change) => {
1277 if change.value_id.is_some() && change.value_id.as_ref().unwrap() > &idlp {
1278 } else {
1279 change.value_id = Some(idlp);
1280 change.value = value.clone();
1281 }
1282 }
1283 None => {
1284 self.inner.changed_elements.insert(
1285 elem_id.compact(),
1286 ElementDelta {
1287 pos: None,
1288 value: value.clone(),
1289 value_updated: true,
1290 value_id: Some(idlp),
1291 },
1292 );
1293 }
1294 }
1295 }
1296
1297 InnerListOp::StyleStart { .. } => unreachable!(),
1298 InnerListOp::StyleEnd => unreachable!(),
1299 InnerListOp::InsertText { .. } => unreachable!(),
1300 }
1301
1302 let is_checkout = matches!(self.inner.current_mode, DiffMode::Checkout);
1303
1304 {
1305 let this = &mut self.list;
1307 if let Some(vv) = vv {
1308 this.tracker.checkout(vv);
1309 }
1310
1311 let real_op = op.op();
1312 match &real_op.content {
1313 crate::op::InnerContent::List(l) => match l {
1314 InnerListOp::Insert { slice, pos } => {
1315 this.tracker.insert(
1316 op.id_full(),
1317 *pos,
1318 RichtextChunk::new_text(slice.0.clone()),
1319 );
1320 }
1321 InnerListOp::Delete(del) => {
1322 this.tracker.delete(
1323 op.id_start(),
1324 del.id_start,
1325 del.start() as usize,
1326 del.atom_len(),
1327 del.is_reversed(),
1328 );
1329 }
1330 InnerListOp::Move { from, elem_id, to } => {
1331 self.inner.move_id_to_elem_id.insert(op.id(), *elem_id);
1332 if !this.tracker.current_vv().includes_id(op.id()) {
1333 let last_pos = if is_checkout {
1334 oplog.with_history_cache(|h| {
1336 let list = &h.get_checkout_index().movable_list;
1337 list.last_pos(
1338 *elem_id,
1339 this.tracker.current_vv(),
1340 Lamport::MAX,
1342 oplog,
1343 )
1344 .unwrap()
1345 .id()
1346 })
1347 } else {
1348 const FAKE_ID: ID = ID {
1356 peer: PeerID::MAX - 2,
1357 counter: 0,
1358 };
1359 FAKE_ID
1360 };
1361 this.tracker.move_item(
1362 op.id_full(),
1363 last_pos,
1364 *from as usize,
1365 *to as usize,
1366 );
1367 }
1368 }
1369 InnerListOp::Set { .. } => {
1370 }
1372 InnerListOp::InsertText { .. }
1373 | InnerListOp::StyleStart { .. }
1374 | InnerListOp::StyleEnd => unreachable!(),
1375 },
1376 _ => unreachable!(),
1377 }
1378 };
1379 }
1380
1381 fn finish_this_round(&mut self) {
1382 self.list.finish_this_round();
1383 }
1384
1385 #[instrument(skip(self, oplog, on_new_container))]
1386 fn calculate_diff(
1387 &mut self,
1388 idx: ContainerIdx,
1389 oplog: &OpLog,
1390 info: DiffCalcVersionInfo,
1391 mut on_new_container: impl FnMut(&ContainerID),
1392 ) -> (InternalDiff, DiffMode) {
1393 let (InternalDiff::ListRaw(list_diff), diff_mode) =
1394 self.list.calculate_diff(idx, oplog, info, |_| {})
1395 else {
1396 unreachable!()
1397 };
1398
1399 assert_eq!(diff_mode, DiffMode::Checkout);
1400 let is_checkout = matches!(
1401 self.inner.current_mode,
1402 DiffMode::Checkout | DiffMode::Import
1403 );
1404 let mut element_changes: FxHashMap<CompactIdLp, ElementDelta> = if is_checkout {
1405 FxHashMap::default()
1406 } else {
1407 std::mem::take(&mut self.inner.changed_elements)
1408 };
1409
1410 if is_checkout {
1411 for id in self.inner.changed_elements.keys() {
1412 element_changes.insert(*id, ElementDelta::placeholder());
1413 }
1414 }
1415
1416 let list_diff: Delta<SmallVec<[IdFull; 1]>, ()> = Delta {
1417 vec: list_diff
1418 .iter()
1419 .map(|x| match x {
1420 &DeltaItem::Retain { retain, .. } => DeltaItem::Retain {
1421 retain,
1422 attributes: (),
1423 },
1424 DeltaItem::Insert { insert, .. } => {
1425 let len = insert.length();
1426 let id = insert.id;
1427 let mut new_insert = SmallVec::with_capacity(len);
1428 for i in 0..len {
1429 let id = id.inc(i as i32);
1430 let elem_id =
1431 if let Some(e) = self.inner.move_id_to_elem_id.get(&id.id()) {
1432 e.compact()
1433 } else {
1434 insert.elem_id.unwrap_or_else(|| id.idlp().compact())
1435 };
1436 if is_checkout {
1437 element_changes.insert(elem_id, ElementDelta::placeholder());
1439 }
1440 new_insert.push(id);
1441 }
1442
1443 DeltaItem::Insert {
1444 insert: new_insert,
1445 attributes: (),
1446 }
1447 }
1448 &DeltaItem::Delete { delete, .. } => DeltaItem::Delete {
1449 delete,
1450 attributes: (),
1451 },
1452 })
1453 .collect(),
1454 };
1455
1456 if is_checkout {
1457 oplog.with_history_cache(|history_cache| {
1458 let checkout_index = &history_cache.get_checkout_index().movable_list;
1459 element_changes.retain(|id, change| {
1460 let id = id.to_id();
1461 let Some(pos) = checkout_index.last_pos(id, info.to_vv, Lamport::MAX, oplog)
1466 else {
1467 return false;
1468 };
1469 let value = checkout_index
1471 .last_value(id, info.to_vv, Lamport::MAX, oplog)
1472 .unwrap();
1473 let old_pos = checkout_index.last_pos(id, info.from_vv, Lamport::MAX, oplog);
1475 let old_value =
1477 checkout_index.last_value(id, info.from_vv, Lamport::MAX, oplog);
1478 if old_pos.is_none() && old_value.is_none() {
1479 if let LoroValue::Container(c) = &value.value {
1480 on_new_container(c);
1481 }
1482 *change = ElementDelta {
1483 pos: Some(pos.idlp()),
1484 value: value.value.clone(),
1485 value_id: Some(IdLp::new(value.peer, value.lamport)),
1486 value_updated: true,
1487 };
1488 } else {
1489 *change = ElementDelta {
1491 pos: Some(pos.idlp()),
1492 value: value.value.clone(),
1493 value_updated: old_value.unwrap().value != value.value,
1494 value_id: Some(IdLp::new(value.peer, value.lamport)),
1495 };
1496 }
1497
1498 true
1499 });
1500 });
1501 }
1502
1503 let diff = MovableListInnerDelta {
1504 list: list_diff,
1505 elements: element_changes,
1506 };
1507
1508 (InternalDiff::MovableList(diff), self.inner.current_mode)
1509 }
1510}
1511
1512impl MovableListDiffCalculator {
1513 fn new(_container: ContainerIdx) -> MovableListDiffCalculator {
1514 MovableListDiffCalculator {
1515 list: Default::default(),
1516 inner: Box::new(MovableListInner {
1517 changed_elements: Default::default(),
1518 current_mode: DiffMode::Checkout,
1519 move_id_to_elem_id: Default::default(),
1520 }),
1521 }
1522 }
1523}
1524
1525#[test]
1526fn test_size() {
1527 let text = RichtextDiffCalculator::new();
1528 let size = std::mem::size_of_val(&text);
1529 assert!(size < 50, "RichtextDiffCalculator size: {}", size);
1530 let list = MovableListDiffCalculator::new(ContainerIdx::from_index_and_type(
1531 0,
1532 loro_common::ContainerType::MovableList,
1533 ));
1534 let size = std::mem::size_of_val(&list);
1535 assert!(size < 50, "MovableListDiffCalculator size: {}", size);
1536 let calc = ContainerDiffCalculator::Richtext(text);
1537 let size = std::mem::size_of_val(&calc);
1538 assert!(size < 50, "ContainerDiffCalculator size: {}", size);
1539}