Skip to main content

scrive_core/
history.rs

1//! Undo/redo as a **branching tree**, the grouping mechanism, and dirty
2//! tracking.
3//!
4//! History stores each committed transaction's ops in **both** directions
5//! (`forward` = parent→node, `inverse` = node→parent). Undo replays a node's
6//! inverse ops and redo replays a child's forward ops — both through the same
7//! `apply` engine (undo/redo are just transactions), so there is
8//! one mutation path in every direction and an inverse can never drift from
9//! what was applied.
10//!
11//! The structure is an **arena tree** (`UndoNode`), not two stacks: node `0`
12//! is the base state, every other node holds one `Element` (the edit from its
13//! parent). A new edit made after an undo becomes a *new child* of the current
14//! node — the branch you undid out of is **retained** as a sibling rather than
15//! discarded, so redo can be steered to it (`History::select_redo_branch`).
16//! Plain `undo`/`redo` follow
17//! `preferred_child`, which always points at the most-recent child, so with no
18//! branch navigation the tree reproduces classic linear undo exactly.
19//!
20//! Grouping here is the **mechanism**, not the policy. A [`GroupingHint`]
21//! supplied per edit says whether to seal the open element and what class the
22//! edit is; consecutive same-class edits merge into one undo node while it stays
23//! open. The classifier that *produces* those hints from keystrokes (seal on
24//! cursor move, glue on space, and the other coalescing rules that match how
25//! mainstream editors group typing into undo steps) lives in the verbs layer.
26//!
27//! Each undo unit also carries the **selection set from either side of it**
28//! (`sel_before` / `sel_after`), so undo hands back the carets the user had
29//! before the unit and redo the carets the unit left — a three-caret keystroke
30//! undoes to three carets, not one. For a merged unit (a typing run) the
31//! before-set is the one captured ahead of the run's FIRST keystroke and the
32//! after-set is overwritten by each merged step, so the unit restores as one
33//! piece in both directions. Both sets are exact, never rebased: undo returns
34//! the buffer byte-for-byte to the state the before-set was captured against,
35//! and redo to the after-set's. They are small next to the op text they
36//! accompany (one `Selection` per caret, against at least one op per caret).
37//!
38//! Dirty tracking uses an alternative-version id (`alt`): a fresh id per recorded
39//! transaction, stored per element and *restored* by undo/redo. This makes
40//! dirtiness a comparison against the id captured at the last save — undoing
41//! back to a save point reads clean, and a divergent edit made after an undo
42//! reads dirty even if the buffer text happens to coincide with the saved text.
43//! Because ids are monotonic and never reused, this stays correct across
44//! branches.
45
46use crate::buffer::Buffer;
47use crate::selection::SelectionSet;
48use crate::transaction::{apply, Committed, EditOp};
49
50/// Coarse edit class, for merge compatibility. The full classifier is the verbs
51/// layer; this is the vocabulary the grouping mechanism keys on.
52#[derive(Copy, Clone, PartialEq, Eq, Debug)]
53pub enum OpClass {
54    /// Character insertion — merges with adjacent typing.
55    Type,
56    /// Backspace/delete — merges with adjacent deletion.
57    Delete,
58    /// Anything discrete (paste, replace, indent, programmatic) — never merges.
59    Other,
60}
61
62/// Grouping instruction for one edit. `seal_before` closes the open element
63/// before this edit records; `seal_after` closes it afterward. `op` decides
64/// merge compatibility with the open element.
65#[derive(Copy, Clone, Debug)]
66pub struct GroupingHint {
67    /// This edit's class.
68    pub op: OpClass,
69    /// Close the open element before recording (start a fresh one).
70    pub seal_before: bool,
71    /// Close the element after recording.
72    pub seal_after: bool,
73}
74
75impl GroupingHint {
76    /// A typing/deleting edit that may merge with its neighbours (seals neither
77    /// side).
78    #[must_use]
79    pub fn mergeable(op: OpClass) -> Self {
80        Self { op, seal_before: false, seal_after: false }
81    }
82
83    /// A discrete edit that is its own undo step (seals both sides).
84    #[must_use]
85    pub fn discrete() -> Self {
86        Self { op: OpClass::Other, seal_before: true, seal_after: true }
87    }
88}
89
90/// One transaction inside an [`Element`], stored in both directions so a tree
91/// edge is traversable either way with no re-derivation. `forward` reproduces
92/// the edit (parent→node, replayed on redo); `inverse` reverts it (node→parent,
93/// replayed on undo). Both are produced by the one [`apply`] engine at record
94/// time (`forward` is the original ops; `inverse` is [`Committed::into_inverse`]).
95#[derive(Clone, Debug)]
96struct Step {
97    forward: Vec<EditOp>,
98    inverse: Vec<EditOp>,
99}
100
101/// One undo unit: the edit from a node's parent to the node, as a chronological
102/// list of [`Step`]s (one for a discrete edit, several for a merged typing run).
103/// `alt_before`/`alt_after` are the document versions on either side of the unit,
104/// and `sel_before`/`sel_after` the selection sets — what undo and redo restore.
105#[derive(Clone, Debug)]
106struct Element {
107    steps: Vec<Step>,
108    op: OpClass,
109    alt_before: u64,
110    alt_after: u64,
111    /// The selections ahead of the unit's FIRST step — set once, when the unit
112    /// opens; a merged step never touches it.
113    sel_before: SelectionSet,
114    /// The selections the unit's LATEST step left (verb caret placement
115    /// included) — overwritten by every merged step.
116    sel_after: SelectionSet,
117}
118
119/// Arena index into [`History::nodes`]. Node `0` is the root (base state).
120type NodeId = usize;
121
122/// One node of the undo tree. The root and pruned tombstones carry no
123/// [`Element`]; every other node's element is the edit from `parent` to here.
124#[derive(Clone, Debug)]
125struct UndoNode {
126    parent: Option<NodeId>,
127    children: Vec<NodeId>,
128    /// The child [`redo`](History::redo) follows when several exist — always the
129    /// most-recent child unless [`select_redo_branch`](History::select_redo_branch)
130    /// re-points it, so plain redo is classic-linear.
131    preferred_child: Option<NodeId>,
132    /// The edit from `parent` to this node (`None` for the root/tombstones).
133    elem: Option<Element>,
134}
135
136/// The branching undo/redo tree plus dirty tracking. Owned by the `Document`
137/// aggregate, which passes its buffer in for the replay.
138#[derive(Debug)]
139pub(crate) struct History {
140    /// Arena; node `0` is the root. Indices are stable for a node's lifetime.
141    nodes: Vec<UndoNode>,
142    /// Where we are in the tree.
143    current: NodeId,
144    /// Whether `current`'s element may still accept a merged edit.
145    open: bool,
146    /// Current alternative-version id.
147    alt: u64,
148    /// Monotonic id source (never reused → branch-safe dirty tracking).
149    alt_seq: u64,
150    /// The `alt` at the last save.
151    saved_alt: u64,
152    /// Opt-in cap on undo reach: keep at most this many undo units on the line to
153    /// `current`. `None` (the default) = unbounded. Dropping the oldest unit also
154    /// drops any branch that diverged before it (older than the kept window).
155    max_undo: Option<usize>,
156}
157
158impl History {
159    pub(crate) fn new() -> Self {
160        Self {
161            nodes: vec![UndoNode { parent: None, children: Vec::new(), preferred_child: None, elem: None }],
162            current: 0,
163            open: false,
164            alt: 0,
165            alt_seq: 0,
166            saved_alt: 0,
167            max_undo: None,
168        }
169    }
170
171    /// Set the undo-reach cap (`None` = unbounded, the default) and prune to it
172    /// immediately. Bounds history memory when a host wants it; the default
173    /// keeps every unit.
174    pub(crate) fn set_max_undo(&mut self, limit: Option<usize>) {
175        self.max_undo = limit;
176        self.prune_if_needed();
177    }
178
179    /// How many undo units are reachable from `current` (its ancestry depth).
180    pub(crate) fn undo_depth(&self) -> usize {
181        let mut depth = 0;
182        let mut node = self.current;
183        while let Some(parent) = self.nodes[node].parent {
184            node = parent;
185            depth += 1;
186        }
187        depth
188    }
189
190    /// Whether there is an edit at `current` to revert.
191    pub(crate) fn can_undo(&self) -> bool {
192        self.current != 0
193    }
194
195    /// Whether `current` has a redo branch to follow.
196    pub(crate) fn can_redo(&self) -> bool {
197        self.nodes[self.current].preferred_child.is_some()
198    }
199
200    /// Whether an edit recorded under `hint` right now would merge into
201    /// `current`'s element rather than open a new unit — the one merge rule,
202    /// shared by [`record`](Self::record) and the commit path, which asks it
203    /// BEFORE applying so it clones the pre-edit selections only for an edit
204    /// that opens a unit (a merged keystroke never needs them).
205    pub(crate) fn merges(&self, hint: GroupingHint) -> bool {
206        self.open
207            && !hint.seal_before
208            && hint.op != OpClass::Other
209            && self.nodes[self.current].elem.as_ref().is_some_and(|e| e.op == hint.op)
210    }
211
212    /// Record a committed transaction under `hint`, given its ops in both
213    /// directions (`forward` = the applied ops, `inverse` =
214    /// [`Committed::into_inverse`]) and the selections on either side of it:
215    /// `before` = the set ahead of the edit — required exactly when the edit
216    /// opens a unit (`!merges(hint)`), ignored when it merges — and `after` =
217    /// the set the edit left, caret placement included.
218    ///
219    /// Merges into `current`'s element when the classes match and nothing seals
220    /// between them; otherwise opens a **new child** of `current` and moves there.
221    /// Unlike a two-stack history there is no redo to clear — a sibling branch
222    /// created earlier simply remains reachable via [`select_redo_branch`].
223    pub(crate) fn record(
224        &mut self,
225        forward: Vec<EditOp>,
226        inverse: Vec<EditOp>,
227        hint: GroupingHint,
228        before: Option<SelectionSet>,
229        after: &SelectionSet,
230    ) {
231        // Decided before `seal_before` closes the element below — `merges`
232        // already reads `seal_before` as "won't merge".
233        let can_merge = self.merges(hint);
234        if hint.seal_before {
235            self.open = false;
236        }
237
238        let alt_before = self.alt;
239        self.alt_seq += 1;
240        self.alt = self.alt_seq;
241        let alt_after = self.alt;
242
243        let step = Step { forward, inverse };
244        if can_merge {
245            let elem = self.nodes[self.current].elem.as_mut().expect("open implies an element");
246            elem.steps.push(step);
247            elem.alt_after = alt_after;
248            // The run's before-set stays the one from ahead of its first step;
249            // only the after-set moves. `assign_from` reuses its buffer, so a
250            // merged keystroke allocates nothing here.
251            elem.sel_after.assign_from(after);
252        } else {
253            let sel_before =
254                before.expect("an edit that opens a unit carries its before-selections (see `merges`)");
255            let parent = self.current;
256            let new_id = self.nodes.len();
257            self.nodes.push(UndoNode {
258                parent: Some(parent),
259                children: Vec::new(),
260                preferred_child: None,
261                elem: Some(Element {
262                    steps: vec![step],
263                    op: hint.op,
264                    alt_before,
265                    alt_after,
266                    sel_before,
267                    sel_after: after.clone(),
268                }),
269            });
270            let parent_node = &mut self.nodes[parent];
271            parent_node.children.push(new_id);
272            parent_node.preferred_child = Some(new_id);
273            self.current = new_id;
274            self.open = true;
275        }
276
277        if hint.seal_after {
278            self.open = false;
279        }
280        self.prune_if_needed();
281    }
282
283    /// Drop the oldest undo units (and any branch older than the kept window) so
284    /// the reach from `current` stays within `max_undo`. A no-op when unbounded
285    /// or already within budget (so the default path pays nothing). Rebuilds the
286    /// arena from the new base — O(kept nodes), and only when over budget.
287    fn prune_if_needed(&mut self) {
288        let Some(max) = self.max_undo else { return };
289        if self.undo_depth() <= max {
290            return;
291        }
292        // The ancestor `max` steps above `current` becomes the new base state,
293        // keeping exactly `max` units on the line to `current` plus every branch
294        // that hangs off them; everything older is dropped.
295        let mut new_base = self.current;
296        for _ in 0..max {
297            new_base = self.nodes[new_base].parent.expect("depth > max implies enough ancestors");
298        }
299        self.rebuild_from(new_base);
300    }
301
302    /// Rebuild the arena keeping only the subtree rooted at `new_base`, remapping
303    /// ids into a fresh dense `Vec`. `new_base` becomes the root (no parent, no
304    /// element — it is now the base state); `current` and every `preferred_child`
305    /// are remapped. Nodes above/beside `new_base` are reclaimed.
306    fn rebuild_from(&mut self, new_base: NodeId) {
307        // BFS the kept subtree; `order[k]` is the old id that becomes new id `k`.
308        let mut remap = vec![usize::MAX; self.nodes.len()];
309        let mut order = vec![new_base];
310        remap[new_base] = 0;
311        let mut i = 0;
312        while i < order.len() {
313            let old = order[i];
314            for &child in &self.nodes[old].children {
315                if remap[child] == usize::MAX {
316                    remap[child] = order.len();
317                    order.push(child);
318                }
319            }
320            i += 1;
321        }
322        let fresh: Vec<UndoNode> = order
323            .iter()
324            .enumerate()
325            .map(|(new_id, &old)| {
326                let node = &self.nodes[old];
327                UndoNode {
328                    // The new base has no parent and no element (it is the base
329                    // state); its old parent lies outside the kept subtree.
330                    parent: if new_id == 0 { None } else { node.parent.map(|p| remap[p]) },
331                    children: node.children.iter().map(|&c| remap[c]).collect(),
332                    preferred_child: node.preferred_child.map(|c| remap[c]),
333                    elem: if new_id == 0 { None } else { node.elem.clone() },
334                }
335            })
336            .collect();
337        self.current = remap[self.current];
338        self.nodes = fresh;
339    }
340
341    /// Undo the edit at `current` against `buffer`, moving to the parent.
342    /// `on_step` fires after each reverted step with its [`Committed`] and the
343    /// post-step buffer, so the caller rebases its derived views through the same
344    /// patch — the one mover that keeps every derived view consistent across
345    /// undo. Returns the unit's **before-selections** for the caller to restore
346    /// (exact for the reverted buffer — see module docs), or `None` if there is
347    /// nothing to undo.
348    pub(crate) fn undo(
349        &mut self,
350        buffer: &mut Buffer,
351        on_step: impl FnMut(&Committed, &Buffer),
352    ) -> Option<SelectionSet> {
353        if self.current == 0 {
354            return None;
355        }
356        let elem = self.nodes[self.current].elem.clone().expect("non-root nodes carry an element");
357        // Revert the element's steps newest-first; each step's inverse is in the
358        // coordinates of the buffer state at that point (see module docs).
359        replay(buffer, elem.steps.iter().rev().map(|s| &s.inverse), on_step);
360        self.current = self.nodes[self.current].parent.expect("non-root nodes have a parent");
361        self.alt = elem.alt_before;
362        self.open = false;
363        Some(elem.sel_before)
364    }
365
366    /// Redo along `current`'s preferred branch — the [`undo`](Self::undo) mirror,
367    /// replaying the child's forward steps through the same mover. Returns the
368    /// unit's **after-selections** for the caller to restore, or `None` if there
369    /// is nothing to redo.
370    pub(crate) fn redo(
371        &mut self,
372        buffer: &mut Buffer,
373        on_step: impl FnMut(&Committed, &Buffer),
374    ) -> Option<SelectionSet> {
375        let child = self.nodes[self.current].preferred_child?;
376        let elem = self.nodes[child].elem.clone().expect("child nodes carry an element");
377        // Re-apply the element's steps oldest-first (forward coordinates).
378        replay(buffer, elem.steps.iter().map(|s| &s.forward), on_step);
379        self.current = child;
380        self.alt = elem.alt_after;
381        self.open = false;
382        Some(elem.sel_after)
383    }
384
385    /// The number of redo branches available from `current` — 1 for the classic
386    /// single-line case, ≥2 where an undo was followed by a divergent edit.
387    pub(crate) fn redo_branch_count(&self) -> usize {
388        self.nodes[self.current].children.len()
389    }
390
391    /// Steer the next [`redo`](Self::redo) to the `index`-th branch of `current`
392    /// (0 = oldest child), returning `false` if out of range. The chosen branch
393    /// becomes `preferred_child`, so it also becomes the default thereafter.
394    pub(crate) fn select_redo_branch(&mut self, index: usize) -> bool {
395        let Some(&child) = self.nodes[self.current].children.get(index) else {
396            return false;
397        };
398        self.nodes[self.current].preferred_child = Some(child);
399        true
400    }
401
402    /// Close the current undo group so the next edit starts a fresh element even
403    /// if its op class would otherwise coalesce. A jump-class selection change —
404    /// such as find navigation moving the caret elsewhere — seals through this so
405    /// typing after the jump never merges with the typing run before it.
406    pub(crate) fn seal(&mut self) {
407        self.open = false;
408    }
409
410    /// Whether the document differs from the last save point.
411    pub(crate) fn is_dirty(&self) -> bool {
412        self.alt != self.saved_alt
413    }
414
415    /// Mark the current state as saved.
416    pub(crate) fn mark_saved(&mut self) {
417        self.saved_alt = self.alt;
418    }
419}
420
421/// Apply each op-set to `buffer` in the given order, firing `on_step` with each
422/// step's [`Committed`] + the post-step buffer (so the caller rebases its derived
423/// views through the same patch). The ops are already stored in the direction
424/// being replayed, so — unlike a two-stack history — nothing is collected or
425/// flipped here. Selections are deliberately NOT rebased step by step: the unit
426/// carries the exact set for the state it lands on, which the caller restores.
427fn replay<'a>(
428    buffer: &mut Buffer,
429    steps: impl Iterator<Item = &'a Vec<EditOp>>,
430    mut on_step: impl FnMut(&Committed, &Buffer),
431) {
432    for step in steps {
433        let committed = apply(buffer, step.clone()).expect("history ops are disjoint by construction");
434        on_step(&committed, buffer);
435    }
436}