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