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