gpui_base/
undo_history.rs1use instant::{Duration, Instant};
2
3#[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 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 pub fn group_interval(mut self, group_interval: Duration) -> Self {
42 self.group_interval = Some(group_interval);
43 self
44 }
45
46 pub fn start_grouping(&mut self) {
48 self.grouping = true;
49 }
50
51 pub fn end_grouping(&mut self) {
53 self.grouping = false;
54 }
55
56 pub fn is_ignoring(&self) -> bool {
58 self.ignoring
59 }
60
61 pub fn set_ignoring(&mut self, ignoring: bool) {
63 self.ignoring = ignoring;
64 }
65
66 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 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 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 pub fn can_undo(&self) -> bool {
119 !self.undos.is_empty()
120 }
121
122 pub fn can_redo(&self) -> bool {
124 !self.redos.is_empty()
125 }
126
127 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}