loro_internal/
diff_calc.rs

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/// Calculate the diff between two versions. given [OpLog][super::oplog::OpLog]
50/// and [AppState][super::state::AppState].
51///
52/// TODO: persist diffCalculator and skip processed version
53#[derive(Debug)]
54pub struct DiffCalculator {
55    /// ContainerIdx -> (depth, calculator)
56    ///
57    /// if depth is None, we need to calculate it again
58    calculators: FxHashMap<ContainerIdx, (Option<NonZeroU16>, ContainerDiffCalculator)>,
59    retain_mode: DiffCalculatorRetainMode,
60}
61
62#[derive(Debug)]
63enum DiffCalculatorRetainMode {
64    /// The diff calculator can only be used once.
65    Once { used: bool },
66    /// The diff calculator will be persisted and can be reused after the diff calc is done.
67    Persist,
68}
69
70/// This mode defines how the diff is calculated and how it should be applied on the state.
71#[derive(Debug, Clone, PartialEq, Eq, Copy)]
72pub(crate) enum DiffMode {
73    /// This is the most general mode of diff calculation.
74    ///
75    /// When applying `Checkout` diff, we already know the current state of the affected registers.
76    /// So there is no need to compare the lamport values.
77    ///
78    /// It can be used whenever a user want to switch to a different version.
79    /// But it is also the slowest mode. It relies on the `ContainerHistoryCache`, which is expensive to build and maintain in memory.
80    Checkout,
81    /// This mode is used when the user imports new updates.
82    ///
83    /// When applying `Import` diff, we may need to know the the current state.
84    /// For example, we may need to compare the current register's lamport with the update's lamport to decide
85    /// what's the new value.
86    ///
87    /// It has stricter requirements than `Checkout`:
88    ///
89    /// - The target version vector must be greater than the current version vector.
90    Import,
91    /// This mode is used when the user imports new updates and all the updates are guaranteed to greater than the current version.
92    ///
93    /// It has stricter requirements than `Import`.
94    /// - All the updates are greater than the current version. No update is concurrent to the current version.
95    /// - So LCA is always the `from` version
96    ImportGreaterUpdates,
97    /// This mode is used when we don't need to build CRDTs to calculate the difference. It is the fastest mode.
98    ///
99    /// It has stricter requirements than `ImportGreaterUpdates`.
100    /// - In `ImportGreaterUpdates`, all the updates are guaranteed to be greater than the current version.
101    /// - In `Linear`, all the updates are ordered, no concurrent update exists.
102    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    /// Create a new diff calculator.
115    ///
116    /// If `persist` is true, the diff calculator will be persisted after the diff calc is done.
117    /// This is useful when we need to cache the diff calculator for future use. But it is slower
118    /// for importing updates and requires more memory.
119    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    /// Calculate the diff between two versions.
136    ///
137    /// Return the diff and the origin diff mode (it's not the diff mode used by the diff calculator.
138    /// It's the expected diff mode inferred from the two version, which can reflect the direction of the
139    /// change).
140    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                    // slice the op if needed
194                    // PERF: we can skip the slice by using the RichOp::new_slice
195                    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                    // checkout use the same diff_calculator, the depth of calculator is not updated
214                    // That may cause the container to be considered deleted
215                    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                        // don't checkout if we have already checked out this container in this round
226                        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        // Because we need to get correct `bring_back` value that indicates container is created during this round of diff calc,
242        // we need to iterate from parents to children. i.e. from smaller depth to larger depth.
243        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            // only visit the affected containers
247            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            // sort by depth and lamport, ensure we iterate from top to bottom
268            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    // TODO: we may remove depth info
345    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/// DiffCalculator should track the history first before it can calculate the difference.
387///
388/// So we need it to first apply all the ops between the two versions.
389///
390/// NOTE: not every op between two versions are included in a certain container.
391/// So there may be some ops that cannot be seen by the container.
392///
393#[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    /// This round of diff calc is finished, we can clear the cache
410    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            // We need to use history cache anyway
462            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                // Reset this field to avoid we use `has_all` to cache the diff calc and use it next round
549                // (In the next round we need to use the checkout mode)
550                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            // .field("tracker", &self.tracker)
583            .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        /// Handle span with unknown content when calculating diff
679        ///
680        /// We can lookup the content of the span by the id in the oplog
681        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 not unknown id
690            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                // need to find the content between id.counter ~ target_end in gc state
696                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                            // We do NOT need an actual value range,
742                            // movable list container will only use the id info
743                            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        /// (op, end_pos)
769        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    /// This should be called after calc_diff
790    ///
791    /// TODO: Refactor, this can be simplified
792    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                                    // need to shift 1 because we insert the start style anchor before this pos
1014                                    *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                                    // Checkout on richtext that export at a gc version that split
1024                                    // start style op and end style op apart. Won't fix for now.
1025                                    // It's such a rare case...
1026                                    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                                    // need to shift 1 because we insert the start style anchor before this pos
1054                                    *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                                        // PERF: can be speedup by acquiring lock on arena
1099                                        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 not unknown id
1120                                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                                    // need to find the content between id.counter ~ target_end in gc state
1128                                    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        // collect the elements that are moved, updated, or inserted
1229
1230        // If it's checkout mode, we don't need to track the changes
1231        // we only need the element ids
1232        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            // Apply change on the list items
1306            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                                // TODO: PERF: this lookup can be optimized
1335                                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                                        // TODO: PERF: Provide the lamport of to version
1341                                        Lamport::MAX,
1342                                        oplog,
1343                                    )
1344                                    .unwrap()
1345                                    .id()
1346                                })
1347                            } else {
1348                                // When it's import or linear mode, we need to use a fake id
1349                                // because we want to avoid using the history cache
1350                                //
1351                                // This ID will not be used. Because it will only be used when
1352                                // we switch to an older version. And we know it's for importing and
1353                                // to version is always after from version (!is_checkout), so that
1354                                // we don't need to checkout to the version before from.
1355                                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                        // don't need to update tracker here
1371                    }
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                                // add the related element id
1438                                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                    // It can be None if the target does not exist before the `to` version
1462                    // But we don't need to calc from, because the deletion is handled by the diff from list items
1463
1464                    // TODO: PERF: Provide the lamport of to version
1465                    let Some(pos) = checkout_index.last_pos(id, info.to_vv, Lamport::MAX, oplog)
1466                    else {
1467                        return false;
1468                    };
1469                    // TODO: PERF: Provide the lamport of to version
1470                    let value = checkout_index
1471                        .last_value(id, info.to_vv, Lamport::MAX, oplog)
1472                        .unwrap();
1473                    // TODO: PERF: Provide the lamport of to version
1474                    let old_pos = checkout_index.last_pos(id, info.from_vv, Lamport::MAX, oplog);
1475                    // TODO: PERF: Provide the lamport of to version
1476                    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                        // TODO: PERF: can be filtered based on the list_diff and whether the pos/value are updated
1490                        *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}