use crate::change::ChangeSet;
use crate::transaction::{EditSource, EditTransaction, UndoGroupId};
#[derive(Debug, Clone)]
struct Entry {
group: UndoGroupId,
forward: ChangeSet,
inverse: ChangeSet,
}
#[derive(Debug, Default)]
pub struct History {
applied: Vec<Entry>,
undone: Vec<Entry>,
next_group: u64,
last_source: Option<EditSource>,
group_broken: bool,
}
impl History {
pub fn new() -> Self {
Self::default()
}
pub fn group_for(&mut self, source: &EditSource) -> UndoGroupId {
let continues = !self.group_broken
&& source.coalesces()
&& self.last_source.as_ref() == Some(source)
&& !self.applied.is_empty();
if continues {
return self.applied[self.applied.len() - 1].group;
}
self.next_group += 1;
UndoGroupId(self.next_group)
}
pub fn break_group(&mut self) {
self.group_broken = true;
}
pub fn push(&mut self, transaction: &EditTransaction, inverse: ChangeSet) {
if transaction.is_empty() {
return;
}
self.undone.clear();
self.group_broken = false;
self.last_source = Some(transaction.source.clone());
self.applied.push(Entry {
group: transaction.undo_group,
forward: transaction.changes.clone(),
inverse,
});
}
pub fn can_undo(&self) -> bool {
!self.applied.is_empty()
}
pub fn can_redo(&self) -> bool {
!self.undone.is_empty()
}
pub fn undo(&mut self) -> Option<ChangeSet> {
let group = self.applied.last()?.group;
let mut composed: Option<ChangeSet> = None;
while self.applied.last().is_some_and(|e| e.group == group) {
let entry = self.applied.pop().expect("just checked");
composed = Some(match composed {
None => entry.inverse.clone(),
Some(acc) => acc.compose(&entry.inverse),
});
self.undone.push(entry);
}
self.group_broken = true;
self.last_source = None;
composed
}
pub fn redo(&mut self) -> Option<ChangeSet> {
let group = self.undone.last()?.group;
let mut composed: Option<ChangeSet> = None;
while self.undone.last().is_some_and(|e| e.group == group) {
let entry = self.undone.pop().expect("just checked");
composed = Some(match composed {
None => entry.forward.clone(),
Some(acc) => acc.compose(&entry.forward),
});
self.applied.push(entry);
}
self.group_broken = true;
self.last_source = None;
composed
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::transaction::Version;
use ropey::Rope;
use termesh_core::{BufferId, ProposalId};
struct Doc {
text: Rope,
history: History,
version: Version,
}
impl Doc {
fn new(text: &str) -> Self {
Self { text: Rope::from_str(text), history: History::new(), version: Version(0) }
}
fn edit(&mut self, from: usize, to: usize, insert: &str, source: EditSource) {
let changes = ChangeSet::replace(self.text.len_chars(), from, to, insert);
let group = self.history.group_for(&source);
let tx = EditTransaction::new(BufferId::new(1), self.version, changes, source, group);
let inverse = tx.changes.invert(&self.text);
self.text = tx.changes.apply(&self.text);
self.version = self.version.next();
self.history.push(&tx, inverse);
}
fn type_char(&mut self, at: usize, ch: &str) {
self.edit(at, at, ch, EditSource::Keyboard);
}
fn undo(&mut self) -> bool {
match self.history.undo() {
Some(cs) => {
self.text = cs.apply(&self.text);
self.version = self.version.next();
true
}
None => false,
}
}
fn redo(&mut self) -> bool {
match self.history.redo() {
Some(cs) => {
self.text = cs.apply(&self.text);
self.version = self.version.next();
true
}
None => false,
}
}
fn text(&self) -> String {
self.text.to_string()
}
}
#[test]
fn nothing_to_undo_at_the_start_of_history() {
let mut doc = Doc::new("hello");
assert!(!doc.history.can_undo());
assert!(!doc.undo());
}
#[test]
fn a_single_edit_undoes_and_redoes() {
let mut doc = Doc::new("hello");
doc.edit(5, 5, " world", EditSource::Paste);
assert_eq!(doc.text(), "hello world");
assert!(doc.undo());
assert_eq!(doc.text(), "hello");
assert!(doc.redo());
assert_eq!(doc.text(), "hello world");
}
#[test]
fn a_run_of_typing_undoes_as_one_step() {
let mut doc = Doc::new("()");
for (i, ch) in "abc".chars().enumerate() {
doc.type_char(1 + i, &ch.to_string());
}
assert_eq!(doc.text(), "(abc)");
assert!(doc.undo());
assert_eq!(doc.text(), "()", "three keystrokes, one undo");
assert!(!doc.history.can_undo());
}
#[test]
fn a_cursor_move_ends_the_run() {
let mut doc = Doc::new("()");
doc.type_char(1, "a");
doc.history.break_group(); doc.type_char(2, "b");
assert_eq!(doc.text(), "(ab)");
doc.undo();
assert_eq!(doc.text(), "(a)", "the break split the run in two");
doc.undo();
assert_eq!(doc.text(), "()");
}
#[test]
fn a_different_source_ends_the_run_without_being_asked() {
let mut doc = Doc::new("()");
doc.type_char(1, "a");
doc.edit(2, 2, "!", EditSource::Formatter);
doc.undo();
assert_eq!(doc.text(), "(a)", "the formatter edit is its own step");
}
#[test]
fn an_accepted_proposal_undoes_in_one_step() {
let mut doc = Doc::new("fn main() {}");
let source = EditSource::Agent(ProposalId::new(1));
let group = doc.history.group_for(&source);
for (from, to, insert) in [(3, 7, "run"), (0, 0, "pub ")] {
let changes = ChangeSet::replace(doc.text.len_chars(), from, to, insert);
let tx =
EditTransaction::new(BufferId::new(1), doc.version, changes, source.clone(), group);
let inverse = tx.changes.invert(&doc.text);
doc.text = tx.changes.apply(&doc.text);
doc.history.push(&tx, inverse);
}
assert_eq!(doc.text(), "pub fn run() {}");
assert!(doc.undo());
assert_eq!(doc.text(), "fn main() {}", "one undo reverses the whole proposal");
assert!(!doc.history.can_undo());
}
#[test]
fn undo_then_type_discards_the_redo_stack() {
let mut doc = Doc::new("a");
doc.edit(1, 1, "b", EditSource::Paste);
doc.undo();
assert!(doc.history.can_redo());
doc.edit(1, 1, "c", EditSource::Paste);
assert!(!doc.history.can_redo(), "redoing onto a diverged document is not offered");
assert_eq!(doc.text(), "ac");
}
#[test]
fn typing_after_an_undo_starts_a_fresh_group() {
let mut doc = Doc::new("()");
doc.type_char(1, "a");
doc.type_char(2, "b");
doc.undo();
assert_eq!(doc.text(), "()");
doc.type_char(1, "z");
doc.undo();
assert_eq!(doc.text(), "()", "the new keystroke must not rejoin the reversed group");
}
#[test]
fn many_edits_round_trip_all_the_way_back() {
let mut doc = Doc::new("start");
let original = doc.text();
for (from, to, insert, source) in [
(5, 5, " middle", EditSource::Paste),
(0, 5, "BEGIN", EditSource::Replace),
(5, 12, "", EditSource::Formatter),
] {
doc.edit(from, to, insert, source);
}
assert_ne!(doc.text(), original);
while doc.undo() {}
assert_eq!(doc.text(), original, "history unwinds completely");
while doc.redo() {}
assert_eq!(doc.text(), "BEGIN");
}
#[test]
fn empty_transactions_are_not_recorded() {
let mut doc = Doc::new("hello");
doc.edit(2, 2, "", EditSource::Keyboard);
assert!(!doc.history.can_undo(), "a no-op edit is not an undo step");
}
#[test]
fn an_empty_edit_cannot_swallow_a_pending_group_break() {
let mut doc = Doc::new("()");
doc.type_char(1, "a");
doc.history.break_group();
doc.edit(2, 2, "", EditSource::Keyboard); doc.type_char(2, "b");
doc.undo();
assert_eq!(doc.text(), "(a)", "the break must survive an edit that did nothing");
}
}