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