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::time::{Duration, SystemTime, UNIX_EPOCH};
8
9use serde::{Deserialize, Serialize};
10
11/// A single entry in the undo or redo stack.
12///
13/// The `timestamp` records the wall-clock time at which the snapshot was
14/// taken (i.e. when `push_undo` was called), enabling the `:earlier` /
15/// `:later` time-travel ex commands to walk the stack by duration rather
16/// than by step count.
17///
18/// Stored as a `ropey::Rope` (O(1) Arc-clone) rather than a `String` so
19/// snapshot cost is negligible even on multi-MB buffers.
20#[derive(Debug, Clone)]
21pub struct UndoEntry {
22    pub rope: ropey::Rope,
23    pub cursor: (usize, usize),
24    pub timestamp: SystemTime,
25    /// Local marks / jumplist / changelist / this-buffer's-global-marks
26    /// snapshot, so undo/redo restore mark-ish positions alongside the
27    /// text instead of leaving them shifted by the edit being undone
28    /// (audit-r2 fix 2). `Default::default()` (all empty) for callers
29    /// that don't populate it — restoring an all-empty snapshot is a
30    /// no-op against a freshly-constructed buffer's own empty state, so
31    /// existing fixtures that only care about text/cursor stay valid.
32    pub marks: MarkSnapshot,
33}
34
35/// Buffer-scoped "edit coherence" state snapshotted alongside a
36/// [`UndoEntry`]'s rope so undo/redo can restore marks, not just text.
37///
38/// Positions are plain `(row, col)` (or `(row, col)` values keyed by
39/// mark char) — no buffer-id tagging needed here even for
40/// `global_marks`, because a `MarkSnapshot` always belongs to exactly
41/// one buffer's undo stack; the engine is responsible for reattaching
42/// its own `buffer_id` when writing entries back into the session-global
43/// marks map (see `Editor::restore_marks`).
44#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
45pub struct MarkSnapshot {
46    /// `ma`-`mz` local marks (`View::marks_cloned`).
47    pub local_marks: BTreeMap<char, (usize, usize)>,
48    /// Back-jumplist (`Ctrl-o` stack), newest at the back.
49    pub jump_back: Vec<(usize, usize)>,
50    /// Forward-jumplist (`Ctrl-i` stack), newest at the back.
51    pub jump_fwd: Vec<(usize, usize)>,
52    /// `` `. ``  / `'.` — position of the most recent change.
53    pub change_last_edit: Option<(usize, usize)>,
54    /// Changelist ring (`g;` / `g,`).
55    pub change_list: Vec<(usize, usize)>,
56    /// Walk cursor into `change_list`; `None` outside a walk.
57    pub change_cursor: Option<usize>,
58    /// `mA`-`mZ` global marks that belong to THIS buffer (bare
59    /// `(row, col)` — the buffer-id is implicit, this buffer).
60    pub global_marks: BTreeMap<char, (usize, usize)>,
61}
62
63// ─── Reversible edge delta (Phase 3a) ──────────────────────────────────────────
64//
65// Phase 2b stored a FULL rope snapshot on every node. Phase 3a stores only a
66// reversible **delta** on each parent→child edge (the root keeps a full base
67// rope) plus a materialization cache, so the in-RAM hot path stays snapshot-fast
68// while a future undofile shrinks from hundreds of MB to KB. This slice changes
69// ONLY internal storage — every public signature, and every observable
70// behaviour, is byte-identical to Phase 2b.
71
72/// A reversible edit between two adjacent buffer states, expressed as a single
73/// spanning replacement in **char-offset space** on the rope.
74///
75/// The index space is ropey `char` offsets throughout — never bytes — so
76/// multi-byte UTF-8 round-trips (a byte offset could split a codepoint). In the
77/// PARENT state `chars[start .. start + old.chars().count()] == old`; replacing
78/// that region with `new` yields the CHILD state, and swapping the two inverts
79/// it. A whole undo group collapses to the one region spanning its edits
80/// (common-prefix / common-suffix diff); a `Vec<Delta>` for disjoint regions is
81/// an acceptable future generalization, but one spanning region is all Phase 3a
82/// needs.
83#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
84pub struct Delta {
85    /// Char offset of the first differing char (the common-prefix length).
86    pub start: usize,
87    /// Chars present in the PARENT but not the CHILD (removed going forward).
88    pub old: String,
89    /// Chars present in the CHILD but not the PARENT (inserted going forward).
90    pub new: String,
91}
92
93/// Common-prefix / common-suffix diff of two ropes → the minimal single spanning
94/// [`Delta`]. Guarantees `apply_forward(a, diff(a, b)) == b` and
95/// `apply_inverse(b, diff(a, b)) == a` for ALL `a`, `b` (see the property
96/// tests). Boundaries are found on bytes (fast) then snapped to char boundaries
97/// so `old`/`new` are always valid UTF-8 and `start` is a true char offset.
98fn diff(parent: &ropey::Rope, child: &ropey::Rope) -> Delta {
99    let a = parent.to_string();
100    let b = child.to_string();
101    let ab = a.as_bytes();
102    let bb = b.as_bytes();
103
104    // Longest common byte prefix, snapped DOWN to a char boundary.
105    let max_pre = ab.len().min(bb.len());
106    let mut pre = 0;
107    while pre < max_pre && ab[pre] == bb[pre] {
108        pre += 1;
109    }
110    while pre > 0 && !a.is_char_boundary(pre) {
111        pre -= 1;
112    }
113
114    // Longest common byte suffix not overlapping the prefix. The cut points
115    // `a_end`/`b_end` sit at identical trailing bytes, so snapping `a_end` UP to
116    // a char boundary snaps `b_end` by the same byte delta simultaneously.
117    let max_suf = max_pre - pre;
118    let mut suf = 0;
119    while suf < max_suf && ab[ab.len() - 1 - suf] == bb[bb.len() - 1 - suf] {
120        suf += 1;
121    }
122    let mut a_end = ab.len() - suf;
123    while a_end < ab.len() && !a.is_char_boundary(a_end) {
124        a_end += 1;
125    }
126    let b_end = bb.len() - (ab.len() - a_end);
127
128    Delta {
129        start: a[..pre].chars().count(),
130        old: a[pre..a_end].to_string(),
131        new: b[pre..b_end].to_string(),
132    }
133}
134
135/// Apply a forward delta (PARENT → CHILD) to `parent`, returning the child rope.
136fn apply_forward(parent: &ropey::Rope, d: &Delta) -> ropey::Rope {
137    let mut r = parent.clone();
138    let old_chars = d.old.chars().count();
139    r.remove(d.start..d.start + old_chars);
140    r.insert(d.start, &d.new);
141    r
142}
143
144/// Apply an inverse delta (CHILD → PARENT) to `child`, returning the parent rope.
145fn apply_inverse(child: &ropey::Rope, d: &Delta) -> ropey::Rope {
146    let mut r = child.clone();
147    let new_chars = d.new.chars().count();
148    r.remove(d.start..d.start + new_chars);
149    r.insert(d.start, &d.old);
150    r
151}
152
153// ─── Undo arena tree (Phase 2b + Phase 3a delta storage) ──────────────────────
154//
155// The undo history is a real arena TREE of buffer states (Phase 2a introduced
156// the arena; Phase 2b makes it branch; Phase 3a stores edges as deltas). An edit
157// after an undo FORKS a new child instead of truncating the forward branch, so
158// old branches stay reachable — matching nvim's undo tree. `seq` is
159// load-bearing: `g-`/`g+` and the `:earlier`/`:later` count forms walk ALL
160// states by global `seq` (see `seq_earlier_step`/`seq_later_step`), while
161// `u`/`<C-r>` stay branch-local (parent / `last_child`).
162//
163// The linear-history subset is unchanged: with no forks the tree is a single
164// root→current→leaf path and every operation degrades to the old two-stack
165// behaviour.
166//
167// - `current` points at the node representing the LIVE buffer state.
168// - The ancestors of `current` (parent, … up to `root`) are the reachable undo
169//   line; `current.parent` is the `u` target.
170// - `current.last_child` is the `<C-r>` target. Landing on any node (undo,
171//   redo, or a `g-`/`g+` jump) rewrites `last_child` down the root→node path so
172//   a later `<C-r>` retraces the branch just taken.
173//
174// Storage (Phase 3a): each non-root node holds the reversible `delta` on its
175// edge from `parent`; the root holds a full `base` rope. A node's content is
176// reconstructed on demand (`materialize`) from the nearest cached ancestor (or
177// the root base) by replaying forward deltas, or — for the `u`/`<C-r>` hot path
178// — from the adjacent warm node by one delta apply. Recently materialized ropes
179// are kept in a bounded LRU (`warm`); `current` is always kept warm. A node's
180// `delta`/content is FINALIZED lazily on the way past it (whenever the live rope
181// is written into it), never read as a restore target until then — so the fresh
182// leaf `current` holds a placeholder edge that is corrected before it matters.
183
184/// Index into [`UndoTree::nodes`]. Slots are reused via a free list, so an id is
185/// only valid while the node it names is live — the tree never hands ids out.
186pub(crate) type NodeId = usize;
187
188/// How many recently-materialized node ropes to keep warm (besides the root
189/// base and `current`, which are always available). A cold jump beyond this
190/// window replays deltas from the nearest warm ancestor — rare and bounded.
191const WARM_CAP: usize = 16;
192
193/// One node of the undo arena tree: a buffer state the user could land on, plus
194/// its links and the reversible edge to its parent. A node with `> 1` child is a
195/// branch point (Phase 2b); `last_child` records which child `<C-r>` follows.
196#[derive(Debug, Clone)]
197pub(crate) struct UndoNode {
198    pub parent: Option<NodeId>,
199    pub children: Vec<NodeId>,
200    pub last_child: Option<NodeId>,
201    /// Reversible edit from the parent's content to this node's content. `None`
202    /// only for the root (and any node promoted to root by pruning), which holds
203    /// `base` instead.
204    pub delta: Option<Delta>,
205    /// Full base rope. `Some` ONLY for the root — the anchor the delta chain
206    /// replays from. Non-root nodes leave this `None` and carry a `delta`.
207    pub base: Option<ropey::Rope>,
208    /// Materialized content, LRU-managed. Warm for `current` and recently
209    /// visited nodes; `None` (cold) otherwise, reconstructable from deltas.
210    pub rope_cache: Option<ropey::Rope>,
211    /// Post-state cursor for this node (restored alongside the text).
212    pub cursor: (usize, usize),
213    /// Wall-clock time this state was created — drives `:earlier`/`:later`.
214    pub timestamp: SystemTime,
215    /// Marks / jumplist / changelist snapshot restored with the text.
216    pub marks: MarkSnapshot,
217    /// Global monotonic order across the whole tree — the change number that
218    /// `g-`/`g+`, `:earlier`/`:later`, and `:undolist` traverse and display.
219    pub seq: u64,
220}
221
222/// Arena tree of [`UndoNode`]s. Replaces the old `undo_stack`/`redo_stack`
223/// `Vec<UndoEntry>` pair on [`crate::Buffer`]; see the module comment for how
224/// `u`/`<C-r>` (branch-local) and `g-`/`g+` (seq-ordered) map onto it, and how
225/// Phase 3a stores edges as deltas behind a materialization cache.
226#[derive(Debug)]
227pub(crate) struct UndoTree {
228    /// Slab; `None` slots are free and recorded in `free`.
229    nodes: Vec<Option<UndoNode>>,
230    /// Reusable slot indices (frees push here, allocs pop here first).
231    free: Vec<NodeId>,
232    /// LRU of node ids with a warm `rope_cache` (root excluded — it uses
233    /// `base`), most-recently-touched last. Bounded by [`WARM_CAP`]; `current`
234    /// is never evicted.
235    warm: Vec<NodeId>,
236    root: NodeId,
237    current: NodeId,
238    next_seq: u64,
239}
240
241impl UndoTree {
242    /// New tree with a single root == current node holding `rope` as its base
243    /// state (the buffer as opened / last saved). The root is always
244    /// materializable from this base.
245    pub(crate) fn new(rope: ropey::Rope) -> Self {
246        let root = UndoNode {
247            parent: None,
248            children: Vec::new(),
249            last_child: None,
250            delta: None,
251            base: Some(rope),
252            rope_cache: None,
253            cursor: (0, 0),
254            timestamp: SystemTime::now(),
255            marks: MarkSnapshot::default(),
256            seq: 0,
257        };
258        Self {
259            nodes: vec![Some(root)],
260            free: Vec::new(),
261            warm: Vec::new(),
262            root: 0,
263            current: 0,
264            next_seq: 1,
265        }
266    }
267
268    // ── slab helpers ─────────────────────────────────────────────────────────
269
270    fn get(&self, id: NodeId) -> &UndoNode {
271        self.nodes[id].as_ref().expect("live NodeId")
272    }
273
274    fn get_mut(&mut self, id: NodeId) -> &mut UndoNode {
275        self.nodes[id].as_mut().expect("live NodeId")
276    }
277
278    fn alloc(&mut self, node: UndoNode) -> NodeId {
279        if let Some(id) = self.free.pop() {
280            self.nodes[id] = Some(node);
281            id
282        } else {
283            self.nodes.push(Some(node));
284            self.nodes.len() - 1
285        }
286    }
287
288    /// Free a single slot (does NOT recurse into children — callers detach
289    /// links first). Drops the node's delta + materialized cache and purges it
290    /// from the warm LRU.
291    fn free(&mut self, id: NodeId) {
292        self.nodes[id] = None;
293        self.free.push(id);
294        self.warm.retain(|&n| n != id);
295    }
296
297    // ── materialization (Phase 3a) ────────────────────────────────────────────
298
299    /// Record `id` as freshly materialized, evicting the coldest cache beyond
300    /// [`WARM_CAP`] (never the root — it has no cache — nor `current`).
301    fn touch_warm(&mut self, id: NodeId) {
302        if id == self.root {
303            return;
304        }
305        self.warm.retain(|&n| n != id);
306        self.warm.push(id);
307        while self.warm.len() > WARM_CAP {
308            let Some(pos) = self.warm.iter().position(|&n| n != self.current) else {
309                break;
310            };
311            let victim = self.warm.remove(pos);
312            if let Some(node) = self.nodes[victim].as_mut() {
313                node.rope_cache = None;
314            }
315        }
316    }
317
318    /// Materialize node `id`'s content, warming its cache. Uses the warm cache
319    /// if present, else the root `base`, else replays forward deltas from the
320    /// nearest materialized ancestor (or the root). Always terminates: the root
321    /// carries a base.
322    fn materialize(&mut self, id: NodeId) -> ropey::Rope {
323        if let Some(r) = &self.get(id).rope_cache {
324            return r.clone();
325        }
326        if let Some(base) = &self.get(id).base {
327            return base.clone();
328        }
329        // Walk up to the nearest ancestor that is warm or is the root, recording
330        // the path of nodes to replay forward.
331        let mut path = Vec::new();
332        let base_rope;
333        let mut anchor = id;
334        loop {
335            path.push(anchor);
336            let par = self
337                .get(anchor)
338                .parent
339                .expect("a non-root, non-based node always has a parent");
340            if let Some(r) = &self.get(par).rope_cache {
341                base_rope = r.clone();
342                break;
343            }
344            if let Some(b) = &self.get(par).base {
345                base_rope = b.clone();
346                break;
347            }
348            anchor = par;
349        }
350        let mut rope = base_rope;
351        for &node in path.iter().rev() {
352            let d = self
353                .get(node)
354                .delta
355                .clone()
356                .expect("a non-root node always carries its edge delta");
357            rope = apply_forward(&rope, &d);
358        }
359        self.get_mut(id).rope_cache = Some(rope.clone());
360        self.touch_warm(id);
361        rope
362    }
363
364    /// Reconstruct node `id`'s restorable [`UndoEntry`] — the byte-for-byte
365    /// equivalent of Phase 2b's `node.snapshot.clone()`.
366    fn entry_of(&mut self, id: NodeId) -> UndoEntry {
367        let rope = self.materialize(id);
368        let n = self.get(id);
369        UndoEntry {
370            rope,
371            cursor: n.cursor,
372            timestamp: n.timestamp,
373            marks: n.marks.clone(),
374        }
375    }
376
377    /// Finalize node `id` to hold `rope` as its content, recomputing its edge
378    /// delta (or the root base) and updating cursor/timestamp/marks. A no-op
379    /// diff is skipped when the content is unchanged (the common case on a
380    /// history walk, where only the fields move) — which also avoids
381    /// materializing the parent, keeping the walk cheap.
382    fn set_node_state(
383        &mut self,
384        id: NodeId,
385        rope: ropey::Rope,
386        cursor: (usize, usize),
387        timestamp: SystemTime,
388        marks: MarkSnapshot,
389    ) {
390        let is_root = self.get(id).parent.is_none();
391        let unchanged = self.get(id).rope_cache.as_ref() == Some(&rope)
392            || (is_root && self.get(id).base.as_ref() == Some(&rope));
393        {
394            let node = self.get_mut(id);
395            node.cursor = cursor;
396            node.timestamp = timestamp;
397            node.marks = marks;
398        }
399        if unchanged {
400            return;
401        }
402        if is_root {
403            self.get_mut(id).base = Some(rope);
404            // The root is materialized from `base`; keep no stale cache.
405            self.get_mut(id).rope_cache = None;
406            self.warm.retain(|&n| n != id);
407        } else {
408            let par = self.get(id).parent.expect("non-root has a parent");
409            let par_rope = self.materialize(par);
410            let d = diff(&par_rope, &rope);
411            let node = self.get_mut(id);
412            node.delta = Some(d);
413            node.rope_cache = Some(rope);
414            self.touch_warm(id);
415        }
416    }
417
418    /// Free `id` and its whole subtree (iteratively, so a long redo chain can't
419    /// overflow the stack).
420    fn free_subtree(&mut self, id: NodeId) {
421        let mut stack = vec![id];
422        while let Some(n) = stack.pop() {
423            let kids = std::mem::take(&mut self.get_mut(n).children);
424            stack.extend(kids);
425            self.free(n);
426        }
427    }
428
429    // ── read-only queries (mirror the old stack accessors) ───────────────────
430
431    /// `undo_stack.is_empty()` ⇔ `current` has no parent (is the root).
432    pub(crate) fn is_at_root(&self) -> bool {
433        self.get(self.current).parent.is_none()
434    }
435
436    /// `!redo_stack.is_empty()` ⇔ `current` has a forward child.
437    pub(crate) fn has_redo(&self) -> bool {
438        self.get(self.current).last_child.is_some()
439    }
440
441    /// `undo_stack.len()` == number of ancestors of `current` (depth from root).
442    pub(crate) fn depth(&self) -> usize {
443        let mut d = 0;
444        let mut n = self.get(self.current).parent;
445        while let Some(p) = n {
446            d += 1;
447            n = self.get(p).parent;
448        }
449        d
450    }
451
452    /// `undo_stack.last().timestamp` == `current.parent`'s timestamp.
453    pub(crate) fn parent_timestamp(&self) -> Option<SystemTime> {
454        self.get(self.current).parent.map(|p| self.get(p).timestamp)
455    }
456
457    /// `redo_stack.last().timestamp` == `current.last_child`'s timestamp.
458    pub(crate) fn child_timestamp(&self) -> Option<SystemTime> {
459        self.get(self.current)
460            .last_child
461            .map(|c| self.get(c).timestamp)
462    }
463
464    // ── mutations ────────────────────────────────────────────────────────────
465
466    /// Commit a new boundary from `current`, growing the tree (Phase 2b).
467    ///
468    /// `entry` is the pre-edit LIVE state. It is written into `current`'s
469    /// snapshot (making `current` a real, restorable state), then a fresh child
470    /// is APPENDED and becomes the new `current` for the edit about to happen.
471    ///
472    /// Unlike Phase 2a this does NOT drop `current`'s existing children: an edit
473    /// after an undo now forks a new branch and the old forward branch(es) stay
474    /// reachable via `g-`/`g+` and `:undolist`, matching nvim's undo tree. The
475    /// new child is made `last_child` so a subsequent `<C-r>` follows the branch
476    /// just created.
477    pub(crate) fn push(&mut self, entry: UndoEntry) {
478        let cur = self.current;
479        // Finalize the node being left with the pre-edit live state, recomputing
480        // its edge delta from its parent (or the root base).
481        self.set_node_state(
482            cur,
483            entry.rope.clone(),
484            entry.cursor,
485            entry.timestamp,
486            entry.marks.clone(),
487        );
488        let seq = self.next_seq;
489        self.next_seq += 1;
490        // Fresh child: identical to `cur` for now (empty edge delta + warm cache
491        // holding the pre-edit rope). Its true post-edit content is finalized on
492        // the way past it (next move) or by the next `push`, at which point the
493        // edge delta is recomputed against `cur`.
494        let child = self.alloc(UndoNode {
495            parent: Some(cur),
496            children: Vec::new(),
497            last_child: None,
498            delta: Some(Delta::default()),
499            base: None,
500            rope_cache: Some(entry.rope),
501            cursor: entry.cursor,
502            timestamp: entry.timestamp,
503            marks: entry.marks,
504            seq,
505        });
506        let cur_node = self.get_mut(cur);
507        // Append (retain old branches); the freshest child is the redo target.
508        cur_node.children.push(child);
509        cur_node.last_child = Some(child);
510        self.current = child;
511        self.touch_warm(child);
512    }
513
514    /// One undo step. `live` is the current buffer state (the node being left);
515    /// it is written into that node but INHERITS the destination (parent)
516    /// timestamp — byte-parity with the old dance, where the pushed redo entry
517    /// took the popped undo entry's timestamp. Returns the parent snapshot to
518    /// restore, or `None` at the root.
519    pub(crate) fn undo_step(
520        &mut self,
521        rope: ropey::Rope,
522        cursor: (usize, usize),
523        marks: MarkSnapshot,
524    ) -> Option<UndoEntry> {
525        let cur = self.current;
526        let par = self.get(cur).parent?;
527        let dest_ts = self.get(par).timestamp;
528        self.set_node_state(cur, rope, cursor, dest_ts, marks);
529        // Redo from the parent must return to the node we just left.
530        self.get_mut(par).last_child = Some(cur);
531        self.current = par;
532        // Hot-path materialization: derive the (possibly cold) parent from the
533        // just-finalized child by one inverse delta apply, so `u` never walks the
534        // ancestor chain even far outside the warm window.
535        if self.get(par).rope_cache.is_none() && self.get(par).base.is_none() {
536            let child_rope = self.get(cur).rope_cache.clone();
537            let child_delta = self.get(cur).delta.clone();
538            if let (Some(cr), Some(d)) = (child_rope, child_delta) {
539                let par_rope = apply_inverse(&cr, &d);
540                self.get_mut(par).rope_cache = Some(par_rope);
541                self.touch_warm(par);
542            }
543        }
544        Some(self.entry_of(par))
545    }
546
547    /// One redo step. Symmetric to [`Self::undo_step`]: `live` is written into
548    /// the node being left (which becomes an undo ancestor) with the
549    /// destination (child) timestamp. Returns the child snapshot to restore, or
550    /// `None` when there is no forward branch.
551    pub(crate) fn redo_step(
552        &mut self,
553        rope: ropey::Rope,
554        cursor: (usize, usize),
555        marks: MarkSnapshot,
556    ) -> Option<UndoEntry> {
557        let cur = self.current;
558        let child = self.get(cur).last_child?;
559        let dest_ts = self.get(child).timestamp;
560        self.set_node_state(cur, rope, cursor, dest_ts, marks);
561        self.current = child;
562        // `cur` is now warm, so materializing the child is one forward apply.
563        Some(self.entry_of(child))
564    }
565
566    // ── seq-ordered tree walk (`g-` / `g+`, `:earlier`/`:later` — Phase 2b) ───
567    //
568    // `u`/`<C-r>` are branch-local (parent / `last_child`); `g-`/`g+` traverse
569    // ALL states by global `seq`, crossing branch boundaries. `g-` restores the
570    // node with the greatest `seq` strictly below `current`'s; `g+` the least
571    // `seq` strictly above. Confirmed against nvim v0.12.4 (`iA<Esc>uiB<Esc>`
572    // then `g-`/`g-g-`/`g-g+` walks empty↔A↔B by change number).
573
574    /// `seq` of the node the buffer currently shows.
575    fn current_seq(&self) -> u64 {
576        self.get(self.current).seq
577    }
578
579    /// Live node with the greatest `seq` strictly below `s` (the `g-` target).
580    fn node_below(&self, s: u64) -> Option<NodeId> {
581        let mut best: Option<(u64, NodeId)> = None;
582        for (id, slot) in self.nodes.iter().enumerate() {
583            if let Some(n) = slot
584                && n.seq < s
585                && best.is_none_or(|(bs, _)| n.seq > bs)
586            {
587                best = Some((n.seq, id));
588            }
589        }
590        best.map(|(_, id)| id)
591    }
592
593    /// Live node with the least `seq` strictly above `s` (the `g+` target).
594    fn node_above(&self, s: u64) -> Option<NodeId> {
595        let mut best: Option<(u64, NodeId)> = None;
596        for (id, slot) in self.nodes.iter().enumerate() {
597            if let Some(n) = slot
598                && n.seq > s
599                && best.is_none_or(|(bs, _)| n.seq < bs)
600            {
601                best = Some((n.seq, id));
602            }
603        }
604        best.map(|(_, id)| id)
605    }
606
607    /// Point `current` at `target` and rewrite `last_child` down the whole
608    /// root→target path, so a later `<C-r>` retraces the branch just landed on
609    /// (nvim parity: landing on a node updates its ancestors' redo direction).
610    fn retarget_current(&mut self, target: NodeId) {
611        self.current = target;
612        let mut node = target;
613        while let Some(p) = self.get(node).parent {
614            self.get_mut(p).last_child = Some(node);
615            node = p;
616        }
617    }
618
619    /// Stash the live buffer state into the node being left (it may be a fresh,
620    /// still-stale leaf), preserving that node's own timestamp, then move.
621    fn stash_and_move(
622        &mut self,
623        target: NodeId,
624        rope: ropey::Rope,
625        cursor: (usize, usize),
626        marks: MarkSnapshot,
627    ) {
628        let cur = self.current;
629        let ts = self.get(cur).timestamp;
630        self.set_node_state(cur, rope, cursor, ts, marks);
631        self.retarget_current(target);
632    }
633
634    /// One `g-` / `:earlier` step: move to the next-lower-`seq` node tree-wide.
635    /// Returns its snapshot to restore, or `None` at the lowest state.
636    pub(crate) fn seq_earlier_step(
637        &mut self,
638        rope: ropey::Rope,
639        cursor: (usize, usize),
640        marks: MarkSnapshot,
641    ) -> Option<UndoEntry> {
642        let target = self.node_below(self.current_seq())?;
643        self.stash_and_move(target, rope, cursor, marks);
644        Some(self.entry_of(target))
645    }
646
647    /// One `g+` / `:later` step: move to the next-higher-`seq` node tree-wide.
648    /// Returns its snapshot to restore, or `None` at the highest state.
649    pub(crate) fn seq_later_step(
650        &mut self,
651        rope: ropey::Rope,
652        cursor: (usize, usize),
653        marks: MarkSnapshot,
654    ) -> Option<UndoEntry> {
655        let target = self.node_above(self.current_seq())?;
656        self.stash_and_move(target, rope, cursor, marks);
657        Some(self.entry_of(target))
658    }
659
660    /// Timestamp of the next-lower-`seq` node (the `:earlier Ns` predicate walks
661    /// the seq order tree-wide, stopping once this dips to/below the cutoff).
662    pub(crate) fn seq_earlier_timestamp(&self) -> Option<SystemTime> {
663        self.node_below(self.current_seq())
664            .map(|id| self.get(id).timestamp)
665    }
666
667    /// Timestamp of the next-higher-`seq` node (the `:later Ns` predicate).
668    pub(crate) fn seq_later_timestamp(&self) -> Option<SystemTime> {
669        self.node_above(self.current_seq())
670            .map(|id| self.get(id).timestamp)
671    }
672
673    /// Leaves of the tree (nodes with no children), each as
674    /// `(seq, depth-from-root, timestamp, is_current)`, sorted by `seq`.
675    /// Drives `:undolist`, which — like nvim — lists only branch leaves.
676    pub(crate) fn leaves(&self) -> Vec<(u64, usize, SystemTime, bool)> {
677        let mut out: Vec<(u64, usize, SystemTime, bool)> = Vec::new();
678        for (id, slot) in self.nodes.iter().enumerate() {
679            let Some(n) = slot else { continue };
680            // The root is the base state (change number 0), never a listed
681            // "change" — like nvim, an untouched buffer lists nothing.
682            if id == self.root || !n.children.is_empty() {
683                continue;
684            }
685            // Depth = number of ancestors (root leaf ⇒ 0).
686            let mut depth = 0;
687            let mut p = n.parent;
688            while let Some(pid) = p {
689                depth += 1;
690                p = self.get(pid).parent;
691            }
692            out.push((n.seq, depth, n.timestamp, id == self.current));
693        }
694        out.sort_by_key(|&(seq, ..)| seq);
695        out
696    }
697
698    /// Number of live nodes (used by [`Self::cap`] as the state budget).
699    fn live_count(&self) -> usize {
700        self.nodes.iter().filter(|n| n.is_some()).count()
701    }
702
703    /// `undo_stack.pop()` — discard the most-recent boundary WITHOUT moving the
704    /// live state. Used by `:s` with zero replacements and by a no-op undo
705    /// group; in both, `current` is the childless leaf the last [`Self::push`]
706    /// created, so reverse that push: drop the leaf, step `current` back to its
707    /// parent (its snapshot equals the unchanged buffer), and restore the
708    /// parent's `last_child`. Retains any sibling branches the push appended to.
709    /// Returns `false` at the root, or if `current` is not a childless leaf
710    /// (nothing safe to pop).
711    pub(crate) fn pop_committed(&mut self) -> bool {
712        let cur = self.current;
713        if !self.get(cur).children.is_empty() {
714            return false;
715        }
716        let Some(par) = self.get(cur).parent else {
717            return false;
718        };
719        let par_node = self.get_mut(par);
720        par_node.children.retain(|&c| c != cur);
721        // The freshest surviving sibling (if any) becomes the redo target again.
722        par_node.last_child = par_node.children.last().copied();
723        self.current = par;
724        // The popped leaf always holds the highest seq (push assigns it last),
725        // so reclaim the seq to keep numbering gapless.
726        if self.get(cur).seq + 1 == self.next_seq {
727            self.next_seq -= 1;
728        }
729        self.free(cur);
730        true
731    }
732
733    /// Node budget (`undolevels`). While the number of undo states (live nodes
734    /// minus the root) exceeds `cap`, prune — branch-aware (Phase 2b):
735    ///
736    /// 1. First drop the lowest-`seq` LEAF that is NOT on the root→`current`
737    ///    path — an abandoned branch tip. This never touches `current` or its
738    ///    ancestors, so the state you're on and its full undo line survive.
739    /// 2. When only the main line remains (no off-path leaves left), fall back
740    ///    to promoting the root's on-path child to root and dropping the old
741    ///    root — the Phase 2a root-side prune, which matches nvim's linear
742    ///    `undolevels` trimming (oldest states drop first).
743    ///
744    /// `cap == 0` means unlimited (matches the old guard).
745    pub(crate) fn cap(&mut self, cap: usize) {
746        if cap == 0 {
747            return;
748        }
749        // Guard against a pathological loop: at most one prune per live node.
750        let mut budget_iters = self.live_count() + 1;
751        while self.live_count().saturating_sub(1) > cap && budget_iters > 0 {
752            budget_iters -= 1;
753            if let Some(leaf) = self.lowest_offpath_leaf() {
754                self.detach_leaf(leaf);
755            } else if !self.prune_root_side() {
756                break;
757            }
758        }
759    }
760
761    /// Ids on the root→`current` path (inclusive), which pruning must never
762    /// touch. Small (one per undo level), so a `Vec` membership check is fine.
763    fn current_path(&self) -> Vec<NodeId> {
764        let mut path = Vec::new();
765        let mut n = Some(self.current);
766        while let Some(id) = n {
767            path.push(id);
768            n = self.get(id).parent;
769        }
770        path
771    }
772
773    /// Lowest-`seq` leaf that is not on the root→`current` path, if any.
774    fn lowest_offpath_leaf(&self) -> Option<NodeId> {
775        let path = self.current_path();
776        let mut best: Option<(u64, NodeId)> = None;
777        for (id, slot) in self.nodes.iter().enumerate() {
778            if let Some(n) = slot
779                && n.children.is_empty()
780                && !path.contains(&id)
781                && best.is_none_or(|(bs, _)| n.seq < bs)
782            {
783                best = Some((n.seq, id));
784            }
785        }
786        best.map(|(_, id)| id)
787    }
788
789    /// Unlink `leaf` from its parent and free it (leaf ⇒ no subtree to recurse).
790    fn detach_leaf(&mut self, leaf: NodeId) {
791        if let Some(par) = self.get(leaf).parent {
792            let par_node = self.get_mut(par);
793            par_node.children.retain(|&c| c != leaf);
794            if par_node.last_child == Some(leaf) {
795                par_node.last_child = par_node.children.last().copied();
796            }
797        }
798        self.free(leaf);
799    }
800
801    /// Promote the root's on-path child to the new root and free the old root.
802    /// Returns `false` when the root is `current` (nothing left to trim).
803    fn prune_root_side(&mut self) -> bool {
804        let root = self.root;
805        if root == self.current {
806            return false;
807        }
808        // The child on the path to `current` (the root always has one here).
809        let path = self.current_path();
810        let Some(&child) = self.get(root).children.iter().find(|c| path.contains(c)) else {
811            return false;
812        };
813        // Any OTHER root children are off-path branches; drop them with the root.
814        let others: Vec<NodeId> = self
815            .get(root)
816            .children
817            .iter()
818            .copied()
819            .filter(|&c| c != child)
820            .collect();
821        for c in others {
822            self.free_subtree(c);
823        }
824        // The promoted child becomes the new root: materialize it (while the old
825        // root still anchors the chain) into a full base rope, then drop its
826        // now-meaningless parent edge. This keeps every delta below it valid.
827        let base = self.materialize(child);
828        {
829            let node = self.get_mut(child);
830            node.parent = None;
831            node.base = Some(base);
832            node.delta = None;
833            node.rope_cache = None;
834        }
835        self.warm.retain(|&n| n != child);
836        self.root = child;
837        self.free(root);
838        true
839    }
840
841    /// `redo_stack.clear()` — drop `current`'s forward branch.
842    pub(crate) fn clear_redo(&mut self) {
843        let cur = self.current;
844        let kids = std::mem::take(&mut self.get_mut(cur).children);
845        self.get_mut(cur).last_child = None;
846        for c in kids {
847            self.free_subtree(c);
848        }
849    }
850
851    /// `undo_stack.clear(); redo_stack.clear()` — collapse to a single root ==
852    /// current node, preserving the live state. Frees every other node.
853    pub(crate) fn clear_all(&mut self) {
854        let cur = self.current;
855        // The survivor becomes a self-contained root: give it a full base rope
856        // (materialized while the chain is still intact) so it needs no parent.
857        let base = self.materialize(cur);
858        for id in 0..self.nodes.len() {
859            if id != cur && self.nodes[id].is_some() {
860                self.nodes[id] = None;
861                self.free.push(id);
862            }
863        }
864        self.warm.clear();
865        let node = self.get_mut(cur);
866        node.parent = None;
867        node.children.clear();
868        node.last_child = None;
869        node.delta = None;
870        node.base = Some(base);
871        node.rope_cache = None;
872        self.root = cur;
873    }
874}
875
876// ─── Serializable projection (Phase 3b) ───────────────────────────────────────
877//
878// The undofile persists the tree as a compact, self-consistent projection: the
879// root's full base text (String) plus, per node, its edge `delta` and links.
880// `rope_cache`/`warm` are runtime-only and dropped — every node reconstructs
881// from the root base + deltas, so the round-trip reproduces identical content
882// at every node. NodeIds are DENSE in the projection (the live-slab holes are
883// compacted away and links remapped), so `from_serializable` rebuilds a fresh
884// arena 1:1 with no free list.
885
886/// One node of the serialized undo tree. Mirrors [`UndoNode`] minus the
887/// runtime-only materialization cache; ids are dense indices into
888/// [`SerTree::nodes`].
889#[derive(Debug, Clone, Serialize, Deserialize)]
890pub struct SerNode {
891    /// Parent index, `None` only for the root.
892    pub parent: Option<u32>,
893    /// Child indices (order preserved; `> 1` ⇒ branch point).
894    pub children: Vec<u32>,
895    /// `<C-r>` target child index.
896    pub last_child: Option<u32>,
897    /// Reversible edge delta from the parent, `None` only for the root.
898    pub delta: Option<Delta>,
899    /// Post-state cursor `(row, col)`.
900    pub cursor: (u32, u32),
901    /// Wall-clock creation time, ms since the UNIX epoch.
902    pub timestamp_unix_ms: u64,
903    /// Marks / jumplist / changelist snapshot.
904    pub marks: MarkSnapshot,
905    /// Global monotonic change number.
906    pub seq: u64,
907}
908
909/// Serializable projection of an [`UndoTree`] for the undofile. Postcard-encoded
910/// (non-self-describing, so a schema/version drift surfaces as a parse `Err`
911/// that the reader discards). See [`UndoTree::to_serializable`] /
912/// [`UndoTree::from_serializable`].
913#[derive(Debug, Clone, Serialize, Deserialize)]
914pub struct SerTree {
915    /// Root base text (the anchor the delta chain replays from).
916    pub base: String,
917    /// Dense node arena (no holes).
918    pub nodes: Vec<SerNode>,
919    /// Root index into `nodes`.
920    pub root: u32,
921    /// Current (live) index into `nodes`.
922    pub current: u32,
923    /// Next `seq` to assign.
924    pub next_seq: u64,
925}
926
927/// [`SystemTime`] → ms since the UNIX epoch (saturating, pre-epoch ⇒ 0).
928fn system_time_to_unix_ms(t: SystemTime) -> u64 {
929    t.duration_since(UNIX_EPOCH)
930        .map(|d| d.as_millis() as u64)
931        .unwrap_or(0)
932}
933
934/// ms since the UNIX epoch → [`SystemTime`].
935fn unix_ms_to_system_time(ms: u64) -> SystemTime {
936    UNIX_EPOCH + Duration::from_millis(ms)
937}
938
939impl UndoTree {
940    /// `seq` of the current (live) node — the header's `current_seq` for the
941    /// undofile (the just-saved content per the §6 invariant).
942    pub(crate) fn current_node_seq(&self) -> u64 {
943        self.get(self.current).seq
944    }
945
946    /// Materialize the current (live) node's content. Used by the swap
947    /// recovery consistency guard (docs §6c) to check a deserialized tree
948    /// agrees with the freshly-recovered buffer text before it's installed.
949    pub(crate) fn current_content(&mut self) -> ropey::Rope {
950        let cur = self.current;
951        self.materialize(cur)
952    }
953
954    /// Stash `rope` into the current node as the live buffer state, preserving
955    /// that node's own cursor/timestamp/marks. Called just before serializing so
956    /// the on-disk tree's `current` edge is exact even when `current` is a fresh
957    /// (still-stale) leaf — the in-session self-heal (first undo/edit stashes
958    /// live) applied eagerly at save time.
959    pub(crate) fn sync_current(&mut self, rope: ropey::Rope) {
960        let cur = self.current;
961        let (cursor, ts, marks) = {
962            let n = self.get(cur);
963            (n.cursor, n.timestamp, n.marks.clone())
964        };
965        self.set_node_state(cur, rope, cursor, ts, marks);
966    }
967
968    /// Project the live tree into a serializable, dense form (holes compacted,
969    /// links remapped). `rope_cache`/`warm` are dropped; the root's `base`
970    /// carries the anchor text and every non-root node its edge `delta`.
971    pub(crate) fn to_serializable(&self) -> SerTree {
972        // Dense remap: old NodeId → new index, in slab order.
973        let mut map: Vec<Option<u32>> = vec![None; self.nodes.len()];
974        let mut order: Vec<NodeId> = Vec::new();
975        for (id, slot) in self.nodes.iter().enumerate() {
976            if slot.is_some() {
977                map[id] = Some(order.len() as u32);
978                order.push(id);
979            }
980        }
981        let remap = |id: NodeId| map[id].expect("live link points at a live node");
982        let nodes = order
983            .iter()
984            .map(|&id| {
985                let n = self.get(id);
986                SerNode {
987                    parent: n.parent.map(remap),
988                    children: n.children.iter().map(|&c| remap(c)).collect(),
989                    last_child: n.last_child.map(remap),
990                    delta: n.delta.clone(),
991                    cursor: (n.cursor.0 as u32, n.cursor.1 as u32),
992                    timestamp_unix_ms: system_time_to_unix_ms(n.timestamp),
993                    marks: n.marks.clone(),
994                    seq: n.seq,
995                }
996            })
997            .collect();
998        let base = self
999            .get(self.root)
1000            .base
1001            .as_ref()
1002            .map(|r| r.to_string())
1003            .unwrap_or_default();
1004        SerTree {
1005            base,
1006            nodes,
1007            root: remap(self.root),
1008            current: remap(self.current),
1009            next_seq: self.next_seq,
1010        }
1011    }
1012
1013    /// Rebuild an arena tree from a projection. Returns `None` on any structural
1014    /// inconsistency (out-of-range link, a non-root node missing its delta, a
1015    /// root carrying one) so a corrupt-but-parseable file degrades to a fresh
1016    /// tree rather than a broken one. The root's content comes from `base`; the
1017    /// current node's is materialized on demand from base + deltas.
1018    pub(crate) fn from_serializable(s: &SerTree) -> Option<Self> {
1019        let len = s.nodes.len();
1020        if len == 0 || s.root as usize >= len || s.current as usize >= len {
1021            return None;
1022        }
1023        // Validate links and the root/non-root delta discipline up front.
1024        for (i, n) in s.nodes.iter().enumerate() {
1025            let is_root = i as u32 == s.root;
1026            match (is_root, &n.delta, &n.parent) {
1027                (true, None, None) => {}
1028                (false, Some(_), Some(_)) => {}
1029                _ => return None,
1030            }
1031            if let Some(p) = n.parent
1032                && p as usize >= len
1033            {
1034                return None;
1035            }
1036            if n.children.iter().any(|&c| c as usize >= len) {
1037                return None;
1038            }
1039            if let Some(c) = n.last_child
1040                && c as usize >= len
1041            {
1042                return None;
1043            }
1044        }
1045        let base = ropey::Rope::from_str(&s.base);
1046        let nodes: Vec<Option<UndoNode>> = s
1047            .nodes
1048            .iter()
1049            .enumerate()
1050            .map(|(i, n)| {
1051                let is_root = i as u32 == s.root;
1052                Some(UndoNode {
1053                    parent: n.parent.map(|p| p as NodeId),
1054                    children: n.children.iter().map(|&c| c as NodeId).collect(),
1055                    last_child: n.last_child.map(|c| c as NodeId),
1056                    delta: n.delta.clone(),
1057                    base: if is_root { Some(base.clone()) } else { None },
1058                    rope_cache: None,
1059                    cursor: (n.cursor.0 as usize, n.cursor.1 as usize),
1060                    timestamp: unix_ms_to_system_time(n.timestamp_unix_ms),
1061                    marks: n.marks.clone(),
1062                    seq: n.seq,
1063                })
1064            })
1065            .collect();
1066        Some(Self {
1067            nodes,
1068            free: Vec::new(),
1069            warm: Vec::new(),
1070            root: s.root as NodeId,
1071            current: s.current as NodeId,
1072            next_seq: s.next_seq,
1073        })
1074    }
1075}
1076
1077#[cfg(test)]
1078impl UndoTree {
1079    /// Ids of every live node, for warm-vs-cold materialization checks.
1080    fn live_ids(&self) -> Vec<NodeId> {
1081        (0..self.nodes.len())
1082            .filter(|&i| self.nodes[i].is_some())
1083            .collect()
1084    }
1085
1086    /// Materialize `id` for a test (public wrapper over the private method).
1087    fn materialize_for_test(&mut self, id: NodeId) -> ropey::Rope {
1088        self.materialize(id)
1089    }
1090
1091    /// Evict every warm cache (root keeps its `base`), forcing the next
1092    /// materialization of any node to reconstruct purely from deltas.
1093    fn drop_all_caches(&mut self) {
1094        for n in self.nodes.iter_mut().flatten() {
1095            n.rope_cache = None;
1096        }
1097        self.warm.clear();
1098    }
1099}
1100
1101#[cfg(test)]
1102mod tree_tests {
1103    use super::*;
1104
1105    fn entry(text: &str) -> UndoEntry {
1106        UndoEntry {
1107            rope: ropey::Rope::from_str(text),
1108            cursor: (0, 0),
1109            timestamp: SystemTime::now(),
1110            marks: MarkSnapshot::default(),
1111        }
1112    }
1113
1114    fn live(text: &str) -> (ropey::Rope, (usize, usize), MarkSnapshot) {
1115        (ropey::Rope::from_str(text), (0, 0), MarkSnapshot::default())
1116    }
1117
1118    #[test]
1119    fn fresh_tree_is_root_current_empty() {
1120        let t = UndoTree::new(ropey::Rope::from_str("hello"));
1121        assert!(t.is_at_root());
1122        assert!(!t.has_redo());
1123        assert_eq!(t.depth(), 0);
1124        assert_eq!(t.root, t.current);
1125    }
1126
1127    #[test]
1128    fn push_links_child_and_advances_current() {
1129        let mut t = UndoTree::new(ropey::Rope::from_str("hello"));
1130        let root = t.current;
1131        t.push(entry("hello"));
1132        // root now parents current; current is a fresh leaf.
1133        assert_eq!(t.get(t.current).parent, Some(root));
1134        assert_eq!(t.get(root).last_child, Some(t.current));
1135        assert_eq!(t.get(root).children, vec![t.current]);
1136        assert_eq!(t.depth(), 1);
1137        assert!(!t.has_redo());
1138        assert!(!t.is_at_root());
1139    }
1140
1141    #[test]
1142    fn undo_then_redo_round_trips_links() {
1143        let mut t = UndoTree::new(ropey::Rope::from_str("s0"));
1144        t.push(entry("s0")); // commit s0, current = n1 (live s1)
1145        let n0 = t.root;
1146        let n1 = t.current;
1147        // undo: current -> n0, restores s0.
1148        let (r, c, m) = live("s1");
1149        let restored = t.undo_step(r, c, m).unwrap();
1150        assert_eq!(restored.rope.to_string(), "s0");
1151        assert_eq!(t.current, n0);
1152        assert!(t.has_redo());
1153        assert_eq!(t.get(n0).last_child, Some(n1));
1154        // redo: current -> n1, restores what we left (s1).
1155        let (r, c, m) = live("s0");
1156        let restored = t.redo_step(r, c, m).unwrap();
1157        assert_eq!(restored.rope.to_string(), "s1");
1158        assert_eq!(t.current, n1);
1159        assert!(!t.has_redo());
1160    }
1161
1162    #[test]
1163    fn undo_at_root_and_redo_at_leaf_are_noops() {
1164        let mut t = UndoTree::new(ropey::Rope::from_str("x"));
1165        let (r, c, m) = live("x");
1166        assert!(t.undo_step(r, c, m).is_none());
1167        let (r, c, m) = live("x");
1168        assert!(t.redo_step(r, c, m).is_none());
1169        assert_eq!(t.depth(), 0);
1170    }
1171
1172    #[test]
1173    fn push_retains_forward_branch() {
1174        // Phase 2b: an edit after an undo forks a new branch; the old forward
1175        // branch is NOT dropped and remains reachable by seq.
1176        let mut t = UndoTree::new(ropey::Rope::from_str("s0"));
1177        t.push(entry("A")); // root -> nA (seq1, "A")
1178        let root = t.root;
1179        let na = t.current;
1180        let (r, c, m) = live("A");
1181        t.undo_step(r, c, m); // back to root, nA is the redo child
1182        assert!(t.has_redo());
1183        // A new edit from the root forks a SECOND child (nB, seq2).
1184        t.push(entry("B"));
1185        let nb = t.current;
1186        assert_ne!(nb, na);
1187        // Both branches live: root now has two children.
1188        assert_eq!(t.get(root).children.len(), 2);
1189        assert!(t.get(root).children.contains(&na));
1190        assert!(t.get(root).children.contains(&nb));
1191        // `<C-r>` follows the freshest branch (nB).
1192        assert_eq!(t.get(root).last_child, Some(nb));
1193        // Four live nodes: root + nA + nB + (nB is current/leaf). No leak of nA.
1194        let live = t.nodes.iter().filter(|n| n.is_some()).count();
1195        assert_eq!(live, 3);
1196    }
1197
1198    #[test]
1199    fn seq_walk_crosses_branches() {
1200        // Mirror nvim `iA<Esc>uiB<Esc>` then g-/g+ (buffer starts empty "").
1201        // `push(entry)` writes `entry` into the node being LEFT (its true
1202        // pre-edit content); the fresh leaf holds the live post-edit state only
1203        // once it is stashed on the way past — exactly the engine's discipline.
1204        let mut t = UndoTree::new(ropey::Rope::from_str(""));
1205        t.push(entry("")); // leave root("") -> nA(seq1), live "A"
1206        let (r, c, m) = live("A");
1207        t.undo_step(r, c, m); // stash "A" into nA, back to root("")
1208        t.push(entry("")); // leave root("") -> nB(seq2), branch, live "B"
1209        let nb = t.current;
1210        // At B (seq2). g- -> greatest seq below 2 = seq1 = "A".
1211        let (r, c, m) = live("B");
1212        let a = t.seq_earlier_step(r, c, m).unwrap();
1213        assert_eq!(a.rope.to_string(), "A");
1214        // g- again -> root "".
1215        let (r, c, m) = live("A");
1216        let root_snap = t.seq_earlier_step(r, c, m).unwrap();
1217        assert_eq!(root_snap.rope.to_string(), "");
1218        // g+ -> back up to seq1 "A".
1219        let (r, c, m) = live("");
1220        let a2 = t.seq_later_step(r, c, m).unwrap();
1221        assert_eq!(a2.rope.to_string(), "A");
1222        // g+ -> seq2 "B" (crosses to the other branch).
1223        let (r, c, m) = live("A");
1224        let b = t.seq_later_step(r, c, m).unwrap();
1225        assert_eq!(b.rope.to_string(), "B");
1226        assert_eq!(t.current, nb);
1227        // At the tip: no higher seq.
1228        let (r, c, m) = live("B");
1229        assert!(t.seq_later_step(r, c, m).is_none());
1230    }
1231
1232    #[test]
1233    fn seq_walk_updates_retrace_path() {
1234        // Land on a deep leaf via g-, then u/u and <C-r>/<C-r> must retrace it
1235        // (nvim `iX<Esc>iY<Esc>uiZ<Esc>g-uu<C-r><C-r>`). State labels: root "R".
1236        let mut t = UndoTree::new(ropey::Rope::from_str("R"));
1237        t.push(entry("R")); // leave root("R") -> nX(seq1), live "X"
1238        t.push(entry("X")); // leave nX("X") -> nY(seq2), live "Y"
1239        let (r, c, m) = live("Y");
1240        t.undo_step(r, c, m); // stash "Y" into nY, back to nX("X")
1241        t.push(entry("X")); // leave nX("X") -> nZ(seq3), branch, live "Z"
1242        // g- from Z(seq3) -> nY(seq2) "Y".
1243        let (r, c, m) = live("Z");
1244        let y = t.seq_earlier_step(r, c, m).unwrap();
1245        assert_eq!(y.rope.to_string(), "Y");
1246        // u,u back to root.
1247        let (r, c, m) = live("Y");
1248        t.undo_step(r, c, m);
1249        let (r, c, m) = live("X");
1250        t.undo_step(r, c, m);
1251        assert!(t.is_at_root());
1252        // <C-r>,<C-r> retraces the branch we landed on: root->X->Y.
1253        let (r, c, m) = live("R");
1254        let x = t.redo_step(r, c, m).unwrap();
1255        assert_eq!(x.rope.to_string(), "X");
1256        let (r, c, m) = live("X");
1257        let y2 = t.redo_step(r, c, m).unwrap();
1258        assert_eq!(y2.rope.to_string(), "Y");
1259    }
1260
1261    #[test]
1262    fn leaves_lists_branch_tips_by_seq() {
1263        // root -> nX -> nY -> nW (leaf, seq3, depth3) and nX -> nZ (leaf, seq4,
1264        // depth2). Mirrors nvim `iX iY iW uu iZ`.
1265        let mut t = UndoTree::new(ropey::Rope::from_str(""));
1266        t.push(entry("X"));
1267        t.push(entry("Y"));
1268        t.push(entry("W"));
1269        let (r, c, m) = live("W");
1270        t.undo_step(r, c, m);
1271        let (r, c, m) = live("Y");
1272        t.undo_step(r, c, m); // back to nX
1273        t.push(entry("Z")); // nX -> nZ(seq4)
1274        let leaves = t.leaves();
1275        // Two leaves: W(seq3, depth3) and Z(seq4, depth2). Z is current.
1276        let dims: Vec<(u64, usize, bool)> =
1277            leaves.iter().map(|&(s, d, _, cur)| (s, d, cur)).collect();
1278        assert_eq!(dims, vec![(3, 3, false), (4, 2, true)]);
1279    }
1280
1281    #[test]
1282    fn cap_prunes_oldest_from_root_side() {
1283        let mut t = UndoTree::new(ropey::Rope::from_str("s"));
1284        for _ in 0..5 {
1285            t.push(entry("s"));
1286        }
1287        assert_eq!(t.depth(), 5);
1288        t.cap(3);
1289        assert_eq!(t.depth(), 3);
1290        // Redo side untouched (there is none), current unchanged.
1291        assert!(!t.has_redo());
1292        // Two oldest slots were reclaimed.
1293        assert_eq!(t.free.len(), 2);
1294    }
1295
1296    #[test]
1297    fn cap_drops_offpath_leaf_before_main_line() {
1298        // Fork two abandoned branches off the root, then extend the main line,
1299        // and cap: the lowest-seq OFF-PATH leaf must go first, and `current`
1300        // plus its ancestors must survive.
1301        let mut t = UndoTree::new(ropey::Rope::from_str(""));
1302        t.push(entry("A")); // root -> nA(seq1) [abandoned branch tip]
1303        let na = t.current;
1304        let (r, c, m) = live("A");
1305        t.undo_step(r, c, m);
1306        t.push(entry("B")); // root -> nB(seq2) [abandoned branch tip]
1307        let nb = t.current;
1308        let (r, c, m) = live("B");
1309        t.undo_step(r, c, m);
1310        t.push(entry("C")); // root -> nC(seq3), the live main line
1311        let nc = t.current;
1312        // 4 live nodes (root, nA, nB, nC) => 3 states. Cap to 2.
1313        assert_eq!(t.leaves().len(), 3);
1314        t.cap(2);
1315        // The lowest-seq off-path leaf (nA, seq1) was dropped; current (nC) and
1316        // its ancestor (root) survive, and the newer off-path leaf nB survives.
1317        assert!(t.nodes[na].is_none());
1318        assert!(t.nodes[nb].is_some());
1319        assert_eq!(t.current, nc);
1320        assert!(!t.is_at_root());
1321        assert!(t.get(t.root).children.contains(&nb));
1322        assert!(t.get(t.root).children.contains(&nc));
1323    }
1324
1325    #[test]
1326    fn pop_committed_reverses_last_push() {
1327        let mut t = UndoTree::new(ropey::Rope::from_str("s0"));
1328        t.push(entry("s0")); // depth 1, current = fresh leaf
1329        assert_eq!(t.depth(), 1);
1330        assert!(t.pop_committed());
1331        // The just-pushed leaf is gone; current stepped back to the root.
1332        assert_eq!(t.depth(), 0);
1333        assert!(t.is_at_root());
1334        assert_eq!(t.free.len(), 1);
1335        // Seq reclaimed so the next push is gapless.
1336        assert_eq!(t.next_seq, 1);
1337    }
1338
1339    #[test]
1340    fn pop_committed_retains_sibling_branches() {
1341        // Fork a branch, then a no-op push at the fork must pop cleanly without
1342        // orphaning the sibling branch.
1343        let mut t = UndoTree::new(ropey::Rope::from_str(""));
1344        t.push(entry("A")); // root -> nA(seq1)
1345        let na = t.current;
1346        let (r, c, m) = live("A");
1347        t.undo_step(r, c, m); // back to root
1348        t.push(entry("B")); // root -> nB(seq2); root children [nA, nB]
1349        let root = t.root;
1350        // A spurious no-op push at nB, then pop it.
1351        assert!(t.pop_committed());
1352        // nB is gone, current back at root; nA branch still intact & reachable.
1353        assert!(t.get(root).children.contains(&na));
1354        assert_eq!(t.get(root).children.len(), 1);
1355        assert_eq!(t.current, root);
1356        let live = t.nodes.iter().filter(|n| n.is_some()).count();
1357        assert_eq!(live, 2); // root + nA
1358    }
1359
1360    #[test]
1361    fn pop_committed_at_root_is_false() {
1362        let mut t = UndoTree::new(ropey::Rope::from_str("s"));
1363        assert!(!t.pop_committed());
1364    }
1365
1366    #[test]
1367    fn clear_redo_drops_forward_only() {
1368        let mut t = UndoTree::new(ropey::Rope::from_str("s0"));
1369        t.push(entry("s0"));
1370        let (r, c, m) = live("s1");
1371        t.undo_step(r, c, m);
1372        assert!(t.has_redo());
1373        assert_eq!(t.depth(), 0);
1374        t.clear_redo();
1375        assert!(!t.has_redo());
1376        assert_eq!(t.depth(), 0);
1377    }
1378
1379    #[test]
1380    fn clear_all_collapses_to_single_node() {
1381        let mut t = UndoTree::new(ropey::Rope::from_str("s"));
1382        for _ in 0..3 {
1383            t.push(entry("s"));
1384        }
1385        t.clear_all();
1386        assert!(t.is_at_root());
1387        assert!(!t.has_redo());
1388        assert_eq!(t.depth(), 0);
1389        assert_eq!(t.root, t.current);
1390    }
1391}
1392
1393// ─── Phase 3a delta-storage tests ─────────────────────────────────────────────
1394//
1395// Correctness of the reversible delta and the warm/cold materialization is
1396// where text gets silently corrupted, so these lean hard on it: exact diff
1397// round-trips over random (incl. multi-byte) content, every node reconstructing
1398// identically warm and cold, and a random op stream cross-checked against a
1399// full-snapshot reference model kept alongside. All randomness is a deterministic
1400// xorshift seeded from a fixed constant — never `SystemTime`/entropy — so a
1401// failure reproduces exactly.
1402#[cfg(test)]
1403mod delta_tests {
1404    use super::*;
1405
1406    /// Deterministic xorshift64* PRNG, fixed-seeded so runs are reproducible.
1407    struct Rng(u64);
1408    impl Rng {
1409        fn new(seed: u64) -> Self {
1410            // xorshift needs a non-zero state.
1411            Rng(if seed == 0 {
1412                0x9E37_79B9_7F4A_7C15
1413            } else {
1414                seed
1415            })
1416        }
1417        fn next_u64(&mut self) -> u64 {
1418            let mut x = self.0;
1419            x ^= x >> 12;
1420            x ^= x << 25;
1421            x ^= x >> 27;
1422            self.0 = x;
1423            x.wrapping_mul(0x2545_F491_4F6C_DD1D)
1424        }
1425        fn below(&mut self, n: usize) -> usize {
1426            (self.next_u64() % n as u64) as usize
1427        }
1428    }
1429
1430    /// A random char-granular mutation of `s`: insert, delete, or replace a
1431    /// span, drawing from an alphabet that mixes ASCII, accented, CJK, and
1432    /// emoji so multi-byte boundaries are exercised.
1433    fn mutate(s: &str, rng: &mut Rng) -> String {
1434        const ALPHABET: [char; 10] = ['a', 'b', '\n', 'é', '日', '本', '🎉', '語', 'x', 'z'];
1435        let chars: Vec<char> = s.chars().collect();
1436        let pick = |rng: &mut Rng| ALPHABET[rng.below(ALPHABET.len())];
1437        match rng.below(3) {
1438            0 => {
1439                let pos = rng.below(chars.len() + 1);
1440                let mut v = chars.clone();
1441                v.insert(pos, pick(rng));
1442                v.into_iter().collect()
1443            }
1444            1 if !chars.is_empty() => {
1445                let pos = rng.below(chars.len());
1446                let mut v = chars.clone();
1447                v.remove(pos);
1448                v.into_iter().collect()
1449            }
1450            _ => {
1451                if chars.is_empty() {
1452                    return pick(rng).to_string();
1453                }
1454                let a = rng.below(chars.len());
1455                let b = (a + rng.below(chars.len() - a + 1)).min(chars.len());
1456                let mut v = chars[..a].to_vec();
1457                v.push(pick(rng));
1458                v.extend_from_slice(&chars[b..]);
1459                v.into_iter().collect()
1460            }
1461        }
1462    }
1463
1464    fn entry_str(s: &str) -> UndoEntry {
1465        UndoEntry {
1466            rope: ropey::Rope::from_str(s),
1467            cursor: (0, 0),
1468            timestamp: SystemTime::now(),
1469            marks: MarkSnapshot::default(),
1470        }
1471    }
1472
1473    // ── (i) delta round-trip: apply(diff(a,b))==b and apply_inverse==a ────────
1474
1475    #[test]
1476    fn diff_round_trips_over_random_evolving_content() {
1477        let mut rng = Rng::new(0x1234_5678_9ABC_DEF0);
1478        let mut s = String::from("seed café 日本語\n🎉");
1479        for _ in 0..4000 {
1480            let t = mutate(&s, &mut rng);
1481            let a = ropey::Rope::from_str(&s);
1482            let b = ropey::Rope::from_str(&t);
1483            let d = diff(&a, &b);
1484            assert_eq!(
1485                apply_forward(&a, &d).to_string(),
1486                t,
1487                "forward a->b failed (start={}, old={:?}, new={:?})",
1488                d.start,
1489                d.old,
1490                d.new
1491            );
1492            assert_eq!(
1493                apply_inverse(&b, &d).to_string(),
1494                s,
1495                "inverse b->a failed (start={}, old={:?}, new={:?})",
1496                d.start,
1497                d.old,
1498                d.new
1499            );
1500            s = t;
1501        }
1502    }
1503
1504    #[test]
1505    fn diff_round_trips_over_unrelated_pairs() {
1506        // Disjoint corpus pairs (not just single-edit neighbours) so the diff's
1507        // prefix/suffix logic is stressed on wholly different multi-byte text.
1508        let corpus = [
1509            "",
1510            "a",
1511            "café\n日本語\n",
1512            "🎉🎉🎉",
1513            "abcdef",
1514            "日本",
1515            "x\ny\nz\n",
1516            "aXb",
1517            "café",
1518            "語日本",
1519            "\n\n\n",
1520            "🎉x🎉y🎉",
1521        ];
1522        let mut rng = Rng::new(0xDEAD_BEEF_CAFE_1234);
1523        for _ in 0..3000 {
1524            let sa = corpus[rng.below(corpus.len())];
1525            let sb = corpus[rng.below(corpus.len())];
1526            let a = ropey::Rope::from_str(sa);
1527            let b = ropey::Rope::from_str(sb);
1528            let d = diff(&a, &b);
1529            assert_eq!(apply_forward(&a, &d).to_string(), sb);
1530            assert_eq!(apply_inverse(&b, &d).to_string(), sa);
1531        }
1532    }
1533
1534    // ── non-ASCII edit → undo → redo round-trip (multi-byte across a leave) ───
1535
1536    #[test]
1537    fn non_ascii_edit_undo_redo_round_trip() {
1538        // Edits land INSIDE multi-byte lines; undo/redo must round-trip the exact
1539        // bytes, proving the char-offset delta never splits a codepoint.
1540        let mut d = Driver::new("café\n日本語\n");
1541        d.edit("cafés\n日本語\n");
1542        d.edit("cafés\n日本語です\n");
1543        d.edit("cafés\n日本語です🎉\n");
1544        assert_eq!(d.undo().as_deref(), Some("cafés\n日本語です\n"));
1545        assert_eq!(d.undo().as_deref(), Some("cafés\n日本語\n"));
1546        assert_eq!(d.undo().as_deref(), Some("café\n日本語\n"));
1547        assert_eq!(d.redo().as_deref(), Some("cafés\n日本語\n"));
1548        assert_eq!(d.redo().as_deref(), Some("cafés\n日本語です\n"));
1549        assert_eq!(d.redo().as_deref(), Some("cafés\n日本語です🎉\n"));
1550        // Cold reconstruction of every node still matches (drop all caches).
1551        assert_warm_equals_cold(&mut d.t);
1552    }
1553
1554    // ── (ii) + (iii) random op stream vs a full-snapshot reference model ──────
1555
1556    #[test]
1557    fn tree_matches_full_snapshot_reference_over_random_ops() {
1558        let mut rng = Rng::new(0x9E37_79B9_7F4A_7C15);
1559        let start = "α\nβγ\n日本🎉\n";
1560        let mut real = UndoTree::new(ropey::Rope::from_str(start));
1561        let mut refr = RefTree::new(start);
1562        let mut live = start.to_string();
1563
1564        for step in 0..6000 {
1565            // Structural predicates stay in lockstep with the reference.
1566            assert_eq!(real.is_at_root(), refr.is_at_root(), "is_at_root @ {step}");
1567            assert_eq!(real.has_redo(), refr.has_redo(), "has_redo @ {step}");
1568            assert_eq!(real.depth(), refr.depth(), "depth @ {step}");
1569
1570            match rng.below(6) {
1571                0 | 1 => {
1572                    // Edit: push the PRE-edit state (engine discipline), then
1573                    // mutate the live buffer.
1574                    let pre = live.clone();
1575                    real.push(entry_str(&pre));
1576                    refr.push(&pre);
1577                    live = mutate(&live, &mut rng);
1578                }
1579                2 => {
1580                    let got = real
1581                        .undo_step(
1582                            ropey::Rope::from_str(&live),
1583                            (0, 0),
1584                            MarkSnapshot::default(),
1585                        )
1586                        .map(|e| e.rope.to_string());
1587                    let want = refr.undo_step(&live);
1588                    assert_eq!(got, want, "undo @ {step}");
1589                    if let Some(c) = got {
1590                        live = c;
1591                    }
1592                }
1593                3 => {
1594                    let got = real
1595                        .redo_step(
1596                            ropey::Rope::from_str(&live),
1597                            (0, 0),
1598                            MarkSnapshot::default(),
1599                        )
1600                        .map(|e| e.rope.to_string());
1601                    let want = refr.redo_step(&live);
1602                    assert_eq!(got, want, "redo @ {step}");
1603                    if let Some(c) = got {
1604                        live = c;
1605                    }
1606                }
1607                4 => {
1608                    let got = real
1609                        .seq_earlier_step(
1610                            ropey::Rope::from_str(&live),
1611                            (0, 0),
1612                            MarkSnapshot::default(),
1613                        )
1614                        .map(|e| e.rope.to_string());
1615                    let want = refr.seq_earlier_step(&live);
1616                    assert_eq!(got, want, "g- @ {step}");
1617                    if let Some(c) = got {
1618                        live = c;
1619                    }
1620                }
1621                _ => {
1622                    let got = real
1623                        .seq_later_step(
1624                            ropey::Rope::from_str(&live),
1625                            (0, 0),
1626                            MarkSnapshot::default(),
1627                        )
1628                        .map(|e| e.rope.to_string());
1629                    let want = refr.seq_later_step(&live);
1630                    assert_eq!(got, want, "g+ @ {step}");
1631                    if let Some(c) = got {
1632                        live = c;
1633                    }
1634                }
1635            }
1636
1637            // (ii) Every so often, assert warm and cold materialization agree
1638            // for every node — a cold-reconstructed node must equal the rope the
1639            // full-snapshot model would have held.
1640            if step % 200 == 0 {
1641                assert_warm_equals_cold(&mut real);
1642            }
1643        }
1644        assert_warm_equals_cold(&mut real);
1645    }
1646
1647    /// For every live node: materialize warm, drop all caches, materialize cold,
1648    /// assert identical. Restores nothing else (test-local).
1649    fn assert_warm_equals_cold(t: &mut UndoTree) {
1650        let ids = t.live_ids();
1651        let warm: Vec<String> = ids
1652            .iter()
1653            .map(|&id| t.materialize_for_test(id).to_string())
1654            .collect();
1655        t.drop_all_caches();
1656        for (i, &id) in ids.iter().enumerate() {
1657            let cold = t.materialize_for_test(id).to_string();
1658            assert_eq!(cold, warm[i], "warm != cold for node {id}");
1659        }
1660    }
1661
1662    /// Engine-faithful driver over the real (delta) [`UndoTree`]: mirrors how
1663    /// `editor.rs` pushes the PRE-edit state and restores returned content.
1664    struct Driver {
1665        t: UndoTree,
1666        live: String,
1667    }
1668    impl Driver {
1669        fn new(s: &str) -> Self {
1670            Driver {
1671                t: UndoTree::new(ropey::Rope::from_str(s)),
1672                live: s.to_string(),
1673            }
1674        }
1675        fn edit(&mut self, new: &str) {
1676            self.t.push(entry_str(&self.live));
1677            self.live = new.to_string();
1678        }
1679        fn undo(&mut self) -> Option<String> {
1680            let e = self.t.undo_step(
1681                ropey::Rope::from_str(&self.live),
1682                (0, 0),
1683                MarkSnapshot::default(),
1684            )?;
1685            self.live = e.rope.to_string();
1686            Some(self.live.clone())
1687        }
1688        fn redo(&mut self) -> Option<String> {
1689            let e = self.t.redo_step(
1690                ropey::Rope::from_str(&self.live),
1691                (0, 0),
1692                MarkSnapshot::default(),
1693            )?;
1694            self.live = e.rope.to_string();
1695            Some(self.live.clone())
1696        }
1697    }
1698
1699    /// Full-snapshot reference tree — Phase 2b's model (a whole rope per node),
1700    /// the oracle the delta tree is cross-checked against. Content only (cursor /
1701    /// marks / timestamps are covered by the existing tree tests).
1702    struct RefNode {
1703        parent: Option<usize>,
1704        children: Vec<usize>,
1705        last_child: Option<usize>,
1706        content: String,
1707        seq: u64,
1708    }
1709    struct RefTree {
1710        nodes: Vec<Option<RefNode>>,
1711        current: usize,
1712        next_seq: u64,
1713    }
1714    impl RefTree {
1715        fn new(s: &str) -> Self {
1716            let root = RefNode {
1717                parent: None,
1718                children: Vec::new(),
1719                last_child: None,
1720                content: s.to_string(),
1721                seq: 0,
1722            };
1723            RefTree {
1724                nodes: vec![Some(root)],
1725                current: 0,
1726                next_seq: 1,
1727            }
1728        }
1729        fn get(&self, id: usize) -> &RefNode {
1730            self.nodes[id].as_ref().unwrap()
1731        }
1732        fn get_mut(&mut self, id: usize) -> &mut RefNode {
1733            self.nodes[id].as_mut().unwrap()
1734        }
1735        fn alloc(&mut self, n: RefNode) -> usize {
1736            self.nodes.push(Some(n));
1737            self.nodes.len() - 1
1738        }
1739        fn is_at_root(&self) -> bool {
1740            self.get(self.current).parent.is_none()
1741        }
1742        fn has_redo(&self) -> bool {
1743            self.get(self.current).last_child.is_some()
1744        }
1745        fn depth(&self) -> usize {
1746            let mut d = 0;
1747            let mut n = self.get(self.current).parent;
1748            while let Some(p) = n {
1749                d += 1;
1750                n = self.get(p).parent;
1751            }
1752            d
1753        }
1754        fn push(&mut self, pre: &str) {
1755            let cur = self.current;
1756            self.get_mut(cur).content = pre.to_string();
1757            let seq = self.next_seq;
1758            self.next_seq += 1;
1759            let child = self.alloc(RefNode {
1760                parent: Some(cur),
1761                children: Vec::new(),
1762                last_child: None,
1763                content: pre.to_string(),
1764                seq,
1765            });
1766            let c = self.get_mut(cur);
1767            c.children.push(child);
1768            c.last_child = Some(child);
1769            self.current = child;
1770        }
1771        fn undo_step(&mut self, live: &str) -> Option<String> {
1772            let cur = self.current;
1773            let par = self.get(cur).parent?;
1774            self.get_mut(cur).content = live.to_string();
1775            self.get_mut(par).last_child = Some(cur);
1776            self.current = par;
1777            Some(self.get(par).content.clone())
1778        }
1779        fn redo_step(&mut self, live: &str) -> Option<String> {
1780            let cur = self.current;
1781            let child = self.get(cur).last_child?;
1782            self.get_mut(cur).content = live.to_string();
1783            self.current = child;
1784            Some(self.get(child).content.clone())
1785        }
1786        fn current_seq(&self) -> u64 {
1787            self.get(self.current).seq
1788        }
1789        fn node_below(&self, s: u64) -> Option<usize> {
1790            let mut best: Option<(u64, usize)> = None;
1791            for (id, slot) in self.nodes.iter().enumerate() {
1792                if let Some(n) = slot
1793                    && n.seq < s
1794                    && best.is_none_or(|(bs, _)| n.seq > bs)
1795                {
1796                    best = Some((n.seq, id));
1797                }
1798            }
1799            best.map(|(_, id)| id)
1800        }
1801        fn node_above(&self, s: u64) -> Option<usize> {
1802            let mut best: Option<(u64, usize)> = None;
1803            for (id, slot) in self.nodes.iter().enumerate() {
1804                if let Some(n) = slot
1805                    && n.seq > s
1806                    && best.is_none_or(|(bs, _)| n.seq < bs)
1807                {
1808                    best = Some((n.seq, id));
1809                }
1810            }
1811            best.map(|(_, id)| id)
1812        }
1813        fn retarget(&mut self, target: usize) {
1814            self.current = target;
1815            let mut node = target;
1816            while let Some(p) = self.get(node).parent {
1817                self.get_mut(p).last_child = Some(node);
1818                node = p;
1819            }
1820        }
1821        fn stash_and_move(&mut self, target: usize, live: &str) {
1822            let cur = self.current;
1823            self.get_mut(cur).content = live.to_string();
1824            self.retarget(target);
1825        }
1826        fn seq_earlier_step(&mut self, live: &str) -> Option<String> {
1827            let target = self.node_below(self.current_seq())?;
1828            self.stash_and_move(target, live);
1829            Some(self.get(target).content.clone())
1830        }
1831        fn seq_later_step(&mut self, live: &str) -> Option<String> {
1832            let target = self.node_above(self.current_seq())?;
1833            self.stash_and_move(target, live);
1834            Some(self.get(target).content.clone())
1835        }
1836    }
1837}
1838
1839// ─── Phase 3b serialize/deserialize tests ─────────────────────────────────────
1840//
1841// The undofile is only as trustworthy as this round-trip: a projection that
1842// loses a branch, mislinks a parent, or reconstructs a node's content wrong
1843// would silently corrupt cross-session undo. These build the headline tree
1844// (5 edits, u, u), project it, rebuild, and assert BOTH the per-node content
1845// (keyed by the stable `seq`) and the live walk (`<C-r>` forward, `u` back)
1846// survive the trip.
1847#[cfg(test)]
1848mod serialize_tests {
1849    use super::*;
1850
1851    fn e(text: &str) -> UndoEntry {
1852        UndoEntry {
1853            rope: ropey::Rope::from_str(text),
1854            cursor: (0, 0),
1855            timestamp: SystemTime::now(),
1856            marks: MarkSnapshot::default(),
1857        }
1858    }
1859    fn l(text: &str) -> (ropey::Rope, (usize, usize), MarkSnapshot) {
1860        (ropey::Rope::from_str(text), (0, 0), MarkSnapshot::default())
1861    }
1862
1863    /// The headline tree: root "s0", five edits to live "s5", then `u` twice so
1864    /// `current` sits on "s3" with the forward branch (s4/s5) retained — exactly
1865    /// the state a `:wq` would persist.
1866    fn headline_tree() -> UndoTree {
1867        let mut t = UndoTree::new(ropey::Rope::from_str("s0"));
1868        for pre in ["s0", "s1", "s2", "s3", "s4"] {
1869            t.push(e(pre)); // engine discipline: push the PRE-edit live state
1870        }
1871        let (r, c, m) = l("s5");
1872        t.undo_step(r, c, m); // -> s4
1873        let (r, c, m) = l("s4");
1874        t.undo_step(r, c, m); // -> s3
1875        t.sync_current(ropey::Rope::from_str("s3")); // stash exact live, like save
1876        t
1877    }
1878
1879    /// Every node's content (keyed by `seq`), materialized cold-then-warm.
1880    fn content_by_seq(t: &mut UndoTree) -> std::collections::BTreeMap<u64, String> {
1881        t.live_ids()
1882            .into_iter()
1883            .map(|id| {
1884                let seq = t.get(id).seq;
1885                (seq, t.materialize_for_test(id).to_string())
1886            })
1887            .collect()
1888    }
1889
1890    #[test]
1891    fn round_trip_reproduces_structure_and_content() {
1892        let mut orig = headline_tree();
1893        let cur_seq = orig.current_node_seq();
1894        let ser = orig.to_serializable();
1895        let orig_content = content_by_seq(&mut orig);
1896
1897        let mut back = UndoTree::from_serializable(&ser).expect("valid projection");
1898        assert_eq!(back.current_node_seq(), cur_seq, "current preserved");
1899        assert_eq!(back.next_seq, orig.next_seq, "next_seq preserved");
1900        // Force cold reconstruction (fresh tree has no warm caches) and compare.
1901        assert_eq!(
1902            content_by_seq(&mut back),
1903            orig_content,
1904            "content at every node reproduced"
1905        );
1906        // Six states: s0..s5.
1907        assert_eq!(orig_content.len(), 6);
1908        assert_eq!(orig_content[&3], "s3");
1909        assert_eq!(orig_content[&5], "s5");
1910    }
1911
1912    #[test]
1913    fn deserialized_tree_walks_forward_and_back() {
1914        let ser = headline_tree().to_serializable();
1915        let mut t = UndoTree::from_serializable(&ser).unwrap();
1916        // `<C-r>` twice: s3 -> s4 -> s5 (the retained forward branch).
1917        let (r, c, m) = l("s3");
1918        assert_eq!(t.redo_step(r, c, m).unwrap().rope.to_string(), "s4");
1919        let (r, c, m) = l("s4");
1920        assert_eq!(t.redo_step(r, c, m).unwrap().rope.to_string(), "s5");
1921        // `u` all the way back to the root.
1922        let mut live = "s5".to_string();
1923        for want in ["s4", "s3", "s2", "s1", "s0"] {
1924            let (r, c, m) = l(&live);
1925            assert_eq!(t.undo_step(r, c, m).unwrap().rope.to_string(), want);
1926            live = want.to_string();
1927        }
1928        assert!(t.is_at_root());
1929    }
1930
1931    #[test]
1932    fn from_serializable_rejects_out_of_range_current() {
1933        let mut ser = headline_tree().to_serializable();
1934        ser.current = ser.nodes.len() as u32; // past the end
1935        assert!(UndoTree::from_serializable(&ser).is_none());
1936    }
1937
1938    #[test]
1939    fn from_serializable_rejects_non_root_missing_delta() {
1940        let mut ser = headline_tree().to_serializable();
1941        // Blank a non-root node's delta ⇒ structurally invalid ⇒ rejected.
1942        let victim = if ser.root == 0 { 1 } else { 0 };
1943        ser.nodes[victim].delta = None;
1944        assert!(UndoTree::from_serializable(&ser).is_none());
1945    }
1946
1947    #[test]
1948    fn multibyte_content_survives_round_trip() {
1949        let mut t = UndoTree::new(ropey::Rope::from_str("café\n日本語"));
1950        t.push(e("café\n日本語"));
1951        t.push(e("cafés\n日本語"));
1952        t.sync_current(ropey::Rope::from_str("cafés\n日本語です🎉"));
1953        let want = content_by_seq(&mut t);
1954        let ser = t.to_serializable();
1955        let mut back = UndoTree::from_serializable(&ser).unwrap();
1956        assert_eq!(content_by_seq(&mut back), want);
1957    }
1958}