1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
|
//! Undo/redo history (MJB-HLR-011).
//!
//! Each committed edit stores the pair (forward transaction, inverse
//! transaction). `cursor` is the number of entries currently *applied*, so
//! entries at or beyond it have been reverted and are available to redo.
//!
//! Undo at the start and redo at the end are no-ops, not errors — reaching
//! either end is ordinary use, not a fault (MJB-LLR-052, MJB-LLR-053).
use super::transaction::Transaction;
#[derive(Debug, Clone)]
struct Entry {
forward: Transaction,
inverse: Transaction,
/// Identifies the buffer state this entry produces. Never reused.
id: usize,
}
#[derive(Debug, Clone, Default)]
pub struct History {
entries: Vec<Entry>,
/// Number of entries applied; also the index of the next redo.
cursor: usize,
/// Monotonic source of entry ids.
///
/// Deliberately *not* the same thing as `cursor`. Using stack depth to
/// identify a state is wrong: saving at depth 3, undoing, then making a
/// different edit returns to depth 3 while the content differs, so a
/// depth comparison would report the buffer clean when it is not.
next_id: usize,
}
impl History {
pub fn new() -> Self {
Self::default()
}
/// MJB-LLR-051: record an applied edit.
///
/// Anything previously undone is discarded — committing a new edit after an
/// undo abandons the branch that was reverted.
pub fn commit(&mut self, forward: Transaction, inverse: Transaction) {
self.entries.truncate(self.cursor);
self.next_id += 1;
self.entries.push(Entry {
forward,
inverse,
id: self.next_id,
});
self.cursor = self.entries.len();
}
/// MJB-LLR-052: the transaction that reverts the most recent edit, or
/// `None` when nothing remains to undo.
pub fn undo(&mut self) -> Option<&Transaction> {
if self.cursor == 0 {
return None;
}
self.cursor -= 1;
Some(&self.entries[self.cursor].inverse)
}
/// MJB-LLR-053: the transaction that reapplies the most recently undone
/// edit, or `None` when nothing remains to redo.
pub fn redo(&mut self) -> Option<&Transaction> {
if self.cursor >= self.entries.len() {
return None;
}
let t = &self.entries[self.cursor].forward;
self.cursor += 1;
Some(t)
}
/// Identifies the current buffer state.
///
/// Zero means pristine — no edit applied. Otherwise it is the id of the
/// most recently applied entry. Two calls return the same value exactly
/// when the buffer content is the same, which is what lets "modified" be
/// *derived* rather than latched; see
/// [`super::document::Document::is_modified`].
pub fn revision(&self) -> usize {
match self.cursor {
0 => 0,
n => self.entries[n - 1].id,
}
}
pub fn can_undo(&self) -> bool {
self.cursor > 0
}
pub fn can_redo(&self) -> bool {
self.cursor < self.entries.len()
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
#[cfg(test)]
mod tests {
use ropey::Rope;
use super::*;
use crate::buffer::transaction::Transaction;
fn edit(text: &str, from: usize, to: usize, ins: Option<&str>) -> (Transaction, Transaction) {
let r = Rope::from_str(text);
let t = Transaction::change(&r, [(from, to, ins.map(str::to_owned))]);
let inv = Transaction::new(t.changes.invert(&r));
(t, inv)
}
#[test]
fn mjb_llr_052_undo_past_start_is_a_noop() {
let mut h = History::new();
assert!(h.undo().is_none());
assert!(!h.can_undo());
// Repeated attempts must stay safe, not underflow the cursor.
assert!(h.undo().is_none());
assert!(h.undo().is_none());
}
#[test]
fn mjb_llr_053_redo_past_end_is_a_noop() {
let mut h = History::new();
let (f, i) = edit("abc", 0, 1, None);
h.commit(f, i);
assert!(h.redo().is_none(), "nothing has been undone yet");
assert!(!h.can_redo());
}
#[test]
fn mjb_llr_051_commit_then_undo_then_redo() {
let mut h = History::new();
let (f, i) = edit("abc", 0, 1, None);
h.commit(f, i);
assert!(h.can_undo());
assert!(h.undo().is_some());
assert!(!h.can_undo());
assert!(h.can_redo());
assert!(h.redo().is_some());
assert!(!h.can_redo());
}
#[test]
fn mjb_llr_051_commit_after_undo_discards_the_redo_branch() {
let mut h = History::new();
let (f1, i1) = edit("abc", 0, 1, None);
let (f2, i2) = edit("bc", 0, 1, None);
h.commit(f1, i1);
h.commit(f2, i2);
h.undo();
assert!(h.can_redo());
let (f3, i3) = edit("bc", 1, 2, None);
h.commit(f3, i3);
assert!(!h.can_redo(), "the undone branch must be discarded");
assert_eq!(h.len(), 2);
}
/// MJB-LLR-116: a revision identifies *content*, not stack depth.
///
/// Regression guard for the trap this replaced: save at depth 2, undo, then
/// make a different edit. The cursor returns to 2, but the buffer no longer
/// matches what was saved, so the revision must differ.
#[test]
fn mjb_llr_116_revision_is_not_stack_depth() {
let mut h = History::new();
let (f1, i1) = edit("abcdef", 0, 1, None);
let (f2, i2) = edit("bcdef", 0, 1, None);
h.commit(f1, i1);
h.commit(f2, i2);
let saved = h.revision();
h.undo();
assert_ne!(h.revision(), saved, "undo leaves a different state");
// A different second edit, landing at the same stack depth.
let (f3, i3) = edit("bcdef", 1, 2, None);
h.commit(f3, i3);
assert_eq!(h.len(), 2, "same depth as when we saved");
assert_ne!(
h.revision(),
saved,
"same depth, different content: must not look saved"
);
}
#[test]
fn mjb_llr_116_revision_is_zero_when_pristine_and_returns_on_undo() {
let mut h = History::new();
assert_eq!(h.revision(), 0);
let (f, i) = edit("abc", 0, 1, None);
h.commit(f, i);
let after = h.revision();
assert_ne!(after, 0);
h.undo();
assert_eq!(h.revision(), 0, "undoing to pristine returns to revision 0");
h.redo();
assert_eq!(h.revision(), after, "redo restores the same revision");
}
#[test]
fn mjb_llr_052_undo_walks_back_in_order() {
let mut h = History::new();
for n in 0..3 {
let (f, i) = edit("abcdef", n, n + 1, None);
h.commit(f, i);
}
assert_eq!(h.len(), 3);
for _ in 0..3 {
assert!(h.undo().is_some());
}
assert!(h.undo().is_none(), "exhausted");
}
}
|