1use neco_textpatch::{apply_patches, inverse_patches, TextPatch};
21use neco_tree::{CursoredTree, PrunePolicy, Tree};
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum EntryKind {
30 Reversible,
32 Snapshot,
34}
35
36#[derive(Debug, Clone)]
44pub struct HistoryEntry {
45 label: String,
46 kind: EntryKind,
47 forward_patches: Option<Vec<TextPatch>>,
48 inverse_patches: Option<Vec<TextPatch>>,
49 snapshot: Option<String>,
50 checkpoint: Option<String>,
51 group_id: Option<u64>,
52}
53
54impl HistoryEntry {
55 pub fn label(&self) -> &str {
57 &self.label
58 }
59
60 pub fn kind(&self) -> EntryKind {
62 self.kind
63 }
64
65 pub fn forward_patches(&self) -> Option<&[TextPatch]> {
67 self.forward_patches.as_deref()
68 }
69
70 pub fn inverse_patches(&self) -> Option<&[TextPatch]> {
72 self.inverse_patches.as_deref()
73 }
74
75 pub fn snapshot(&self) -> Option<&str> {
77 self.snapshot.as_deref()
78 }
79
80 pub fn checkpoint(&self) -> Option<&str> {
82 self.checkpoint.as_deref()
83 }
84}
85
86#[derive(Debug, Clone)]
92pub struct UndoResult {
93 pub kind: EntryKind,
94 pub inverse_patches: Option<Vec<TextPatch>>,
95 pub snapshot: Option<String>,
96 pub label: String,
97}
98
99#[derive(Debug, Clone)]
101pub struct RedoResult {
102 pub kind: EntryKind,
103 pub forward_patches: Option<Vec<TextPatch>>,
104 pub snapshot: Option<String>,
105 pub label: String,
106}
107
108#[derive(Debug, Clone)]
110pub enum JumpStep {
111 Undo(UndoResult),
113 Redo(RedoResult),
115}
116
117#[derive(Debug, Clone)]
129pub struct EditHistory {
130 tree: CursoredTree<HistoryEntry>,
131 checkpoint_interval: u32,
132 edits_since_checkpoint: u32,
133 active_group_id: Option<u64>,
134 group_counter: u64,
135}
136
137impl EditHistory {
138 pub fn new(initial_text: &str) -> Self {
140 let root_entry = HistoryEntry {
141 label: String::new(),
142 kind: EntryKind::Snapshot,
143 forward_patches: None,
144 inverse_patches: None,
145 snapshot: Some(initial_text.to_string()),
146 checkpoint: Some(initial_text.to_string()),
147 group_id: None,
148 };
149 Self {
150 tree: CursoredTree::new(root_entry),
151 checkpoint_interval: 20,
152 edits_since_checkpoint: 0,
153 active_group_id: None,
154 group_counter: 0,
155 }
156 }
157
158 pub fn push_edit(&mut self, label: &str, current_text: &str, forward: Vec<TextPatch>) -> u64 {
166 let inverse = inverse_patches(current_text, &forward);
167 let checkpoint = self.maybe_checkpoint(current_text, &forward);
168
169 let entry = HistoryEntry {
170 label: label.to_string(),
171 kind: EntryKind::Reversible,
172 forward_patches: Some(forward),
173 inverse_patches: Some(inverse),
174 snapshot: None,
175 checkpoint,
176 group_id: self.active_group_id,
177 };
178 self.tree.push(entry)
179 }
180
181 pub fn push_snapshot(&mut self, label: &str, full_text: String) -> u64 {
188 self.edits_since_checkpoint = 0;
189 let entry = HistoryEntry {
190 label: label.to_string(),
191 kind: EntryKind::Snapshot,
192 forward_patches: None,
193 inverse_patches: None,
194 snapshot: Some(full_text.clone()),
195 checkpoint: Some(full_text),
196 group_id: self.active_group_id,
197 };
198 self.tree.push(entry)
199 }
200
201 pub fn begin_group(&mut self, _label: &str) {
205 if self.active_group_id.is_none() {
206 self.group_counter += 1;
207 self.active_group_id = Some(self.group_counter);
208 }
209 }
210
211 pub fn end_group(&mut self) {
213 self.active_group_id = None;
214 }
215
216 pub fn undo(&mut self) -> Option<Vec<UndoResult>> {
221 let mut results = Vec::new();
222 let (first_group_id, first_result) = self.undo_one()?;
223 results.push(first_result);
224
225 if let Some(group_id) = first_group_id {
226 while self.tree.has_parent() {
227 let entry = self.tree.current().value();
228 if entry.group_id != Some(group_id) {
229 break;
230 }
231 let (_, result) = self.undo_one()?;
232 results.push(result);
233 }
234 }
235
236 Some(results)
237 }
238
239 pub fn redo(&mut self) -> Option<Vec<RedoResult>> {
244 let mut results = Vec::new();
245 let (first_group_id, first_result) = self.redo_one()?;
246 results.push(first_result);
247
248 if let Some(group_id) = first_group_id {
249 while self.tree.has_children() {
250 let next_index = self.tree.current().child_count() - 1;
251 let next_entry = self.tree.current().children()[next_index].value();
252 if next_entry.group_id != Some(group_id) {
253 break;
254 }
255 let (_, result) = self.redo_one()?;
256 results.push(result);
257 }
258 }
259
260 Some(results)
261 }
262
263 pub fn jump_to(&mut self, id: u64) -> Option<Vec<JumpStep>> {
270 let current_path = self.tree.find_path_to(self.tree.current_id())?;
271 let target_path = self.tree.find_path_to(id)?;
272
273 let lca_depth = current_path
275 .iter()
276 .zip(target_path.iter())
277 .take_while(|(a, b)| a == b)
278 .count();
279
280 let mut steps = Vec::new();
281
282 let undo_count = current_path.len() - lca_depth;
284 for _ in 0..undo_count {
285 if let Some((_, result)) = self.undo_one() {
286 steps.push(JumpStep::Undo(result));
287 }
288 }
289
290 let redo_indices = &target_path[lca_depth..];
292 for &child_index in redo_indices {
293 if let Some((_, result)) = self.redo_child(child_index) {
294 steps.push(JumpStep::Redo(result));
295 }
296 }
297
298 Some(steps)
299 }
300
301 pub fn can_undo(&self) -> bool {
303 self.tree.has_parent()
304 }
305
306 pub fn can_redo(&self) -> bool {
308 self.tree.has_children()
309 }
310
311 pub fn current_id(&self) -> u64 {
313 self.tree.current_id()
314 }
315
316 pub fn current_label(&self) -> &str {
318 self.tree.current().value().label()
319 }
320
321 pub fn current_entry(&self) -> &HistoryEntry {
323 self.tree.current().value()
324 }
325
326 pub fn set_checkpoint_interval(&mut self, interval: u32) {
329 self.checkpoint_interval = interval;
330 }
331
332 pub fn prune(&mut self, policy: PrunePolicy) {
334 self.tree.prune(policy);
335 }
336
337 pub fn tree(&self) -> &Tree<HistoryEntry> {
339 self.tree.tree()
340 }
341
342 pub fn cursored_tree(&self) -> &CursoredTree<HistoryEntry> {
344 &self.tree
345 }
346
347 fn maybe_checkpoint(&mut self, current_text: &str, forward: &[TextPatch]) -> Option<String> {
350 self.edits_since_checkpoint += 1;
351 if self.edits_since_checkpoint >= self.checkpoint_interval {
352 self.edits_since_checkpoint = 0;
353 apply_patches(current_text, forward).ok()
354 } else {
355 None
356 }
357 }
358
359 fn undo_one(&mut self) -> Option<(Option<u64>, UndoResult)> {
360 if !self.tree.has_parent() {
361 return None;
362 }
363 let entry = self.tree.current().value();
364 let group_id = entry.group_id;
365 let result = UndoResult {
366 kind: entry.kind(),
367 inverse_patches: entry.inverse_patches.clone(),
368 snapshot: self.find_parent_snapshot(),
369 label: entry.label.clone(),
370 };
371 self.tree.go_parent();
372 Some((group_id, result))
373 }
374
375 fn redo_one(&mut self) -> Option<(Option<u64>, RedoResult)> {
376 if !self.tree.has_children() {
377 return None;
378 }
379 let child_index = self.tree.current().child_count() - 1;
380 self.redo_child(child_index)
381 }
382
383 fn redo_child(&mut self, child_index: usize) -> Option<(Option<u64>, RedoResult)> {
384 if !self.tree.go_child(child_index) {
385 return None;
386 }
387 let entry = self.tree.current().value();
388 Some((
389 entry.group_id,
390 RedoResult {
391 kind: entry.kind(),
392 forward_patches: entry.forward_patches.clone(),
393 snapshot: entry.snapshot.clone(),
394 label: entry.label.clone(),
395 },
396 ))
397 }
398
399 fn find_parent_snapshot(&self) -> Option<String> {
400 let current_path = self.tree.cursor_path();
404 if current_path.is_empty() {
405 return None;
406 }
407 let parent_path = ¤t_path[..current_path.len() - 1];
408 self.resolve_snapshot_at_path(parent_path)
409 }
410
411 fn resolve_snapshot_at_path(&self, path: &[usize]) -> Option<String> {
412 let mut node = self.tree.root();
415 let entry = node.value();
416 let mut latest = entry.snapshot.as_ref().or(entry.checkpoint.as_ref());
417
418 for &index in path {
419 node = node.children().get(index)?;
420 let e = node.value();
421 if e.snapshot.is_some() {
422 latest = e.snapshot.as_ref();
423 } else if e.checkpoint.is_some() {
424 latest = e.checkpoint.as_ref();
425 }
426 }
427 latest.cloned()
428 }
429}
430
431#[cfg(test)]
436mod tests {
437 use super::*;
438 use neco_textpatch::apply_patches;
439
440 fn make_insert(offset: usize, text: &str) -> Vec<TextPatch> {
441 vec![TextPatch::insert(offset, text)]
442 }
443
444 fn make_delete(start: usize, end: usize) -> Vec<TextPatch> {
445 vec![TextPatch::delete(start, end).unwrap()]
446 }
447
448 fn make_replace(start: usize, end: usize, text: &str) -> Vec<TextPatch> {
449 vec![TextPatch::replace(start, end, text).unwrap()]
450 }
451
452 #[test]
455 fn new_creates_root_with_initial_snapshot() {
456 let h = EditHistory::new("hello");
457 assert_eq!(h.current_id(), 0);
458 assert!(!h.can_undo());
459 assert!(!h.can_redo());
460
461 let root = h.tree().root().value();
462 assert_eq!(root.kind(), EntryKind::Snapshot);
463 assert_eq!(root.snapshot(), Some("hello"));
464 assert_eq!(root.checkpoint(), Some("hello"));
465 }
466
467 #[test]
470 fn push_edit_records_forward_and_inverse() {
471 let mut h = EditHistory::new("abc");
472 let text = "abc";
473 let forward = make_replace(1, 2, "B");
474 let id = h.push_edit("replace b", text, forward);
475
476 assert_eq!(h.current_id(), id);
477 assert!(h.can_undo());
478
479 let entry = h.cursored_tree().current().value();
480 assert_eq!(entry.kind(), EntryKind::Reversible);
481 assert_eq!(entry.label(), "replace b");
482
483 let fwd = entry.forward_patches().unwrap();
485 assert_eq!(fwd.len(), 1);
486 assert_eq!(fwd[0].replacement(), "B");
487
488 let inv = entry.inverse_patches().unwrap();
490 assert_eq!(inv.len(), 1);
491 assert_eq!(inv[0].replacement(), "b");
492 }
493
494 #[test]
495 fn push_edit_inverse_roundtrip() {
496 let original = "hello world";
497 let mut h = EditHistory::new(original);
498
499 let forward = make_replace(6, 11, "rust");
500 h.push_edit("change world", original, forward.clone());
501
502 let modified = apply_patches(original, &forward).unwrap();
503 assert_eq!(modified, "hello rust");
504
505 let entry = h.cursored_tree().current().value();
506 let inv = entry.inverse_patches().unwrap();
507 let restored = apply_patches(&modified, inv).unwrap();
508 assert_eq!(restored, original);
509 }
510
511 #[test]
514 fn push_snapshot_stores_full_text() {
515 let mut h = EditHistory::new("old");
516 let id = h.push_snapshot("reload", "new content".to_string());
517
518 assert_eq!(h.current_id(), id);
519 let entry = h.cursored_tree().current().value();
520 assert_eq!(entry.kind(), EntryKind::Snapshot);
521 assert_eq!(entry.snapshot(), Some("new content"));
522 }
523
524 #[test]
527 fn undo_returns_inverse_and_moves_to_parent() {
528 let mut h = EditHistory::new("abc");
529 h.push_edit("insert", "abc", make_insert(3, "d"));
530
531 assert!(h.can_undo());
532 let result = h.undo().unwrap().remove(0);
533 assert_eq!(result.kind, EntryKind::Reversible);
534 assert_eq!(result.label, "insert");
535 assert!(result.inverse_patches.is_some());
536 assert_eq!(h.current_id(), 0);
537 assert!(!h.can_undo());
538 }
539
540 #[test]
541 fn undo_at_root_returns_none() {
542 let mut h = EditHistory::new("text");
543 assert!(h.undo().is_none());
544 }
545
546 #[test]
547 fn redo_returns_forward_and_moves_to_child() {
548 let mut h = EditHistory::new("abc");
549 h.push_edit("insert", "abc", make_insert(3, "d"));
550 h.undo();
551
552 assert!(h.can_redo());
553 let result = h.redo().unwrap().remove(0);
554 assert_eq!(result.kind, EntryKind::Reversible);
555 assert_eq!(result.label, "insert");
556 assert!(result.forward_patches.is_some());
557 assert!(!h.can_redo());
558 }
559
560 #[test]
561 fn redo_at_leaf_returns_none() {
562 let mut h = EditHistory::new("text");
563 h.push_edit("edit", "text", make_insert(4, "!"));
564 assert!(h.redo().is_none());
565 }
566
567 #[test]
568 fn grouped_edits_undo_and_redo_together() {
569 let mut h = EditHistory::new("abc");
570 h.begin_group("group");
571 h.push_edit("first", "abc", make_insert(3, "d"));
572 h.push_edit("second", "abcd", make_insert(4, "e"));
573 h.end_group();
574
575 let undo = h.undo().unwrap();
576 assert_eq!(undo.len(), 2);
577 assert_eq!(h.current_id(), 0);
578
579 let redo = h.redo().unwrap();
580 assert_eq!(redo.len(), 2);
581 assert_eq!(h.current_id(), 2);
582 }
583
584 #[test]
587 fn undo_then_push_creates_new_branch() {
588 let mut h = EditHistory::new("abc");
589 let id1 = h.push_edit("first", "abc", make_insert(3, "1"));
590 h.undo(); let id2 = h.push_edit("second", "abc", make_insert(3, "2"));
592
593 assert_ne!(id1, id2);
594 assert_eq!(h.tree().root().child_count(), 2);
596 assert_eq!(h.current_id(), id2);
597 }
598
599 #[test]
602 fn jump_to_returns_undo_redo_steps() {
603 let mut h = EditHistory::new("abc");
604 let a = h.push_edit("a", "abc", make_insert(3, "d"));
605 let _a1 = h.push_edit("a1", "abcd", make_insert(4, "e"));
606
607 let steps = h.jump_to(0).unwrap();
609 assert_eq!(steps.len(), 2);
610 assert!(matches!(steps[0], JumpStep::Undo(_)));
611 assert!(matches!(steps[1], JumpStep::Undo(_)));
612 assert_eq!(h.current_id(), 0);
613
614 let steps = h.jump_to(a).unwrap();
616 assert_eq!(steps.len(), 1);
617 assert!(matches!(steps[0], JumpStep::Redo(_)));
618 assert_eq!(h.current_id(), a);
619 }
620
621 #[test]
622 fn jump_to_nonexistent_returns_none() {
623 let mut h = EditHistory::new("abc");
624 assert!(h.jump_to(999).is_none());
625 }
626
627 #[test]
628 fn jump_to_current_returns_empty_steps() {
629 let mut h = EditHistory::new("abc");
630 let a = h.push_edit("a", "abc", make_insert(3, "d"));
631 let steps = h.jump_to(a).unwrap();
632 assert!(steps.is_empty());
633 }
634
635 #[test]
636 fn jump_to_through_snapshot_node() {
637 let mut h = EditHistory::new("abc");
638 h.push_edit("edit", "abc", make_insert(3, "d"));
639 let snap_id = h.push_snapshot("reload", "xyz".to_string());
640 h.push_edit("after reload", "xyz", make_insert(3, "!"));
641
642 let steps = h.jump_to(0).unwrap();
644 assert_eq!(steps.len(), 3);
645 assert!(matches!(steps[0], JumpStep::Undo(_)));
646 assert!(matches!(steps[1], JumpStep::Undo(_)));
647 assert!(matches!(steps[2], JumpStep::Undo(_)));
648 assert_eq!(h.current_id(), 0);
649
650 let steps = h.jump_to(snap_id).unwrap();
652 assert_eq!(steps.len(), 2);
653 assert_eq!(h.current_id(), snap_id);
654 }
655
656 #[test]
657 fn jump_to_inside_group_is_node_precise() {
658 let mut h = EditHistory::new("abc");
659 h.begin_group("group");
660 let first = h.push_edit("first", "abc", make_insert(3, "d"));
661 let second = h.push_edit("second", "abcd", make_insert(4, "e"));
662 h.end_group();
663
664 assert_eq!(h.current_id(), second);
665
666 let steps = h.jump_to(first).unwrap();
667 assert_eq!(steps.len(), 1);
668 assert!(matches!(steps[0], JumpStep::Undo(_)));
669 assert_eq!(h.current_id(), first);
670 }
671
672 #[test]
675 fn checkpoint_is_inserted_at_interval() {
676 let mut h = EditHistory::new("x");
677 h.set_checkpoint_interval(3);
678
679 let mut text = "x".to_string();
680 for i in 0..5 {
681 let ch = char::from(b'a' + i);
682 let forward = make_insert(text.len(), &ch.to_string());
683 h.push_edit(&format!("add {ch}"), &text, forward.clone());
684 text = apply_patches(&text, &forward).unwrap();
685 }
686
687 let node_3 = h.tree().find(3).unwrap().value();
689 assert!(
690 node_3.checkpoint().is_some(),
691 "edit #3 should have a checkpoint"
692 );
693
694 let node_1 = h.tree().find(1).unwrap().value();
696 assert!(node_1.checkpoint().is_none());
697 let node_2 = h.tree().find(2).unwrap().value();
698 assert!(node_2.checkpoint().is_none());
699 }
700
701 #[test]
704 fn can_undo_can_redo_reflect_cursor_position() {
705 let mut h = EditHistory::new("abc");
706 assert!(!h.can_undo());
707 assert!(!h.can_redo());
708
709 h.push_edit("edit", "abc", make_insert(3, "d"));
710 assert!(h.can_undo());
711 assert!(!h.can_redo());
712
713 h.undo();
714 assert!(!h.can_undo());
715 assert!(h.can_redo());
716 }
717
718 #[test]
721 fn prune_removes_old_branches() {
722 let mut h = EditHistory::new("abc");
723 h.push_edit("a", "abc", make_insert(3, "1"));
724 h.undo();
725 h.push_edit("b", "abc", make_insert(3, "2"));
726 h.undo();
727 h.push_edit("c", "abc", make_insert(3, "3"));
728
729 assert_eq!(h.tree().root().child_count(), 3);
731
732 h.prune(PrunePolicy::KeepLastN(1));
733 assert_eq!(h.tree().root().child_count(), 1);
735 }
736
737 #[test]
740 fn undo_snapshot_entry_provides_parent_snapshot() {
741 let mut h = EditHistory::new("original");
742 h.push_snapshot("reload", "reloaded".to_string());
743
744 let result = h.undo().unwrap().remove(0);
745 assert_eq!(result.kind, EntryKind::Snapshot);
746 assert_eq!(result.snapshot.as_deref(), Some("original"));
747 }
748
749 #[test]
752 fn multiple_edits_undo_redo_roundtrip() {
753 let mut h = EditHistory::new("hello");
754 let mut text = "hello".to_string();
755
756 let fwd1 = make_insert(5, " world");
758 h.push_edit("add world", &text, fwd1.clone());
759 text = apply_patches(&text, &fwd1).unwrap();
760 assert_eq!(text, "hello world");
761
762 let fwd2 = make_replace(6, 11, "rust");
764 h.push_edit("change lang", &text, fwd2.clone());
765 text = apply_patches(&text, &fwd2).unwrap();
766 assert_eq!(text, "hello rust");
767
768 let u2 = h.undo().unwrap().remove(0);
770 text = apply_patches(&text, u2.inverse_patches.as_ref().unwrap()).unwrap();
771 assert_eq!(text, "hello world");
772
773 let u1 = h.undo().unwrap().remove(0);
775 text = apply_patches(&text, u1.inverse_patches.as_ref().unwrap()).unwrap();
776 assert_eq!(text, "hello");
777
778 let r1 = h.redo().unwrap().remove(0);
780 text = apply_patches(&text, r1.forward_patches.as_ref().unwrap()).unwrap();
781 assert_eq!(text, "hello world");
782
783 let r2 = h.redo().unwrap().remove(0);
785 text = apply_patches(&text, r2.forward_patches.as_ref().unwrap()).unwrap();
786 assert_eq!(text, "hello rust");
787 }
788
789 #[test]
792 fn delete_undo_restores_text() {
793 let mut h = EditHistory::new("abcdef");
794 let fwd = make_delete(2, 4);
795 h.push_edit("delete cd", "abcdef", fwd.clone());
796
797 let modified = apply_patches("abcdef", &fwd).unwrap();
798 assert_eq!(modified, "abef");
799
800 let u = h.undo().unwrap().remove(0);
801 let restored = apply_patches(&modified, u.inverse_patches.as_ref().unwrap()).unwrap();
802 assert_eq!(restored, "abcdef");
803 }
804}