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#[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}