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