Skip to main content

editor/
history.rs

1//! Undo and redo, as coalesced snapshots.
2//!
3//! A whole [`Doc`] per step rather than a diff. That is what [`ui::input::TextField`]
4//! settled on for a string, and the argument carries: a document held in memory
5//! is small next to the machinery a transaction log needs, and a snapshot cannot
6//! be wrong about what it restores. Steps, not keystrokes — a run of typing
7//! coalesces into one, so the limit is deeper than it looks.
8//!
9//! Coalescing is by **adjacency rather than by a pause**, so there is no timing
10//! threshold to invent: the next edit joins the last group when it is the same
11//! kind and picks up where that one left off. Anything else — a motion, a
12//! structural change, a click — starts a new group.
13
14use std::collections::VecDeque;
15
16use markdown::{Cursor, Doc, Selection};
17
18/// How many steps a document keeps.
19///
20/// Deeper than a text field's, because a document is the thing people actually
21/// walk backwards through, and bounded because an unbounded history of a
22/// growing document is a slow leak nothing reclaims.
23pub const DEFAULT_UNDO_LIMIT: usize = 100;
24
25/// What an edit did, so a run of the same kind can coalesce into one step
26/// instead of giving the document back a character at a time.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum EditKind {
29    Insert,
30    Delete,
31    /// Anything structural — a split, an indent, a block turned into another.
32    /// Never coalesces: these are the steps a reader wants to land on.
33    Structure,
34}
35
36#[derive(Clone)]
37struct Snapshot {
38    doc: Doc,
39    selection: Selection,
40}
41
42pub struct History {
43    /// Points to return to, oldest first.
44    undo: VecDeque<Snapshot>,
45    /// Undone points, newest last. Cleared by any fresh edit — the usual model,
46    /// and the only one where redo cannot resurrect a branch the document has
47    /// already diverged from.
48    redo: Vec<Snapshot>,
49    limit: usize,
50    /// The kind of the last edit and where it *left* the caret, which is what
51    /// decides whether the next edit joins that group or starts a new one.
52    last: Option<(EditKind, Cursor)>,
53}
54
55impl Default for History {
56    fn default() -> Self {
57        Self {
58            undo: VecDeque::new(),
59            redo: Vec::new(),
60            limit: DEFAULT_UNDO_LIMIT,
61            last: None,
62        }
63    }
64}
65
66impl History {
67    pub fn with_limit(limit: usize) -> Self {
68        Self {
69            limit,
70            ..Self::default()
71        }
72    }
73
74    /// Record the state *before* an edit of `kind`; [`History::landed`] closes
75    /// it afterwards. A run of insertions leaves one step, so undo gives back
76    /// the word rather than the letter.
77    pub fn record(&mut self, kind: EditKind, doc: &Doc, selection: Selection) {
78        self.redo.clear();
79        if self.joins(kind, selection) {
80            return;
81        }
82        self.undo.push_back(Snapshot {
83            doc: doc.clone(),
84            selection,
85        });
86        while self.undo.len() > self.limit {
87            self.undo.pop_front();
88        }
89        self.last = None;
90    }
91
92    /// Close the edit, noting where it left the caret. The next edit joins this
93    /// group only if it starts from exactly here.
94    pub fn landed(&mut self, kind: EditKind, selection: Selection) {
95        self.last = (kind != EditKind::Structure).then_some((kind, selection.head));
96    }
97
98    /// Whether this edit continues the group the last one opened — same kind,
99    /// same text, and picking up exactly where that one stopped.
100    fn joins(&self, kind: EditKind, selection: Selection) -> bool {
101        if kind == EditKind::Structure || self.undo.is_empty() {
102            return false;
103        }
104        self.last == Some((kind, selection.head)) && selection.is_collapsed()
105    }
106
107    /// Anything that is not an edit ends the group — a motion, a click, a
108    /// focus change. Without this, typing a word, clicking elsewhere and typing
109    /// again would undo as one step across two places.
110    pub fn interrupt(&mut self) {
111        self.last = None;
112    }
113
114    /// Step back, handing the caller the document to restore. Pushes what it
115    /// was given onto the redo stack.
116    pub fn undo(&mut self, doc: &Doc, selection: Selection) -> Option<(Doc, Selection)> {
117        let previous = self.undo.pop_back()?;
118        self.redo.push(Snapshot {
119            doc: doc.clone(),
120            selection,
121        });
122        self.last = None;
123        Some((previous.doc, previous.selection))
124    }
125
126    pub fn redo(&mut self, doc: &Doc, selection: Selection) -> Option<(Doc, Selection)> {
127        let next = self.redo.pop()?;
128        self.undo.push_back(Snapshot {
129            doc: doc.clone(),
130            selection,
131        });
132        self.last = None;
133        Some((next.doc, next.selection))
134    }
135}