1use crate::change::ChangeSet;
12use crate::transaction::{EditSource, EditTransaction, UndoGroupId};
13
14#[derive(Debug, Clone)]
15struct Entry {
16 group: UndoGroupId,
17 forward: ChangeSet,
18 inverse: ChangeSet,
20}
21
22#[derive(Debug, Default)]
28pub struct History {
29 applied: Vec<Entry>,
30 undone: Vec<Entry>,
33 next_group: u64,
34 last_source: Option<EditSource>,
36 group_broken: bool,
38}
39
40impl History {
41 pub fn new() -> Self {
42 Self::default()
43 }
44
45 pub fn group_for(&mut self, source: &EditSource) -> UndoGroupId {
50 let continues = !self.group_broken
51 && source.coalesces()
52 && self.last_source.as_ref() == Some(source)
53 && !self.applied.is_empty();
54
55 if continues {
56 return self.applied[self.applied.len() - 1].group;
57 }
58
59 self.next_group += 1;
60 UndoGroupId(self.next_group)
61 }
62
63 pub fn break_group(&mut self) {
68 self.group_broken = true;
69 }
70
71 pub fn push(&mut self, transaction: &EditTransaction, inverse: ChangeSet) {
74 if transaction.is_empty() {
75 return;
76 }
77 self.undone.clear();
80 self.group_broken = false;
84 self.last_source = Some(transaction.source.clone());
85 self.applied.push(Entry {
86 group: transaction.undo_group,
87 forward: transaction.changes.clone(),
88 inverse,
89 });
90 }
91
92 pub fn can_undo(&self) -> bool {
93 !self.applied.is_empty()
94 }
95
96 pub fn can_redo(&self) -> bool {
97 !self.undone.is_empty()
98 }
99
100 pub fn undo(&mut self) -> Option<ChangeSet> {
105 let group = self.applied.last()?.group;
106
107 let mut composed: Option<ChangeSet> = None;
108 while self.applied.last().is_some_and(|e| e.group == group) {
109 let entry = self.applied.pop().expect("just checked");
110 composed = Some(match composed {
112 None => entry.inverse.clone(),
113 Some(acc) => acc.compose(&entry.inverse),
114 });
115 self.undone.push(entry);
116 }
117
118 self.group_broken = true;
120 self.last_source = None;
121 composed
122 }
123
124 pub fn redo(&mut self) -> Option<ChangeSet> {
126 let group = self.undone.last()?.group;
127
128 let mut composed: Option<ChangeSet> = None;
129 while self.undone.last().is_some_and(|e| e.group == group) {
130 let entry = self.undone.pop().expect("just checked");
131 composed = Some(match composed {
133 None => entry.forward.clone(),
134 Some(acc) => acc.compose(&entry.forward),
135 });
136 self.applied.push(entry);
137 }
138
139 self.group_broken = true;
140 self.last_source = None;
141 composed
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148 use crate::transaction::Version;
149 use ropey::Rope;
150 use termesh_core::{BufferId, ProposalId};
151
152 struct Doc {
154 text: Rope,
155 history: History,
156 version: Version,
157 }
158
159 impl Doc {
160 fn new(text: &str) -> Self {
161 Self { text: Rope::from_str(text), history: History::new(), version: Version(0) }
162 }
163
164 fn edit(&mut self, from: usize, to: usize, insert: &str, source: EditSource) {
165 let changes = ChangeSet::replace(self.text.len_chars(), from, to, insert);
166 let group = self.history.group_for(&source);
167 let tx = EditTransaction::new(BufferId::new(1), self.version, changes, source, group);
168 let inverse = tx.changes.invert(&self.text);
170 self.text = tx.changes.apply(&self.text);
171 self.version = self.version.next();
172 self.history.push(&tx, inverse);
173 }
174
175 fn type_char(&mut self, at: usize, ch: &str) {
176 self.edit(at, at, ch, EditSource::Keyboard);
177 }
178
179 fn undo(&mut self) -> bool {
180 match self.history.undo() {
181 Some(cs) => {
182 self.text = cs.apply(&self.text);
183 self.version = self.version.next();
184 true
185 }
186 None => false,
187 }
188 }
189
190 fn redo(&mut self) -> bool {
191 match self.history.redo() {
192 Some(cs) => {
193 self.text = cs.apply(&self.text);
194 self.version = self.version.next();
195 true
196 }
197 None => false,
198 }
199 }
200
201 fn text(&self) -> String {
202 self.text.to_string()
203 }
204 }
205
206 #[test]
207 fn nothing_to_undo_at_the_start_of_history() {
208 let mut doc = Doc::new("hello");
209 assert!(!doc.history.can_undo());
210 assert!(!doc.undo());
211 }
212
213 #[test]
214 fn a_single_edit_undoes_and_redoes() {
215 let mut doc = Doc::new("hello");
216 doc.edit(5, 5, " world", EditSource::Paste);
217 assert_eq!(doc.text(), "hello world");
218
219 assert!(doc.undo());
220 assert_eq!(doc.text(), "hello");
221 assert!(doc.redo());
222 assert_eq!(doc.text(), "hello world");
223 }
224
225 #[test]
226 fn a_run_of_typing_undoes_as_one_step() {
227 let mut doc = Doc::new("()");
228 for (i, ch) in "abc".chars().enumerate() {
229 doc.type_char(1 + i, &ch.to_string());
230 }
231 assert_eq!(doc.text(), "(abc)");
232
233 assert!(doc.undo());
234 assert_eq!(doc.text(), "()", "three keystrokes, one undo");
235 assert!(!doc.history.can_undo());
236 }
237
238 #[test]
239 fn a_cursor_move_ends_the_run() {
240 let mut doc = Doc::new("()");
241 doc.type_char(1, "a");
242 doc.history.break_group(); doc.type_char(2, "b");
244 assert_eq!(doc.text(), "(ab)");
245
246 doc.undo();
247 assert_eq!(doc.text(), "(a)", "the break split the run in two");
248 doc.undo();
249 assert_eq!(doc.text(), "()");
250 }
251
252 #[test]
253 fn a_different_source_ends_the_run_without_being_asked() {
254 let mut doc = Doc::new("()");
255 doc.type_char(1, "a");
256 doc.edit(2, 2, "!", EditSource::Formatter);
257 doc.undo();
258 assert_eq!(doc.text(), "(a)", "the formatter edit is its own step");
259 }
260
261 #[test]
264 fn an_accepted_proposal_undoes_in_one_step() {
265 let mut doc = Doc::new("fn main() {}");
266 let source = EditSource::Agent(ProposalId::new(1));
267
268 let group = doc.history.group_for(&source);
270 for (from, to, insert) in [(3, 7, "run"), (0, 0, "pub ")] {
271 let changes = ChangeSet::replace(doc.text.len_chars(), from, to, insert);
272 let tx =
273 EditTransaction::new(BufferId::new(1), doc.version, changes, source.clone(), group);
274 let inverse = tx.changes.invert(&doc.text);
275 doc.text = tx.changes.apply(&doc.text);
276 doc.history.push(&tx, inverse);
277 }
278 assert_eq!(doc.text(), "pub fn run() {}");
279
280 assert!(doc.undo());
281 assert_eq!(doc.text(), "fn main() {}", "one undo reverses the whole proposal");
282 assert!(!doc.history.can_undo());
283 }
284
285 #[test]
286 fn undo_then_type_discards_the_redo_stack() {
287 let mut doc = Doc::new("a");
288 doc.edit(1, 1, "b", EditSource::Paste);
289 doc.undo();
290 assert!(doc.history.can_redo());
291
292 doc.edit(1, 1, "c", EditSource::Paste);
293 assert!(!doc.history.can_redo(), "redoing onto a diverged document is not offered");
294 assert_eq!(doc.text(), "ac");
295 }
296
297 #[test]
298 fn typing_after_an_undo_starts_a_fresh_group() {
299 let mut doc = Doc::new("()");
300 doc.type_char(1, "a");
301 doc.type_char(2, "b");
302 doc.undo();
303 assert_eq!(doc.text(), "()");
304
305 doc.type_char(1, "z");
306 doc.undo();
307 assert_eq!(doc.text(), "()", "the new keystroke must not rejoin the reversed group");
308 }
309
310 #[test]
311 fn many_edits_round_trip_all_the_way_back() {
312 let mut doc = Doc::new("start");
313 let original = doc.text();
314 for (from, to, insert, source) in [
315 (5, 5, " middle", EditSource::Paste),
316 (0, 5, "BEGIN", EditSource::Replace),
317 (5, 12, "", EditSource::Formatter),
318 ] {
319 doc.edit(from, to, insert, source);
320 }
321 assert_ne!(doc.text(), original);
322
323 while doc.undo() {}
324 assert_eq!(doc.text(), original, "history unwinds completely");
325
326 while doc.redo() {}
327 assert_eq!(doc.text(), "BEGIN");
328 }
329
330 #[test]
331 fn empty_transactions_are_not_recorded() {
332 let mut doc = Doc::new("hello");
333 doc.edit(2, 2, "", EditSource::Keyboard);
334 assert!(!doc.history.can_undo(), "a no-op edit is not an undo step");
335 }
336
337 #[test]
338 fn an_empty_edit_cannot_swallow_a_pending_group_break() {
339 let mut doc = Doc::new("()");
340 doc.type_char(1, "a");
341 doc.history.break_group(); doc.edit(2, 2, "", EditSource::Keyboard); doc.type_char(2, "b");
345
346 doc.undo();
347 assert_eq!(doc.text(), "(a)", "the break must survive an edit that did nothing");
348 }
349}