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 ui::history::SnapshotHistory;
15
16use markdown::{Cursor, Doc, Selection};
17
18use crate::{comment::Anchor, editor::Mode};
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    /// Which form the document was being edited in, so a step back across a
42    /// switch to the source lands in the form it was taken in.
43    mode: Mode,
44    selection: Selection,
45    /// Carried with the document because an undo replaces it wholesale: there
46    /// is no delta to map an anchor through, so the anchors of that moment have
47    /// to be the ones that come back.
48    anchors: Vec<Anchor>,
49}
50
51pub struct History {
52    snapshots: SnapshotHistory<Snapshot>,
53    /// The kind of the last edit and where it *left* the caret, which is what
54    /// decides whether the next edit joins that group or starts a new one.
55    last: Option<(EditKind, Cursor)>,
56}
57
58impl Default for History {
59    fn default() -> Self {
60        Self {
61            snapshots: SnapshotHistory::new(DEFAULT_UNDO_LIMIT),
62            last: None,
63        }
64    }
65}
66
67impl History {
68    pub fn with_limit(limit: usize) -> Self {
69        Self {
70            snapshots: SnapshotHistory::new(limit),
71            last: None,
72        }
73    }
74
75    /// Record the state *before* an edit of `kind`; [`History::landed`] closes
76    /// it afterwards. A run of insertions leaves one step, so undo gives back
77    /// the word rather than the letter.
78    pub fn record(
79        &mut self,
80        kind: EditKind,
81        mode: Mode,
82        doc: &Doc,
83        selection: Selection,
84        anchors: &[Anchor],
85    ) {
86        let joins = self.joins(kind, selection);
87        self.snapshots.record((!joins).then(|| Snapshot {
88            doc: doc.clone(),
89            mode,
90            selection,
91            anchors: anchors.to_vec(),
92        }));
93        if !joins {
94            self.last = None;
95        }
96    }
97
98    /// Close the edit, noting where it left the caret. The next edit joins this
99    /// group only if it starts from exactly here.
100    pub fn landed(&mut self, kind: EditKind, selection: Selection) {
101        self.last = (kind != EditKind::Structure).then_some((kind, selection.head));
102    }
103
104    /// Whether this edit continues the group the last one opened — same kind,
105    /// same text, and picking up exactly where that one stopped.
106    fn joins(&self, kind: EditKind, selection: Selection) -> bool {
107        if kind == EditKind::Structure || !self.snapshots.can_undo() {
108            return false;
109        }
110        self.last == Some((kind, selection.head)) && selection.is_collapsed()
111    }
112
113    /// Anything that is not an edit ends the group — a motion, a click, a
114    /// focus change. Without this, typing a word, clicking elsewhere and typing
115    /// again would undo as one step across two places.
116    pub fn interrupt(&mut self) {
117        self.last = None;
118    }
119
120    /// Step back, handing the caller the state to restore. Pushes what it was
121    /// given onto the redo stack.
122    pub fn undo(
123        &mut self,
124        mode: Mode,
125        doc: &Doc,
126        selection: Selection,
127        anchors: &[Anchor],
128    ) -> Option<Step> {
129        let previous = self.snapshots.undo(|| Snapshot {
130            doc: doc.clone(),
131            mode,
132            selection,
133            anchors: anchors.to_vec(),
134        })?;
135        self.last = None;
136        Some(previous.into())
137    }
138
139    pub fn redo(
140        &mut self,
141        mode: Mode,
142        doc: &Doc,
143        selection: Selection,
144        anchors: &[Anchor],
145    ) -> Option<Step> {
146        let next = self.snapshots.redo(|| Snapshot {
147            doc: doc.clone(),
148            mode,
149            selection,
150            anchors: anchors.to_vec(),
151        })?;
152        self.last = None;
153        Some(next.into())
154    }
155}
156
157/// The state a step restores, which is a whole moment rather than a diff.
158pub struct Step {
159    pub doc: Doc,
160    pub mode: Mode,
161    pub selection: Selection,
162    pub anchors: Vec<Anchor>,
163}
164
165impl From<Snapshot> for Step {
166    fn from(snapshot: Snapshot) -> Self {
167        Self {
168            doc: snapshot.doc,
169            mode: snapshot.mode,
170            selection: snapshot.selection,
171            anchors: snapshot.anchors,
172        }
173    }
174}