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#[derive(Default)]
54pub struct History {
55 undo: Vec<Snapshot>,
56 redo: Vec<Snapshot>,
57 /// The kind of the run currently open. `None` means the next edit starts a
58 /// new step regardless of its kind.
59 open_run: Option<EditKind>,
60}
61
62impl History {
63 /// Record the state before an edit, unless it continues the open run.
64 ///
65 /// Continuing a run means *not* pushing: the snapshot already on the stack
66 /// predates the whole run, which is exactly the state undo should restore.
67 pub fn record(&mut self, kind: EditKind, before: Rope, selections: &Selections) {
68 self.redo.clear();
69 if self.open_run == Some(kind) && kind != EditKind::Other {
70 return;
71 }
72 self.undo.push(Snapshot {
73 rope: before,
74 selections: selections.clone(),
75 });
76 self.open_run = Some(kind);
77 }
78
79 /// End the open run, so the next edit starts a new undo step.
80 ///
81 /// Called on anything that is not an edit — a motion, a click, a save.
82 pub fn boundary(&mut self) {
83 self.open_run = None;
84 }
85
86 /// Returns the state to restore, banking `current` for redo.
87 pub fn undo(&mut self, current: Rope, selections: &Selections) -> Option<Snapshot> {
88 let previous = self.undo.pop()?;
89 self.redo.push(Snapshot {
90 rope: current,
91 selections: selections.clone(),
92 });
93 // An undo always ends the run: typing after an undo must not fold into
94 // the step that was just undone.
95 self.open_run = None;
96 Some(previous)
97 }
98
99 pub fn redo(&mut self, current: Rope, selections: &Selections) -> Option<Snapshot> {
100 let next = self.redo.pop()?;
101 self.undo.push(Snapshot {
102 rope: current,
103 selections: selections.clone(),
104 });
105 self.open_run = None;
106 Some(next)
107 }
108}