Skip to main content

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}