use crate::editor::cursor::Position;
use crate::editor::document::Document;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Change {
pub at: usize,
pub removed: String,
pub inserted: String,
}
impl Change {
#[must_use]
pub fn insertion(at: usize, text: impl Into<String>) -> Self {
Self {
at,
removed: String::new(),
inserted: text.into(),
}
}
#[must_use]
pub fn deletion(at: usize, text: impl Into<String>) -> Self {
Self {
at,
removed: text.into(),
inserted: String::new(),
}
}
#[must_use]
fn inverted(&self) -> Self {
Self {
at: self.at,
removed: self.inserted.clone(),
inserted: self.removed.clone(),
}
}
fn apply(&self, document: &mut Document) {
if !self.removed.is_empty() {
document.remove(self.at, self.at + self.removed.chars().count());
}
if !self.inserted.is_empty() {
document.insert(self.at, &self.inserted);
}
}
}
#[derive(Debug, Clone)]
pub struct Transaction {
changes: Vec<Change>,
before: Position,
after: Position,
}
impl Transaction {
fn absorbs(&self, change: &Change) -> bool {
let Some(last) = self.changes.last() else {
return false;
};
let typing = last.removed.is_empty()
&& change.removed.is_empty()
&& change.at == last.at + last.inserted.chars().count();
let backspacing = last.inserted.is_empty()
&& change.inserted.is_empty()
&& change.at + change.removed.chars().count() == last.at;
typing || backspacing
}
}
#[derive(Debug)]
pub struct History {
undo: Vec<Transaction>,
redo: Vec<Transaction>,
open: bool,
limit: usize,
}
impl Default for History {
fn default() -> Self {
Self {
undo: Vec::new(),
redo: Vec::new(),
open: false,
limit: 1000,
}
}
}
impl History {
pub fn record(&mut self, change: Change, before: Position, after: Position) {
if change.removed.is_empty() && change.inserted.is_empty() {
return;
}
self.redo.clear();
if self.open
&& let Some(top) = self.undo.last_mut()
&& top.absorbs(&change)
{
top.changes.push(change);
top.after = after;
return;
}
self.undo.push(Transaction {
changes: vec![change],
before,
after,
});
self.open = true;
if self.undo.len() > self.limit {
self.undo.remove(0);
}
}
pub fn checkpoint(&mut self) {
self.open = false;
}
pub fn undo(&mut self, document: &mut Document) -> Option<Position> {
let transaction = self.undo.pop()?;
for change in transaction.changes.iter().rev() {
change.inverted().apply(document);
}
let position = transaction.before;
self.redo.push(transaction);
self.open = false;
Some(position)
}
pub fn redo(&mut self, document: &mut Document) -> Option<Position> {
let transaction = self.redo.pop()?;
for change in &transaction.changes {
change.apply(document);
}
let position = transaction.after;
self.undo.push(transaction);
self.open = false;
Some(position)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn doc(text: &str) -> Document {
Document::from_text(text, None)
}
fn type_text(history: &mut History, document: &mut Document, at: usize, text: &str) {
for (offset, ch) in text.chars().enumerate() {
let index = at + offset;
document.insert(index, &ch.to_string());
history.record(
Change::insertion(index, ch.to_string()),
Position::new(0, index),
Position::new(0, index + 1),
);
}
}
#[test]
fn consecutive_typing_undoes_as_one_step() {
let mut document = doc("");
let mut history = History::default();
type_text(&mut history, &mut document, 0, "hello");
assert_eq!(document.text().to_string(), "hello");
history.undo(&mut document);
assert_eq!(document.text().to_string(), "");
assert!(history.undo(&mut document).is_none());
}
#[test]
fn a_checkpoint_splits_typing_into_separate_steps() {
let mut document = doc("");
let mut history = History::default();
type_text(&mut history, &mut document, 0, "ab");
history.checkpoint();
type_text(&mut history, &mut document, 2, "cd");
history.undo(&mut document);
assert_eq!(document.text().to_string(), "ab");
history.undo(&mut document);
assert_eq!(document.text().to_string(), "");
}
#[test]
fn redo_replays_what_undo_took_back() {
let mut document = doc("");
let mut history = History::default();
type_text(&mut history, &mut document, 0, "abc");
history.undo(&mut document);
history.redo(&mut document);
assert_eq!(document.text().to_string(), "abc");
}
#[test]
fn a_new_edit_discards_the_redo_branch() {
let mut document = doc("");
let mut history = History::default();
type_text(&mut history, &mut document, 0, "abc");
history.undo(&mut document);
type_text(&mut history, &mut document, 0, "x");
assert!(history.redo(&mut document).is_none());
}
#[test]
fn undo_restores_the_caret_position() {
let mut document = doc("abc");
let mut history = History::default();
document.remove(1, 2);
history.record(
Change::deletion(1, "b"),
Position::new(0, 2),
Position::new(0, 1),
);
assert_eq!(history.undo(&mut document), Some(Position::new(0, 2)));
assert_eq!(document.text().to_string(), "abc");
}
#[test]
fn a_multi_change_transaction_reverts_back_to_front() {
let mut document = doc("aXbXc");
let mut history = History::default();
document.remove(3, 4);
document.remove(1, 2);
assert_eq!(document.text().to_string(), "abc");
history.undo.push(Transaction {
changes: vec![Change::deletion(3, "X"), Change::deletion(1, "X")],
before: Position::ZERO,
after: Position::ZERO,
});
history.undo(&mut document);
assert_eq!(document.text().to_string(), "aXbXc");
}
}