Skip to main content

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