use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Edit {
pub at: usize,
pub deleted: String,
pub inserted: String,
}
impl Edit {
pub fn inverse(&self) -> Edit {
Edit {
at: self.at,
deleted: self.inserted.clone(),
inserted: self.deleted.clone(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EditGroup {
pub edits: Vec<Edit>,
pub cursor_before: usize,
pub cursor_after: usize,
}
const MAX_GROUP_EDITS: usize = 32;
pub struct History {
log: Vec<EditGroup>,
undo_ptr: Option<usize>,
group_open: bool,
last_kind: Option<EditKind>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EditKind {
InsertChar,
DeleteLeft,
Other,
}
impl History {
pub fn new() -> Self {
History {
log: Vec::new(),
undo_ptr: None,
group_open: false,
last_kind: None,
}
}
pub fn record(&mut self, edit: Edit, kind: EditKind, cursor_before: usize, cursor_after: usize) {
self.undo_ptr = None;
let coalesce = self.group_open
&& kind != EditKind::Other
&& self.last_kind == Some(kind)
&& self.log.last().is_some_and(|g| g.edits.len() < MAX_GROUP_EDITS);
if coalesce {
let group = self.log.last_mut().expect("group_open implies non-empty log");
group.edits.push(edit);
group.cursor_after = cursor_after;
} else {
self.log.push(EditGroup {
edits: vec![edit],
cursor_before,
cursor_after,
});
self.group_open = kind != EditKind::Other;
}
self.last_kind = Some(kind);
}
pub fn record_group(&mut self, edits: Vec<Edit>, cursor_before: usize, cursor_after: usize) {
self.undo_ptr = None;
self.log.push(EditGroup {
edits,
cursor_before,
cursor_after,
});
self.group_open = false;
self.last_kind = None;
}
pub fn break_group(&mut self) {
self.group_open = false;
self.last_kind = None;
}
pub fn break_chain(&mut self) {
self.undo_ptr = None;
}
pub fn next_undo(&mut self) -> Option<EditGroup> {
let ptr = self.undo_ptr.unwrap_or(self.log.len());
if ptr == 0 {
return None;
}
Some(self.log[ptr - 1].clone())
}
pub fn confirm_undo(&mut self, inverse: EditGroup) {
let ptr = self.undo_ptr.unwrap_or(self.log.len());
self.log.push(inverse);
self.group_open = false;
self.last_kind = None;
self.undo_ptr = Some(ptr - 1);
}
pub fn export(&self) -> (Vec<EditGroup>, Option<usize>) {
(self.log.clone(), self.undo_ptr)
}
pub fn restore(log: Vec<EditGroup>, undo_ptr: Option<usize>) -> Self {
let undo_ptr = undo_ptr.filter(|&p| p <= log.len());
History {
log,
undo_ptr,
group_open: false,
last_kind: None,
}
}
}
impl Default for History {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ins(at: usize, s: &str) -> Edit {
Edit {
at,
deleted: String::new(),
inserted: s.to_string(),
}
}
#[test]
fn typing_coalesces() {
let mut h = History::new();
h.record(ins(0, "a"), EditKind::InsertChar, 0, 1);
h.record(ins(1, "b"), EditKind::InsertChar, 1, 2);
assert_eq!(h.log.len(), 1);
assert_eq!(h.log[0].edits.len(), 2);
}
#[test]
fn movement_breaks_group() {
let mut h = History::new();
h.record(ins(0, "a"), EditKind::InsertChar, 0, 1);
h.break_group();
h.record(ins(1, "b"), EditKind::InsertChar, 1, 2);
assert_eq!(h.log.len(), 2);
}
#[test]
fn undo_walks_back_and_appends() {
let mut h = History::new();
h.record(ins(0, "a"), EditKind::Other, 0, 1);
h.record(ins(1, "b"), EditKind::Other, 1, 2);
let g = h.next_undo().unwrap(); assert_eq!(g.edits[0].inserted, "b");
let inverse = EditGroup {
edits: g.edits.iter().rev().map(Edit::inverse).collect(),
cursor_before: 2,
cursor_after: 1,
};
h.confirm_undo(inverse);
assert_eq!(h.log.len(), 3);
let g = h.next_undo().unwrap(); assert_eq!(g.edits[0].inserted, "a");
}
#[test]
fn broken_chain_undoes_the_undo() {
let mut h = History::new();
h.record(ins(0, "a"), EditKind::Other, 0, 1);
let g = h.next_undo().unwrap();
let inverse = EditGroup {
edits: g.edits.iter().rev().map(Edit::inverse).collect(),
cursor_before: 1,
cursor_after: 0,
};
h.confirm_undo(inverse);
h.break_chain();
let g = h.next_undo().unwrap();
assert_eq!(g.edits[0].deleted, "a");
}
}