Skip to main content

hjkl_buffer/
undo.rs

1//! Undo/redo entry type for per-buffer undo history.
2//!
3//! Lives in `hjkl-buffer` so that [`crate::Buffer`] can own the undo stack
4//! directly, keeping per-buffer state co-located with the rope.
5
6use std::collections::BTreeMap;
7use std::sync::Arc;
8use std::time::{Duration, SystemTime, UNIX_EPOCH};
9
10use serde::{Deserialize, Serialize};
11
12/// A single entry in the undo or redo stack.
13///
14/// The `timestamp` records the wall-clock time at which the snapshot was
15/// taken (i.e. when `push_undo` was called), enabling the `:earlier` /
16/// `:later` time-travel ex commands to walk the stack by duration rather
17/// than by step count.
18///
19/// Stored as a `ropey::Rope` (O(1) Arc-clone) rather than a `String` so
20/// snapshot cost is negligible even on multi-MB buffers.
21#[derive(Debug, Clone)]
22pub struct UndoEntry {
23    pub rope: ropey::Rope,
24    pub cursor: (usize, usize),
25    pub timestamp: SystemTime,
26    /// Local marks / jumplist / changelist / this-buffer's-global-marks
27    /// snapshot, so undo/redo restore mark-ish positions alongside the
28    /// text instead of leaving them shifted by the edit being undone
29    /// (audit-r2 fix 2). `Default::default()` (all empty) for callers
30    /// that don't populate it — restoring an all-empty snapshot is a
31    /// no-op against a freshly-constructed buffer's own empty state, so
32    /// existing fixtures that only care about text/cursor stay valid.
33    pub marks: MarkSnapshot,
34}
35
36/// Buffer-scoped "edit coherence" state snapshotted alongside a
37/// [`UndoEntry`]'s rope so undo/redo can restore marks, not just text.
38///
39/// Positions are plain `(row, col)` (or `(row, col)` values keyed by
40/// mark char) — no buffer-id tagging needed here even for
41/// `global_marks`, because a `MarkSnapshot` always belongs to exactly
42/// one buffer's undo stack; the engine is responsible for reattaching
43/// its own `buffer_id` when writing entries back into the session-global
44/// marks map (see `Editor::restore_marks`).
45#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
46pub struct MarkSnapshot {
47    /// `ma`-`mz` local marks (`View::marks_cloned`).
48    pub local_marks: BTreeMap<char, (usize, usize)>,
49    /// Back-jumplist (`Ctrl-o` stack), newest at the back.
50    pub jump_back: Vec<(usize, usize)>,
51    /// Forward-jumplist (`Ctrl-i` stack), newest at the back.
52    pub jump_fwd: Vec<(usize, usize)>,
53    /// `` `. ``  / `'.` — position of the most recent change.
54    pub change_last_edit: Option<(usize, usize)>,
55    /// Changelist ring (`g;` / `g,`).
56    pub change_list: Vec<(usize, usize)>,
57    /// Walk cursor into `change_list`; `None` outside a walk.
58    pub change_cursor: Option<usize>,
59    /// `mA`-`mZ` global marks that belong to THIS buffer (bare
60    /// `(row, col)` — the buffer-id is implicit, this buffer).
61    pub global_marks: BTreeMap<char, (usize, usize)>,
62}
63
64// ─── Reversible edge delta (Phase 3a) ──────────────────────────────────────────
65//
66// Phase 2b stored a FULL rope snapshot on every node. Phase 3a stores only a
67// reversible **delta** on each parent→child edge (the root keeps a full base
68// rope) plus a materialization cache, so the in-RAM hot path stays snapshot-fast
69// while a future undofile shrinks from hundreds of MB to KB. This slice changes
70// ONLY internal storage — every public signature, and every observable
71// behaviour, is byte-identical to Phase 2b.
72
73/// A reversible edit between two adjacent buffer states, expressed as a single
74/// spanning replacement in **char-offset space** on the rope.
75///
76/// The index space is ropey `char` offsets throughout — never bytes — so
77/// multi-byte UTF-8 round-trips (a byte offset could split a codepoint). In the
78/// PARENT state `chars[start .. start + old.chars().count()] == old`; replacing
79/// that region with `new` yields the CHILD state, and swapping the two inverts
80/// it. A whole undo group collapses to the one region spanning its edits
81/// (common-prefix / common-suffix diff); a `Vec<Delta>` for disjoint regions is
82/// an acceptable future generalization, but one spanning region is all Phase 3a
83/// needs.
84#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
85pub struct Delta {
86    /// Char offset of the first differing char (the common-prefix length).
87    pub start: usize,
88    /// Chars present in the PARENT but not the CHILD (removed going forward).
89    pub old: String,
90    /// Chars present in the CHILD but not the PARENT (inserted going forward).
91    pub new: String,
92}
93
94/// Is `byte_idx` a char boundary of `r`? (`str::is_char_boundary` for ropes.)
95///
96/// Ropey always splits chunks ON char boundaries, so the question is answerable
97/// inside the one chunk containing the byte — O(log N), no materialization.
98fn is_char_boundary(r: &ropey::Rope, byte_idx: usize) -> bool {
99    if byte_idx == 0 || byte_idx == r.len_bytes() {
100        return true;
101    }
102    let (chunk, chunk_start, _, _) = r.chunk_at_byte(byte_idx);
103    chunk.is_char_boundary(byte_idx - chunk_start)
104}
105
106/// Length of the longest common byte PREFIX of `a` and `b`, capped at `max`.
107///
108/// Walks both ropes' chunks in lockstep — never materializes either rope. Two
109/// fast paths keep the common editor case near-free: identical chunk pointers
110/// (ropey leaves are `Arc`-shared, so a clone-then-edit child shares every leaf
111/// outside the edit) are accepted without reading bytes, and otherwise whole
112/// overlapping runs are compared with one slice `==` (memcmp).
113fn common_prefix_bytes(a: &ropey::Rope, b: &ropey::Rope, max: usize) -> usize {
114    let mut a_chunks = a.chunks();
115    let mut b_chunks = b.chunks();
116    let mut at: &[u8] = &[];
117    let mut bt: &[u8] = &[];
118    let mut n = 0;
119    while n < max {
120        if at.is_empty() {
121            match a_chunks.next() {
122                Some(c) => at = c.as_bytes(),
123                None => break,
124            }
125            continue;
126        }
127        if bt.is_empty() {
128            match b_chunks.next() {
129                Some(c) => bt = c.as_bytes(),
130                None => break,
131            }
132            continue;
133        }
134        // Same shared leaf, wholly within the cap: equal without looking.
135        if at.as_ptr() == bt.as_ptr() && at.len() == bt.len() && at.len() <= max - n {
136            n += at.len();
137            at = &[];
138            bt = &[];
139            continue;
140        }
141        let m = at.len().min(bt.len()).min(max - n);
142        if at[..m] == bt[..m] {
143            n += m;
144            at = &at[m..];
145            bt = &bt[m..];
146        } else {
147            let mut i = 0;
148            while at[i] == bt[i] {
149                i += 1;
150            }
151            n += i;
152            break;
153        }
154    }
155    n
156}
157
158/// Length of the longest common byte SUFFIX of `a` and `b`, capped at `max`.
159///
160/// The mirror of [`common_prefix_bytes`], walking both ropes' chunk cursors
161/// backwards from the end via [`ropey::iter::Chunks::prev`].
162fn common_suffix_bytes(a: &ropey::Rope, b: &ropey::Rope, max: usize) -> usize {
163    let mut a_chunks = a.chunks_at_byte(a.len_bytes()).0;
164    let mut b_chunks = b.chunks_at_byte(b.len_bytes()).0;
165    let mut at: &[u8] = &[];
166    let mut bt: &[u8] = &[];
167    let mut n = 0;
168    while n < max {
169        if at.is_empty() {
170            match a_chunks.prev() {
171                Some(c) => at = c.as_bytes(),
172                None => break,
173            }
174            continue;
175        }
176        if bt.is_empty() {
177            match b_chunks.prev() {
178                Some(c) => bt = c.as_bytes(),
179                None => break,
180            }
181            continue;
182        }
183        if at.as_ptr() == bt.as_ptr() && at.len() == bt.len() && at.len() <= max - n {
184            n += at.len();
185            at = &[];
186            bt = &[];
187            continue;
188        }
189        let m = at.len().min(bt.len()).min(max - n);
190        let (a_tail, b_tail) = (&at[at.len() - m..], &bt[bt.len() - m..]);
191        if a_tail == b_tail {
192            n += m;
193            at = &at[..at.len() - m];
194            bt = &bt[..bt.len() - m];
195        } else {
196            let mut i = 0;
197            while a_tail[m - 1 - i] == b_tail[m - 1 - i] {
198                i += 1;
199            }
200            n += i;
201            break;
202        }
203    }
204    n
205}
206
207/// Common-prefix / common-suffix diff of two ropes → the minimal single spanning
208/// [`Delta`]. Guarantees `apply_forward(a, diff(a, b)) == b` and
209/// `apply_inverse(b, diff(a, b)) == a` for ALL `a`, `b` (see the property
210/// tests). Boundaries are found on bytes (fast) then snapped to char boundaries
211/// so `old`/`new` are always valid UTF-8 and `start` is a true char offset.
212///
213/// The scans walk the ropes' chunks directly and only the differing MIDDLE is
214/// materialized — a full `to_string()` of both sides used to dominate every edit
215/// on a multi-MB buffer (measured 1.55 ms + ~6.4 MB of allocation per push at
216/// 3.2 MB). `diff_reference` in the tests below is the old materializing
217/// implementation, kept as the differential-test oracle.
218fn diff(parent: &ropey::Rope, child: &ropey::Rope) -> Delta {
219    let a_len = parent.len_bytes();
220    let b_len = child.len_bytes();
221
222    // Longest common byte prefix, snapped DOWN to a char boundary.
223    let max_pre = a_len.min(b_len);
224    let mut pre = common_prefix_bytes(parent, child, max_pre);
225    while pre > 0 && !is_char_boundary(parent, pre) {
226        pre -= 1;
227    }
228
229    // Longest common byte suffix not overlapping the prefix. The cut points
230    // `a_end`/`b_end` sit at identical trailing bytes, so snapping `a_end` UP to
231    // a char boundary snaps `b_end` by the same byte delta simultaneously.
232    let suf = common_suffix_bytes(parent, child, max_pre - pre);
233    let mut a_end = a_len - suf;
234    while a_end < a_len && !is_char_boundary(parent, a_end) {
235        a_end += 1;
236    }
237    let b_end = b_len - (a_len - a_end);
238
239    Delta {
240        start: parent.byte_to_char(pre),
241        old: parent.byte_slice(pre..a_end).to_string(),
242        new: child.byte_slice(pre..b_end).to_string(),
243    }
244}
245
246/// Apply a forward delta (PARENT → CHILD) to `parent`, returning the child rope.
247fn apply_forward(parent: &ropey::Rope, d: &Delta) -> ropey::Rope {
248    let mut r = parent.clone();
249    let old_chars = d.old.chars().count();
250    r.remove(d.start..d.start + old_chars);
251    r.insert(d.start, &d.new);
252    r
253}
254
255/// Apply an inverse delta (CHILD → PARENT) to `child`, returning the parent rope.
256fn apply_inverse(child: &ropey::Rope, d: &Delta) -> ropey::Rope {
257    let mut r = child.clone();
258    let new_chars = d.new.chars().count();
259    r.remove(d.start..d.start + new_chars);
260    r.insert(d.start, &d.old);
261    r
262}
263
264// ─── Undo arena tree (Phase 2b + Phase 3a delta storage) ──────────────────────
265//
266// The undo history is a real arena TREE of buffer states (Phase 2a introduced
267// the arena; Phase 2b makes it branch; Phase 3a stores edges as deltas). An edit
268// after an undo FORKS a new child instead of truncating the forward branch, so
269// old branches stay reachable — matching nvim's undo tree. `seq` is
270// load-bearing: `g-`/`g+` and the `:earlier`/`:later` count forms walk ALL
271// states by global `seq` (see `seq_earlier_step`/`seq_later_step`), while
272// `u`/`<C-r>` stay branch-local (parent / `last_child`).
273//
274// The linear-history subset is unchanged: with no forks the tree is a single
275// root→current→leaf path and every operation degrades to the old two-stack
276// behaviour.
277//
278// - `current` points at the node representing the LIVE buffer state.
279// - The ancestors of `current` (parent, … up to `root`) are the reachable undo
280//   line; `current.parent` is the `u` target.
281// - `current.last_child` is the `<C-r>` target. Landing on any node (undo,
282//   redo, or a `g-`/`g+` jump) rewrites `last_child` down the root→node path so
283//   a later `<C-r>` retraces the branch just taken.
284//
285// Storage (Phase 3a): each non-root node holds the reversible `delta` on its
286// edge from `parent`; the root holds a full `base` rope. A node's content is
287// reconstructed on demand (`materialize`) from the nearest cached ancestor (or
288// the root base) by replaying forward deltas, or — for the `u`/`<C-r>` hot path
289// — from the adjacent warm node by one delta apply. Recently materialized ropes
290// are kept in a bounded LRU (`warm`); `current` is always kept warm. A node's
291// `delta`/content is FINALIZED lazily on the way past it (whenever the live rope
292// is written into it), never read as a restore target until then — so the fresh
293// leaf `current` holds a placeholder edge that is corrected before it matters.
294//
295// Keyframes (issue #302): the warm LRU alone bounds nothing — a `g-` onto a node
296// far outside it replayed the WHOLE chain from the root, so one jump was O(depth)
297// and `:earlier 9999` was O(depth²) (measured 212 ms for a 1024-deep history).
298// Every node at a depth that is a multiple of `KEYFRAME_INTERVAL` therefore PINS
299// its materialized rope, capping any single replay at `KEYFRAME_INTERVAL - 1`
300// applies; and `materialize` caches every intermediate it replays, so the
301// step-by-step walk pays that replay once per interval rather than once per step.
302// Keyframes are a pure in-memory cache: they are recomputable from the root base
303// plus the deltas, so they are NOT part of the `SerTree` on-disk projection, and
304// dropping every one of them changes only speed, never content.
305
306/// Keyframe spacing, in nodes of depth. Every node whose depth from the root is
307/// a multiple of this pins its materialized rope, so `materialize` never replays
308/// more than `KEYFRAME_INTERVAL - 1` deltas from the nearest anchor.
309///
310/// **Why 16.** The cost of a keyframe is *not* a document copy. `ropey::Rope` is
311/// a persistent tree with `Arc`-shared leaves, so a snapshot taken between small
312/// edits shares every chunk outside the edited path with its neighbours and only
313/// retains the O(log N) interior nodes the edit rewrote. Measured marginal RSS of
314/// retaining one such snapshot (1024 small edits, keeping every 16th):
315///
316/// | document | bytes retained per keyframe |
317/// | --- | --- |
318/// | 119 KiB | ~3.0 KiB |
319/// | 11.9 MiB | ~7.2 KiB |
320/// | 11.9 MiB, 4 KiB edits | ~11.5 KiB |
321///
322/// i.e. essentially independent of document size — a 200 MB buffer does not pay
323/// 200 MB per keyframe. At one keyframe per 16 nodes that is well under a KiB of
324/// amortized overhead per undo state, next to the `Delta` (two `String`s of the
325/// changed span) every node already stores unconditionally. 16 also sits under
326/// [`WARM_CAP`], which is what lets a full walk stay linear (see there).
327///
328/// The one shape that would break the "cheap" argument — an edit that rewrites
329/// the entire document, so consecutive states share nothing — already costs two
330/// full-document `String`s in that node's own `Delta`, so the keyframe adds at
331/// most another 1/16 of a cost the tree was paying anyway.
332const KEYFRAME_INTERVAL: usize = 16;
333
334/// Hard ceiling on how many keyframes are pinned at once; beyond it the
335/// least-recently-touched keyframe is unpinned (it becomes an ordinary cold
336/// node, replayable as before — correctness is unaffected).
337///
338/// Deliberately a COUNT, not a byte budget: by the measurement on
339/// [`KEYFRAME_INTERVAL`] a keyframe's real retention is roughly document-size
340/// *independent*, so a byte budget computed from `len_bytes()` would be a wild
341/// over-estimate and would switch keyframes off precisely on the large documents
342/// that need them most. 512 keyframes covers 8192 undo states — past any sane
343/// `undolevels` — for a measured ceiling of a few MiB.
344const KEYFRAME_CAP: usize = 512;
345
346/// Index into [`UndoTree::nodes`]. Slots are reused via a free list, so an id is
347/// only valid while the node it names is live — the tree never hands ids out.
348pub type NodeId = usize;
349
350/// How many recently-materialized ORDINARY node ropes to keep warm (besides the
351/// root base, `current`, and the pinned keyframes, which are always available).
352///
353/// Kept above [`KEYFRAME_INTERVAL`] on purpose: `materialize` caches every
354/// intermediate it replays, so one keyframe interval's worth of intermediates has
355/// to survive here for a step-by-step history walk (`:earlier 9999`) to cost one
356/// replay per INTERVAL rather than one per step — the difference between an O(N)
357/// and an O(N·K) walk.
358const WARM_CAP: usize = 32;
359
360/// One node of the undo arena tree: a buffer state the user could land on, plus
361/// its links and the reversible edge to its parent. A node with `> 1` child is a
362/// branch point (Phase 2b); `last_child` records which child `<C-r>` follows.
363#[derive(Debug, Clone)]
364pub struct UndoNode {
365    pub parent: Option<NodeId>,
366    pub children: Vec<NodeId>,
367    pub last_child: Option<NodeId>,
368    /// Reversible edit from the parent's content to this node's content. `None`
369    /// only for the root (and any node promoted to root by pruning), which holds
370    /// `base` instead.
371    pub delta: Option<Delta>,
372    /// Full base rope. `Some` ONLY for the root — the anchor the delta chain
373    /// replays from. Non-root nodes leave this `None` and carry a `delta`.
374    pub base: Option<ropey::Rope>,
375    /// Materialized content, LRU-managed. Warm for `current`, recently visited
376    /// nodes, and keyframe-depth nodes (which are pinned rather than aged out);
377    /// `None` (cold) otherwise, reconstructable from deltas.
378    pub rope_cache: Option<ropey::Rope>,
379    /// Distance from the root, root == 0. Assigned once at creation and never
380    /// renumbered — root-side pruning shifts the whole numbering down uniformly,
381    /// which leaves keyframes exactly [`KEYFRAME_INTERVAL`] apart either way.
382    /// Purely a cache-placement input: a wrong depth costs speed, never content.
383    pub depth: usize,
384    /// Post-state cursor for this node (restored alongside the text).
385    pub cursor: (usize, usize),
386    /// Wall-clock time this state was created — drives `:earlier`/`:later`.
387    pub timestamp: SystemTime,
388    /// Marks / jumplist / changelist snapshot restored with the text.
389    ///
390    /// Shared (`Arc`) rather than owned: a `push` writes the SAME snapshot
391    /// into the node being left and into the fresh child, and a
392    /// `MarkSnapshot` is up to five collections. Nodes never mutate it in
393    /// place — it is only ever replaced wholesale — so sharing is invisible.
394    pub marks: Arc<MarkSnapshot>,
395    /// Global monotonic order across the whole tree — the change number that
396    /// `g-`/`g+`, `:earlier`/`:later`, and `:undolist` traverse and display.
397    pub seq: u64,
398}
399
400/// Arena tree of [`UndoNode`]s. Replaces the old `undo_stack`/`redo_stack`
401/// `Vec<UndoEntry>` pair on [`crate::Buffer`]; see the module comment for how
402/// `u`/`<C-r>` (branch-local) and `g-`/`g+` (seq-ordered) map onto it, and how
403/// Phase 3a stores edges as deltas behind a materialization cache.
404#[derive(Debug)]
405pub struct UndoTree {
406    /// Slab; `None` slots are free and recorded in `free`.
407    nodes: Vec<Option<UndoNode>>,
408    /// Reusable slot indices (frees push here, allocs pop here first).
409    free: Vec<NodeId>,
410    /// LRU of ORDINARY node ids with a warm `rope_cache` (root and keyframe-depth
411    /// nodes excluded — they live in `base` / `keyframes`), most-recently-touched
412    /// last. Bounded by [`WARM_CAP`]; `current` is never evicted.
413    warm: Vec<NodeId>,
414    /// Node ids at a keyframe depth whose `rope_cache` is PINNED — the replay
415    /// anchors that bound `materialize` at [`KEYFRAME_INTERVAL`] applies.
416    /// Most-recently-touched last, bounded by [`KEYFRAME_CAP`].
417    keyframes: Vec<NodeId>,
418    root: NodeId,
419    current: NodeId,
420    next_seq: u64,
421}
422
423/// Trim `list` (an LRU, oldest first) down to `cap`, dropping the evicted
424/// nodes' materialized ropes. `current` is never evicted — the live state must
425/// stay available without a replay.
426///
427/// A free function over the pieces rather than a method so it can hold `&mut`
428/// on one arena field and one list at the same time.
429fn evict_to(list: &mut Vec<NodeId>, nodes: &mut [Option<UndoNode>], current: NodeId, cap: usize) {
430    while list.len() > cap {
431        let Some(pos) = list.iter().position(|&n| n != current) else {
432            break;
433        };
434        let victim = list.remove(pos);
435        if let Some(node) = nodes[victim].as_mut() {
436            node.rope_cache = None;
437        }
438    }
439}
440
441impl UndoTree {
442    /// New tree with a single root == current node holding `rope` as its base
443    /// state (the buffer as opened / last saved). The root is always
444    /// materializable from this base.
445    pub(crate) fn new(rope: ropey::Rope) -> Self {
446        let root = UndoNode {
447            parent: None,
448            children: Vec::new(),
449            last_child: None,
450            delta: None,
451            base: Some(rope),
452            rope_cache: None,
453            depth: 0,
454            cursor: (0, 0),
455            timestamp: SystemTime::now(),
456            marks: Arc::default(),
457            seq: 0,
458        };
459        Self {
460            nodes: vec![Some(root)],
461            free: Vec::new(),
462            warm: Vec::new(),
463            keyframes: Vec::new(),
464            root: 0,
465            current: 0,
466            next_seq: 1,
467        }
468    }
469
470    // ── slab helpers ─────────────────────────────────────────────────────────
471
472    fn get(&self, id: NodeId) -> &UndoNode {
473        self.nodes[id].as_ref().expect("live NodeId")
474    }
475
476    fn get_mut(&mut self, id: NodeId) -> &mut UndoNode {
477        self.nodes[id].as_mut().expect("live NodeId")
478    }
479
480    fn alloc(&mut self, node: UndoNode) -> NodeId {
481        if let Some(id) = self.free.pop() {
482            self.nodes[id] = Some(node);
483            id
484        } else {
485            self.nodes.push(Some(node));
486            self.nodes.len() - 1
487        }
488    }
489
490    /// Free a single slot (does NOT recurse into children — callers detach
491    /// links first). Drops the node's delta + materialized cache and purges it
492    /// from both cache LRUs.
493    fn free(&mut self, id: NodeId) {
494        self.nodes[id] = None;
495        self.free.push(id);
496        self.warm.retain(|&n| n != id);
497        self.keyframes.retain(|&n| n != id);
498    }
499
500    // ── materialization (Phase 3a + keyframes) ───────────────────────────────
501
502    /// Is `id` at a keyframe depth, i.e. should its materialized rope be PINNED
503    /// as a replay anchor rather than aged out of the ordinary warm LRU?
504    ///
505    /// The root qualifies arithmetically (depth 0) but is excluded: it carries a
506    /// full `base` and is already an anchor.
507    fn is_keyframe(&self, id: NodeId) -> bool {
508        id != self.root && self.get(id).depth.is_multiple_of(KEYFRAME_INTERVAL)
509    }
510
511    /// Record `id` as freshly materialized. Keyframe-depth nodes go into the
512    /// pinned `keyframes` LRU (bounded by [`KEYFRAME_CAP`]), everything else into
513    /// the ordinary `warm` LRU (bounded by [`WARM_CAP`]). Neither ever evicts
514    /// `current`; the root is skipped entirely (it has no cache, it has `base`).
515    fn touch_warm(&mut self, id: NodeId) {
516        if id == self.root {
517            return;
518        }
519        let (list, cap) = if self.is_keyframe(id) {
520            (&mut self.keyframes, KEYFRAME_CAP)
521        } else {
522            (&mut self.warm, WARM_CAP)
523        };
524        list.retain(|&n| n != id);
525        list.push(id);
526        evict_to(list, &mut self.nodes, self.current, cap);
527    }
528
529    /// Materialize node `id`'s content, warming its cache. Uses the node's own
530    /// cache if present, else the root `base`, else replays forward deltas from
531    /// the nearest materialized ancestor — a warm node, a pinned keyframe, or the
532    /// root. Always terminates: the root carries a base.
533    ///
534    /// Every intermediate along the replay is cached too, not just the target:
535    /// they were computed anyway and a `ropey::Rope` clone is an `Arc` bump, so
536    /// caching them is free — and it is what makes a step-by-step history walk
537    /// (`g-` held down, `:earlier 9999`) pay ONE replay per keyframe interval
538    /// instead of one per step.
539    fn materialize(&mut self, id: NodeId) -> ropey::Rope {
540        if let Some(r) = &self.get(id).rope_cache {
541            return r.clone();
542        }
543        if let Some(base) = &self.get(id).base {
544            return base.clone();
545        }
546        // Walk up to the nearest ancestor that holds content (warm cache, pinned
547        // keyframe, or the root base), recording the path to replay forward.
548        // Bounded by the keyframe spacing whenever the ancestor chain has been
549        // materialized before.
550        let mut path = Vec::new();
551        let base_rope;
552        let mut anchor = id;
553        loop {
554            path.push(anchor);
555            let par = self
556                .get(anchor)
557                .parent
558                .expect("a non-root, non-based node always has a parent");
559            if let Some(r) = &self.get(par).rope_cache {
560                base_rope = r.clone();
561                break;
562            }
563            if let Some(b) = &self.get(par).base {
564                base_rope = b.clone();
565                break;
566            }
567            anchor = par;
568        }
569        let mut rope = base_rope;
570        // `path` is target-first, so replaying in reverse ends on `id` — which
571        // therefore lands last in its LRU and cannot be the eviction picked by
572        // its own `touch_warm`.
573        for &node in path.iter().rev() {
574            let d = self
575                .get(node)
576                .delta
577                .as_ref()
578                .expect("a non-root node always carries its edge delta");
579            rope = apply_forward(&rope, d);
580            self.get_mut(node).rope_cache = Some(rope.clone());
581            self.touch_warm(node);
582        }
583        rope
584    }
585
586    /// Reconstruct node `id`'s restorable [`UndoEntry`] — the byte-for-byte
587    /// equivalent of Phase 2b's `node.snapshot.clone()`.
588    fn entry_of(&mut self, id: NodeId) -> UndoEntry {
589        let rope = self.materialize(id);
590        let n = self.get(id);
591        UndoEntry {
592            rope,
593            cursor: n.cursor,
594            timestamp: n.timestamp,
595            marks: (*n.marks).clone(),
596        }
597    }
598
599    /// Finalize node `id` to hold `rope` as its content, recomputing its edge
600    /// delta (or the root base) and updating cursor/timestamp/marks. A no-op
601    /// diff is skipped when the content is unchanged (the common case on a
602    /// history walk, where only the fields move) — which also avoids
603    /// materializing the parent, keeping the walk cheap.
604    fn set_node_state(
605        &mut self,
606        id: NodeId,
607        rope: ropey::Rope,
608        cursor: (usize, usize),
609        timestamp: SystemTime,
610        marks: Arc<MarkSnapshot>,
611    ) {
612        let is_root = self.get(id).parent.is_none();
613        let unchanged = self.get(id).rope_cache.as_ref() == Some(&rope)
614            || (is_root && self.get(id).base.as_ref() == Some(&rope));
615        {
616            let node = self.get_mut(id);
617            node.cursor = cursor;
618            node.timestamp = timestamp;
619            node.marks = marks;
620        }
621        if unchanged {
622            return;
623        }
624        if is_root {
625            self.get_mut(id).base = Some(rope);
626            // The root is materialized from `base`; keep no stale cache.
627            self.get_mut(id).rope_cache = None;
628            self.warm.retain(|&n| n != id);
629            self.keyframes.retain(|&n| n != id);
630        } else {
631            let par = self.get(id).parent.expect("non-root has a parent");
632            let par_rope = self.materialize(par);
633            let d = diff(&par_rope, &rope);
634            let node = self.get_mut(id);
635            node.delta = Some(d);
636            node.rope_cache = Some(rope);
637            self.touch_warm(id);
638        }
639    }
640
641    /// Free `id` and its whole subtree (iteratively, so a long redo chain can't
642    /// overflow the stack).
643    fn free_subtree(&mut self, id: NodeId) {
644        let mut stack = vec![id];
645        while let Some(n) = stack.pop() {
646            let kids = std::mem::take(&mut self.get_mut(n).children);
647            stack.extend(kids);
648            self.free(n);
649        }
650    }
651
652    // ── read-only queries (mirror the old stack accessors) ───────────────────
653
654    /// `undo_stack.is_empty()` ⇔ `current` has no parent (is the root).
655    pub(crate) fn is_at_root(&self) -> bool {
656        self.get(self.current).parent.is_none()
657    }
658
659    /// `!redo_stack.is_empty()` ⇔ `current` has a forward child.
660    pub(crate) fn has_redo(&self) -> bool {
661        self.get(self.current).last_child.is_some()
662    }
663
664    /// `undo_stack.len()` == number of ancestors of `current` (depth from root).
665    pub(crate) fn depth(&self) -> usize {
666        let mut d = 0;
667        let mut n = self.get(self.current).parent;
668        while let Some(p) = n {
669            d += 1;
670            n = self.get(p).parent;
671        }
672        d
673    }
674
675    /// `undo_stack.last().timestamp` == `current.parent`'s timestamp.
676    pub(crate) fn parent_timestamp(&self) -> Option<SystemTime> {
677        self.get(self.current).parent.map(|p| self.get(p).timestamp)
678    }
679
680    /// `redo_stack.last().timestamp` == `current.last_child`'s timestamp.
681    pub(crate) fn child_timestamp(&self) -> Option<SystemTime> {
682        self.get(self.current)
683            .last_child
684            .map(|c| self.get(c).timestamp)
685    }
686
687    // ── mutations ────────────────────────────────────────────────────────────
688
689    /// Commit a new boundary from `current`, growing the tree (Phase 2b).
690    ///
691    /// `entry` is the pre-edit LIVE state. It is written into `current`'s
692    /// snapshot (making `current` a real, restorable state), then a fresh child
693    /// is APPENDED and becomes the new `current` for the edit about to happen.
694    ///
695    /// Unlike Phase 2a this does NOT drop `current`'s existing children: an edit
696    /// after an undo now forks a new branch and the old forward branch(es) stay
697    /// reachable via `g-`/`g+` and `:undolist`, matching nvim's undo tree. The
698    /// new child is made `last_child` so a subsequent `<C-r>` follows the branch
699    /// just created.
700    pub(crate) fn push(&mut self, entry: UndoEntry) {
701        let cur = self.current;
702        // ONE snapshot serves both the node being left and the fresh child —
703        // this runs per edit, and a deep `MarkSnapshot` copy is up to five
704        // collection allocations.
705        let marks = Arc::new(entry.marks);
706        // Finalize the node being left with the pre-edit live state, recomputing
707        // its edge delta from its parent (or the root base).
708        self.set_node_state(
709            cur,
710            entry.rope.clone(),
711            entry.cursor,
712            entry.timestamp,
713            Arc::clone(&marks),
714        );
715        let seq = self.next_seq;
716        self.next_seq += 1;
717        // Fresh child: identical to `cur` for now (empty edge delta + warm cache
718        // holding the pre-edit rope). Its true post-edit content is finalized on
719        // the way past it (next move) or by the next `push`, at which point the
720        // edge delta is recomputed against `cur`.
721        let child_depth = self.get(cur).depth + 1;
722        let child = self.alloc(UndoNode {
723            parent: Some(cur),
724            children: Vec::new(),
725            last_child: None,
726            delta: Some(Delta::default()),
727            base: None,
728            rope_cache: Some(entry.rope),
729            depth: child_depth,
730            cursor: entry.cursor,
731            timestamp: entry.timestamp,
732            marks,
733            seq,
734        });
735        let cur_node = self.get_mut(cur);
736        // Append (retain old branches); the freshest child is the redo target.
737        cur_node.children.push(child);
738        cur_node.last_child = Some(child);
739        self.current = child;
740        self.touch_warm(child);
741    }
742
743    /// One undo step. `live` is the current buffer state (the node being left);
744    /// it is written into that node but INHERITS the destination (parent)
745    /// timestamp — byte-parity with the old dance, where the pushed redo entry
746    /// took the popped undo entry's timestamp. Returns the parent snapshot to
747    /// restore, or `None` at the root.
748    pub(crate) fn undo_step(
749        &mut self,
750        rope: ropey::Rope,
751        cursor: (usize, usize),
752        marks: MarkSnapshot,
753    ) -> Option<UndoEntry> {
754        let cur = self.current;
755        let par = self.get(cur).parent?;
756        let dest_ts = self.get(par).timestamp;
757        self.set_node_state(cur, rope, cursor, dest_ts, Arc::new(marks));
758        // Redo from the parent must return to the node we just left.
759        self.get_mut(par).last_child = Some(cur);
760        self.current = par;
761        // Hot-path materialization: derive the (possibly cold) parent from the
762        // just-finalized child by one inverse delta apply, so `u` never walks the
763        // ancestor chain even far outside the warm window.
764        if self.get(par).rope_cache.is_none() && self.get(par).base.is_none() {
765            let child_rope = self.get(cur).rope_cache.clone();
766            let child_delta = self.get(cur).delta.clone();
767            if let (Some(cr), Some(d)) = (child_rope, child_delta) {
768                let par_rope = apply_inverse(&cr, &d);
769                self.get_mut(par).rope_cache = Some(par_rope);
770                self.touch_warm(par);
771            }
772        }
773        Some(self.entry_of(par))
774    }
775
776    /// One redo step. Symmetric to [`Self::undo_step`]: `live` is written into
777    /// the node being left (which becomes an undo ancestor) with the
778    /// destination (child) timestamp. Returns the child snapshot to restore, or
779    /// `None` when there is no forward branch.
780    pub(crate) fn redo_step(
781        &mut self,
782        rope: ropey::Rope,
783        cursor: (usize, usize),
784        marks: MarkSnapshot,
785    ) -> Option<UndoEntry> {
786        let cur = self.current;
787        let child = self.get(cur).last_child?;
788        let dest_ts = self.get(child).timestamp;
789        self.set_node_state(cur, rope, cursor, dest_ts, Arc::new(marks));
790        self.current = child;
791        // `cur` is now warm, so materializing the child is one forward apply.
792        Some(self.entry_of(child))
793    }
794
795    // ── seq-ordered tree walk (`g-` / `g+`, `:earlier`/`:later` — Phase 2b) ───
796    //
797    // `u`/`<C-r>` are branch-local (parent / `last_child`); `g-`/`g+` traverse
798    // ALL states by global `seq`, crossing branch boundaries. `g-` restores the
799    // node with the greatest `seq` strictly below `current`'s; `g+` the least
800    // `seq` strictly above. Confirmed against nvim v0.12.4 (`iA<Esc>uiB<Esc>`
801    // then `g-`/`g-g-`/`g-g+` walks empty↔A↔B by change number).
802
803    /// `seq` of the node the buffer currently shows.
804    fn current_seq(&self) -> u64 {
805        self.get(self.current).seq
806    }
807
808    /// Live node with the greatest `seq` strictly below `s` (the `g-` target).
809    fn node_below(&self, s: u64) -> Option<NodeId> {
810        let mut best: Option<(u64, NodeId)> = None;
811        for (id, slot) in self.nodes.iter().enumerate() {
812            if let Some(n) = slot
813                && n.seq < s
814                && best.is_none_or(|(bs, _)| n.seq > bs)
815            {
816                best = Some((n.seq, id));
817            }
818        }
819        best.map(|(_, id)| id)
820    }
821
822    /// Live node with the least `seq` strictly above `s` (the `g+` target).
823    fn node_above(&self, s: u64) -> Option<NodeId> {
824        let mut best: Option<(u64, NodeId)> = None;
825        for (id, slot) in self.nodes.iter().enumerate() {
826            if let Some(n) = slot
827                && n.seq > s
828                && best.is_none_or(|(bs, _)| n.seq < bs)
829            {
830                best = Some((n.seq, id));
831            }
832        }
833        best.map(|(_, id)| id)
834    }
835
836    /// Point `current` at `target` and rewrite `last_child` down the whole
837    /// root→target path, so a later `<C-r>` retraces the branch just landed on
838    /// (nvim parity: landing on a node updates its ancestors' redo direction).
839    fn retarget_current(&mut self, target: NodeId) {
840        self.current = target;
841        let mut node = target;
842        while let Some(p) = self.get(node).parent {
843            self.get_mut(p).last_child = Some(node);
844            node = p;
845        }
846    }
847
848    /// Stash the live buffer state into the node being left (it may be a fresh,
849    /// still-stale leaf), preserving that node's own timestamp, then move.
850    fn stash_and_move(
851        &mut self,
852        target: NodeId,
853        rope: ropey::Rope,
854        cursor: (usize, usize),
855        marks: MarkSnapshot,
856    ) {
857        let cur = self.current;
858        let ts = self.get(cur).timestamp;
859        self.set_node_state(cur, rope, cursor, ts, Arc::new(marks));
860        self.retarget_current(target);
861    }
862
863    /// One `g-` / `:earlier` step: move to the next-lower-`seq` node tree-wide.
864    /// Returns its snapshot to restore, or `None` at the lowest state.
865    pub(crate) fn seq_earlier_step(
866        &mut self,
867        rope: ropey::Rope,
868        cursor: (usize, usize),
869        marks: MarkSnapshot,
870    ) -> Option<UndoEntry> {
871        let target = self.node_below(self.current_seq())?;
872        self.stash_and_move(target, rope, cursor, marks);
873        Some(self.entry_of(target))
874    }
875
876    /// One `g+` / `:later` step: move to the next-higher-`seq` node tree-wide.
877    /// Returns its snapshot to restore, or `None` at the highest state.
878    pub(crate) fn seq_later_step(
879        &mut self,
880        rope: ropey::Rope,
881        cursor: (usize, usize),
882        marks: MarkSnapshot,
883    ) -> Option<UndoEntry> {
884        let target = self.node_above(self.current_seq())?;
885        self.stash_and_move(target, rope, cursor, marks);
886        Some(self.entry_of(target))
887    }
888
889    /// Timestamp of the next-lower-`seq` node (the `:earlier Ns` predicate walks
890    /// the seq order tree-wide, stopping once this dips to/below the cutoff).
891    pub(crate) fn seq_earlier_timestamp(&self) -> Option<SystemTime> {
892        self.node_below(self.current_seq())
893            .map(|id| self.get(id).timestamp)
894    }
895
896    /// Timestamp of the next-higher-`seq` node (the `:later Ns` predicate).
897    pub(crate) fn seq_later_timestamp(&self) -> Option<SystemTime> {
898        self.node_above(self.current_seq())
899            .map(|id| self.get(id).timestamp)
900    }
901
902    /// Leaves of the tree (nodes with no children), each as
903    /// `(seq, depth-from-root, timestamp, is_current)`, sorted by `seq`.
904    /// Drives `:undolist`, which — like nvim — lists only branch leaves.
905    pub(crate) fn leaves(&self) -> Vec<(u64, usize, SystemTime, bool)> {
906        let mut out: Vec<(u64, usize, SystemTime, bool)> = Vec::new();
907        for (id, slot) in self.nodes.iter().enumerate() {
908            let Some(n) = slot else { continue };
909            // The root is the base state (change number 0), never a listed
910            // "change" — like nvim, an untouched buffer lists nothing.
911            if id == self.root || !n.children.is_empty() {
912                continue;
913            }
914            // Depth = number of ancestors (root leaf ⇒ 0).
915            let mut depth = 0;
916            let mut p = n.parent;
917            while let Some(pid) = p {
918                depth += 1;
919                p = self.get(pid).parent;
920            }
921            out.push((n.seq, depth, n.timestamp, id == self.current));
922        }
923        out.sort_by_key(|&(seq, ..)| seq);
924        out
925    }
926
927    /// Number of live nodes (used by [`Self::cap`] as the state budget).
928    fn live_count(&self) -> usize {
929        self.nodes.iter().filter(|n| n.is_some()).count()
930    }
931
932    /// `undo_stack.pop()` — discard the most-recent boundary WITHOUT moving the
933    /// live state. Used by `:s` with zero replacements and by a no-op undo
934    /// group; in both, `current` is the childless leaf the last [`Self::push`]
935    /// created, so reverse that push: drop the leaf, step `current` back to its
936    /// parent (its snapshot equals the unchanged buffer), and restore the
937    /// parent's `last_child`. Retains any sibling branches the push appended to.
938    /// Returns `false` at the root, or if `current` is not a childless leaf
939    /// (nothing safe to pop).
940    pub(crate) fn pop_committed(&mut self) -> bool {
941        let cur = self.current;
942        if !self.get(cur).children.is_empty() {
943            return false;
944        }
945        let Some(par) = self.get(cur).parent else {
946            return false;
947        };
948        let par_node = self.get_mut(par);
949        par_node.children.retain(|&c| c != cur);
950        // The freshest surviving sibling (if any) becomes the redo target again.
951        par_node.last_child = par_node.children.last().copied();
952        self.current = par;
953        // The popped leaf always holds the highest seq (push assigns it last),
954        // so reclaim the seq to keep numbering gapless.
955        if self.get(cur).seq + 1 == self.next_seq {
956            self.next_seq -= 1;
957        }
958        self.free(cur);
959        true
960    }
961
962    /// Node budget (`undolevels`). While the number of undo states (live nodes
963    /// minus the root) exceeds `cap`, prune — branch-aware (Phase 2b):
964    ///
965    /// 1. First drop the lowest-`seq` LEAF that is NOT on the root→`current`
966    ///    path — an abandoned branch tip. This never touches `current` or its
967    ///    ancestors, so the state you're on and its full undo line survive.
968    /// 2. When only the main line remains (no off-path leaves left), fall back
969    ///    to promoting the root's on-path child to root and dropping the old
970    ///    root — the Phase 2a root-side prune, which matches nvim's linear
971    ///    `undolevels` trimming (oldest states drop first).
972    ///
973    /// `cap == 0` means unlimited (matches the old guard).
974    pub(crate) fn cap(&mut self, cap: usize) {
975        if cap == 0 {
976            return;
977        }
978        // Guard against a pathological loop: at most one prune per live node.
979        let mut budget_iters = self.live_count() + 1;
980        while self.live_count().saturating_sub(1) > cap && budget_iters > 0 {
981            budget_iters -= 1;
982            if let Some(leaf) = self.lowest_offpath_leaf() {
983                self.detach_leaf(leaf);
984            } else if !self.prune_root_side() {
985                break;
986            }
987        }
988    }
989
990    /// Ids on the root→`current` path (inclusive), which pruning must never
991    /// touch. Small (one per undo level), so a `Vec` membership check is fine.
992    fn current_path(&self) -> Vec<NodeId> {
993        let mut path = Vec::new();
994        let mut n = Some(self.current);
995        while let Some(id) = n {
996            path.push(id);
997            n = self.get(id).parent;
998        }
999        path
1000    }
1001
1002    /// Lowest-`seq` leaf that is not on the root→`current` path, if any.
1003    fn lowest_offpath_leaf(&self) -> Option<NodeId> {
1004        let path = self.current_path();
1005        let mut best: Option<(u64, NodeId)> = None;
1006        for (id, slot) in self.nodes.iter().enumerate() {
1007            if let Some(n) = slot
1008                && n.children.is_empty()
1009                && !path.contains(&id)
1010                && best.is_none_or(|(bs, _)| n.seq < bs)
1011            {
1012                best = Some((n.seq, id));
1013            }
1014        }
1015        best.map(|(_, id)| id)
1016    }
1017
1018    /// Unlink `leaf` from its parent and free it (leaf ⇒ no subtree to recurse).
1019    fn detach_leaf(&mut self, leaf: NodeId) {
1020        if let Some(par) = self.get(leaf).parent {
1021            let par_node = self.get_mut(par);
1022            par_node.children.retain(|&c| c != leaf);
1023            if par_node.last_child == Some(leaf) {
1024                par_node.last_child = par_node.children.last().copied();
1025            }
1026        }
1027        self.free(leaf);
1028    }
1029
1030    /// Promote the root's on-path child to the new root and free the old root.
1031    /// Returns `false` when the root is `current` (nothing left to trim).
1032    fn prune_root_side(&mut self) -> bool {
1033        let root = self.root;
1034        if root == self.current {
1035            return false;
1036        }
1037        // The child on the path to `current` (the root always has one here).
1038        let path = self.current_path();
1039        let Some(&child) = self.get(root).children.iter().find(|c| path.contains(c)) else {
1040            return false;
1041        };
1042        // Any OTHER root children are off-path branches; drop them with the root.
1043        let others: Vec<NodeId> = self
1044            .get(root)
1045            .children
1046            .iter()
1047            .copied()
1048            .filter(|&c| c != child)
1049            .collect();
1050        for c in others {
1051            self.free_subtree(c);
1052        }
1053        // The promoted child becomes the new root: materialize it (while the old
1054        // root still anchors the chain) into a full base rope, then drop its
1055        // now-meaningless parent edge. This keeps every delta below it valid.
1056        let base = self.materialize(child);
1057        {
1058            let node = self.get_mut(child);
1059            node.parent = None;
1060            node.base = Some(base);
1061            node.delta = None;
1062            node.rope_cache = None;
1063        }
1064        self.warm.retain(|&n| n != child);
1065        self.keyframes.retain(|&n| n != child);
1066        self.root = child;
1067        self.free(root);
1068        true
1069    }
1070
1071    /// `redo_stack.clear()` — drop `current`'s forward branch.
1072    pub(crate) fn clear_redo(&mut self) {
1073        let cur = self.current;
1074        let kids = std::mem::take(&mut self.get_mut(cur).children);
1075        self.get_mut(cur).last_child = None;
1076        for c in kids {
1077            self.free_subtree(c);
1078        }
1079    }
1080
1081    /// `undo_stack.clear(); redo_stack.clear()` — collapse to a single root ==
1082    /// current node, preserving the live state. Frees every other node.
1083    pub(crate) fn clear_all(&mut self) {
1084        let cur = self.current;
1085        // The survivor becomes a self-contained root: give it a full base rope
1086        // (materialized while the chain is still intact) so it needs no parent.
1087        let base = self.materialize(cur);
1088        for id in 0..self.nodes.len() {
1089            if id != cur && self.nodes[id].is_some() {
1090                self.nodes[id] = None;
1091                self.free.push(id);
1092            }
1093        }
1094        self.warm.clear();
1095        self.keyframes.clear();
1096        let node = self.get_mut(cur);
1097        node.parent = None;
1098        node.children.clear();
1099        node.last_child = None;
1100        node.delta = None;
1101        node.base = Some(base);
1102        node.rope_cache = None;
1103        // The survivor is the new root: restart the depth numbering under it so
1104        // its descendants land on the keyframe ladder from 0 again.
1105        node.depth = 0;
1106        self.root = cur;
1107    }
1108}
1109
1110// ─── Serializable projection (Phase 3b) ───────────────────────────────────────
1111//
1112// The undofile persists the tree as a compact, self-consistent projection: the
1113// root's full base text (String) plus, per node, its edge `delta` and links.
1114// `rope_cache`/`warm` are runtime-only and dropped — every node reconstructs
1115// from the root base + deltas, so the round-trip reproduces identical content
1116// at every node. NodeIds are DENSE in the projection (the live-slab holes are
1117// compacted away and links remapped), so `from_serializable` rebuilds a fresh
1118// arena 1:1 with no free list.
1119
1120/// One node of the serialized undo tree. Mirrors [`UndoNode`] minus the
1121/// runtime-only materialization cache; ids are dense indices into
1122/// [`SerTree::nodes`].
1123#[derive(Debug, Clone, Serialize, Deserialize)]
1124pub struct SerNode {
1125    /// Parent index, `None` only for the root.
1126    pub parent: Option<u32>,
1127    /// Child indices (order preserved; `> 1` ⇒ branch point).
1128    pub children: Vec<u32>,
1129    /// `<C-r>` target child index.
1130    pub last_child: Option<u32>,
1131    /// Reversible edge delta from the parent, `None` only for the root.
1132    pub delta: Option<Delta>,
1133    /// Post-state cursor `(row, col)`.
1134    pub cursor: (u32, u32),
1135    /// Wall-clock creation time, ms since the UNIX epoch.
1136    pub timestamp_unix_ms: u64,
1137    /// Marks / jumplist / changelist snapshot.
1138    pub marks: MarkSnapshot,
1139    /// Global monotonic change number.
1140    pub seq: u64,
1141}
1142
1143/// Serializable projection of an [`UndoTree`] for the undofile. Postcard-encoded
1144/// (non-self-describing, so a schema/version drift surfaces as a parse `Err`
1145/// that the reader discards). See [`UndoTree::to_serializable`] /
1146/// [`UndoTree::from_serializable`].
1147#[derive(Debug, Clone, Serialize, Deserialize)]
1148pub struct SerTree {
1149    /// Root base text (the anchor the delta chain replays from).
1150    pub base: String,
1151    /// Dense node arena (no holes).
1152    pub nodes: Vec<SerNode>,
1153    /// Root index into `nodes`.
1154    pub root: u32,
1155    /// Current (live) index into `nodes`.
1156    pub current: u32,
1157    /// Next `seq` to assign.
1158    pub next_seq: u64,
1159}
1160
1161/// [`SystemTime`] → ms since the UNIX epoch (saturating, pre-epoch ⇒ 0).
1162fn system_time_to_unix_ms(t: SystemTime) -> u64 {
1163    t.duration_since(UNIX_EPOCH)
1164        .map_or(0, |d| d.as_millis() as u64)
1165}
1166
1167/// ms since the UNIX epoch → [`SystemTime`].
1168fn unix_ms_to_system_time(ms: u64) -> SystemTime {
1169    UNIX_EPOCH + Duration::from_millis(ms)
1170}
1171
1172impl UndoTree {
1173    /// `seq` of the current (live) node — the header's `current_seq` for the
1174    /// undofile (the just-saved content per the §6 invariant).
1175    pub(crate) fn current_node_seq(&self) -> u64 {
1176        self.get(self.current).seq
1177    }
1178
1179    /// Materialize the current (live) node's content. Used by the swap
1180    /// recovery consistency guard (docs §6c) to check a deserialized tree
1181    /// agrees with the freshly-recovered buffer text before it's installed.
1182    pub(crate) fn current_content(&mut self) -> ropey::Rope {
1183        let cur = self.current;
1184        self.materialize(cur)
1185    }
1186
1187    /// Stash `rope` into the current node as the live buffer state, preserving
1188    /// that node's own cursor/timestamp/marks. Called just before serializing so
1189    /// the on-disk tree's `current` edge is exact even when `current` is a fresh
1190    /// (still-stale) leaf — the in-session self-heal (first undo/edit stashes
1191    /// live) applied eagerly at save time.
1192    pub(crate) fn sync_current(&mut self, rope: ropey::Rope) {
1193        let cur = self.current;
1194        let (cursor, ts, marks) = {
1195            let n = self.get(cur);
1196            (n.cursor, n.timestamp, n.marks.clone())
1197        };
1198        self.set_node_state(cur, rope, cursor, ts, marks);
1199    }
1200
1201    /// Project the live tree into a serializable, dense form (holes compacted,
1202    /// links remapped). `rope_cache`/`warm` are dropped; the root's `base`
1203    /// carries the anchor text and every non-root node its edge `delta`.
1204    pub(crate) fn to_serializable(&self) -> SerTree {
1205        // Dense remap: old NodeId → new index, in slab order.
1206        let mut map: Vec<Option<u32>> = vec![None; self.nodes.len()];
1207        let mut order: Vec<NodeId> = Vec::new();
1208        for (id, slot) in self.nodes.iter().enumerate() {
1209            if slot.is_some() {
1210                map[id] = Some(order.len() as u32);
1211                order.push(id);
1212            }
1213        }
1214        let remap = |id: NodeId| map[id].expect("live link points at a live node");
1215        let nodes = order
1216            .iter()
1217            .map(|&id| {
1218                let n = self.get(id);
1219                SerNode {
1220                    parent: n.parent.map(remap),
1221                    children: n.children.iter().map(|&c| remap(c)).collect(),
1222                    last_child: n.last_child.map(remap),
1223                    delta: n.delta.clone(),
1224                    cursor: (n.cursor.0 as u32, n.cursor.1 as u32),
1225                    timestamp_unix_ms: system_time_to_unix_ms(n.timestamp),
1226                    marks: (*n.marks).clone(),
1227                    seq: n.seq,
1228                }
1229            })
1230            .collect();
1231        let base = self
1232            .get(self.root)
1233            .base
1234            .as_ref()
1235            .map(|r| r.to_string())
1236            .unwrap_or_default();
1237        SerTree {
1238            base,
1239            nodes,
1240            root: remap(self.root),
1241            current: remap(self.current),
1242            next_seq: self.next_seq,
1243        }
1244    }
1245
1246    /// Rebuild an arena tree from a projection. Returns `None` on any structural
1247    /// inconsistency (out-of-range link, a non-root node missing its delta, a
1248    /// root carrying one) so a corrupt-but-parseable file degrades to a fresh
1249    /// tree rather than a broken one. The root's content comes from `base`; the
1250    /// current node's is materialized on demand from base + deltas.
1251    pub(crate) fn from_serializable(s: &SerTree) -> Option<Self> {
1252        let len = s.nodes.len();
1253        if len == 0 || s.root as usize >= len || s.current as usize >= len {
1254            return None;
1255        }
1256        // Validate links and the root/non-root delta discipline up front.
1257        for (i, n) in s.nodes.iter().enumerate() {
1258            let is_root = i as u32 == s.root;
1259            match (is_root, &n.delta, &n.parent) {
1260                (true, None, None) => {}
1261                (false, Some(_), Some(_)) => {}
1262                _ => return None,
1263            }
1264            if let Some(p) = n.parent
1265                && p as usize >= len
1266            {
1267                return None;
1268            }
1269            if n.children.iter().any(|&c| c as usize >= len) {
1270                return None;
1271            }
1272            if let Some(c) = n.last_child
1273                && c as usize >= len
1274            {
1275                return None;
1276            }
1277        }
1278        let base = ropey::Rope::from_str(&s.base);
1279        let depths = depths_from_root(s);
1280        let nodes: Vec<Option<UndoNode>> = s
1281            .nodes
1282            .iter()
1283            .enumerate()
1284            .map(|(i, n)| {
1285                let is_root = i as u32 == s.root;
1286                Some(UndoNode {
1287                    parent: n.parent.map(|p| p as NodeId),
1288                    children: n.children.iter().map(|&c| c as NodeId).collect(),
1289                    last_child: n.last_child.map(|c| c as NodeId),
1290                    delta: n.delta.clone(),
1291                    base: if is_root { Some(base.clone()) } else { None },
1292                    rope_cache: None,
1293                    depth: depths[i],
1294                    cursor: (n.cursor.0 as usize, n.cursor.1 as usize),
1295                    timestamp: unix_ms_to_system_time(n.timestamp_unix_ms),
1296                    marks: Arc::new(n.marks.clone()),
1297                    seq: n.seq,
1298                })
1299            })
1300            .collect();
1301        Some(Self {
1302            nodes,
1303            free: Vec::new(),
1304            warm: Vec::new(),
1305            keyframes: Vec::new(),
1306            root: s.root as NodeId,
1307            current: s.current as NodeId,
1308            next_seq: s.next_seq,
1309        })
1310    }
1311}
1312
1313/// Depth-from-root of every node in a projection, by BFS over `children`.
1314///
1315/// Depth is NOT part of the on-disk format — it is derivable, and the undofile
1316/// deliberately stores only what is not (issue #302: keyframes are an in-memory
1317/// cache, so nothing about them enters `SerTree`). The `seen` guard makes this
1318/// terminate on a malformed file whose links form a cycle; anything unreachable
1319/// from the root keeps depth 0, which at worst places a keyframe oddly.
1320fn depths_from_root(s: &SerTree) -> Vec<usize> {
1321    let mut depths = vec![0usize; s.nodes.len()];
1322    let mut seen = vec![false; s.nodes.len()];
1323    let mut queue = std::collections::VecDeque::new();
1324    seen[s.root as usize] = true;
1325    queue.push_back(s.root as usize);
1326    while let Some(i) = queue.pop_front() {
1327        for &c in &s.nodes[i].children {
1328            let c = c as usize;
1329            if !seen[c] {
1330                seen[c] = true;
1331                depths[c] = depths[i] + 1;
1332                queue.push_back(c);
1333            }
1334        }
1335    }
1336    depths
1337}
1338
1339#[cfg(test)]
1340impl UndoTree {
1341    /// Ids of every live node, for warm-vs-cold materialization checks.
1342    fn live_ids(&self) -> Vec<NodeId> {
1343        (0..self.nodes.len())
1344            .filter(|&i| self.nodes[i].is_some())
1345            .collect()
1346    }
1347
1348    /// Materialize `id` for a test (public wrapper over the private method).
1349    fn materialize_for_test(&mut self, id: NodeId) -> ropey::Rope {
1350        self.materialize(id)
1351    }
1352
1353    /// Evict every cache INCLUDING the pinned keyframes (root keeps its `base`),
1354    /// forcing the next materialization of any node to reconstruct purely from
1355    /// deltas off the root — the strongest cold path there is.
1356    fn drop_all_caches(&mut self) {
1357        for n in self.nodes.iter_mut().flatten() {
1358            n.rope_cache = None;
1359        }
1360        self.warm.clear();
1361        self.keyframes.clear();
1362    }
1363
1364    /// Evict only the ordinary warm LRU, leaving the pinned keyframes — the
1365    /// steady state a deep history walk actually runs in.
1366    fn drop_warm_caches(&mut self) {
1367        for id in std::mem::take(&mut self.warm) {
1368            if let Some(n) = self.nodes[id].as_mut() {
1369                n.rope_cache = None;
1370            }
1371        }
1372    }
1373
1374    /// How many forward delta applies `materialize(id)` would perform right now
1375    /// (0 when `id` already holds content). This is the cost keyframes exist to
1376    /// bound, made assertable.
1377    fn replay_distance(&self, id: NodeId) -> usize {
1378        let mut n = 0;
1379        let mut cur = id;
1380        loop {
1381            let node = self.get(cur);
1382            if node.rope_cache.is_some() || node.base.is_some() {
1383                return n;
1384            }
1385            n += 1;
1386            match node.parent {
1387                Some(p) => cur = p,
1388                None => return n,
1389            }
1390        }
1391    }
1392
1393    /// Reconstruct `id`'s content the naive way: walk to the root and replay
1394    /// every forward delta off the root `base`, consulting NO cache and NO
1395    /// keyframe. The differential oracle for keyframe-accelerated
1396    /// [`Self::materialize`] — the two must agree exactly, always.
1397    fn materialize_naive(&self, id: NodeId) -> ropey::Rope {
1398        let mut path = vec![id];
1399        let mut cur = id;
1400        while let Some(p) = self.get(cur).parent {
1401            path.push(p);
1402            cur = p;
1403        }
1404        let mut rope = self
1405            .get(cur)
1406            .base
1407            .clone()
1408            .expect("the root always carries a base");
1409        // Skip the root itself (it has no edge delta); replay root-ward → target.
1410        for &node in path.iter().rev().skip(1) {
1411            let d = self
1412                .get(node)
1413                .delta
1414                .as_ref()
1415                .expect("a non-root node always carries its edge delta");
1416            rope = apply_forward(&rope, d);
1417        }
1418        rope
1419    }
1420}
1421
1422#[cfg(test)]
1423mod tree_tests {
1424    use super::*;
1425
1426    fn entry(text: &str) -> UndoEntry {
1427        UndoEntry {
1428            rope: ropey::Rope::from_str(text),
1429            cursor: (0, 0),
1430            timestamp: SystemTime::now(),
1431            marks: MarkSnapshot::default(),
1432        }
1433    }
1434
1435    fn live(text: &str) -> (ropey::Rope, (usize, usize), MarkSnapshot) {
1436        (ropey::Rope::from_str(text), (0, 0), MarkSnapshot::default())
1437    }
1438
1439    #[test]
1440    fn fresh_tree_is_root_current_empty() {
1441        let t = UndoTree::new(ropey::Rope::from_str("hello"));
1442        assert!(t.is_at_root());
1443        assert!(!t.has_redo());
1444        assert_eq!(t.depth(), 0);
1445        assert_eq!(t.root, t.current);
1446    }
1447
1448    #[test]
1449    fn push_links_child_and_advances_current() {
1450        let mut t = UndoTree::new(ropey::Rope::from_str("hello"));
1451        let root = t.current;
1452        t.push(entry("hello"));
1453        // root now parents current; current is a fresh leaf.
1454        assert_eq!(t.get(t.current).parent, Some(root));
1455        assert_eq!(t.get(root).last_child, Some(t.current));
1456        assert_eq!(t.get(root).children, vec![t.current]);
1457        assert_eq!(t.depth(), 1);
1458        assert!(!t.has_redo());
1459        assert!(!t.is_at_root());
1460    }
1461
1462    #[test]
1463    fn undo_then_redo_round_trips_links() {
1464        let mut t = UndoTree::new(ropey::Rope::from_str("s0"));
1465        t.push(entry("s0")); // commit s0, current = n1 (live s1)
1466        let n0 = t.root;
1467        let n1 = t.current;
1468        // undo: current -> n0, restores s0.
1469        let (r, c, m) = live("s1");
1470        let restored = t.undo_step(r, c, m).unwrap();
1471        assert_eq!(restored.rope.to_string(), "s0");
1472        assert_eq!(t.current, n0);
1473        assert!(t.has_redo());
1474        assert_eq!(t.get(n0).last_child, Some(n1));
1475        // redo: current -> n1, restores what we left (s1).
1476        let (r, c, m) = live("s0");
1477        let restored = t.redo_step(r, c, m).unwrap();
1478        assert_eq!(restored.rope.to_string(), "s1");
1479        assert_eq!(t.current, n1);
1480        assert!(!t.has_redo());
1481    }
1482
1483    #[test]
1484    fn undo_at_root_and_redo_at_leaf_are_noops() {
1485        let mut t = UndoTree::new(ropey::Rope::from_str("x"));
1486        let (r, c, m) = live("x");
1487        assert!(t.undo_step(r, c, m).is_none());
1488        let (r, c, m) = live("x");
1489        assert!(t.redo_step(r, c, m).is_none());
1490        assert_eq!(t.depth(), 0);
1491    }
1492
1493    #[test]
1494    fn push_retains_forward_branch() {
1495        // Phase 2b: an edit after an undo forks a new branch; the old forward
1496        // branch is NOT dropped and remains reachable by seq.
1497        let mut t = UndoTree::new(ropey::Rope::from_str("s0"));
1498        t.push(entry("A")); // root -> nA (seq1, "A")
1499        let root = t.root;
1500        let na = t.current;
1501        let (r, c, m) = live("A");
1502        t.undo_step(r, c, m); // back to root, nA is the redo child
1503        assert!(t.has_redo());
1504        // A new edit from the root forks a SECOND child (nB, seq2).
1505        t.push(entry("B"));
1506        let nb = t.current;
1507        assert_ne!(nb, na);
1508        // Both branches live: root now has two children.
1509        assert_eq!(t.get(root).children.len(), 2);
1510        assert!(t.get(root).children.contains(&na));
1511        assert!(t.get(root).children.contains(&nb));
1512        // `<C-r>` follows the freshest branch (nB).
1513        assert_eq!(t.get(root).last_child, Some(nb));
1514        // Four live nodes: root + nA + nB + (nB is current/leaf). No leak of nA.
1515        let live = t.nodes.iter().filter(|n| n.is_some()).count();
1516        assert_eq!(live, 3);
1517    }
1518
1519    #[test]
1520    fn seq_walk_crosses_branches() {
1521        // Mirror nvim `iA<Esc>uiB<Esc>` then g-/g+ (buffer starts empty "").
1522        // `push(entry)` writes `entry` into the node being LEFT (its true
1523        // pre-edit content); the fresh leaf holds the live post-edit state only
1524        // once it is stashed on the way past — exactly the engine's discipline.
1525        let mut t = UndoTree::new(ropey::Rope::from_str(""));
1526        t.push(entry("")); // leave root("") -> nA(seq1), live "A"
1527        let (r, c, m) = live("A");
1528        t.undo_step(r, c, m); // stash "A" into nA, back to root("")
1529        t.push(entry("")); // leave root("") -> nB(seq2), branch, live "B"
1530        let nb = t.current;
1531        // At B (seq2). g- -> greatest seq below 2 = seq1 = "A".
1532        let (r, c, m) = live("B");
1533        let a = t.seq_earlier_step(r, c, m).unwrap();
1534        assert_eq!(a.rope.to_string(), "A");
1535        // g- again -> root "".
1536        let (r, c, m) = live("A");
1537        let root_snap = t.seq_earlier_step(r, c, m).unwrap();
1538        assert_eq!(root_snap.rope.to_string(), "");
1539        // g+ -> back up to seq1 "A".
1540        let (r, c, m) = live("");
1541        let a2 = t.seq_later_step(r, c, m).unwrap();
1542        assert_eq!(a2.rope.to_string(), "A");
1543        // g+ -> seq2 "B" (crosses to the other branch).
1544        let (r, c, m) = live("A");
1545        let b = t.seq_later_step(r, c, m).unwrap();
1546        assert_eq!(b.rope.to_string(), "B");
1547        assert_eq!(t.current, nb);
1548        // At the tip: no higher seq.
1549        let (r, c, m) = live("B");
1550        assert!(t.seq_later_step(r, c, m).is_none());
1551    }
1552
1553    #[test]
1554    fn seq_walk_updates_retrace_path() {
1555        // Land on a deep leaf via g-, then u/u and <C-r>/<C-r> must retrace it
1556        // (nvim `iX<Esc>iY<Esc>uiZ<Esc>g-uu<C-r><C-r>`). State labels: root "R".
1557        let mut t = UndoTree::new(ropey::Rope::from_str("R"));
1558        t.push(entry("R")); // leave root("R") -> nX(seq1), live "X"
1559        t.push(entry("X")); // leave nX("X") -> nY(seq2), live "Y"
1560        let (r, c, m) = live("Y");
1561        t.undo_step(r, c, m); // stash "Y" into nY, back to nX("X")
1562        t.push(entry("X")); // leave nX("X") -> nZ(seq3), branch, live "Z"
1563        // g- from Z(seq3) -> nY(seq2) "Y".
1564        let (r, c, m) = live("Z");
1565        let y = t.seq_earlier_step(r, c, m).unwrap();
1566        assert_eq!(y.rope.to_string(), "Y");
1567        // u,u back to root.
1568        let (r, c, m) = live("Y");
1569        t.undo_step(r, c, m);
1570        let (r, c, m) = live("X");
1571        t.undo_step(r, c, m);
1572        assert!(t.is_at_root());
1573        // <C-r>,<C-r> retraces the branch we landed on: root->X->Y.
1574        let (r, c, m) = live("R");
1575        let x = t.redo_step(r, c, m).unwrap();
1576        assert_eq!(x.rope.to_string(), "X");
1577        let (r, c, m) = live("X");
1578        let y2 = t.redo_step(r, c, m).unwrap();
1579        assert_eq!(y2.rope.to_string(), "Y");
1580    }
1581
1582    #[test]
1583    fn leaves_lists_branch_tips_by_seq() {
1584        // root -> nX -> nY -> nW (leaf, seq3, depth3) and nX -> nZ (leaf, seq4,
1585        // depth2). Mirrors nvim `iX iY iW uu iZ`.
1586        let mut t = UndoTree::new(ropey::Rope::from_str(""));
1587        t.push(entry("X"));
1588        t.push(entry("Y"));
1589        t.push(entry("W"));
1590        let (r, c, m) = live("W");
1591        t.undo_step(r, c, m);
1592        let (r, c, m) = live("Y");
1593        t.undo_step(r, c, m); // back to nX
1594        t.push(entry("Z")); // nX -> nZ(seq4)
1595        let leaves = t.leaves();
1596        // Two leaves: W(seq3, depth3) and Z(seq4, depth2). Z is current.
1597        let dims: Vec<(u64, usize, bool)> =
1598            leaves.iter().map(|&(s, d, _, cur)| (s, d, cur)).collect();
1599        assert_eq!(dims, vec![(3, 3, false), (4, 2, true)]);
1600    }
1601
1602    #[test]
1603    fn cap_prunes_oldest_from_root_side() {
1604        let mut t = UndoTree::new(ropey::Rope::from_str("s"));
1605        for _ in 0..5 {
1606            t.push(entry("s"));
1607        }
1608        assert_eq!(t.depth(), 5);
1609        t.cap(3);
1610        assert_eq!(t.depth(), 3);
1611        // Redo side untouched (there is none), current unchanged.
1612        assert!(!t.has_redo());
1613        // Two oldest slots were reclaimed.
1614        assert_eq!(t.free.len(), 2);
1615    }
1616
1617    #[test]
1618    fn cap_drops_offpath_leaf_before_main_line() {
1619        // Fork two abandoned branches off the root, then extend the main line,
1620        // and cap: the lowest-seq OFF-PATH leaf must go first, and `current`
1621        // plus its ancestors must survive.
1622        let mut t = UndoTree::new(ropey::Rope::from_str(""));
1623        t.push(entry("A")); // root -> nA(seq1) [abandoned branch tip]
1624        let na = t.current;
1625        let (r, c, m) = live("A");
1626        t.undo_step(r, c, m);
1627        t.push(entry("B")); // root -> nB(seq2) [abandoned branch tip]
1628        let nb = t.current;
1629        let (r, c, m) = live("B");
1630        t.undo_step(r, c, m);
1631        t.push(entry("C")); // root -> nC(seq3), the live main line
1632        let nc = t.current;
1633        // 4 live nodes (root, nA, nB, nC) => 3 states. Cap to 2.
1634        assert_eq!(t.leaves().len(), 3);
1635        t.cap(2);
1636        // The lowest-seq off-path leaf (nA, seq1) was dropped; current (nC) and
1637        // its ancestor (root) survive, and the newer off-path leaf nB survives.
1638        assert!(t.nodes[na].is_none());
1639        assert!(t.nodes[nb].is_some());
1640        assert_eq!(t.current, nc);
1641        assert!(!t.is_at_root());
1642        assert!(t.get(t.root).children.contains(&nb));
1643        assert!(t.get(t.root).children.contains(&nc));
1644    }
1645
1646    #[test]
1647    fn pop_committed_reverses_last_push() {
1648        let mut t = UndoTree::new(ropey::Rope::from_str("s0"));
1649        t.push(entry("s0")); // depth 1, current = fresh leaf
1650        assert_eq!(t.depth(), 1);
1651        assert!(t.pop_committed());
1652        // The just-pushed leaf is gone; current stepped back to the root.
1653        assert_eq!(t.depth(), 0);
1654        assert!(t.is_at_root());
1655        assert_eq!(t.free.len(), 1);
1656        // Seq reclaimed so the next push is gapless.
1657        assert_eq!(t.next_seq, 1);
1658    }
1659
1660    #[test]
1661    fn pop_committed_retains_sibling_branches() {
1662        // Fork a branch, then a no-op push at the fork must pop cleanly without
1663        // orphaning the sibling branch.
1664        let mut t = UndoTree::new(ropey::Rope::from_str(""));
1665        t.push(entry("A")); // root -> nA(seq1)
1666        let na = t.current;
1667        let (r, c, m) = live("A");
1668        t.undo_step(r, c, m); // back to root
1669        t.push(entry("B")); // root -> nB(seq2); root children [nA, nB]
1670        let root = t.root;
1671        // A spurious no-op push at nB, then pop it.
1672        assert!(t.pop_committed());
1673        // nB is gone, current back at root; nA branch still intact & reachable.
1674        assert!(t.get(root).children.contains(&na));
1675        assert_eq!(t.get(root).children.len(), 1);
1676        assert_eq!(t.current, root);
1677        let live = t.nodes.iter().filter(|n| n.is_some()).count();
1678        assert_eq!(live, 2); // root + nA
1679    }
1680
1681    #[test]
1682    fn pop_committed_at_root_is_false() {
1683        let mut t = UndoTree::new(ropey::Rope::from_str("s"));
1684        assert!(!t.pop_committed());
1685    }
1686
1687    #[test]
1688    fn clear_redo_drops_forward_only() {
1689        let mut t = UndoTree::new(ropey::Rope::from_str("s0"));
1690        t.push(entry("s0"));
1691        let (r, c, m) = live("s1");
1692        t.undo_step(r, c, m);
1693        assert!(t.has_redo());
1694        assert_eq!(t.depth(), 0);
1695        t.clear_redo();
1696        assert!(!t.has_redo());
1697        assert_eq!(t.depth(), 0);
1698    }
1699
1700    #[test]
1701    fn clear_all_collapses_to_single_node() {
1702        let mut t = UndoTree::new(ropey::Rope::from_str("s"));
1703        for _ in 0..3 {
1704            t.push(entry("s"));
1705        }
1706        t.clear_all();
1707        assert!(t.is_at_root());
1708        assert!(!t.has_redo());
1709        assert_eq!(t.depth(), 0);
1710        assert_eq!(t.root, t.current);
1711    }
1712}
1713
1714// ─── Phase 3a delta-storage tests ─────────────────────────────────────────────
1715//
1716// Correctness of the reversible delta and the warm/cold materialization is
1717// where text gets silently corrupted, so these lean hard on it: exact diff
1718// round-trips over random (incl. multi-byte) content, every node reconstructing
1719// identically warm and cold, and a random op stream cross-checked against a
1720// full-snapshot reference model kept alongside. All randomness is a deterministic
1721// xorshift seeded from a fixed constant — never `SystemTime`/entropy — so a
1722// failure reproduces exactly.
1723#[cfg(test)]
1724mod delta_tests {
1725    use super::*;
1726
1727    /// Iteration count for the randomized differential loops below, capped
1728    /// hard under miri.
1729    ///
1730    /// These loops are worth thousands of steps on a normal run: their value
1731    /// is statistical, shaking out logic bugs in the diff / keyframe code from
1732    /// a random op mix. That is a property of *executing* them, and miri
1733    /// interprets instead — six loops totalling ~22 300 iterations is the bulk
1734    /// of the weekly miri job's runtime, enough to push it past an hour.
1735    ///
1736    /// miri is there to catch UB, and UB shows up on the code *paths*, not on
1737    /// the thousandth repetition of one — so a short pass covers what miri can
1738    /// actually detect. Normal runs are untouched and keep the full count.
1739    fn stress_iters(n: usize) -> usize {
1740        if cfg!(miri) { n.min(50) } else { n }
1741    }
1742
1743    /// Deterministic xorshift64* PRNG, fixed-seeded so runs are reproducible.
1744    struct Rng(u64);
1745    impl Rng {
1746        fn new(seed: u64) -> Self {
1747            // xorshift needs a non-zero state.
1748            Self(if seed == 0 {
1749                0x9E37_79B9_7F4A_7C15
1750            } else {
1751                seed
1752            })
1753        }
1754        fn next_u64(&mut self) -> u64 {
1755            let mut x = self.0;
1756            x ^= x >> 12;
1757            x ^= x << 25;
1758            x ^= x >> 27;
1759            self.0 = x;
1760            x.wrapping_mul(0x2545_F491_4F6C_DD1D)
1761        }
1762        fn below(&mut self, n: usize) -> usize {
1763            (self.next_u64() % n as u64) as usize
1764        }
1765    }
1766
1767    /// A random char-granular mutation of `s`: insert, delete, or replace a
1768    /// span, drawing from an alphabet that mixes ASCII, accented, CJK, and
1769    /// emoji so multi-byte boundaries are exercised.
1770    fn mutate(s: &str, rng: &mut Rng) -> String {
1771        const ALPHABET: [char; 10] = ['a', 'b', '\n', 'é', '日', '本', '🎉', '語', 'x', 'z'];
1772        let chars: Vec<char> = s.chars().collect();
1773        let pick = |rng: &mut Rng| ALPHABET[rng.below(ALPHABET.len())];
1774        match rng.below(3) {
1775            0 => {
1776                let pos = rng.below(chars.len() + 1);
1777                let mut v = chars.clone();
1778                v.insert(pos, pick(rng));
1779                v.into_iter().collect()
1780            }
1781            1 if !chars.is_empty() => {
1782                let pos = rng.below(chars.len());
1783                let mut v = chars.clone();
1784                v.remove(pos);
1785                v.into_iter().collect()
1786            }
1787            _ => {
1788                if chars.is_empty() {
1789                    return pick(rng).to_string();
1790                }
1791                let a = rng.below(chars.len());
1792                let b = (a + rng.below(chars.len() - a + 1)).min(chars.len());
1793                let mut v = chars[..a].to_vec();
1794                v.push(pick(rng));
1795                v.extend_from_slice(&chars[b..]);
1796                v.into_iter().collect()
1797            }
1798        }
1799    }
1800
1801    fn entry_str(s: &str) -> UndoEntry {
1802        UndoEntry {
1803            rope: ropey::Rope::from_str(s),
1804            cursor: (0, 0),
1805            timestamp: SystemTime::now(),
1806            marks: MarkSnapshot::default(),
1807        }
1808    }
1809
1810    // ── (0) differential oracle: the pre-chunk-walk `diff` ────────────────────
1811    //
1812    // The original implementation, verbatim, materializing BOTH ropes with
1813    // `to_string()` before scanning bytes. `diff` was rewritten to walk chunks
1814    // instead (no full materialization); this is the semantic pin — the two must
1815    // agree on the EXACT `Delta` for every input, not merely round-trip.
1816
1817    fn diff_reference(parent: &ropey::Rope, child: &ropey::Rope) -> Delta {
1818        let a = parent.to_string();
1819        let b = child.to_string();
1820        let ab = a.as_bytes();
1821        let bb = b.as_bytes();
1822
1823        let max_pre = ab.len().min(bb.len());
1824        let mut pre = 0;
1825        while pre < max_pre && ab[pre] == bb[pre] {
1826            pre += 1;
1827        }
1828        while pre > 0 && !a.is_char_boundary(pre) {
1829            pre -= 1;
1830        }
1831
1832        let max_suf = max_pre - pre;
1833        let mut suf = 0;
1834        while suf < max_suf && ab[ab.len() - 1 - suf] == bb[bb.len() - 1 - suf] {
1835            suf += 1;
1836        }
1837        let mut a_end = ab.len() - suf;
1838        while a_end < ab.len() && !a.is_char_boundary(a_end) {
1839            a_end += 1;
1840        }
1841        let b_end = bb.len() - (ab.len() - a_end);
1842
1843        Delta {
1844            start: a[..pre].chars().count(),
1845            old: a[pre..a_end].to_string(),
1846            new: b[pre..b_end].to_string(),
1847        }
1848    }
1849
1850    /// Assert the chunk-walking `diff` is byte-identical to `diff_reference`,
1851    /// over BOTH single-chunk ropes and multi-chunk ones (ropey only splits past
1852    /// its ~1 KiB leaf size, so short fixtures alone would never exercise the
1853    /// cross-chunk cursor logic).
1854    #[track_caller]
1855    fn assert_diff_matches_reference(sa: &str, sb: &str) {
1856        let a = ropey::Rope::from_str(sa);
1857        let b = ropey::Rope::from_str(sb);
1858        assert_eq!(
1859            diff(&a, &b),
1860            diff_reference(&a, &b),
1861            "diff != reference for {sa:?} -> {sb:?}"
1862        );
1863        // Same content, but built by insertion so the two ropes have DIFFERENT,
1864        // misaligned chunk layouts — the reference sees only bytes, the walker
1865        // sees chunk seams, and they must still agree.
1866        let mut a2 = ropey::Rope::new();
1867        a2.insert(0, sa);
1868        let mut b2 = ropey::Rope::new();
1869        for (i, c) in sb.chars().enumerate() {
1870            b2.insert_char(i, c);
1871        }
1872        assert_eq!(
1873            diff(&a2, &b2),
1874            diff_reference(&a2, &b2),
1875            "diff != reference (misaligned chunks) for {sa:?} -> {sb:?}"
1876        );
1877    }
1878
1879    #[test]
1880    // Deliberately NOT size-scaled for miri: the documents here are sized to span
1881    // several of ropey's ~1 KB leaf chunks, which is the entire property under
1882    // test, so shrinking them would quietly test something weaker. Running them
1883    // interpreted costs >10 min on its own. hjkl-buffer has no `unsafe`, so miri's
1884    // reach is UB in ropey/std on these code paths — already covered by the ~185
1885    // other tests in this crate that do run under it.
1886    #[cfg_attr(miri, ignore = "multi-chunk documents are too slow interpreted")]
1887    fn diff_matches_reference_on_edge_cases() {
1888        let cases: &[(&str, &str)] = &[
1889            // equal / empty
1890            ("", ""),
1891            ("", "a"),
1892            ("a", ""),
1893            ("abc", "abc"),
1894            ("café🎉", "café🎉"),
1895            // prefix-only / suffix-only change
1896            ("abcdef", "abcdefXY"),
1897            ("abcdefXY", "abcdef"),
1898            ("Xabcdef", "abcdef"),
1899            ("abcdef", "Xabcdef"),
1900            // change at position 0 and at the very end
1901            ("abcdef", "Zbcdef"),
1902            ("abcdef", "abcdeZ"),
1903            // overlapping repeats — prefix and suffix scans would collide
1904            ("abcabc", "abc"),
1905            ("abc", "abcabc"),
1906            ("aaaa", "aa"),
1907            ("aa", "aaaa"),
1908            ("abab", "ababab"),
1909            ("xyxyxy", "xyxy"),
1910            // multi-byte chars sitting exactly on the cut points
1911            ("café", "cafés"),
1912            ("cafés", "café"),
1913            ("café", "cafè"),
1914            ("日本語", "日語"),
1915            ("日本語", "日本本語"),
1916            ("🎉🎉🎉", "🎉🎉"),
1917            ("🎉🎉", "🎉🎉🎉"),
1918            ("🎉x🎉", "🎉y🎉"),
1919            ("a🎉b", "a🎊b"),
1920            ("é", "e"),
1921            ("e", "é"),
1922            ("🎉", ""),
1923            ("", "🎉"),
1924            // byte-level suffix match that is NOT a char boundary: the tails of
1925            // 'é' (0xC3 0xA9) and 'é' share no byte, but 日 (E6 97 A5) vs 旦
1926            // (E6 97 A6) share a two-byte prefix mid-codepoint.
1927            ("日", "旦"),
1928            ("x日y", "x旦y"),
1929            ("語", "誤"),
1930            // long enough to be multi-chunk in both ropes
1931            (
1932                &"the quick brown fox ".repeat(400),
1933                &"the quick brown fox ".repeat(400),
1934            ),
1935        ];
1936        for (sa, sb) in cases {
1937            assert_diff_matches_reference(sa, sb);
1938        }
1939
1940        // Multi-chunk with an edit in the middle / at each end.
1941        let big: String = "the quick brown fox jumps over the lazy dog\n".repeat(200);
1942        let mid = big.len() / 2;
1943        let mut edited = big.clone();
1944        edited.insert(mid, 'Z');
1945        assert_diff_matches_reference(&big, &edited);
1946        assert_diff_matches_reference(&edited, &big);
1947        assert_diff_matches_reference(&big, &format!("Z{big}"));
1948        assert_diff_matches_reference(&big, &format!("{big}Z"));
1949        assert_diff_matches_reference(&big, &big.repeat(2));
1950
1951        // Multi-chunk with multi-byte chars straddling likely leaf seams.
1952        let uni: String = "café 日本語 🎉 αβγ\n".repeat(200);
1953        let umid = uni.len() / 2;
1954        let umid = (0..=umid).rev().find(|i| uni.is_char_boundary(*i)).unwrap();
1955        let mut uedited = uni.clone();
1956        uedited.insert(umid, '🎊');
1957        assert_diff_matches_reference(&uni, &uedited);
1958        assert_diff_matches_reference(&uedited, &uni);
1959    }
1960
1961    #[test]
1962    fn diff_matches_reference_over_random_evolving_content() {
1963        let mut rng = Rng::new(0x0BAD_F00D_1234_5678);
1964        let mut s = String::from("seed café 日本語\n🎉");
1965        for _ in 0..stress_iters(4000) {
1966            let t = mutate(&s, &mut rng);
1967            let a = ropey::Rope::from_str(&s);
1968            let b = ropey::Rope::from_str(&t);
1969            assert_eq!(diff(&a, &b), diff_reference(&a, &b), "{s:?} -> {t:?}");
1970            assert_eq!(diff(&b, &a), diff_reference(&b, &a), "{t:?} -> {s:?}");
1971            s = t;
1972        }
1973    }
1974
1975    #[test]
1976    fn diff_matches_reference_on_shared_leaf_clones() {
1977        // The `Arc`-shared-leaf fast path: `child` is a CLONE of `parent` plus
1978        // one edit, so most chunks are pointer-identical. Exercised at several
1979        // edit positions across a multi-chunk rope, plus deletes and the
1980        // degenerate no-op clone.
1981        let base: String = "the quick brown fox jumps over the lazy dog\n".repeat(300);
1982        let parent = ropey::Rope::from_str(&base);
1983        assert_eq!(
1984            diff(&parent, &parent.clone()),
1985            diff_reference(&parent, &parent.clone())
1986        );
1987        let n = parent.len_chars();
1988        for at in [0, 1, n / 4, n / 2, n - 1, n] {
1989            let mut child = parent.clone();
1990            child.insert_char(at, '𝄞');
1991            assert_eq!(
1992                diff(&parent, &child),
1993                diff_reference(&parent, &child),
1994                "@{at}"
1995            );
1996            assert_eq!(
1997                diff(&child, &parent),
1998                diff_reference(&child, &parent),
1999                "@{at}"
2000            );
2001        }
2002        for at in [0, n / 3, n - 10] {
2003            let mut child = parent.clone();
2004            child.remove(at..at + 5);
2005            assert_eq!(
2006                diff(&parent, &child),
2007                diff_reference(&parent, &child),
2008                "-{at}"
2009            );
2010            assert_eq!(
2011                diff(&child, &parent),
2012                diff_reference(&child, &parent),
2013                "-{at}"
2014            );
2015        }
2016    }
2017
2018    #[test]
2019    // Same reasoning as `diff_matches_reference_on_edge_cases`: the 300-line base
2020    // document exists to force multi-chunk ropes, so it is not size-scaled and the
2021    // test is skipped under miri rather than weakened.
2022    #[cfg_attr(miri, ignore = "multi-chunk documents are too slow interpreted")]
2023    fn diff_matches_reference_over_random_multi_chunk_pairs() {
2024        // Random pairs built from a multi-chunk corpus, so chunk seams land in
2025        // arbitrary places relative to the common prefix/suffix.
2026        let mut rng = Rng::new(0xF00D_BEEF_0BAD_C0DE);
2027        let units = ["ab", "café ", "日本語", "🎉", "\n", "x", "語日", "é"];
2028        let build = |rng: &mut Rng| -> String {
2029            let mut s = String::new();
2030            for _ in 0..rng.below(400) {
2031                s.push_str(units[rng.below(units.len())]);
2032            }
2033            s
2034        };
2035        for _ in 0..stress_iters(300) {
2036            let sa = build(&mut rng);
2037            // Half the pairs share a long common prefix/suffix with `sa`.
2038            let sb = if rng.below(2) == 0 {
2039                build(&mut rng)
2040            } else {
2041                let mut t = sa.clone();
2042                if !t.is_empty() {
2043                    let cut = rng.below(t.chars().count() + 1);
2044                    let byte = t.char_indices().nth(cut).map_or(t.len(), |(i, _)| i);
2045                    t.insert_str(byte, "🎊zz");
2046                }
2047                t
2048            };
2049            let a = ropey::Rope::from_str(&sa);
2050            let b = ropey::Rope::from_str(&sb);
2051            assert_eq!(diff(&a, &b), diff_reference(&a, &b));
2052            assert_eq!(diff(&b, &a), diff_reference(&b, &a));
2053        }
2054    }
2055
2056    // ── (i) delta round-trip: apply(diff(a,b))==b and apply_inverse==a ────────
2057
2058    #[test]
2059    fn diff_round_trips_over_random_evolving_content() {
2060        let mut rng = Rng::new(0x1234_5678_9ABC_DEF0);
2061        let mut s = String::from("seed café 日本語\n🎉");
2062        for _ in 0..stress_iters(4000) {
2063            let t = mutate(&s, &mut rng);
2064            let a = ropey::Rope::from_str(&s);
2065            let b = ropey::Rope::from_str(&t);
2066            let d = diff(&a, &b);
2067            assert_eq!(
2068                apply_forward(&a, &d).to_string(),
2069                t,
2070                "forward a->b failed (start={}, old={:?}, new={:?})",
2071                d.start,
2072                d.old,
2073                d.new
2074            );
2075            assert_eq!(
2076                apply_inverse(&b, &d).to_string(),
2077                s,
2078                "inverse b->a failed (start={}, old={:?}, new={:?})",
2079                d.start,
2080                d.old,
2081                d.new
2082            );
2083            s = t;
2084        }
2085    }
2086
2087    #[test]
2088    fn diff_round_trips_over_unrelated_pairs() {
2089        // Disjoint corpus pairs (not just single-edit neighbours) so the diff's
2090        // prefix/suffix logic is stressed on wholly different multi-byte text.
2091        let corpus = [
2092            "",
2093            "a",
2094            "café\n日本語\n",
2095            "🎉🎉🎉",
2096            "abcdef",
2097            "日本",
2098            "x\ny\nz\n",
2099            "aXb",
2100            "café",
2101            "語日本",
2102            "\n\n\n",
2103            "🎉x🎉y🎉",
2104        ];
2105        let mut rng = Rng::new(0xDEAD_BEEF_CAFE_1234);
2106        for _ in 0..stress_iters(3000) {
2107            let sa = corpus[rng.below(corpus.len())];
2108            let sb = corpus[rng.below(corpus.len())];
2109            let a = ropey::Rope::from_str(sa);
2110            let b = ropey::Rope::from_str(sb);
2111            let d = diff(&a, &b);
2112            assert_eq!(apply_forward(&a, &d).to_string(), sb);
2113            assert_eq!(apply_inverse(&b, &d).to_string(), sa);
2114        }
2115    }
2116
2117    // ── non-ASCII edit → undo → redo round-trip (multi-byte across a leave) ───
2118
2119    #[test]
2120    fn non_ascii_edit_undo_redo_round_trip() {
2121        // Edits land INSIDE multi-byte lines; undo/redo must round-trip the exact
2122        // bytes, proving the char-offset delta never splits a codepoint.
2123        let mut d = Driver::new("café\n日本語\n");
2124        d.edit("cafés\n日本語\n");
2125        d.edit("cafés\n日本語です\n");
2126        d.edit("cafés\n日本語です🎉\n");
2127        assert_eq!(d.undo().as_deref(), Some("cafés\n日本語です\n"));
2128        assert_eq!(d.undo().as_deref(), Some("cafés\n日本語\n"));
2129        assert_eq!(d.undo().as_deref(), Some("café\n日本語\n"));
2130        assert_eq!(d.redo().as_deref(), Some("cafés\n日本語\n"));
2131        assert_eq!(d.redo().as_deref(), Some("cafés\n日本語です\n"));
2132        assert_eq!(d.redo().as_deref(), Some("cafés\n日本語です🎉\n"));
2133        // Cold reconstruction of every node still matches (drop all caches).
2134        assert_warm_equals_cold(&mut d.t);
2135    }
2136
2137    // ── (ii) + (iii) random op stream vs a full-snapshot reference model ──────
2138
2139    #[test]
2140    fn tree_matches_full_snapshot_reference_over_random_ops() {
2141        let mut rng = Rng::new(0x9E37_79B9_7F4A_7C15);
2142        let start = "α\nβγ\n日本🎉\n";
2143        let mut real = UndoTree::new(ropey::Rope::from_str(start));
2144        let mut refr = RefTree::new(start);
2145        let mut live = start.to_string();
2146
2147        for step in 0..stress_iters(6000) {
2148            // Structural predicates stay in lockstep with the reference.
2149            assert_eq!(real.is_at_root(), refr.is_at_root(), "is_at_root @ {step}");
2150            assert_eq!(real.has_redo(), refr.has_redo(), "has_redo @ {step}");
2151            assert_eq!(real.depth(), refr.depth(), "depth @ {step}");
2152
2153            match rng.below(6) {
2154                0 | 1 => {
2155                    // Edit: push the PRE-edit state (engine discipline), then
2156                    // mutate the live buffer.
2157                    let pre = live.clone();
2158                    real.push(entry_str(&pre));
2159                    refr.push(&pre);
2160                    live = mutate(&live, &mut rng);
2161                }
2162                2 => {
2163                    let got = real
2164                        .undo_step(
2165                            ropey::Rope::from_str(&live),
2166                            (0, 0),
2167                            MarkSnapshot::default(),
2168                        )
2169                        .map(|e| e.rope.to_string());
2170                    let want = refr.undo_step(&live);
2171                    assert_eq!(got, want, "undo @ {step}");
2172                    if let Some(c) = got {
2173                        live = c;
2174                    }
2175                }
2176                3 => {
2177                    let got = real
2178                        .redo_step(
2179                            ropey::Rope::from_str(&live),
2180                            (0, 0),
2181                            MarkSnapshot::default(),
2182                        )
2183                        .map(|e| e.rope.to_string());
2184                    let want = refr.redo_step(&live);
2185                    assert_eq!(got, want, "redo @ {step}");
2186                    if let Some(c) = got {
2187                        live = c;
2188                    }
2189                }
2190                4 => {
2191                    let got = real
2192                        .seq_earlier_step(
2193                            ropey::Rope::from_str(&live),
2194                            (0, 0),
2195                            MarkSnapshot::default(),
2196                        )
2197                        .map(|e| e.rope.to_string());
2198                    let want = refr.seq_earlier_step(&live);
2199                    assert_eq!(got, want, "g- @ {step}");
2200                    if let Some(c) = got {
2201                        live = c;
2202                    }
2203                }
2204                _ => {
2205                    let got = real
2206                        .seq_later_step(
2207                            ropey::Rope::from_str(&live),
2208                            (0, 0),
2209                            MarkSnapshot::default(),
2210                        )
2211                        .map(|e| e.rope.to_string());
2212                    let want = refr.seq_later_step(&live);
2213                    assert_eq!(got, want, "g+ @ {step}");
2214                    if let Some(c) = got {
2215                        live = c;
2216                    }
2217                }
2218            }
2219
2220            // (ii) Every so often, assert warm and cold materialization agree
2221            // for every node — a cold-reconstructed node must equal the rope the
2222            // full-snapshot model would have held.
2223            if step % 200 == 0 {
2224                assert_warm_equals_cold(&mut real);
2225            }
2226        }
2227        assert_warm_equals_cold(&mut real);
2228    }
2229
2230    // ── (iv) keyframes: accelerated materialize vs the naive root replay ──────
2231    //
2232    // Keyframes (issue #302) pin a materialized rope every `KEYFRAME_INTERVAL`
2233    // nodes so a cold `g-` replays O(K) deltas instead of O(depth). They are a
2234    // CACHE: whatever they accelerate must be bit-identical to replaying every
2235    // delta from the root base with no cache at all. `materialize_naive` is that
2236    // oracle, in the same spirit as `diff_reference` above.
2237
2238    /// For every live node: the keyframe-accelerated `materialize` must equal the
2239    /// naive root-base replay exactly.
2240    #[track_caller]
2241    fn assert_materialize_matches_naive(t: &mut UndoTree) {
2242        for id in t.live_ids() {
2243            let naive = t.materialize_naive(id).to_string();
2244            let got = t.materialize_for_test(id).to_string();
2245            assert_eq!(got, naive, "accelerated != naive root replay for node {id}");
2246        }
2247    }
2248
2249    /// A linear history `n` states deep (so it crosses many keyframe intervals),
2250    /// plus the expected content of each state indexed by `seq`/depth. Every node
2251    /// is finalized, including the tip.
2252    fn deep_linear_history(n: usize) -> (UndoTree, Vec<String>) {
2253        // Under miri the document is shrunk 10x. `n` is deliberately NOT
2254        // touched: the keyframe ladder, the `n > 4 * KEYFRAME_INTERVAL`
2255        // assertion and every depth-related property stay exactly as they are
2256        // on a normal run — only the per-step rope volume drops. Without this
2257        // a single one of these tests ran for over 22 minutes under miri
2258        // (interpreted, not executed) and stalled the weekly job. The base
2259        // keeps its multi-line and multi-byte content, which is the part that
2260        // matters for the rope/delta paths.
2261        let reps = if cfg!(miri) { 2 } else { 20 };
2262        let base: String =
2263            "the quick brown fox\njumps over the lazy dog\ncafé 日本語 🎉\n".repeat(reps);
2264        let mut t = UndoTree::new(ropey::Rope::from_str(&base));
2265        let mut states = vec![base.clone()];
2266        let mut live = base;
2267        for i in 0..n {
2268            // Engine discipline: commit the PRE-edit state, then mutate.
2269            t.push(entry_str(&live));
2270            live = format!("e{i} {live}");
2271            states.push(live.clone());
2272        }
2273        // Stash the tip's live content so no node is left holding a stale edge.
2274        t.sync_current(ropey::Rope::from_str(&live));
2275        (t, states)
2276    }
2277
2278    #[test]
2279    fn deep_history_walks_back_and_forward_exactly() {
2280        // The `:earlier 9999` / `:later 9999` shape, deep enough that most jumps
2281        // land outside the warm window and go through a keyframe.
2282        // 65 under miri still satisfies the `> 4 * KEYFRAME_INTERVAL` floor
2283        // asserted below, so the walk still crosses four keyframes — the
2284        // property under test. See `deep_linear_history` for why.
2285        let n = if cfg!(miri) { 65 } else { 200 };
2286        assert!(n > 4 * KEYFRAME_INTERVAL);
2287        let (mut t, states) = deep_linear_history(n);
2288
2289        let mut live = states[n].clone();
2290        for want in (0..n).rev() {
2291            let got = t
2292                .seq_earlier_step(
2293                    ropey::Rope::from_str(&live),
2294                    (0, 0),
2295                    MarkSnapshot::default(),
2296                )
2297                .expect("history is deeper than the walk");
2298            live = got.rope.to_string();
2299            assert_eq!(live, states[want], "g- onto seq {want}");
2300        }
2301        assert!(
2302            t.seq_earlier_step(
2303                ropey::Rope::from_str(&live),
2304                (0, 0),
2305                MarkSnapshot::default()
2306            )
2307            .is_none(),
2308            "walk ended at the oldest state"
2309        );
2310        for (seq, want) in states.iter().enumerate().skip(1) {
2311            let got = t
2312                .seq_later_step(
2313                    ropey::Rope::from_str(&live),
2314                    (0, 0),
2315                    MarkSnapshot::default(),
2316                )
2317                .expect("history is deeper than the walk");
2318            live = got.rope.to_string();
2319            assert_eq!(&live, want, "g+ onto seq {seq}");
2320        }
2321        assert_materialize_matches_naive(&mut t);
2322        assert_warm_equals_cold(&mut t);
2323    }
2324
2325    #[test]
2326    fn keyframes_bound_the_cold_replay_distance() {
2327        // 65 still spans four keyframe intervals, which is what makes the
2328        // per-node replay-distance bound below meaningful. See
2329        // `deep_linear_history` for why miri gets a smaller history.
2330        let n = if cfg!(miri) { 65 } else { 200 };
2331        let (mut t, _) = deep_linear_history(n);
2332        // Steady state: the ordinary warm entries have aged out, the keyframes
2333        // are still pinned. Every node must be within one interval of an anchor.
2334        t.drop_warm_caches();
2335        for id in t.live_ids() {
2336            let d = t.replay_distance(id);
2337            assert!(
2338                d < KEYFRAME_INTERVAL,
2339                "node {id} (depth {}) replays {d} deltas, over the keyframe bound",
2340                t.get(id).depth
2341            );
2342        }
2343        // Drop the keyframes too and the bound is gone — proof that it is the
2344        // keyframes doing the bounding and not the warm LRU or the tree shape.
2345        let deepest = *t
2346            .live_ids()
2347            .iter()
2348            .max_by_key(|&&id| t.get(id).depth)
2349            .unwrap();
2350        t.drop_all_caches();
2351        assert!(t.replay_distance(deepest) > KEYFRAME_INTERVAL);
2352        // One materialize off the fully-cold tree re-pins the whole ladder.
2353        t.materialize_for_test(deepest);
2354        t.drop_warm_caches();
2355        for id in t.live_ids() {
2356            assert!(t.replay_distance(id) < KEYFRAME_INTERVAL, "node {id}");
2357        }
2358    }
2359
2360    #[test]
2361    fn keyframe_materialize_matches_naive_over_random_ops() {
2362        // Push-heavy op mix so the tree gets deep enough to cross many keyframe
2363        // intervals, with undo/redo/g-/g+ and periodic `cap` pruning mixed in —
2364        // pruning renumbers nothing but does free nodes and re-root the tree, so
2365        // it is where a stale keyframe would surface as corrupted text.
2366        let mut rng = Rng::new(0x0FF1_CE00_D15E_A5E5);
2367        let start = "α\nβγ\n日本🎉\nthe quick brown fox\n";
2368        let mut t = UndoTree::new(ropey::Rope::from_str(start));
2369        let mut live = start.to_string();
2370
2371        for step in 0..stress_iters(5000) {
2372            match rng.below(10) {
2373                0..=5 => {
2374                    let pre = live.clone();
2375                    t.push(entry_str(&pre));
2376                    live = mutate(&live, &mut rng);
2377                }
2378                6 => {
2379                    if let Some(e) = t.undo_step(
2380                        ropey::Rope::from_str(&live),
2381                        (0, 0),
2382                        MarkSnapshot::default(),
2383                    ) {
2384                        live = e.rope.to_string();
2385                    }
2386                }
2387                7 => {
2388                    if let Some(e) = t.redo_step(
2389                        ropey::Rope::from_str(&live),
2390                        (0, 0),
2391                        MarkSnapshot::default(),
2392                    ) {
2393                        live = e.rope.to_string();
2394                    }
2395                }
2396                8 => {
2397                    if let Some(e) = t.seq_earlier_step(
2398                        ropey::Rope::from_str(&live),
2399                        (0, 0),
2400                        MarkSnapshot::default(),
2401                    ) {
2402                        live = e.rope.to_string();
2403                    }
2404                }
2405                _ => {
2406                    if let Some(e) = t.seq_later_step(
2407                        ropey::Rope::from_str(&live),
2408                        (0, 0),
2409                        MarkSnapshot::default(),
2410                    ) {
2411                        live = e.rope.to_string();
2412                    }
2413                }
2414            }
2415            // Whatever the tree hands back must be what the naive replay of the
2416            // node it landed on says — checked every step, cheaply.
2417            let cur = t.current;
2418            assert_eq!(
2419                t.materialize_for_test(cur).to_string(),
2420                t.materialize_naive(cur).to_string(),
2421                "current node diverged @ {step}"
2422            );
2423            if step % 250 == 0 {
2424                assert_materialize_matches_naive(&mut t);
2425            }
2426            if step % 700 == 0 {
2427                t.cap(60);
2428            }
2429        }
2430        assert_materialize_matches_naive(&mut t);
2431        assert_warm_equals_cold(&mut t);
2432    }
2433
2434    #[test]
2435    fn deserialized_deep_tree_rebuilds_the_keyframe_ladder() {
2436        // Depth is NOT serialized (keyframes are a cache, the on-disk format is
2437        // untouched), so a loaded tree has to recompute it — otherwise every
2438        // cross-session `g-` would be a full replay again.
2439        // 65 still spans four keyframe intervals, so the reloaded tree must
2440        // still rebuild a real ladder rather than a trivial one. See
2441        // `deep_linear_history` for why miri gets a smaller history.
2442        let n = if cfg!(miri) { 65 } else { 100 };
2443        let (t, states) = deep_linear_history(n);
2444        let ser = t.to_serializable();
2445        let mut back = UndoTree::from_serializable(&ser).expect("valid projection");
2446
2447        let deepest = *back
2448            .live_ids()
2449            .iter()
2450            .max_by_key(|&&id| back.get(id).depth)
2451            .unwrap();
2452        assert_eq!(back.get(deepest).depth, n, "depths recomputed on load");
2453        assert_eq!(back.materialize_for_test(deepest).to_string(), states[n]);
2454        assert_materialize_matches_naive(&mut back);
2455
2456        back.drop_warm_caches();
2457        for id in back.live_ids() {
2458            assert!(back.replay_distance(id) < KEYFRAME_INTERVAL, "node {id}");
2459        }
2460    }
2461
2462    /// For every live node: materialize warm, drop all caches, materialize cold,
2463    /// assert identical. Restores nothing else (test-local).
2464    fn assert_warm_equals_cold(t: &mut UndoTree) {
2465        let ids = t.live_ids();
2466        let warm: Vec<String> = ids
2467            .iter()
2468            .map(|&id| t.materialize_for_test(id).to_string())
2469            .collect();
2470        t.drop_all_caches();
2471        for (i, &id) in ids.iter().enumerate() {
2472            let cold = t.materialize_for_test(id).to_string();
2473            assert_eq!(cold, warm[i], "warm != cold for node {id}");
2474        }
2475    }
2476
2477    /// Engine-faithful driver over the real (delta) [`UndoTree`]: mirrors how
2478    /// `editor.rs` pushes the PRE-edit state and restores returned content.
2479    struct Driver {
2480        t: UndoTree,
2481        live: String,
2482    }
2483    impl Driver {
2484        fn new(s: &str) -> Self {
2485            Self {
2486                t: UndoTree::new(ropey::Rope::from_str(s)),
2487                live: s.to_string(),
2488            }
2489        }
2490        fn edit(&mut self, new: &str) {
2491            self.t.push(entry_str(&self.live));
2492            self.live = new.to_string();
2493        }
2494        fn undo(&mut self) -> Option<String> {
2495            let e = self.t.undo_step(
2496                ropey::Rope::from_str(&self.live),
2497                (0, 0),
2498                MarkSnapshot::default(),
2499            )?;
2500            self.live = e.rope.to_string();
2501            Some(self.live.clone())
2502        }
2503        fn redo(&mut self) -> Option<String> {
2504            let e = self.t.redo_step(
2505                ropey::Rope::from_str(&self.live),
2506                (0, 0),
2507                MarkSnapshot::default(),
2508            )?;
2509            self.live = e.rope.to_string();
2510            Some(self.live.clone())
2511        }
2512    }
2513
2514    /// Full-snapshot reference tree — Phase 2b's model (a whole rope per node),
2515    /// the oracle the delta tree is cross-checked against. Content only (cursor /
2516    /// marks / timestamps are covered by the existing tree tests).
2517    struct RefNode {
2518        parent: Option<usize>,
2519        children: Vec<usize>,
2520        last_child: Option<usize>,
2521        content: String,
2522        seq: u64,
2523    }
2524    struct RefTree {
2525        nodes: Vec<Option<RefNode>>,
2526        current: usize,
2527        next_seq: u64,
2528    }
2529    impl RefTree {
2530        fn new(s: &str) -> Self {
2531            let root = RefNode {
2532                parent: None,
2533                children: Vec::new(),
2534                last_child: None,
2535                content: s.to_string(),
2536                seq: 0,
2537            };
2538            Self {
2539                nodes: vec![Some(root)],
2540                current: 0,
2541                next_seq: 1,
2542            }
2543        }
2544        fn get(&self, id: usize) -> &RefNode {
2545            self.nodes[id].as_ref().unwrap()
2546        }
2547        fn get_mut(&mut self, id: usize) -> &mut RefNode {
2548            self.nodes[id].as_mut().unwrap()
2549        }
2550        fn alloc(&mut self, n: RefNode) -> usize {
2551            self.nodes.push(Some(n));
2552            self.nodes.len() - 1
2553        }
2554        fn is_at_root(&self) -> bool {
2555            self.get(self.current).parent.is_none()
2556        }
2557        fn has_redo(&self) -> bool {
2558            self.get(self.current).last_child.is_some()
2559        }
2560        fn depth(&self) -> usize {
2561            let mut d = 0;
2562            let mut n = self.get(self.current).parent;
2563            while let Some(p) = n {
2564                d += 1;
2565                n = self.get(p).parent;
2566            }
2567            d
2568        }
2569        fn push(&mut self, pre: &str) {
2570            let cur = self.current;
2571            self.get_mut(cur).content = pre.to_string();
2572            let seq = self.next_seq;
2573            self.next_seq += 1;
2574            let child = self.alloc(RefNode {
2575                parent: Some(cur),
2576                children: Vec::new(),
2577                last_child: None,
2578                content: pre.to_string(),
2579                seq,
2580            });
2581            let c = self.get_mut(cur);
2582            c.children.push(child);
2583            c.last_child = Some(child);
2584            self.current = child;
2585        }
2586        fn undo_step(&mut self, live: &str) -> Option<String> {
2587            let cur = self.current;
2588            let par = self.get(cur).parent?;
2589            self.get_mut(cur).content = live.to_string();
2590            self.get_mut(par).last_child = Some(cur);
2591            self.current = par;
2592            Some(self.get(par).content.clone())
2593        }
2594        fn redo_step(&mut self, live: &str) -> Option<String> {
2595            let cur = self.current;
2596            let child = self.get(cur).last_child?;
2597            self.get_mut(cur).content = live.to_string();
2598            self.current = child;
2599            Some(self.get(child).content.clone())
2600        }
2601        fn current_seq(&self) -> u64 {
2602            self.get(self.current).seq
2603        }
2604        fn node_below(&self, s: u64) -> Option<usize> {
2605            let mut best: Option<(u64, usize)> = None;
2606            for (id, slot) in self.nodes.iter().enumerate() {
2607                if let Some(n) = slot
2608                    && n.seq < s
2609                    && best.is_none_or(|(bs, _)| n.seq > bs)
2610                {
2611                    best = Some((n.seq, id));
2612                }
2613            }
2614            best.map(|(_, id)| id)
2615        }
2616        fn node_above(&self, s: u64) -> Option<usize> {
2617            let mut best: Option<(u64, usize)> = None;
2618            for (id, slot) in self.nodes.iter().enumerate() {
2619                if let Some(n) = slot
2620                    && n.seq > s
2621                    && best.is_none_or(|(bs, _)| n.seq < bs)
2622                {
2623                    best = Some((n.seq, id));
2624                }
2625            }
2626            best.map(|(_, id)| id)
2627        }
2628        fn retarget(&mut self, target: usize) {
2629            self.current = target;
2630            let mut node = target;
2631            while let Some(p) = self.get(node).parent {
2632                self.get_mut(p).last_child = Some(node);
2633                node = p;
2634            }
2635        }
2636        fn stash_and_move(&mut self, target: usize, live: &str) {
2637            let cur = self.current;
2638            self.get_mut(cur).content = live.to_string();
2639            self.retarget(target);
2640        }
2641        fn seq_earlier_step(&mut self, live: &str) -> Option<String> {
2642            let target = self.node_below(self.current_seq())?;
2643            self.stash_and_move(target, live);
2644            Some(self.get(target).content.clone())
2645        }
2646        fn seq_later_step(&mut self, live: &str) -> Option<String> {
2647            let target = self.node_above(self.current_seq())?;
2648            self.stash_and_move(target, live);
2649            Some(self.get(target).content.clone())
2650        }
2651    }
2652}
2653
2654// ─── Phase 3b serialize/deserialize tests ─────────────────────────────────────
2655//
2656// The undofile is only as trustworthy as this round-trip: a projection that
2657// loses a branch, mislinks a parent, or reconstructs a node's content wrong
2658// would silently corrupt cross-session undo. These build the headline tree
2659// (5 edits, u, u), project it, rebuild, and assert BOTH the per-node content
2660// (keyed by the stable `seq`) and the live walk (`<C-r>` forward, `u` back)
2661// survive the trip.
2662#[cfg(test)]
2663mod serialize_tests {
2664    use super::*;
2665
2666    fn e(text: &str) -> UndoEntry {
2667        UndoEntry {
2668            rope: ropey::Rope::from_str(text),
2669            cursor: (0, 0),
2670            timestamp: SystemTime::now(),
2671            marks: MarkSnapshot::default(),
2672        }
2673    }
2674    fn l(text: &str) -> (ropey::Rope, (usize, usize), MarkSnapshot) {
2675        (ropey::Rope::from_str(text), (0, 0), MarkSnapshot::default())
2676    }
2677
2678    /// The headline tree: root "s0", five edits to live "s5", then `u` twice so
2679    /// `current` sits on "s3" with the forward branch (s4/s5) retained — exactly
2680    /// the state a `:wq` would persist.
2681    fn headline_tree() -> UndoTree {
2682        let mut t = UndoTree::new(ropey::Rope::from_str("s0"));
2683        for pre in ["s0", "s1", "s2", "s3", "s4"] {
2684            t.push(e(pre)); // engine discipline: push the PRE-edit live state
2685        }
2686        let (r, c, m) = l("s5");
2687        t.undo_step(r, c, m); // -> s4
2688        let (r, c, m) = l("s4");
2689        t.undo_step(r, c, m); // -> s3
2690        t.sync_current(ropey::Rope::from_str("s3")); // stash exact live, like save
2691        t
2692    }
2693
2694    /// Every node's content (keyed by `seq`), materialized cold-then-warm.
2695    fn content_by_seq(t: &mut UndoTree) -> std::collections::BTreeMap<u64, String> {
2696        t.live_ids()
2697            .into_iter()
2698            .map(|id| {
2699                let seq = t.get(id).seq;
2700                (seq, t.materialize_for_test(id).to_string())
2701            })
2702            .collect()
2703    }
2704
2705    #[test]
2706    fn round_trip_reproduces_structure_and_content() {
2707        let mut orig = headline_tree();
2708        let cur_seq = orig.current_node_seq();
2709        let ser = orig.to_serializable();
2710        let orig_content = content_by_seq(&mut orig);
2711
2712        let mut back = UndoTree::from_serializable(&ser).expect("valid projection");
2713        assert_eq!(back.current_node_seq(), cur_seq, "current preserved");
2714        assert_eq!(back.next_seq, orig.next_seq, "next_seq preserved");
2715        // Force cold reconstruction (fresh tree has no warm caches) and compare.
2716        assert_eq!(
2717            content_by_seq(&mut back),
2718            orig_content,
2719            "content at every node reproduced"
2720        );
2721        // Six states: s0..s5.
2722        assert_eq!(orig_content.len(), 6);
2723        assert_eq!(orig_content[&3], "s3");
2724        assert_eq!(orig_content[&5], "s5");
2725    }
2726
2727    #[test]
2728    fn deserialized_tree_walks_forward_and_back() {
2729        let ser = headline_tree().to_serializable();
2730        let mut t = UndoTree::from_serializable(&ser).unwrap();
2731        // `<C-r>` twice: s3 -> s4 -> s5 (the retained forward branch).
2732        let (r, c, m) = l("s3");
2733        assert_eq!(t.redo_step(r, c, m).unwrap().rope.to_string(), "s4");
2734        let (r, c, m) = l("s4");
2735        assert_eq!(t.redo_step(r, c, m).unwrap().rope.to_string(), "s5");
2736        // `u` all the way back to the root.
2737        let mut live = "s5".to_string();
2738        for want in ["s4", "s3", "s2", "s1", "s0"] {
2739            let (r, c, m) = l(&live);
2740            assert_eq!(t.undo_step(r, c, m).unwrap().rope.to_string(), want);
2741            live = want.to_string();
2742        }
2743        assert!(t.is_at_root());
2744    }
2745
2746    #[test]
2747    fn from_serializable_rejects_out_of_range_current() {
2748        let mut ser = headline_tree().to_serializable();
2749        ser.current = ser.nodes.len() as u32; // past the end
2750        assert!(UndoTree::from_serializable(&ser).is_none());
2751    }
2752
2753    #[test]
2754    fn from_serializable_rejects_non_root_missing_delta() {
2755        let mut ser = headline_tree().to_serializable();
2756        // Blank a non-root node's delta ⇒ structurally invalid ⇒ rejected.
2757        let victim = if ser.root == 0 { 1 } else { 0 };
2758        ser.nodes[victim].delta = None;
2759        assert!(UndoTree::from_serializable(&ser).is_none());
2760    }
2761
2762    #[test]
2763    fn multibyte_content_survives_round_trip() {
2764        let mut t = UndoTree::new(ropey::Rope::from_str("café\n日本語"));
2765        t.push(e("café\n日本語"));
2766        t.push(e("cafés\n日本語"));
2767        t.sync_current(ropey::Rope::from_str("cafés\n日本語です🎉"));
2768        let want = content_by_seq(&mut t);
2769        let ser = t.to_serializable();
2770        let mut back = UndoTree::from_serializable(&ser).unwrap();
2771        assert_eq!(content_by_seq(&mut back), want);
2772    }
2773}