typ_buffer/undo.rs
1use ropey::Rope;
2
3use crate::selection::Selections;
4
5/// What an edit did, for the purpose of deciding whether it continues the
6/// previous one.
7///
8/// Coarse on purpose: the question is only "is this the same kind of thing the
9/// user was already doing", and a finer taxonomy would split runs the user
10/// experiences as one.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum EditKind {
13 Insert,
14 Delete,
15 /// Anything that should always stand alone — a paste, a replace-all.
16 Other,
17}
18
19/// The buffer as it stood before an edit, and where the cursors were.
20///
21/// Storing the selections is what makes undo put the caret back where the edit
22/// happened rather than wherever clamping left it. Every editor in the field
23/// does this; an undo that leaves the cursor somewhere unrelated is disorienting
24/// enough that users stop trusting it.
25#[derive(Clone)]
26pub struct Snapshot {
27 pub rope: Rope,
28 pub selections: Selections,
29}
30
31/// Whole-content undo history, stored as rope snapshots.
32///
33/// A snapshot is a `Rope` rather than a `String` because ropey clones are O(1)
34/// and copy-on-write: two snapshots share every node they have in common, so a
35/// deep undo stack over a large file costs the edits, not one full copy of the
36/// text per step. `to_string()` would allocate and copy the whole buffer on
37/// every keystroke.
38///
39/// Whole-content snapshots stay the right call at this size — they are correct
40/// for any edit shape, and with structural sharing they are no longer expensive
41/// enough to justify per-edit deltas.
42///
43/// **Runs have no clock.** VS Code and Zed break undo groups on an idle timer.
44/// A timer means the buffer needs a clock, which means tests need to inject one,
45/// which means the rule is only ever exercised through a fake. The rule here is
46/// structural instead: consecutive edits of the same `EditKind` coalesce, and
47/// anything that is not an edit — a motion, a click, a save — calls `boundary`.
48/// That is deterministic and it matches what a user means by "undo what I just
49/// typed": the run ends when they moved.
50///
51/// ponytail: pausing mid-word for ten minutes without moving still coalesces.
52/// If that ever bites, a timer goes beside this rule, not instead of it.
53/// How many undo steps are kept.
54///
55/// vim's `undolevels` default, and there is no reason to be cleverer until
56/// someone measures a session where it bites. Structural sharing makes each
57/// snapshot cheap but not free — every one pins the rope nodes it replaced, so
58/// an uncapped stack is an uncapped retention of every version of the file for
59/// as long as the editor is open.
60pub const MAX_UNDO_STEPS: usize = 1000;
61
62#[derive(Default)]
63pub struct History {
64 undo: Vec<Snapshot>,
65 redo: Vec<Snapshot>,
66 /// The kind of the run currently open. `None` means the next edit starts a
67 /// new step regardless of its kind.
68 open_run: Option<EditKind>,
69}
70
71impl History {
72 /// Record the state before an edit, unless it continues the open run.
73 ///
74 /// Continuing a run means *not* pushing: the snapshot already on the stack
75 /// predates the whole run, which is exactly the state undo should restore.
76 pub fn record(&mut self, kind: EditKind, before: Rope, selections: &Selections) {
77 self.redo.clear();
78 if self.open_run == Some(kind) && kind != EditKind::Other {
79 return;
80 }
81 self.undo.push(Snapshot {
82 rope: before,
83 selections: selections.clone(),
84 });
85 // Forget the oldest step, never the newest. `remove(0)` is O(n) on a
86 // 1000-element Vec of cheap clones and runs once per *step*, not per
87 // keystroke — a VecDeque would trade that for a less obvious type on
88 // every other line of this file.
89 if self.undo.len() > MAX_UNDO_STEPS {
90 self.undo.remove(0);
91 }
92 self.open_run = Some(kind);
93 }
94
95 /// How many steps are on the undo stack. For tests and for a future status
96 /// segment; nothing in the editor branches on it.
97 pub fn depth(&self) -> usize {
98 self.undo.len()
99 }
100
101 /// End the open run, so the next edit starts a new undo step.
102 ///
103 /// Called on anything that is not an edit — a motion, a click, a save.
104 pub fn boundary(&mut self) {
105 self.open_run = None;
106 }
107
108 /// Returns the state to restore, banking `current` for redo.
109 pub fn undo(&mut self, current: Rope, selections: &Selections) -> Option<Snapshot> {
110 let previous = self.undo.pop()?;
111 self.redo.push(Snapshot {
112 rope: current,
113 selections: selections.clone(),
114 });
115 // An undo always ends the run: typing after an undo must not fold into
116 // the step that was just undone.
117 self.open_run = None;
118 Some(previous)
119 }
120
121 pub fn redo(&mut self, current: Rope, selections: &Selections) -> Option<Snapshot> {
122 let next = self.redo.pop()?;
123 self.undo.push(Snapshot {
124 rope: current,
125 selections: selections.clone(),
126 });
127 self.open_run = None;
128 Some(next)
129 }
130}