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}