Skip to main content

ochrepad_core/
history.rs

1//! Undo/redo over [`Transaction`]s, with coalescing of consecutive typing.
2
3use std::time::Duration;
4
5use crate::changeset::ChangeSet;
6use crate::selection::SelectionSet;
7
8/// The concrete result of resolving an action: the edits plus the selections
9/// before and after.
10#[derive(Clone, Debug)]
11pub struct Transaction {
12    pub changes: ChangeSet,
13    pub inverse: ChangeSet,
14    pub before: SelectionSet,
15    pub after: SelectionSet,
16}
17
18/// Which consecutive edits may merge into one undo step.
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20pub enum EditKind {
21    /// A single grapheme typed at every caret (not a newline).
22    Insert,
23    DeleteBackward,
24    DeleteForward,
25    /// Never coalesces.
26    Other,
27}
28
29/// Typing pauses longer than this start a new undo step.
30pub const COALESCE_TIMEOUT: Duration = Duration::from_secs(1);
31
32/// Identifies a document state. Undo/redo restore the state's revision, so
33/// `revision == saved_revision` exactly when the text matches the saved file.
34pub type Revision = u64;
35
36#[derive(Clone, Debug)]
37pub struct UndoStep {
38    pub txns: Vec<Transaction>,
39    pub kind: EditKind,
40    pub rev_before: Revision,
41    pub rev_after: Revision,
42    last_edit_at: Duration,
43}
44
45#[derive(Clone, Debug, Default)]
46pub struct History {
47    undo: Vec<UndoStep>,
48    redo: Vec<UndoStep>,
49    /// False after cursor movement, undo/redo or save: the next edit starts a
50    /// new step even if it would otherwise coalesce.
51    coalesce_open: bool,
52    /// Nesting depth of explicit groups (macro playback, replace all).
53    group_depth: usize,
54    /// Whether the open group already has a step on the undo stack.
55    group_started: bool,
56}
57
58impl History {
59    pub fn push(
60        &mut self,
61        txn: Transaction,
62        kind: EditKind,
63        rev_before: Revision,
64        rev_after: Revision,
65        now: Duration,
66    ) {
67        self.redo.clear();
68        let extend_top = if self.group_depth > 0 {
69            self.group_started
70        } else {
71            self.coalesce_open
72                && kind != EditKind::Other
73                && self.undo.last().is_some_and(|top| {
74                    top.kind == kind
75                        && top.rev_after == rev_before
76                        && now.saturating_sub(top.last_edit_at) <= COALESCE_TIMEOUT
77                })
78        };
79        match self.undo.last_mut() {
80            Some(top) if extend_top => {
81                top.txns.push(txn);
82                top.rev_after = rev_after;
83                top.last_edit_at = now;
84            }
85            _ => self.undo.push(UndoStep {
86                txns: vec![txn],
87                kind,
88                rev_before,
89                rev_after,
90                last_edit_at: now,
91            }),
92        }
93        self.group_started = self.group_depth > 0;
94        self.coalesce_open = true;
95    }
96
97    pub fn break_coalescing(&mut self) {
98        self.coalesce_open = false;
99    }
100
101    /// Everything pushed until the matching `end_group` becomes one undo step.
102    pub fn begin_group(&mut self) {
103        if self.group_depth == 0 {
104            self.group_started = false;
105        }
106        self.group_depth += 1;
107    }
108
109    pub fn end_group(&mut self) {
110        self.group_depth = self.group_depth.saturating_sub(1);
111        if self.group_depth == 0 {
112            self.group_started = false;
113            self.coalesce_open = false;
114        }
115    }
116
117    pub fn can_undo(&self) -> bool {
118        !self.undo.is_empty()
119    }
120
121    pub fn can_redo(&self) -> bool {
122        !self.redo.is_empty()
123    }
124
125    pub(crate) fn pop_undo(&mut self) -> Option<UndoStep> {
126        self.coalesce_open = false;
127        self.group_started = false;
128        self.undo.pop()
129    }
130
131    pub(crate) fn push_redo(&mut self, step: UndoStep) {
132        self.redo.push(step);
133    }
134
135    pub(crate) fn pop_redo(&mut self) -> Option<UndoStep> {
136        self.coalesce_open = false;
137        self.group_started = false;
138        self.redo.pop()
139    }
140
141    pub(crate) fn push_undo(&mut self, step: UndoStep) {
142        self.undo.push(step);
143    }
144}