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
18use crate::comment::Anchor;
19
20/// How many steps a document keeps.
21///
22/// Deeper than a text field's, because a document is the thing people actually
23/// walk backwards through, and bounded because an unbounded history of a
24/// growing document is a slow leak nothing reclaims.
25pub const DEFAULT_UNDO_LIMIT: usize = 100;
26
27/// What an edit did, so a run of the same kind can coalesce into one step
28/// instead of giving the document back a character at a time.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum EditKind {
31    Insert,
32    Delete,
33    /// Anything structural — a split, an indent, a block turned into another.
34    /// Never coalesces: these are the steps a reader wants to land on.
35    Structure,
36}
37
38#[derive(Clone)]
39struct Snapshot {
40    doc: Doc,
41    selection: Selection,
42    /// Carried with the document because an undo replaces it wholesale: there
43    /// is no delta to map an anchor through, so the anchors of that moment have
44    /// to be the ones that come back.
45    anchors: Vec<Anchor>,
46}
47
48pub struct History {
49    /// Points to return to, oldest first.
50    undo: VecDeque<Snapshot>,
51    /// Undone points, newest last. Cleared by any fresh edit — the usual model,
52    /// and the only one where redo cannot resurrect a branch the document has
53    /// already diverged from.
54    redo: Vec<Snapshot>,
55    limit: usize,
56    /// The kind of the last edit and where it *left* the caret, which is what
57    /// decides whether the next edit joins that group or starts a new one.
58    last: Option<(EditKind, Cursor)>,
59}
60
61impl Default for History {
62    fn default() -> Self {
63        Self {
64            undo: VecDeque::new(),
65            redo: Vec::new(),
66            limit: DEFAULT_UNDO_LIMIT,
67            last: None,
68        }
69    }
70}
71
72impl History {
73    pub fn with_limit(limit: usize) -> Self {
74        Self {
75            limit,
76            ..Self::default()
77        }
78    }
79
80    /// Record the state *before* an edit of `kind`; [`History::landed`] closes
81    /// it afterwards. A run of insertions leaves one step, so undo gives back
82    /// the word rather than the letter.
83    pub fn record(&mut self, kind: EditKind, doc: &Doc, selection: Selection, anchors: &[Anchor]) {
84        self.redo.clear();
85        if self.joins(kind, selection) {
86            return;
87        }
88        self.undo.push_back(Snapshot {
89            doc: doc.clone(),
90            selection,
91            anchors: anchors.to_vec(),
92        });
93        while self.undo.len() > self.limit {
94            self.undo.pop_front();
95        }
96        self.last = None;
97    }
98
99    /// Close the edit, noting where it left the caret. The next edit joins this
100    /// group only if it starts from exactly here.
101    pub fn landed(&mut self, kind: EditKind, selection: Selection) {
102        self.last = (kind != EditKind::Structure).then_some((kind, selection.head));
103    }
104
105    /// Whether this edit continues the group the last one opened — same kind,
106    /// same text, and picking up exactly where that one stopped.
107    fn joins(&self, kind: EditKind, selection: Selection) -> bool {
108        if kind == EditKind::Structure || self.undo.is_empty() {
109            return false;
110        }
111        self.last == Some((kind, selection.head)) && selection.is_collapsed()
112    }
113
114    /// Anything that is not an edit ends the group — a motion, a click, a
115    /// focus change. Without this, typing a word, clicking elsewhere and typing
116    /// again would undo as one step across two places.
117    pub fn interrupt(&mut self) {
118        self.last = None;
119    }
120
121    /// Step back, handing the caller the state to restore. Pushes what it was
122    /// given onto the redo stack.
123    pub fn undo(&mut self, doc: &Doc, selection: Selection, anchors: &[Anchor]) -> Option<Step> {
124        let previous = self.undo.pop_back()?;
125        self.redo.push(Snapshot {
126            doc: doc.clone(),
127            selection,
128            anchors: anchors.to_vec(),
129        });
130        self.last = None;
131        Some(previous.into())
132    }
133
134    pub fn redo(&mut self, doc: &Doc, selection: Selection, anchors: &[Anchor]) -> Option<Step> {
135        let next = self.redo.pop()?;
136        self.undo.push_back(Snapshot {
137            doc: doc.clone(),
138            selection,
139            anchors: anchors.to_vec(),
140        });
141        self.last = None;
142        Some(next.into())
143    }
144}
145
146/// The state a step restores, which is a whole moment rather than a diff.
147pub struct Step {
148    pub doc: Doc,
149    pub selection: Selection,
150    pub anchors: Vec<Anchor>,
151}
152
153impl From<Snapshot> for Step {
154    fn from(snapshot: Snapshot) -> Self {
155        Self {
156            doc: snapshot.doc,
157            selection: snapshot.selection,
158            anchors: snapshot.anchors,
159        }
160    }
161}