Skip to main content

tiptap_rusty_parser/
diff.rs

1//! Structural diff: compute a path-addressed [`Change`] list between two
2//! [`Node`] trees, and [`apply`] it to reproduce the target.
3//!
4//! ```
5//! use tiptap_rusty_parser::Document;
6//!
7//! let a = Document::from_json_str(
8//!     r#"{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hi"}]}]}"#,
9//! ).unwrap();
10//! let b = Document::from_json_str(
11//!     r#"{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"bye"}]}]}"#,
12//! ).unwrap();
13//!
14//! let changes = a.diff(&b);          // -> Vec<Change>
15//! let mut c = a.clone();
16//! c.apply(&changes).unwrap();        // reproduce `b`
17//! assert_eq!(c, b);
18//! ```
19//!
20//! ## Path convention
21//! Node-local changes ([`Change::SetAttr`], `RemoveAttr`, `SetText`, `SetMarks`,
22//! `SetExtra`, `RemoveExtra`, `Replace`) address the **target node** by its
23//! index path. Child-list changes ([`Change::Insert`], [`Change::Remove`])
24//! address the **parent** node, with `index` selecting the child — mirroring
25//! [`Node::insert_child`] / [`Node::remove_child`].
26//!
27//! ## Apply contract
28//! [`apply`] executes changes strictly in order; child `index` values are
29//! interpreted against the *live* (already-partially-mutated) list. A list
30//! produced by [`diff`] always reproduces the target exactly:
31//! `apply(&mut a, &diff(a, b))` yields `b`.
32//!
33//! Empty-vs-absent container shapes (e.g. `"content":[]` vs no `content`) are
34//! preserved: when the field/child ops can't express the difference, the node
35//! is replaced wholesale so the round-trip stays exact.
36//!
37//! ## Move detection
38//! A child relocated within a list is emitted as a single [`Change::Move`]
39//! (no subtree clone) instead of a remove+insert: after LCS alignment, leftover
40//! deletions and insertions that are *equal by value* are paired as moves, and
41//! their live indices are derived by simulating the op stream — so the result
42//! reproduces the target exactly regardless of how moves interleave with plain
43//! inserts/removes. Pairing is greedy/FIFO, so duplicate-equal children give a
44//! correct (if not always minimal) result. [`invert`] needs no special handling:
45//! it re-diffs the reverse direction, re-deriving the inverse moves.
46//!
47//! ## v1 limitations
48//! - Matching is LCS-by-equality; modifies are paired positionally within the
49//!   gaps between matched anchors (still correct, just not always minimal).
50
51use crate::node::{Mark, Node};
52use crate::text_diff::{self, DiffGranularity, DiffOptions};
53use serde::{Deserialize, Serialize};
54use serde_json::{Map, Value};
55use std::collections::{HashMap, HashSet};
56use std::fmt;
57
58/// A single structural change between two [`Node`] trees.
59///
60/// Serializes as a tagged object, e.g. `{"op":"setText","path":[0,0],"text":"hi"}`.
61#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
62#[serde(tag = "op", rename_all = "camelCase", rename_all_fields = "camelCase")]
63pub enum Change {
64    /// Set (insert or overwrite) attribute `key` on the node at `path`.
65    SetAttr {
66        /// Index path of the target node.
67        path: Vec<usize>,
68        /// Attribute key.
69        key: String,
70        /// New attribute value.
71        value: Value,
72    },
73    /// Remove attribute `key` from the node at `path`.
74    RemoveAttr {
75        /// Index path of the target node.
76        path: Vec<usize>,
77        /// Attribute key.
78        key: String,
79    },
80    /// Set the text payload of the node at `path` (`None` clears it).
81    SetText {
82        /// Index path of the target node.
83        path: Vec<usize>,
84        /// New text payload, or `None` to clear.
85        text: Option<String>,
86    },
87    /// Replace the whole mark list of the node at `path` (`None` clears it).
88    SetMarks {
89        /// Index path of the target node.
90        path: Vec<usize>,
91        /// New mark list, or `None` to clear.
92        marks: Option<Vec<Mark>>,
93    },
94    /// Set (insert or overwrite) unknown top-level field `key` on the node at `path`.
95    SetExtra {
96        /// Index path of the target node.
97        path: Vec<usize>,
98        /// Field key.
99        key: String,
100        /// New field value.
101        value: Value,
102    },
103    /// Remove unknown top-level field `key` from the node at `path`.
104    RemoveExtra {
105        /// Index path of the target node.
106        path: Vec<usize>,
107        /// Field key.
108        key: String,
109    },
110    /// Insert `node` as a child of the node at `path` (the parent), at `index`.
111    Insert {
112        /// Index path of the **parent** node.
113        path: Vec<usize>,
114        /// Child position to insert at.
115        index: usize,
116        /// The node to insert.
117        node: Node,
118    },
119    /// Remove the child at `index` of the node at `path` (the parent).
120    Remove {
121        /// Index path of the **parent** node.
122        path: Vec<usize>,
123        /// Child position to remove.
124        index: usize,
125    },
126    /// Replace the node at `path` wholesale (used when its `type` changes).
127    Replace {
128        /// Index path of the target node (empty = root).
129        path: Vec<usize>,
130        /// The replacement node.
131        node: Node,
132    },
133    /// Relocate a child within the same parent's list, without cloning its
134    /// subtree. `from`/`to` are interpreted against the *live* list: the child
135    /// at `from` is removed first, then re-inserted so it lands at index `to`
136    /// in the post-removal list — observationally `Remove{from}` + `Insert{to}`,
137    /// so it composes with the same live-index cursor model as the other ops.
138    Move {
139        /// Index path of the **parent** node.
140        path: Vec<usize>,
141        /// Live index of the child to move (before this op).
142        from: usize,
143        /// Target index in the list after the child is removed.
144        to: usize,
145    },
146    /// Splice a text node's payload: replace the scalar range
147    /// `[from, from+len_del)` with `insert`. Offsets count Unicode scalars.
148    /// Emitted by [`Node::diff_with`](crate::Node::diff_with) in inline/smart
149    /// granularity instead of a whole-string [`SetText`](Change::SetText).
150    ///
151    /// Appended last (rather than grouped next to [`SetText`](Change::SetText))
152    /// so existing variants keep their discriminants — only the new-variant
153    /// break remains, not a discriminant shift for downstream `as` casts.
154    SpliceText {
155        /// Index path of the target text node.
156        path: Vec<usize>,
157        /// Scalar offset where the splice starts.
158        from: usize,
159        /// Number of scalars removed.
160        len_del: usize,
161        /// Text inserted at `from`.
162        insert: String,
163    },
164}
165
166/// Error from [`apply`] when a change can't be located (no node at the path, or
167/// a child index out of range). Lists produced by [`diff`] never fail.
168#[derive(Debug, Clone, PartialEq, Eq)]
169pub struct ApplyError {
170    /// The path that could not be resolved.
171    pub path: Vec<usize>,
172}
173
174impl fmt::Display for ApplyError {
175    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
176        write!(f, "apply: no node at path {:?}", self.path)
177    }
178}
179
180impl std::error::Error for ApplyError {}
181
182impl Node {
183    /// Structural diff from `self` to `other`: a [`Change`] list that, when
184    /// [`applied`](apply) to a clone of `self`, reproduces `other`.
185    pub fn diff(&self, other: &Node) -> Vec<Change> {
186        self.diff_with(other, &DiffOptions::default())
187    }
188
189    /// Structural diff with [`DiffOptions`] — e.g. inline/smart text granularity
190    /// (character-level [`Change::SpliceText`] instead of whole [`Change::SetText`]).
191    pub fn diff_with(&self, other: &Node, opts: &DiffOptions) -> Vec<Change> {
192        let mut out = Vec::new();
193        let mut path = Vec::new();
194        diff_node(self, other, &mut path, &mut out, opts);
195        out
196    }
197
198    /// Apply `changes` to `self` in order. See [`apply`].
199    pub fn apply(&mut self, changes: &[Change]) -> std::result::Result<(), ApplyError> {
200        apply(self, changes)
201    }
202
203    /// Invert `changes` relative to `self` (the pre-image). See [`invert`].
204    pub fn invert(&self, changes: &[Change]) -> std::result::Result<Vec<Change>, ApplyError> {
205        invert(self, changes)
206    }
207}
208
209/// Structural diff between two nodes. Free-function form of [`Node::diff`].
210pub fn diff(a: &Node, b: &Node) -> Vec<Change> {
211    a.diff(b)
212}
213
214/// Apply a [`Change`] list to `root` in order, mutating it in place.
215///
216/// Applying `diff(a, b)` to a clone of `a` reproduces `b` exactly. Returns
217/// [`ApplyError`] only for externally-authored lists whose paths/indices don't
218/// resolve.
219pub fn apply(root: &mut Node, changes: &[Change]) -> std::result::Result<(), ApplyError> {
220    for change in changes {
221        apply_one(root, change)?;
222    }
223    Ok(())
224}
225
226/// Invert a change list: produce the reverse changes that, applied to the
227/// result of `apply(base, changes)`, restore `base` — the basis for undo.
228///
229/// Computed as `diff(apply(base, changes), base)`: replay the forward changes,
230/// then diff back to `base`. This reuses the diff round-trip guarantee, so it
231/// handles every value/shape edge exactly — subject to the same non-minimality
232/// caveats as [`diff`] (e.g. no move detection). Errors only if `changes`
233/// itself doesn't apply to `base`.
234///
235/// ```
236/// use tiptap_rusty_parser::Document;
237/// let a = Document::from_json_str(r#"{"type":"doc","content":[{"type":"paragraph"}]}"#).unwrap();
238/// let b = Document::from_json_str(r#"{"type":"doc","content":[{"type":"heading"}]}"#).unwrap();
239/// let forward = a.diff(&b);
240/// let undo = a.invert(&forward).unwrap();
241/// let mut c = b.clone();
242/// c.apply(&undo).unwrap();
243/// assert_eq!(c, a);
244/// ```
245pub fn invert(base: &Node, changes: &[Change]) -> std::result::Result<Vec<Change>, ApplyError> {
246    let mut result = base.clone();
247    apply(&mut result, changes)?;
248    Ok(result.diff(base))
249}
250
251// ---- diff internals -----------------------------------------------------
252
253fn diff_node(a: &Node, b: &Node, path: &mut Vec<usize>, out: &mut Vec<Change>, opts: &DiffOptions) {
254    if a == b {
255        return; // prune identical subtrees (the main perf lever)
256    }
257    if a.node_type != b.node_type
258        || empty_shape_mismatch(
259            a.attrs.as_ref().map(Map::is_empty),
260            b.attrs.as_ref().map(Map::is_empty),
261        )
262        || empty_shape_mismatch(
263            a.content.as_ref().map(Vec::is_empty),
264            b.content.as_ref().map(Vec::is_empty),
265        )
266    {
267        // Type change, or an empty-vs-None container shape the field/child ops
268        // can't express (e.g. `"content":[]` -> absent) -> wholesale replace.
269        out.push(Change::Replace {
270            path: path.clone(),
271            node: b.clone(),
272        });
273        return;
274    }
275    diff_attrs(a.attrs.as_ref(), b.attrs.as_ref(), path, out);
276    if a.text != b.text {
277        diff_text_field(a.text.as_deref(), b.text.as_deref(), path, out, opts);
278    }
279    if a.marks != b.marks {
280        out.push(Change::SetMarks {
281            path: path.clone(),
282            marks: b.marks.clone(),
283        });
284    }
285    diff_extra(&a.extra, &b.extra, path, out);
286    diff_children(a.children(), b.children(), path, out, opts);
287}
288
289/// Emit the change(s) for a differing text payload, honoring the granularity.
290fn diff_text_field(
291    a: Option<&str>,
292    b: Option<&str>,
293    path: &mut [usize],
294    out: &mut Vec<Change>,
295    opts: &DiffOptions,
296) {
297    // Character-level splices only make sense when both sides are present text.
298    if let (Some(at), Some(bt)) = (a, b) {
299        match opts.text {
300            DiffGranularity::Inline => {
301                let segs = text_diff::diff_text(at, bt);
302                text_diff::splices_from_segments(path, &segs, out);
303                return;
304            }
305            DiffGranularity::Smart { replace_threshold } => {
306                let segs = text_diff::diff_text(at, bt);
307                let total = at.chars().count() + bt.chars().count();
308                let changed = text_diff::changed_scalars(&segs);
309                if total == 0 || (changed as f32 / total as f32) <= replace_threshold {
310                    text_diff::splices_from_segments(path, &segs, out);
311                    return;
312                }
313            }
314            DiffGranularity::Block => {}
315        }
316    }
317    out.push(Change::SetText {
318        path: path.to_vec(),
319        text: b.map(str::to_owned),
320    });
321}
322
323/// Whether an `Option<container>` shape difference (where the arg is
324/// `Some(is_empty)` / `None`) can't be reconciled by the key/child ops, which
325/// normalize emptied containers to `None`. A present-but-empty container
326/// (`Some(true)`, e.g. parsed from `[]`/`{}`) needs an exact match on the other
327/// side; otherwise the node must be replaced wholesale to round-trip exactly.
328fn empty_shape_mismatch(a_is_empty: Option<bool>, b_is_empty: Option<bool>) -> bool {
329    match b_is_empty {
330        Some(true) => a_is_empty != Some(true),
331        None => a_is_empty == Some(true),
332        Some(false) => false,
333    }
334}
335
336fn diff_attrs(
337    a: Option<&Map<String, Value>>,
338    b: Option<&Map<String, Value>>,
339    path: &mut [usize],
340    out: &mut Vec<Change>,
341) {
342    let empty = Map::new();
343    let am = a.unwrap_or(&empty);
344    let bm = b.unwrap_or(&empty);
345    for (k, v) in bm {
346        if am.get(k) != Some(v) {
347            out.push(Change::SetAttr {
348                path: path.to_vec(),
349                key: k.clone(),
350                value: v.clone(),
351            });
352        }
353    }
354    for k in am.keys() {
355        if !bm.contains_key(k) {
356            out.push(Change::RemoveAttr {
357                path: path.to_vec(),
358                key: k.clone(),
359            });
360        }
361    }
362}
363
364fn diff_extra(
365    am: &Map<String, Value>,
366    bm: &Map<String, Value>,
367    path: &mut [usize],
368    out: &mut Vec<Change>,
369) {
370    for (k, v) in bm {
371        if am.get(k) != Some(v) {
372            out.push(Change::SetExtra {
373                path: path.to_vec(),
374                key: k.clone(),
375                value: v.clone(),
376            });
377        }
378    }
379    for k in am.keys() {
380        if !bm.contains_key(k) {
381            out.push(Change::RemoveExtra {
382                path: path.to_vec(),
383                key: k.clone(),
384            });
385        }
386    }
387}
388
389/// One LCS-alignment step over the (trimmed) middle child slices.
390enum Step {
391    Match,
392    Del(usize),
393    Ins(usize),
394}
395
396/// A live-list element identity used by the move simulation: either an original
397/// `a`-child (reused/relocated/modified in place) or a freshly inserted node.
398#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
399enum Key {
400    Orig(usize),
401    New(usize),
402}
403
404/// Positional pairing of a leftover deletion with a leftover insertion in the
405/// same gap: a same-type modify (recurse) or a different-type wholesale replace.
406enum Role {
407    Modify(usize),
408    Replace(usize),
409}
410
411fn diff_children(
412    a: &[Node],
413    b: &[Node],
414    path: &mut Vec<usize>,
415    out: &mut Vec<Change>,
416    opts: &DiffOptions,
417) {
418    // Trim common prefix/suffix (cheap; shrinks the LCS DP and handles the
419    // common append/prepend cases in linear time).
420    let mut start = 0;
421    while start < a.len() && start < b.len() && a[start] == b[start] {
422        start += 1;
423    }
424    let mut ea = a.len();
425    let mut eb = b.len();
426    while ea > start && eb > start && a[ea - 1] == b[eb - 1] {
427        ea -= 1;
428        eb -= 1;
429    }
430
431    let am = &a[start..ea];
432    let bm = &b[start..eb];
433    if am.is_empty() && bm.is_empty() {
434        return;
435    }
436
437    // Equality classes: one id per distinct child value, so alignment and move
438    // pairing compare integers instead of whole subtrees (O(size) once).
439    let (ida, idb) = intern(am, bm);
440    let steps = lcs_align(&ida, &idb);
441
442    // Reconstruct the alignment: which bm index each am index matched (kept in
443    // place), plus the per-gap unmatched deletions/insertions between anchors.
444    let mut gaps: Vec<(Vec<usize>, Vec<usize>)> = Vec::new();
445    let mut cur_dels: Vec<usize> = Vec::new();
446    let mut cur_inss: Vec<usize> = Vec::new();
447    let mut bm_match_src: HashMap<usize, usize> = HashMap::new(); // bm_j -> am_i
448    let (mut ai, mut bj) = (0usize, 0usize);
449    for step in &steps {
450        match step {
451            Step::Match => {
452                gaps.push((std::mem::take(&mut cur_dels), std::mem::take(&mut cur_inss)));
453                bm_match_src.insert(bj, ai);
454                ai += 1;
455                bj += 1;
456            }
457            Step::Del(i) => {
458                cur_dels.push(*i);
459                ai += 1;
460            }
461            Step::Ins(j) => {
462                cur_inss.push(*j);
463                bj += 1;
464            }
465        }
466    }
467    gaps.push((cur_dels, cur_inss));
468
469    // Move detection: pair value-equal nodes that LCS split into a deletion and
470    // an insertion. Greedy/FIFO over equal values -> stable on duplicates: each
471    // insertion takes the first unused deletion of its class (per-class queues).
472    let mut dels_by_id: HashMap<u32, std::collections::VecDeque<usize>> = HashMap::new();
473    for (dels, _) in &gaps {
474        for &d in dels {
475            dels_by_id.entry(ida[d]).or_default().push_back(d);
476        }
477    }
478    let mut bm_move_src: HashMap<usize, usize> = HashMap::new(); // bm_j -> am_d
479    let mut am_moved: HashSet<usize> = HashSet::new();
480    for (_, inss) in &gaps {
481        for &j in inss {
482            if let Some(d) = dels_by_id.get_mut(&idb[j]).and_then(|q| q.pop_front()) {
483                bm_move_src.insert(j, d);
484                am_moved.insert(d);
485            }
486        }
487    }
488
489    // Per-gap modify/replace pairing on the remaining (non-moved) leftovers.
490    let mut bm_role: HashMap<usize, Role> = HashMap::new();
491    let mut removed_am: Vec<usize> = Vec::new();
492    for (dels, inss) in &gaps {
493        let rem_dels: Vec<usize> = dels
494            .iter()
495            .copied()
496            .filter(|d| !am_moved.contains(d))
497            .collect();
498        let rem_inss: Vec<usize> = inss
499            .iter()
500            .copied()
501            .filter(|j| !bm_move_src.contains_key(j))
502            .collect();
503        let pairs = rem_dels.len().min(rem_inss.len());
504        for k in 0..pairs {
505            let (d, j) = (rem_dels[k], rem_inss[k]);
506            if am[d].node_type == bm[j].node_type {
507                bm_role.insert(j, Role::Modify(d)); // same type -> recurse in place
508            } else {
509                bm_role.insert(j, Role::Replace(d)); // type change -> wholesale
510            }
511        }
512        removed_am.extend_from_slice(&rem_dels[pairs..]);
513    }
514
515    // Target key sequence for the middle (what the live list must become), as
516    // dense ids: `Orig(i)` -> `i`, `New(j)` -> `am.len() + j`. Plus an
517    // id -> target-index lookup used to place relocated nodes minimally.
518    let n_orig = am.len() as u32;
519    let mut target: Vec<u32> = Vec::with_capacity(bm.len());
520    for j in 0..bm.len() {
521        let key = if let Some(&i) = bm_match_src.get(&j) {
522            Key::Orig(i)
523        } else if let Some(&d) = bm_move_src.get(&j) {
524            Key::Orig(d)
525        } else {
526            match bm_role.get(&j) {
527                Some(Role::Modify(d) | Role::Replace(d)) => Key::Orig(*d),
528                None => Key::New(j),
529            }
530        };
531        target.push(match key {
532            Key::Orig(i) => i as u32,
533            Key::New(j) => n_orig + j as u32,
534        });
535    }
536    let n_ids = am.len() + bm.len();
537    let mut target_pos = vec![u32::MAX; n_ids];
538    for (t, &k) in target.iter().enumerate() {
539        target_pos[k as usize] = t as u32;
540    }
541    let mut is_moved = vec![false; n_ids];
542    for &d in &am_moved {
543        is_moved[d] = true;
544    }
545
546    // 1. Plain removes. `removed_am` is ascending (gap order, then index order
547    //    within a gap), so the live index is the original minus prior removals.
548    for (k, &d) in removed_am.iter().enumerate() {
549        out.push(Change::Remove {
550            path: path.clone(),
551            index: start + d - k,
552        });
553    }
554    let mut is_removed = vec![false; am.len()];
555    for &d in &removed_am {
556        is_removed[d] = true;
557    }
558    let post_remove = || (0..n_orig).filter(|&i| !is_removed[i as usize]);
559
560    // 2. Simulate the live list and emit ops whose indices are valid by
561    //    construction. Absolute index = start + position in the middle.
562    let mark = out.len();
563    if !settle_sorted(
564        bm,
565        &target,
566        &target_pos,
567        &is_moved,
568        am_moved.len(),
569        post_remove(),
570        start,
571        path,
572        out,
573    ) {
574        // The sorted settle cascaded (re-moving nodes it already placed, which
575        // blows up on many value-equal reordered siblings): redo it with
576        // exactly one Move per relocated node.
577        out.truncate(mark);
578        settle_cursor(bm, &target, &is_moved, post_remove(), start, path, out);
579    }
580
581    // 3. Value fixes for modify/replace pairs, at their final positions (the
582    //    structural layout is now settled, so index = start + bm index).
583    for (j, target_node) in bm.iter().enumerate() {
584        match bm_role.get(&j) {
585            Some(Role::Modify(d)) => {
586                path.push(start + j);
587                diff_node(&am[*d], target_node, path, out, opts);
588                path.pop();
589            }
590            Some(Role::Replace(_)) => {
591                let mut child = path.clone();
592                child.push(start + j);
593                out.push(Change::Replace {
594                    path: child,
595                    node: target_node.clone(),
596                });
597            }
598            None => {}
599        }
600    }
601}
602
603/// Settle each target position left to right, maintaining the invariant
604/// `live[..p] == target[..p]`. New nodes are inserted; otherwise relocate one
605/// genuinely-moved node (the displaced occupant, or the wanted node if it's the
606/// one that moved) to its sorted destination, never disturbing the matched
607/// skeleton. Usually one Move per moved node; returns `false` (output partial)
608/// once more than `budget` Moves would be needed, i.e. it started cascading.
609#[allow(clippy::too_many_arguments)]
610fn settle_sorted(
611    bm: &[Node],
612    target: &[u32],
613    target_pos: &[u32],
614    is_moved: &[bool],
615    budget: usize,
616    live0: impl Iterator<Item = u32>,
617    start: usize,
618    path: &[usize],
619    out: &mut Vec<Change>,
620) -> bool {
621    let n_orig = is_moved.len() - bm.len();
622    let mut live = LiveList::new(live0, is_moved.len());
623    // `present.prefix(t)` = live elements whose target index is < t.
624    let mut present = Fenwick::new(target.len());
625    for (t, &k) in target.iter().enumerate() {
626        if (k as usize) < n_orig {
627            present.add(t);
628        }
629    }
630    let mut moves = 0;
631    let mut p = 0;
632    while p < target.len() {
633        let want = target[p];
634        let cur = live.get(p);
635        if cur == Some(want) {
636            p += 1;
637            continue;
638        }
639        if want as usize >= n_orig {
640            out.push(Change::Insert {
641                path: path.to_vec(),
642                index: start + p,
643                node: bm[want as usize - n_orig].clone(),
644            });
645            live.insert(p, want);
646            present.add(p);
647            p += 1;
648            continue;
649        }
650        if moves == budget {
651            return false;
652        }
653        moves += 1;
654        // `want` is an original node not yet at `p`. Move the displaced occupant
655        // if it is the relocated one, else pull `want` (itself relocated) here.
656        let cur = cur.expect("original node missing from live list");
657        let mover = if is_moved[cur as usize] { cur } else { want };
658        let from = live.index_of(mover);
659        // Destination = number of currently-present elements that precede it in
660        // the target order (the mover itself is not counted).
661        let dest = present.prefix(target_pos[mover as usize] as usize);
662        out.push(Change::Move {
663            path: path.to_vec(),
664            from: start + from,
665            to: start + dest,
666        });
667        let k = live.remove(from);
668        live.insert(dest, k);
669        // Re-check `p` (the move may or may not have settled this slot).
670    }
671    true
672}
673
674/// Fallback settle with exactly one Move per relocated node. Live list =
675/// placed nodes (a prefix of `target`) interleaved with not-yet-placed
676/// originals, all placed ones before `cur`; past `cur` only unplaced originals,
677/// in original order. Non-moved originals keep their relative order in
678/// `target`, so they are reached by advancing `cur`; moved ones are pulled to it.
679fn settle_cursor(
680    bm: &[Node],
681    target: &[u32],
682    is_moved: &[bool],
683    live0: impl Iterator<Item = u32>,
684    start: usize,
685    path: &[usize],
686    out: &mut Vec<Change>,
687) {
688    let n_orig = is_moved.len() - bm.len();
689    let mut live = LiveList::new(live0, is_moved.len());
690    let mut cur = 0;
691    for &want in target {
692        if want as usize >= n_orig {
693            out.push(Change::Insert {
694                path: path.to_vec(),
695                index: start + cur,
696                node: bm[want as usize - n_orig].clone(),
697            });
698            live.insert(cur, want);
699            cur += 1;
700            continue;
701        }
702        let at = live.index_of(want);
703        if !is_moved[want as usize] {
704            debug_assert!(at >= cur, "non-moved node behind the cursor");
705            cur = at + 1;
706            continue;
707        }
708        if at == cur {
709            cur += 1;
710            continue;
711        }
712        live.remove(at);
713        if at < cur {
714            cur -= 1;
715        }
716        out.push(Change::Move {
717            path: path.to_vec(),
718            from: start + at,
719            to: start + cur,
720        });
721        live.insert(cur, want);
722        cur += 1;
723    }
724    debug_assert_eq!(
725        live.to_vec(),
726        target,
727        "diff move simulation diverged from target"
728    );
729}
730
731/// Order-statistic list of key ids for the move simulation, split into
732/// ~√n-sized blocks so `get`/`index_of`/`insert`/`remove` cost O(√n) instead
733/// of O(n).
734struct LiveList {
735    blocks: Vec<Vec<u32>>,
736    /// Key id -> index of the block holding it.
737    block_of: Vec<u32>,
738    cap: usize,
739}
740
741impl LiveList {
742    fn new(keys: impl Iterator<Item = u32>, n_ids: usize) -> Self {
743        let mut l = LiveList {
744            blocks: vec![keys.collect()],
745            block_of: vec![0; n_ids],
746            cap: 16,
747        };
748        l.rebuild();
749        l
750    }
751
752    /// Re-chunk into blocks of `cap` (≈ √len) and refresh `block_of`.
753    fn rebuild(&mut self) {
754        let all = self.to_vec();
755        self.cap = (all.len() as f64).sqrt() as usize + 16;
756        self.blocks = all.chunks(self.cap).map(<[u32]>::to_vec).collect();
757        if self.blocks.is_empty() {
758            self.blocks.push(Vec::new());
759        }
760        for (b, blk) in self.blocks.iter().enumerate() {
761            for &k in blk {
762                self.block_of[k as usize] = b as u32;
763            }
764        }
765    }
766
767    fn to_vec(&self) -> Vec<u32> {
768        self.blocks.concat()
769    }
770
771    /// Block and in-block offset of live index `idx` (`idx == len` maps to the
772    /// end of the last block).
773    fn locate(&self, mut idx: usize) -> (usize, usize) {
774        let last = self.blocks.len() - 1;
775        for (b, blk) in self.blocks.iter().enumerate() {
776            if idx < blk.len() || b == last {
777                return (b, idx);
778            }
779            idx -= blk.len();
780        }
781        unreachable!()
782    }
783
784    fn get(&self, idx: usize) -> Option<u32> {
785        let (b, o) = self.locate(idx);
786        self.blocks[b].get(o).copied()
787    }
788
789    fn index_of(&self, key: u32) -> usize {
790        let b = self.block_of[key as usize] as usize;
791        let before: usize = self.blocks[..b].iter().map(Vec::len).sum();
792        before
793            + self.blocks[b]
794                .iter()
795                .position(|&k| k == key)
796                .expect("key in its block")
797    }
798
799    fn remove(&mut self, idx: usize) -> u32 {
800        let (b, o) = self.locate(idx);
801        self.blocks[b].remove(o)
802    }
803
804    fn insert(&mut self, idx: usize, key: u32) {
805        let (b, o) = self.locate(idx);
806        self.blocks[b].insert(o, key);
807        self.block_of[key as usize] = b as u32;
808        if self.blocks[b].len() > 2 * self.cap {
809            self.rebuild();
810        }
811    }
812}
813
814/// Fenwick (binary indexed) tree of counts over target indices.
815struct Fenwick(Vec<u32>);
816
817impl Fenwick {
818    fn new(n: usize) -> Self {
819        Fenwick(vec![0; n])
820    }
821
822    /// Count one entry at index `i` (0-based tree: `self.0[k]` covers
823    /// `k & (k + 1) ..= k`).
824    fn add(&mut self, mut i: usize) {
825        while i < self.0.len() {
826            self.0[i] += 1;
827            i |= i + 1;
828        }
829    }
830
831    /// Count of entries at indices `< i`.
832    fn prefix(&self, mut i: usize) -> usize {
833        let mut s = 0;
834        while i > 0 {
835            s += self.0[i - 1] as usize;
836            i &= i - 1;
837        }
838        s
839    }
840}
841
842/// Below this many DP cells (`m·n`) the exact table LCS runs (small lists, the
843/// common case); above it Myers' O((m+n)·D) greedy LCS, which is linear for the
844/// typical "few edits in a long list". Both produce a longest common
845/// subsequence; tie-breaking between equally long alignments may differ.
846const DP_CELLS: usize = 1 << 16;
847
848/// Myers gives up past this many edits (its trace is O(D²)); then the table LCS
849/// runs if it fits in [`DP_FALLBACK_CELLS`], else no anchors are kept.
850const MYERS_MAX_D: usize = 4096;
851const DP_FALLBACK_CELLS: usize = 1 << 24;
852
853/// Longest-common-subsequence alignment of two child slices, by equality class
854/// (see [`intern`]).
855fn lcs_align(a: &[u32], b: &[u32]) -> Vec<Step> {
856    let (m, n) = (a.len(), b.len());
857    if m == 0 {
858        return (0..n).map(Step::Ins).collect();
859    }
860    if n == 0 {
861        return (0..m).map(Step::Del).collect();
862    }
863    if m.saturating_mul(n) <= DP_CELLS {
864        return lcs_table(a, b);
865    }
866    if let Some(steps) = myers(a, b, MYERS_MAX_D) {
867        return steps;
868    }
869    if m.saturating_mul(n) <= DP_FALLBACK_CELLS {
870        return lcs_table(a, b);
871    }
872    // Pathological (huge and mostly different): no anchors. Still correct:
873    // the gap pairs everything positionally as modify/replace.
874    (0..m).map(Step::Del).chain((0..n).map(Step::Ins)).collect()
875}
876
877/// Exact LCS by dynamic programming, O(m·n) time and space.
878fn lcs_table(a: &[u32], b: &[u32]) -> Vec<Step> {
879    let (m, n) = (a.len(), b.len());
880    // dp[i][j] = LCS length of a[i..] vs b[j..], flattened row-major.
881    let w = n + 1;
882    let mut dp = vec![0u32; (m + 1) * w];
883    for i in (0..m).rev() {
884        for j in (0..n).rev() {
885            dp[i * w + j] = if a[i] == b[j] {
886                dp[(i + 1) * w + j + 1] + 1
887            } else {
888                dp[(i + 1) * w + j].max(dp[i * w + j + 1])
889            };
890        }
891    }
892    let mut steps = Vec::with_capacity(m.max(n));
893    let (mut i, mut j) = (0usize, 0usize);
894    while i < m && j < n {
895        if a[i] == b[j] {
896            steps.push(Step::Match);
897            i += 1;
898            j += 1;
899        } else if dp[(i + 1) * w + j] >= dp[i * w + j + 1] {
900            steps.push(Step::Del(i));
901            i += 1;
902        } else {
903            steps.push(Step::Ins(j));
904            j += 1;
905        }
906    }
907    while i < m {
908        steps.push(Step::Del(i));
909        i += 1;
910    }
911    while j < n {
912        steps.push(Step::Ins(j));
913        j += 1;
914    }
915    steps
916}
917
918/// Myers' greedy shortest-edit-script (= LCS) alignment, O((m+n)·D) time and
919/// O(D²) trace space. `None` if more than `max_d` edits are needed.
920fn myers(a: &[u32], b: &[u32], max_d: usize) -> Option<Vec<Step>> {
921    // Any script needs at least |n - m| edits: bail before allocating.
922    if a.len().abs_diff(b.len()) > max_d {
923        return None;
924    }
925    let (n, m) = (a.len() as isize, b.len() as isize);
926    let max_d = max_d.min(a.len() + b.len()) as isize;
927    let off = max_d + 1;
928    // v[k + off] = furthest x on diagonal k = x - y.
929    let mut v = vec![0isize; (2 * off + 1) as usize];
930    // trace[d] = v[-d..=d] before round d.
931    let mut trace: Vec<Vec<isize>> = Vec::new();
932    let mut end_d = None;
933    'outer: for d in 0..=max_d {
934        trace.push(v[(off - d) as usize..=(off + d) as usize].to_vec());
935        let mut k = -d;
936        while k <= d {
937            let down = k == -d || (k != d && v[(off + k - 1) as usize] < v[(off + k + 1) as usize]);
938            let mut x = if down {
939                v[(off + k + 1) as usize]
940            } else {
941                v[(off + k - 1) as usize] + 1
942            };
943            let mut y = x - k;
944            while x < n && y < m && a[x as usize] == b[y as usize] {
945                x += 1;
946                y += 1;
947            }
948            v[(off + k) as usize] = x;
949            if x >= n && y >= m {
950                end_d = Some(d);
951                break 'outer;
952            }
953            k += 2;
954        }
955    }
956    end_d?;
957    // Backtrack from (n, m) through the saved rounds.
958    let mut steps = Vec::with_capacity((n + m) as usize);
959    let (mut x, mut y) = (n, m);
960    for d in (0..trace.len() as isize).rev() {
961        if d == 0 {
962            while x > 0 && y > 0 {
963                steps.push(Step::Match);
964                x -= 1;
965                y -= 1;
966            }
967            break;
968        }
969        let vd = &trace[d as usize];
970        let get = |k: isize| vd[(k + d) as usize];
971        let k = x - y;
972        let prev_k = if k == -d || (k != d && get(k - 1) < get(k + 1)) {
973            k + 1
974        } else {
975            k - 1
976        };
977        let prev_x = get(prev_k);
978        let prev_y = prev_x - prev_k;
979        while x > prev_x && y > prev_y {
980            steps.push(Step::Match);
981            x -= 1;
982            y -= 1;
983        }
984        if x == prev_x {
985            steps.push(Step::Ins((y - 1) as usize));
986        } else {
987            steps.push(Step::Del((x - 1) as usize));
988        }
989        x = prev_x;
990        y = prev_y;
991    }
992    steps.reverse();
993    Some(steps)
994}
995
996/// Map every child of `a` and `b` to an equality-class id: equal ids iff the
997/// nodes are equal (`==`). Hash-bucketed, each node is hashed once and compared
998/// deeply only against its bucket's representatives, so the cost is O(total
999/// size) instead of O(m·n) subtree comparisons.
1000fn intern(a: &[Node], b: &[Node]) -> (Vec<u32>, Vec<u32>) {
1001    let mut buckets: HashMap<u64, Vec<(u32, &Node)>> = HashMap::with_capacity(a.len() + b.len());
1002    let mut next = 0u32;
1003    let mut out_a = Vec::with_capacity(a.len());
1004    let mut out_b = Vec::with_capacity(b.len());
1005    for (side, nodes) in [(0, a), (1, b)] {
1006        for n in nodes {
1007            let bucket = buckets.entry(node_hash(n)).or_default();
1008            let found = bucket.iter().find(|(_, rep)| *rep == n).map(|(i, _)| *i);
1009            let i = found.unwrap_or_else(|| {
1010                let i = next;
1011                next += 1;
1012                bucket.push((i, n));
1013                i
1014            });
1015            if side == 0 {
1016                out_a.push(i);
1017            } else {
1018                out_b.push(i);
1019            }
1020        }
1021    }
1022    (out_a, out_b)
1023}
1024
1025/// Structural hash consistent with `Node: PartialEq` (maps hash
1026/// order-insensitively, like their equality).
1027fn node_hash(n: &Node) -> u64 {
1028    let mut h = Fx::default();
1029    hash_node(n, &mut h);
1030    h.0
1031}
1032
1033#[derive(Default)]
1034struct Fx(u64);
1035
1036impl Fx {
1037    #[inline]
1038    fn add(&mut self, x: u64) {
1039        self.0 = (self.0.rotate_left(5) ^ x).wrapping_mul(0x51_7c_c1_b7_27_22_0a_95);
1040    }
1041    #[inline]
1042    fn bytes(&mut self, s: &[u8]) {
1043        let (chunks, rest) = s.as_chunks::<8>();
1044        for c in chunks {
1045            self.add(u64::from_le_bytes(*c));
1046        }
1047        let mut tail = [0u8; 8];
1048        tail[..rest.len()].copy_from_slice(rest);
1049        self.add(u64::from_le_bytes(tail) ^ ((s.len() as u64) << 56));
1050    }
1051    #[inline]
1052    fn opt_str(&mut self, s: Option<&str>) {
1053        match s {
1054            None => self.add(0x9e37),
1055            Some(s) => self.bytes(s.as_bytes()),
1056        }
1057    }
1058}
1059
1060fn hash_node(n: &Node, h: &mut Fx) {
1061    h.opt_str(n.node_type.as_deref());
1062    hash_opt_map(n.attrs.as_ref(), h);
1063    match &n.content {
1064        None => h.add(1),
1065        Some(c) => {
1066            h.add(2 ^ ((c.len() as u64) << 8));
1067            for child in c {
1068                hash_node(child, h);
1069            }
1070        }
1071    }
1072    match &n.marks {
1073        None => h.add(3),
1074        Some(ms) => {
1075            h.add(4 ^ ((ms.len() as u64) << 8));
1076            for m in ms {
1077                h.bytes(m.mark_type.as_bytes());
1078                hash_opt_map(m.attrs.as_ref(), h);
1079                hash_map(&m.extra, h);
1080            }
1081        }
1082    }
1083    h.opt_str(n.text.as_deref());
1084    hash_map(&n.extra, h);
1085}
1086
1087fn hash_opt_map(m: Option<&Map<String, Value>>, h: &mut Fx) {
1088    match m {
1089        None => h.add(5),
1090        Some(m) => hash_map(m, h),
1091    }
1092}
1093
1094/// Order-insensitive: sum of independent per-entry hashes.
1095fn hash_map(m: &Map<String, Value>, h: &mut Fx) {
1096    let mut sum = 0u64;
1097    for (k, v) in m {
1098        let mut e = Fx::default();
1099        e.bytes(k.as_bytes());
1100        hash_value(v, &mut e);
1101        sum = sum.wrapping_add(e.0);
1102    }
1103    h.add(6 ^ ((m.len() as u64) << 8));
1104    h.add(sum);
1105}
1106
1107fn hash_value(v: &Value, h: &mut Fx) {
1108    match v {
1109        Value::Null => h.add(10),
1110        Value::Bool(b) => h.add(11 + *b as u64),
1111        Value::Number(n) => {
1112            if let Some(u) = n.as_u64() {
1113                h.add(13);
1114                h.add(u);
1115            } else if let Some(i) = n.as_i64() {
1116                h.add(14);
1117                h.add(i as u64);
1118            } else {
1119                // Float: `==` treats 0.0 and -0.0 as equal.
1120                let f = n.as_f64().unwrap_or(0.0);
1121                h.add(15);
1122                h.add(if f == 0.0 { 0 } else { f.to_bits() });
1123            }
1124        }
1125        Value::String(s) => {
1126            h.add(16);
1127            h.bytes(s.as_bytes());
1128        }
1129        Value::Array(a) => {
1130            h.add(17 ^ ((a.len() as u64) << 8));
1131            for x in a {
1132                hash_value(x, h);
1133            }
1134        }
1135        Value::Object(m) => {
1136            h.add(18);
1137            hash_map(m, h);
1138        }
1139    }
1140}
1141
1142// ---- apply internals ----------------------------------------------------
1143
1144fn node_at_mut<'a>(
1145    root: &'a mut Node,
1146    path: &[usize],
1147) -> std::result::Result<&'a mut Node, ApplyError> {
1148    root.node_at_mut(path).ok_or_else(|| ApplyError {
1149        path: path.to_vec(),
1150    })
1151}
1152
1153fn apply_one(root: &mut Node, change: &Change) -> std::result::Result<(), ApplyError> {
1154    match change {
1155        Change::SetAttr { path, key, value } => {
1156            node_at_mut(root, path)?.set_attr(key.clone(), value.clone());
1157        }
1158        Change::RemoveAttr { path, key } => {
1159            node_at_mut(root, path)?.remove_attr(key);
1160        }
1161        Change::SetText { path, text } => {
1162            node_at_mut(root, path)?.text = text.clone();
1163        }
1164        Change::SpliceText {
1165            path,
1166            from,
1167            len_del,
1168            insert,
1169        } => {
1170            let node = node_at_mut(root, path)?;
1171            // Validate against the current text *without* mutating, so an
1172            // out-of-range or overflowing splice leaves the node untouched.
1173            // Scalar offsets translate to byte offsets (scalar-safe); a scalar
1174            // index equal to the length maps to the byte length (end splice).
1175            let (start, end) = {
1176                let cur = node.text.as_deref().unwrap_or("");
1177                let scalars = cur.chars().count();
1178                let end_scalar = match from.checked_add(*len_del) {
1179                    Some(e) if *from <= scalars && e <= scalars => e,
1180                    _ => return Err(ApplyError { path: path.clone() }),
1181                };
1182                let start = cur.char_indices().nth(*from).map_or(cur.len(), |(b, _)| b);
1183                let end = cur
1184                    .char_indices()
1185                    .nth(end_scalar)
1186                    .map_or(cur.len(), |(b, _)| b);
1187                (start, end)
1188            };
1189            // A pure no-op (nothing removed, nothing inserted) must not
1190            // materialize a `None` text field into `Some("")` — the model
1191            // preserves missing-vs-empty for faithful round-tripping.
1192            if start != end || !insert.is_empty() {
1193                node.text
1194                    .get_or_insert_with(String::new)
1195                    .replace_range(start..end, insert);
1196            }
1197        }
1198        Change::SetMarks { path, marks } => {
1199            node_at_mut(root, path)?.marks = marks.clone();
1200        }
1201        Change::SetExtra { path, key, value } => {
1202            node_at_mut(root, path)?
1203                .extra
1204                .insert(key.clone(), value.clone());
1205        }
1206        Change::RemoveExtra { path, key } => {
1207            node_at_mut(root, path)?.extra.remove(key);
1208        }
1209        Change::Insert { path, index, node } => {
1210            let parent = node_at_mut(root, path)?;
1211            if *index > parent.child_count() {
1212                let mut p = path.clone();
1213                p.push(*index);
1214                return Err(ApplyError { path: p });
1215            }
1216            parent.insert_child(*index, node.clone());
1217        }
1218        Change::Remove { path, index } => {
1219            if node_at_mut(root, path)?.remove_child(*index).is_none() {
1220                let mut p = path.clone();
1221                p.push(*index);
1222                return Err(ApplyError { path: p });
1223            }
1224        }
1225        Change::Replace { path, node } => {
1226            if path.is_empty() {
1227                *root = node.clone();
1228            } else {
1229                let (parent_path, last) = path.split_at(path.len() - 1);
1230                let parent = node_at_mut(root, parent_path)?;
1231                if parent.replace_child(last[0], node.clone()).is_none() {
1232                    return Err(ApplyError { path: path.clone() });
1233                }
1234            }
1235        }
1236        Change::Move { path, from, to } => {
1237            let parent = node_at_mut(root, path)?;
1238            let len = parent.child_count();
1239            // `from` indexes the live list; `to` indexes the list after removal,
1240            // so its valid range is 0..=len-1.
1241            if *from >= len || *to >= len {
1242                let mut p = path.clone();
1243                p.push(if *from >= len { *from } else { *to });
1244                return Err(ApplyError { path: p });
1245            }
1246            let node = parent.remove_child(*from).expect("from bounds-checked");
1247            parent.insert_child(*to, node);
1248        }
1249    }
1250    Ok(())
1251}