Skip to main content

gpui_base/
undo_history.rs

1use instant::{Duration, Instant};
2
3/// A history of grouped undo transactions.
4#[derive(Debug)]
5pub struct UndoHistory<T> {
6    undos: Vec<Vec<T>>,
7    redos: Vec<Vec<T>>,
8    last_changed_at: Option<Instant>,
9    max_undos: usize,
10    group_interval: Option<Duration>,
11    grouping: bool,
12    ignoring: bool,
13}
14
15impl<T> UndoHistory<T> {
16    pub fn new() -> Self {
17        Self {
18            undos: Vec::new(),
19            redos: Vec::new(),
20            last_changed_at: None,
21            max_undos: 1000,
22            group_interval: None,
23            grouping: false,
24            ignoring: false,
25        }
26    }
27
28    /// Set the maximum number of undo transactions to keep, defaults to 1000.
29    ///
30    /// Lowering the limit immediately removes the oldest undo transactions.
31    pub fn max_undos(mut self, max_undos: usize) -> Self {
32        self.max_undos = max_undos;
33        self.enforce_max_undos();
34        self
35    }
36
37    /// Set the interval in which consecutive changes are grouped.
38    ///
39    /// A successful undo or redo ends timed grouping. Explicit grouping is
40    /// independent and can still append to the current transaction.
41    pub fn group_interval(mut self, group_interval: Duration) -> Self {
42        self.group_interval = Some(group_interval);
43        self
44    }
45
46    /// Start explicitly grouping pushed changes into one transaction.
47    pub fn start_grouping(&mut self) {
48        self.grouping = true;
49    }
50
51    /// End explicit grouping of pushed changes.
52    pub fn end_grouping(&mut self) {
53        self.grouping = false;
54    }
55
56    /// Returns whether history recording is currently ignored.
57    pub fn is_ignoring(&self) -> bool {
58        self.ignoring
59    }
60
61    /// Sets whether history recording is currently ignored.
62    pub fn set_ignoring(&mut self, ignoring: bool) {
63        self.ignoring = ignoring;
64    }
65
66    /// Pushes a change into the current transaction or starts a new one.
67    pub fn push(&mut self, item: T) {
68        if self.ignoring || self.max_undos == 0 {
69            return;
70        }
71
72        let group_with_previous = self.grouping
73            || self.last_changed_at.is_some_and(|last_changed_at| {
74                self.group_interval
75                    .is_some_and(|interval| last_changed_at.elapsed() <= interval)
76            });
77
78        if group_with_previous && !self.undos.is_empty() {
79            self.undos.last_mut().unwrap().push(item);
80        } else {
81            self.undos.push(vec![item]);
82            self.enforce_max_undos();
83        }
84
85        self.last_changed_at = Some(Instant::now());
86        self.redos.clear();
87    }
88
89    /// Undoes the latest transaction, returning changes newest first.
90    pub fn undo(&mut self) -> Option<Vec<T>>
91    where
92        T: Clone,
93    {
94        let transaction = self.undos.pop()?;
95        let changes = transaction.iter().rev().cloned().collect();
96        self.redos.push(transaction);
97        self.last_changed_at = None;
98        Some(changes)
99    }
100
101    /// Redoes the latest undone transaction, returning changes oldest first.
102    pub fn redo(&mut self) -> Option<Vec<T>>
103    where
104        T: Clone,
105    {
106        if self.max_undos == 0 {
107            return None;
108        }
109        let transaction = self.redos.pop()?;
110        let changes = transaction.clone();
111        self.undos.push(transaction);
112        self.enforce_max_undos();
113        self.last_changed_at = None;
114        Some(changes)
115    }
116
117    /// Returns whether an undo transaction is available.
118    pub fn can_undo(&self) -> bool {
119        !self.undos.is_empty()
120    }
121
122    /// Returns whether a redo transaction is available.
123    pub fn can_redo(&self) -> bool {
124        !self.redos.is_empty()
125    }
126
127    /// Clears both undo and redo transactions.
128    pub fn clear(&mut self) {
129        self.undos.clear();
130        self.redos.clear();
131        self.last_changed_at = None;
132    }
133
134    fn enforce_max_undos(&mut self) {
135        let excess = self.undos.len().saturating_sub(self.max_undos);
136        if excess > 0 {
137            self.undos.drain(..excess);
138        }
139    }
140}
141
142impl<T> Default for UndoHistory<T> {
143    fn default() -> Self {
144        Self::new()
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use instant::Duration;
151
152    use super::UndoHistory;
153
154    #[test]
155    fn explicit_grouping_undoes_newest_first_and_redoes_oldest_first() {
156        let mut history = UndoHistory::new();
157        history.start_grouping();
158        history.push(1);
159        history.push(2);
160        history.push(3);
161        history.end_grouping();
162
163        assert_eq!(history.undo(), Some(vec![3, 2, 1]));
164        assert_eq!(history.redo(), Some(vec![1, 2, 3]));
165    }
166
167    #[test]
168    fn ungrouped_pushes_form_separate_transactions() {
169        let mut history = UndoHistory::new();
170        history.push(1);
171        history.push(2);
172
173        assert_eq!(history.undo(), Some(vec![2]));
174        assert_eq!(history.undo(), Some(vec![1]));
175    }
176
177    #[test]
178    fn group_interval_combines_immediate_pushes() {
179        let mut history = UndoHistory::new().group_interval(Duration::from_secs(60));
180        history.push(1);
181        history.push(2);
182
183        assert_eq!(history.undo(), Some(vec![2, 1]));
184    }
185
186    #[test]
187    fn undo_breaks_timed_grouping_across_the_branch_boundary() {
188        let mut history = UndoHistory::new();
189        history.push(1);
190        history.push(2);
191        history = history.group_interval(Duration::from_secs(60));
192        assert_eq!(history.undo(), Some(vec![2]));
193
194        history.push(3);
195
196        assert_eq!(history.undo(), Some(vec![3]));
197        assert_eq!(history.undo(), Some(vec![1]));
198    }
199
200    #[test]
201    fn redo_breaks_timed_grouping_across_the_branch_boundary() {
202        let mut history = UndoHistory::new();
203        history.push(1);
204        history.push(2);
205        assert_eq!(history.undo(), Some(vec![2]));
206        assert_eq!(history.redo(), Some(vec![2]));
207        history = history.group_interval(Duration::from_secs(60));
208
209        history.push(3);
210
211        assert_eq!(history.undo(), Some(vec![3]));
212        assert_eq!(history.undo(), Some(vec![2]));
213        assert_eq!(history.undo(), Some(vec![1]));
214    }
215
216    #[test]
217    fn explicit_grouping_still_appends_after_undo() {
218        let mut history = UndoHistory::new();
219        history.push(1);
220        history.push(2);
221        assert_eq!(history.undo(), Some(vec![2]));
222
223        history.start_grouping();
224        history.push(3);
225        history.end_grouping();
226
227        assert_eq!(history.undo(), Some(vec![3, 1]));
228    }
229
230    #[test]
231    fn a_new_push_clears_redo() {
232        let mut history = UndoHistory::new();
233        history.push(1);
234        history.push(2);
235        assert_eq!(history.undo(), Some(vec![2]));
236
237        history.push(3);
238
239        assert!(!history.can_redo());
240        assert_eq!(history.redo(), None);
241    }
242
243    #[test]
244    fn ignoring_drops_pushes() {
245        let mut history = UndoHistory::new();
246        history.set_ignoring(true);
247        history.push(1);
248
249        assert!(history.is_ignoring());
250        assert!(!history.can_undo());
251        assert_eq!(history.undo(), None);
252    }
253
254    #[test]
255    fn clear_clears_both_directions() {
256        let mut history = UndoHistory::new();
257        history.push(1);
258        history.undo();
259
260        history.clear();
261
262        assert!(!history.can_undo());
263        assert!(!history.can_redo());
264    }
265
266    #[test]
267    fn max_undos_evicts_the_oldest_transaction() {
268        let mut history = UndoHistory::new().max_undos(2);
269        history.push(1);
270        history.push(2);
271        history.push(3);
272
273        assert_eq!(history.undo(), Some(vec![3]));
274        assert_eq!(history.undo(), Some(vec![2]));
275        assert_eq!(history.undo(), None);
276    }
277
278    #[test]
279    fn lowering_max_undos_evicts_oldest_populated_transactions_immediately() {
280        let mut history = UndoHistory::new();
281        history.push(1);
282        history.push(2);
283        history.push(3);
284
285        history = history.max_undos(2);
286
287        assert_eq!(history.undo(), Some(vec![3]));
288        assert_eq!(history.undo(), Some(vec![2]));
289        assert_eq!(history.undo(), None);
290    }
291
292    #[test]
293    fn redo_after_lowering_max_undos_preserves_the_cap() {
294        let mut history = UndoHistory::new();
295        history.push(1);
296        history.push(2);
297        history.push(3);
298        assert_eq!(history.undo(), Some(vec![3]));
299
300        history = history.max_undos(1);
301        assert_eq!(history.redo(), Some(vec![3]));
302
303        assert_eq!(history.undo(), Some(vec![3]));
304        assert_eq!(history.undo(), None);
305    }
306
307    #[test]
308    fn redo_at_zero_max_undos_keeps_the_transaction_available() {
309        let mut history = UndoHistory::new();
310        history.push(1);
311        assert_eq!(history.undo(), Some(vec![1]));
312
313        history = history.max_undos(0);
314
315        assert_eq!(history.redo(), None);
316        assert!(history.can_redo());
317        history = history.max_undos(1);
318        assert_eq!(history.redo(), Some(vec![1]));
319    }
320
321    #[test]
322    fn zero_max_undos_retains_no_transactions() {
323        let mut history = UndoHistory::new().max_undos(0);
324        history.push(1);
325
326        assert!(!history.can_undo());
327        assert_eq!(history.undo(), None);
328    }
329}