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}