Skip to main content

pfx_editor_doc/
history.rs

1use std::path::PathBuf;
2
3use crate::edit::{Caret, Edit};
4use crate::error::DocError;
5use crate::files::Files;
6
7pub const DEPTH: usize = 256;
8pub const GAP: f64 = 1.0;
9
10#[derive(Clone, Debug, PartialEq, Eq)]
11pub struct Changed {
12    pub label: String,
13    pub files: Vec<PathBuf>,
14    pub caret: Option<Caret>,
15}
16
17impl Changed {
18    pub fn is_empty(&self) -> bool {
19        self.files.is_empty()
20    }
21}
22
23#[derive(Debug)]
24pub struct History {
25    undo: Vec<Box<dyn Edit>>,
26    redo: Vec<Box<dyn Edit>>,
27    depth: usize,
28    typed_at: Option<f64>,
29}
30
31impl Default for History {
32    fn default() -> History {
33        History::new(DEPTH)
34    }
35}
36
37pub fn run(files: &mut Files, edit: &mut dyn Edit) -> Result<Changed, DocError> {
38    let touched = edit.files();
39    let before: Vec<Option<u64>> = touched.iter().map(|p| files.generation_of(p)).collect();
40    files.attempt(&touched, |files| edit.apply(files))?;
41    let changed = touched
42        .into_iter()
43        .zip(before)
44        .filter(|(path, generation)| files.generation_of(path) != *generation)
45        .map(|(path, _)| path)
46        .collect();
47    Ok(Changed {
48        label: edit.label().to_string(),
49        files: changed,
50        caret: edit.caret(),
51    })
52}
53
54fn keep(entries: &mut Vec<Box<dyn Edit>>, files: &Files) -> usize {
55    let mut scratch = files.clone();
56    let before = entries.len();
57    let mut kept: Vec<Box<dyn Edit>> = entries
58        .drain(..)
59        .rev()
60        .filter(|entry| run(&mut scratch, entry.inverse().as_mut()).is_ok())
61        .collect();
62    kept.reverse();
63    *entries = kept;
64    before - entries.len()
65}
66
67impl History {
68    pub fn new(depth: usize) -> History {
69        History {
70            undo: Vec::new(),
71            redo: Vec::new(),
72            depth: depth.max(1),
73            typed_at: None,
74        }
75    }
76
77    pub fn depth(&self) -> usize {
78        self.depth
79    }
80
81    pub fn set_depth(&mut self, depth: usize) {
82        self.depth = depth.max(1);
83        self.trim();
84    }
85
86    fn trim(&mut self) {
87        if self.undo.len() > self.depth {
88            let extra = self.undo.len() - self.depth;
89            self.undo.drain(..extra);
90        }
91    }
92
93    pub fn apply(&mut self, files: &mut Files, edit: impl Edit) -> Result<Changed, DocError> {
94        self.record(files, Box::new(edit), None)
95    }
96
97    pub fn apply_boxed(
98        &mut self,
99        files: &mut Files,
100        edit: Box<dyn Edit>,
101    ) -> Result<Changed, DocError> {
102        self.record(files, edit, None)
103    }
104
105    pub fn typed(
106        &mut self,
107        files: &mut Files,
108        edit: impl Edit,
109        now: f64,
110        gap: f64,
111    ) -> Result<Changed, DocError> {
112        self.record(files, Box::new(edit), Some((now, gap)))
113    }
114
115    fn record(
116        &mut self,
117        files: &mut Files,
118        mut edit: Box<dyn Edit>,
119        typed: Option<(f64, f64)>,
120    ) -> Result<Changed, DocError> {
121        let changed = run(files, edit.as_mut())?;
122        if changed.is_empty() {
123            return Ok(changed);
124        }
125        self.redo.clear();
126        let merged = match (typed, self.typed_at, self.undo.last()) {
127            (Some((now, gap)), Some(at), Some(top)) if now >= at && now - at <= gap => {
128                top.merge(edit.as_ref())
129            }
130            _ => None,
131        };
132        match merged {
133            Some(merged) => {
134                let top = self.undo.len() - 1;
135                self.undo[top] = merged;
136            }
137            None => {
138                self.undo.push(edit);
139                self.trim();
140            }
141        }
142        self.typed_at = typed.map(|(now, _)| now);
143        Ok(changed)
144    }
145
146    pub fn undo(&mut self, files: &mut Files) -> Result<Option<Changed>, DocError> {
147        self.step(files, true)
148    }
149
150    pub fn redo(&mut self, files: &mut Files) -> Result<Option<Changed>, DocError> {
151        self.step(files, false)
152    }
153
154    fn step(&mut self, files: &mut Files, undo: bool) -> Result<Option<Changed>, DocError> {
155        let (from, to) = if undo {
156            (&mut self.undo, &mut self.redo)
157        } else {
158            (&mut self.redo, &mut self.undo)
159        };
160        let Some(top) = from.last() else {
161            return Ok(None);
162        };
163        let mut back = top.inverse();
164        let changed = run(files, back.as_mut())?;
165        from.pop();
166        to.push(back);
167        self.typed_at = None;
168        self.trim();
169        Ok(Some(changed))
170    }
171
172    pub fn seal(&mut self) {
173        self.typed_at = None;
174    }
175
176    pub fn clear(&mut self) {
177        self.undo.clear();
178        self.redo.clear();
179        self.typed_at = None;
180    }
181
182    pub fn can_undo(&self) -> bool {
183        !self.undo.is_empty()
184    }
185
186    pub fn can_redo(&self) -> bool {
187        !self.redo.is_empty()
188    }
189
190    pub fn undo_len(&self) -> usize {
191        self.undo.len()
192    }
193
194    pub fn redo_len(&self) -> usize {
195        self.redo.len()
196    }
197
198    pub fn undo_label(&self) -> Option<&str> {
199        self.undo.last().map(|edit| edit.label())
200    }
201
202    pub fn redo_label(&self) -> Option<&str> {
203        self.redo.last().map(|edit| edit.label())
204    }
205
206    pub fn undo_labels(&self) -> Vec<&str> {
207        self.undo.iter().map(|edit| edit.label()).collect()
208    }
209
210    pub fn redo_labels(&self) -> Vec<&str> {
211        self.redo.iter().map(|edit| edit.label()).collect()
212    }
213
214    pub fn prune(&mut self, files: &Files) -> usize {
215        self.typed_at = None;
216        keep(&mut self.undo, files) + keep(&mut self.redo, files)
217    }
218}
219
220#[cfg(test)]
221mod tests {
222    use std::path::Path;
223
224    use super::*;
225    use crate::patch::{TomlPatch, parse_path};
226    use crate::splice::TextSplice;
227    use toml_edit::value;
228
229    const FILE: &str = "a.toml";
230
231    fn files(text: &str) -> Files {
232        let mut files = Files::new();
233        files.open(FILE, text);
234        files
235    }
236
237    fn text(files: &Files) -> &str {
238        files.text(Path::new(FILE)).unwrap()
239    }
240
241    fn set(key: &str, n: i64) -> TomlPatch {
242        TomlPatch::set(FILE, parse_path(key).unwrap(), value(n)).labelled(key)
243    }
244
245    fn typed(at: usize, what: &str) -> TextSplice {
246        TextSplice::new(FILE, at..at, "", what)
247            .with_carets(at..at, at + what.len()..at + what.len())
248    }
249
250    #[test]
251    fn a_new_step_clears_the_redo_side() {
252        let mut f = files("a = 0\n");
253        let mut history = History::default();
254        history.apply(&mut f, set("one", 1)).unwrap();
255        history.apply(&mut f, set("two", 2)).unwrap();
256        history.undo(&mut f).unwrap().unwrap();
257        assert_eq!(
258            (history.undo_labels(), history.redo_labels()),
259            (vec!["one"], vec!["two"])
260        );
261        assert_eq!(history.redo_label(), Some("two"));
262        history.apply(&mut f, set("three", 3)).unwrap();
263        assert_eq!(
264            (history.undo_labels(), history.redo_labels()),
265            (vec!["one", "three"], vec![])
266        );
267    }
268
269    #[test]
270    fn undo_and_redo_say_what_changed() {
271        let mut f = files("a = 0\n");
272        let mut history = History::default();
273        history.apply(&mut f, set("a", 1)).unwrap();
274        let changed = history.undo(&mut f).unwrap().unwrap();
275        assert_eq!(changed.label, "a");
276        assert_eq!(changed.files, vec![PathBuf::from(FILE)]);
277        assert_eq!(text(&f), "a = 0\n");
278        assert!(history.undo(&mut f).unwrap().is_none());
279        history.redo(&mut f).unwrap().unwrap();
280        assert_eq!(text(&f), "a = 1\n");
281        assert!(history.redo(&mut f).unwrap().is_none());
282    }
283
284    #[test]
285    fn the_stack_keeps_its_last_steps() {
286        let mut f = files("a = 0\n");
287        let mut history = History::new(8);
288        for step in 1..=11 {
289            history
290                .apply(&mut f, set("a", step).labelled(step.to_string()))
291                .unwrap();
292        }
293        assert_eq!(history.undo_len(), 8);
294        assert_eq!(history.undo_labels()[0], "4");
295        history.set_depth(2);
296        assert_eq!(history.undo_labels(), vec!["10", "11"]);
297    }
298
299    #[test]
300    fn a_refused_edit_records_nothing_and_a_no_op_is_not_a_step() {
301        let mut f = files("a = 0\n");
302        let mut history = History::default();
303        let generation = f.generation();
304        assert!(
305            history
306                .apply(&mut f, TomlPatch::unset(FILE, parse_path("b").unwrap()))
307                .is_err()
308        );
309        assert!(history.apply(&mut f, set("a", 0)).unwrap().is_empty());
310        assert_eq!(history.undo_len(), 0);
311        assert_eq!(f.generation(), generation);
312    }
313
314    #[test]
315    fn typing_merges_within_the_gap_and_splits_across_it_or_a_jump() {
316        let mut f = files("a = 0\n");
317        let mut history = History::default();
318        history.typed(&mut f, typed(0, "#"), 0.0, GAP).unwrap();
319        history.typed(&mut f, typed(1, " "), 0.5, GAP).unwrap();
320        history.typed(&mut f, typed(2, "x"), 1.4, GAP).unwrap();
321        assert_eq!(history.undo_len(), 1);
322        history.typed(&mut f, typed(3, "y"), 3.0, GAP).unwrap();
323        assert_eq!(history.undo_len(), 2);
324        history.typed(&mut f, typed(0, "z"), 3.1, GAP).unwrap();
325        assert_eq!(history.undo_len(), 3);
326        assert_eq!(text(&f), "z# xya = 0\n");
327        let changed = history.undo(&mut f).unwrap().unwrap();
328        assert_eq!(changed.caret.unwrap().selection, 0..0);
329        history.undo(&mut f).unwrap();
330        assert_eq!(text(&f), "# xa = 0\n");
331        let changed = history.undo(&mut f).unwrap().unwrap();
332        assert_eq!(changed.caret.unwrap().selection, 0..0);
333        assert_eq!(text(&f), "a = 0\n");
334    }
335
336    #[test]
337    fn a_patch_or_an_undo_ends_the_typing_run() {
338        let mut f = files("a = 0\n");
339        let mut history = History::default();
340        history.typed(&mut f, typed(0, "#"), 0.0, GAP).unwrap();
341        history.apply(&mut f, set("b", 1)).unwrap();
342        history.typed(&mut f, typed(1, " "), 0.1, GAP).unwrap();
343        assert_eq!(history.undo_len(), 3);
344        history.undo(&mut f).unwrap();
345        history.typed(&mut f, typed(1, "!"), 0.2, GAP).unwrap();
346        assert_eq!(history.undo_len(), 3);
347        history.seal();
348        history.typed(&mut f, typed(2, "!"), 0.3, GAP).unwrap();
349        assert_eq!(history.undo_len(), 4);
350    }
351
352    #[test]
353    fn pruning_drops_only_the_steps_that_no_longer_apply() {
354        let mut f = files("a = 0\n[t]\nx = 0\n");
355        let mut history = History::default();
356        history.apply(&mut f, set("t.x", 1)).unwrap();
357        history.typed(&mut f, typed(0, "# x\n"), 0.0, GAP).unwrap();
358        history.apply(&mut f, set("a", 1)).unwrap();
359        f.reload(Path::new(FILE), "a = 1\n").unwrap();
360        assert_eq!(history.prune(&f), 2);
361        assert_eq!(history.undo_labels(), vec!["a"]);
362        history.undo(&mut f).unwrap();
363        assert_eq!(text(&f), "a = 0\n");
364    }
365}