freya_edit/
editor_history.rs

1use std::time::{
2    Duration,
3    Instant,
4};
5
6use ropey::Rope;
7
8use crate::text_editor::TextSelection;
9
10#[derive(Clone, Debug, PartialEq)]
11pub enum HistoryChange {
12    InsertChar {
13        idx: usize,
14        len: usize,
15        ch: char,
16        selection: TextSelection,
17    },
18    InsertText {
19        idx: usize,
20        len: usize,
21        text: String,
22        selection: TextSelection,
23    },
24    Remove {
25        idx: usize,
26        len: usize,
27        text: String,
28        selection: TextSelection,
29    },
30}
31
32#[derive(Clone, Debug, PartialEq)]
33pub struct HistoryTransaction {
34    pub timestamp: Instant,
35    pub changes: Vec<HistoryChange>,
36}
37
38#[derive(Clone, Debug)]
39pub struct EditorHistory {
40    pub transactions: Vec<HistoryTransaction>,
41    pub current_transaction: usize,
42    // Incremental counter for every transaction.
43    pub version: usize,
44    /// After how many seconds since the last transaction a change should be grouped with the last transaction.
45    transaction_threshold_groping: Duration,
46}
47
48impl EditorHistory {
49    pub fn new(transaction_threshold_groping: Duration) -> Self {
50        Self {
51            transactions: Vec::default(),
52            current_transaction: 0,
53            version: 0,
54            transaction_threshold_groping,
55        }
56    }
57
58    pub fn push_change(&mut self, change: HistoryChange) {
59        if self.can_redo() {
60            self.transactions.drain(self.current_transaction..);
61        }
62
63        let last_transaction = self
64            .transactions
65            .get_mut(self.current_transaction.saturating_sub(1));
66        if let Some(last_transaction) = last_transaction
67            && last_transaction.timestamp.elapsed() <= self.transaction_threshold_groping
68        {
69            last_transaction.changes.push(change);
70            last_transaction.timestamp = Instant::now();
71            return;
72        }
73
74        self.transactions.push(HistoryTransaction {
75            timestamp: Instant::now(),
76            changes: vec![change],
77        });
78
79        self.current_transaction = self.transactions.len();
80        self.version += 1;
81    }
82
83    pub fn current_change(&self) -> usize {
84        self.current_transaction
85    }
86
87    pub fn any_pending_changes(&self) -> usize {
88        self.transactions.len() - self.current_transaction
89    }
90
91    pub fn can_undo(&self) -> bool {
92        self.current_transaction > 0
93    }
94
95    pub fn can_redo(&self) -> bool {
96        self.current_transaction < self.transactions.len()
97    }
98
99    pub fn undo(&mut self, rope: &mut Rope) -> Option<TextSelection> {
100        if !self.can_undo() {
101            return None;
102        }
103
104        let last_transaction = self.transactions.get(self.current_transaction - 1);
105        if let Some(last_transaction) = last_transaction {
106            let mut selection = None;
107            for change in last_transaction.changes.iter().rev() {
108                match change {
109                    HistoryChange::Remove {
110                        idx,
111                        text,
112                        selection: previous_selection,
113                        ..
114                    } => {
115                        let start = rope.utf16_cu_to_char(*idx);
116                        rope.insert(start, text);
117                        selection = Some(previous_selection.clone());
118                    }
119                    HistoryChange::InsertChar {
120                        idx,
121                        len,
122                        selection: previous_selection,
123                        ..
124                    } => {
125                        let start = rope.utf16_cu_to_char(*idx);
126                        let end = rope.utf16_cu_to_char(*idx + len);
127                        rope.remove(start..end);
128                        selection = Some(previous_selection.clone());
129                    }
130                    HistoryChange::InsertText {
131                        idx,
132                        len,
133                        selection: previous_selection,
134                        ..
135                    } => {
136                        let start = rope.utf16_cu_to_char(*idx);
137                        let end = rope.utf16_cu_to_char(*idx + len);
138                        rope.remove(start..end);
139                        selection = Some(previous_selection.clone());
140                    }
141                }
142            }
143
144            self.current_transaction -= 1;
145            self.version += 1;
146            selection
147        } else {
148            None
149        }
150    }
151
152    pub fn redo(&mut self, rope: &mut Rope) -> Option<TextSelection> {
153        if !self.can_redo() {
154            return None;
155        }
156
157        let last_transaction = self.transactions.get(self.current_transaction);
158        if let Some(last_transaction) = last_transaction {
159            let mut cursor_pos = None;
160            for change in &last_transaction.changes {
161                cursor_pos = Some(match change {
162                    HistoryChange::Remove { idx, len, .. } => {
163                        let start = rope.utf16_cu_to_char(*idx);
164                        let end = rope.utf16_cu_to_char(*idx + len);
165                        rope.remove(start..end);
166                        *idx
167                    }
168                    HistoryChange::InsertChar { idx, len, ch, .. } => {
169                        let start = rope.utf16_cu_to_char(*idx);
170                        rope.insert_char(start, *ch);
171                        *idx + len
172                    }
173                    HistoryChange::InsertText { idx, text, len, .. } => {
174                        let start = rope.utf16_cu_to_char(*idx);
175                        rope.insert(start, text);
176                        *idx + len
177                    }
178                });
179            }
180            self.current_transaction += 1;
181            self.version += 1;
182            cursor_pos.map(TextSelection::new_cursor)
183        } else {
184            None
185        }
186    }
187
188    pub fn clear_redos(&mut self) {
189        if self.can_redo() {
190            self.transactions.drain(self.current_transaction..);
191        }
192    }
193
194    pub fn clear(&mut self) {
195        self.transactions.clear();
196        self.current_transaction = 0;
197        self.version = 0;
198    }
199}
200
201#[cfg(test)]
202mod test {
203    use std::time::Duration;
204
205    use ropey::Rope;
206
207    use super::{
208        EditorHistory,
209        HistoryChange,
210    };
211    use crate::text_editor::TextSelection;
212
213    #[test]
214    fn test_undo_redo() {
215        let mut rope = Rope::new();
216        let mut history = EditorHistory::new(Duration::ZERO);
217
218        rope.insert(0, "Hello World");
219
220        assert!(!history.can_undo());
221        assert!(!history.can_redo());
222
223        rope.insert(11, "\n!!!!");
224        history.push_change(HistoryChange::InsertText {
225            idx: 11,
226            text: "\n!!!!".to_owned(),
227            len: "\n!!!!".len(),
228            selection: TextSelection::new_cursor(11),
229        });
230
231        assert!(history.can_undo());
232        assert!(!history.can_redo());
233        assert_eq!(rope.to_string(), "Hello World\n!!!!");
234
235        history.undo(&mut rope);
236
237        assert!(!history.can_undo());
238        assert!(history.can_redo());
239        assert_eq!(rope.to_string(), "Hello World");
240
241        rope.insert(11, "\n!!!!");
242        history.push_change(HistoryChange::InsertText {
243            idx: 11,
244            text: "\n!!!!".to_owned(),
245            len: "\n!!!!".len(),
246            selection: TextSelection::new_cursor(11),
247        });
248        rope.insert(16, "\n!!!!");
249        history.push_change(HistoryChange::InsertText {
250            idx: 16,
251            text: "\n!!!!".to_owned(),
252            len: "\n!!!!".len(),
253            selection: TextSelection::new_cursor(16),
254        });
255        rope.insert(21, "\n!!!!");
256        history.push_change(HistoryChange::InsertText {
257            idx: 21,
258            text: "\n!!!!".to_owned(),
259            len: "\n!!!!".len(),
260            selection: TextSelection::new_cursor(21),
261        });
262
263        assert_eq!(history.any_pending_changes(), 0);
264        assert!(history.can_undo());
265        assert!(!history.can_redo());
266        assert_eq!(rope.to_string(), "Hello World\n!!!!\n!!!!\n!!!!");
267
268        history.undo(&mut rope);
269        assert_eq!(history.any_pending_changes(), 1);
270        assert_eq!(rope.to_string(), "Hello World\n!!!!\n!!!!");
271        history.undo(&mut rope);
272        assert_eq!(history.any_pending_changes(), 2);
273        assert_eq!(rope.to_string(), "Hello World\n!!!!");
274        history.undo(&mut rope);
275        assert_eq!(history.any_pending_changes(), 3);
276        assert_eq!(rope.to_string(), "Hello World");
277
278        assert!(!history.can_undo());
279        assert!(history.can_redo());
280
281        history.redo(&mut rope);
282        assert_eq!(rope.to_string(), "Hello World\n!!!!");
283        history.redo(&mut rope);
284        assert_eq!(rope.to_string(), "Hello World\n!!!!\n!!!!");
285        history.redo(&mut rope);
286        assert_eq!(rope.to_string(), "Hello World\n!!!!\n!!!!\n!!!!");
287
288        assert_eq!(history.any_pending_changes(), 0);
289        assert!(history.can_undo());
290        assert!(!history.can_redo());
291
292        history.undo(&mut rope);
293        assert_eq!(rope.to_string(), "Hello World\n!!!!\n!!!!");
294        assert_eq!(history.any_pending_changes(), 1);
295        history.undo(&mut rope);
296        assert_eq!(rope.to_string(), "Hello World\n!!!!");
297        assert_eq!(history.any_pending_changes(), 2);
298
299        rope.insert_char(0, '.');
300        history.push_change(HistoryChange::InsertChar {
301            idx: 0,
302            ch: '.',
303            len: 1,
304            selection: TextSelection::new_cursor(0),
305        });
306        assert_eq!(history.any_pending_changes(), 0);
307    }
308
309    #[test]
310    fn test_undo_restores_cursor_selection() {
311        let mut rope = Rope::new();
312        let mut history = EditorHistory::new(Duration::ZERO);
313
314        rope.insert(0, "Hello World");
315
316        rope.insert(11, "!");
317        history.push_change(HistoryChange::InsertChar {
318            idx: 11,
319            ch: '!',
320            len: 1,
321            selection: TextSelection::new_cursor(11),
322        });
323
324        let selection = history.undo(&mut rope).unwrap();
325        assert_eq!(selection, TextSelection::new_cursor(11));
326        assert_eq!(rope.to_string(), "Hello World");
327    }
328
329    #[test]
330    fn test_undo_restores_range_selection() {
331        let mut rope = Rope::new();
332        let mut history = EditorHistory::new(Duration::ZERO);
333
334        rope.insert(0, "Hello World");
335
336        let start = rope.utf16_cu_to_char(0);
337        let end = rope.utf16_cu_to_char(5);
338        rope.remove(start..end);
339        history.push_change(HistoryChange::Remove {
340            idx: 0,
341            text: "Hello".to_owned(),
342            len: 5,
343            selection: TextSelection::new_range((0, 5)),
344        });
345        assert_eq!(rope.to_string(), " World");
346
347        let selection = history.undo(&mut rope).unwrap();
348        assert_eq!(selection, TextSelection::new_range((0, 5)));
349        assert_eq!(rope.to_string(), "Hello World");
350    }
351
352    #[test]
353    fn test_redo_returns_cursor_at_end() {
354        let mut rope = Rope::new();
355        let mut history = EditorHistory::new(Duration::ZERO);
356
357        rope.insert(0, "Hello");
358
359        rope.insert(5, " World");
360        history.push_change(HistoryChange::InsertText {
361            idx: 5,
362            text: " World".to_owned(),
363            len: 6,
364            selection: TextSelection::new_cursor(5),
365        });
366
367        history.undo(&mut rope);
368        assert_eq!(rope.to_string(), "Hello");
369
370        let selection = history.redo(&mut rope).unwrap();
371        assert_eq!(selection, TextSelection::new_cursor(11));
372        assert_eq!(rope.to_string(), "Hello World");
373    }
374
375    #[test]
376    fn test_undo_redo_with_remove() {
377        let mut rope = Rope::new();
378        let mut history = EditorHistory::new(Duration::ZERO);
379
380        rope.insert(0, "Hello World");
381
382        let start = rope.utf16_cu_to_char(5);
383        let end = rope.utf16_cu_to_char(11);
384        rope.remove(start..end);
385        history.push_change(HistoryChange::Remove {
386            idx: 5,
387            text: " World".to_owned(),
388            len: 6,
389            selection: TextSelection::new_cursor(11),
390        });
391        assert_eq!(rope.to_string(), "Hello");
392
393        let selection = history.undo(&mut rope).unwrap();
394        assert_eq!(selection, TextSelection::new_cursor(11));
395        assert_eq!(rope.to_string(), "Hello World");
396
397        let selection = history.redo(&mut rope).unwrap();
398        assert_eq!(selection, TextSelection::new_cursor(5));
399        assert_eq!(rope.to_string(), "Hello");
400    }
401}