Skip to main content

lattice_core/
undo.rs

1//! Undo stack.
2//!
3//! Phase 0 ships a linear undo stack: each entry is the inverse of one applied
4//! edit (or one batch of edits applied as a single command). The branching
5//! undo *tree* per §5.1 is a later refinement; the linear stack is forward
6//! compatible because branching is built by retaining alternative redo paths
7//! when a new edit is applied while redo entries exist.
8
9use lattice_protocol::edit::Edit;
10
11/// One entry on the undo stack: an ordered list of edits whose application
12/// inverts the user-visible operation. Storing a list (not a single Edit) lets
13/// a batch of edits applied atomically be undone atomically.
14#[derive(Debug, Clone)]
15pub struct UndoEntry {
16    /// The edits that invert the operation, in the order they must be
17    /// applied — the reverse of the order the original edits were applied.
18    /// Each edit's range is in the coordinates left by the edit before it.
19    pub inverse_edits: Vec<Edit>,
20    /// Description for status messages / dot-repeat. Empty for unnamed batches.
21    pub label: String,
22}
23
24/// A linear undo / redo stack of [`UndoEntry`]s.
25///
26/// This is a passive container: it never touches a buffer. The owner (in
27/// practice [`Document`](crate::Document)) applies the popped entry's
28/// edits and records the resulting inverse on the other side — see
29/// [`Self::pop_for_undo`] / [`Self::record_redo`]. Pushing a new entry
30/// discards redo history (no undo tree yet).
31///
32/// # Examples
33///
34/// ```
35/// use lattice_core::{UndoEntry, UndoStack};
36///
37/// let entry = |label: &str| UndoEntry { inverse_edits: vec![], label: label.into() };
38/// let mut stack = UndoStack::new();
39/// stack.push(entry("a"));
40/// stack.push(entry("b"));
41///
42/// // Undo "b": pop it, apply its edits (elided), record the redo side.
43/// let undone = stack.pop_for_undo().map(|e| e.label);
44/// assert_eq!(undone.as_deref(), Some("b"));
45/// stack.record_redo(entry("b"));
46/// assert_eq!((stack.undo_depth(), stack.redo_depth()), (1, 1));
47///
48/// // A fresh edit drops the redo history.
49/// stack.push(entry("c"));
50/// assert_eq!((stack.undo_depth(), stack.redo_depth()), (2, 0));
51/// ```
52#[derive(Debug, Default, Clone)]
53pub struct UndoStack {
54    undo: Vec<UndoEntry>,
55    redo: Vec<UndoEntry>,
56}
57
58impl UndoStack {
59    /// An empty stack.
60    pub fn new() -> Self {
61        Self::default()
62    }
63
64    /// Record a new undo entry. Any pending redo history is dropped.
65    pub fn push(&mut self, entry: UndoEntry) {
66        self.undo.push(entry);
67        self.redo.clear();
68    }
69
70    /// Fold `inverses` into the most recent undo entry instead of
71    /// pushing a new one -- the primitive behind undo-group coalescing
72    /// (a vim insert session collapses to a single undo unit). The
73    /// caller passes the just-applied operation's inverse edits in the
74    /// same stored order [`push`](Self::push) would use (reverse-application order);
75    /// they are prepended so the combined entry still replays
76    /// newest -> oldest during undo (`inv(eN) .. inv(e1)`).
77    ///
78    /// Redo is intentionally not cleared: an amend never diverges the
79    /// history (the initiating `push` that opened the group already
80    /// cleared redo, and no redo can accrue mid-group). If there is no
81    /// top entry to amend -- which the group bookkeeping is meant to
82    /// prevent -- it falls back to a plain push so the edit stays
83    /// undoable rather than being silently lost.
84    pub fn amend_top(&mut self, mut inverses: Vec<Edit>) {
85        match self.undo.last_mut() {
86            Some(top) => {
87                inverses.append(&mut top.inverse_edits);
88                top.inverse_edits = inverses;
89            }
90            None => self.undo.push(UndoEntry {
91                inverse_edits: inverses,
92                label: String::new(),
93            }),
94        }
95    }
96
97    /// Pop the most recent undo entry, or `None` if there is none.
98    ///
99    /// This does **not** touch the redo stack: the caller applies
100    /// `entry.inverse_edits` to the buffer and passes the resulting
101    /// "inverse-of-the-inverse" back via [`Self::record_redo`].
102    pub fn pop_for_undo(&mut self) -> Option<UndoEntry> {
103        self.undo.pop()
104    }
105
106    /// Reciprocal of `pop_for_undo`. Stores the edit set that would replay the
107    /// undone operation onto the redo stack.
108    pub fn record_redo(&mut self, redo_entry: UndoEntry) {
109        self.redo.push(redo_entry);
110    }
111
112    /// Pop the most recent redo entry, or `None` if there is none. Like
113    /// [`Self::pop_for_undo`], the caller applies it and records the result
114    /// via [`Self::record_undo`].
115    pub fn pop_for_redo(&mut self) -> Option<UndoEntry> {
116        self.redo.pop()
117    }
118
119    /// Reciprocal of [`Self::pop_for_redo`]: push the edit set that undoes a
120    /// just-redone operation. Unlike [`Self::push`] it leaves the redo stack
121    /// intact, so further redos remain available.
122    pub fn record_undo(&mut self, undo_entry: UndoEntry) {
123        self.undo.push(undo_entry);
124    }
125
126    /// Number of entries available to undo. [`Document`](crate::Document)
127    /// compares it against the depth recorded at save time to decide
128    /// dirtiness.
129    pub fn undo_depth(&self) -> usize {
130        self.undo.len()
131    }
132
133    /// Number of entries available to redo.
134    pub fn redo_depth(&self) -> usize {
135        self.redo.len()
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    #![allow(clippy::unwrap_used, clippy::panic)]
142    use super::*;
143    use lattice_protocol::edit::Edit;
144    use lattice_protocol::position::Position;
145
146    fn entry(label: &str) -> UndoEntry {
147        UndoEntry {
148            inverse_edits: vec![Edit::insert(Position::ZERO, "x")],
149            label: label.into(),
150        }
151    }
152
153    #[test]
154    fn new_stack_is_empty() {
155        let s = UndoStack::new();
156        assert_eq!(s.undo_depth(), 0);
157        assert_eq!(s.redo_depth(), 0);
158    }
159
160    #[test]
161    fn push_increments_undo_depth() {
162        let mut s = UndoStack::new();
163        s.push(entry("a"));
164        s.push(entry("b"));
165        assert_eq!(s.undo_depth(), 2);
166        assert_eq!(s.redo_depth(), 0);
167    }
168
169    #[test]
170    fn pop_for_undo_returns_in_lifo_order() {
171        let mut s = UndoStack::new();
172        s.push(entry("a"));
173        s.push(entry("b"));
174        let top = s.pop_for_undo().unwrap();
175        assert_eq!(top.label, "b");
176        let next = s.pop_for_undo().unwrap();
177        assert_eq!(next.label, "a");
178        assert!(s.pop_for_undo().is_none());
179    }
180
181    #[test]
182    fn record_redo_pushes_to_redo_stack() {
183        let mut s = UndoStack::new();
184        s.push(entry("a"));
185        let popped = s.pop_for_undo().unwrap();
186        s.record_redo(popped);
187        assert_eq!(s.undo_depth(), 0);
188        assert_eq!(s.redo_depth(), 1);
189    }
190
191    #[test]
192    fn pop_for_redo_returns_in_lifo_order() {
193        let mut s = UndoStack::new();
194        s.record_redo(entry("first"));
195        s.record_redo(entry("second"));
196        assert_eq!(s.pop_for_redo().unwrap().label, "second");
197        assert_eq!(s.pop_for_redo().unwrap().label, "first");
198        assert!(s.pop_for_redo().is_none());
199    }
200
201    #[test]
202    fn push_clears_pending_redo() {
203        // Standard undo invariant: making a new edit while there is a redo
204        // history must drop that history (the user has diverged onto a new
205        // branch). The branching tree variant in §5.1 will preserve it; the
206        // linear stack does not.
207        let mut s = UndoStack::new();
208        s.push(entry("a"));
209        let popped = s.pop_for_undo().unwrap();
210        s.record_redo(popped);
211        assert_eq!(s.redo_depth(), 1);
212
213        s.push(entry("b"));
214        assert_eq!(s.redo_depth(), 0);
215    }
216
217    #[test]
218    fn amend_top_prepends_into_the_latest_entry() {
219        // Two edits folded into one entry: the second edit's inverse is
220        // prepended so undo replays newest -> oldest. Depth stays 1.
221        let mut s = UndoStack::new();
222        s.push(UndoEntry {
223            inverse_edits: vec![Edit::insert(Position::ZERO, "first")],
224            label: String::new(),
225        });
226        s.amend_top(vec![Edit::insert(Position::new(0, 5), "second")]);
227        assert_eq!(s.undo_depth(), 1);
228        let top = s.pop_for_undo().unwrap();
229        assert_eq!(top.inverse_edits.len(), 2);
230        // Prepended: the later edit's inverse comes first.
231        assert_eq!(
232            top.inverse_edits[0],
233            Edit::insert(Position::new(0, 5), "second")
234        );
235        assert_eq!(top.inverse_edits[1], Edit::insert(Position::ZERO, "first"));
236    }
237
238    #[test]
239    fn amend_top_on_empty_stack_falls_back_to_push() {
240        let mut s = UndoStack::new();
241        s.amend_top(vec![Edit::insert(Position::ZERO, "x")]);
242        assert_eq!(s.undo_depth(), 1);
243    }
244
245    #[test]
246    fn amend_top_does_not_clear_redo() {
247        // Unlike `push`, amending an open group must not drop redo.
248        let mut s = UndoStack::new();
249        s.record_redo(entry("r"));
250        s.push(entry("open")); // clears redo per the push invariant...
251        s.record_redo(entry("r2")); // ...re-seed to prove amend leaves it be
252        s.amend_top(vec![Edit::insert(Position::ZERO, "y")]);
253        assert_eq!(s.redo_depth(), 1);
254    }
255
256    #[test]
257    fn record_undo_does_not_clear_redo() {
258        // record_undo is the bookkeeping primitive used by `Document::redo`
259        // -- it should NOT clear the redo stack the way `push` does.
260        let mut s = UndoStack::new();
261        s.record_redo(entry("r"));
262        s.record_undo(entry("u"));
263        assert_eq!(s.undo_depth(), 1);
264        assert_eq!(s.redo_depth(), 1);
265    }
266
267    #[test]
268    fn full_undo_redo_dance() {
269        let mut s = UndoStack::new();
270        s.push(entry("op"));
271        let popped = s.pop_for_undo().unwrap();
272        // pretend we re-applied the inverse and computed an inverse-of-inverse:
273        s.record_redo(entry("inv-of-inv"));
274        let redone = s.pop_for_redo().unwrap();
275        s.record_undo(entry("re-recorded"));
276        assert_eq!(s.undo_depth(), 1);
277        assert_eq!(s.redo_depth(), 0);
278        let _ = (popped, redone);
279    }
280}