use std::collections::VecDeque;
use std::time::{Duration, Instant};
const MAX_DEPTH: usize = 64;
const COALESCE_WINDOW: Duration = Duration::from_millis(1_200);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EditKind {
Typing,
Atomic,
}
#[derive(Debug, Clone)]
pub struct History<T> {
undo: VecDeque<T>,
redo: VecDeque<T>,
last_edit: Option<(EditKind, Instant)>,
}
impl<T> Default for History<T> {
fn default() -> Self {
Self {
undo: VecDeque::new(),
redo: VecDeque::new(),
last_edit: None,
}
}
}
impl<T> History<T> {
pub fn new() -> Self {
Self::default()
}
pub fn will_coalesce(&self, kind: EditKind) -> bool {
kind == EditKind::Typing
&& self.last_edit.is_some_and(|(last_kind, at)| {
last_kind == EditKind::Typing && at.elapsed() < COALESCE_WINDOW
})
&& !self.undo.is_empty()
}
pub fn touch_coalesce(&mut self) {
if let Some((_, at)) = &mut self.last_edit {
*at = Instant::now();
}
}
pub fn push(&mut self, current: T, kind: EditKind) {
self.undo.push_back(current);
while self.undo.len() > MAX_DEPTH {
self.undo.pop_front();
}
self.redo.clear();
self.last_edit = Some((kind, Instant::now()));
}
pub fn before_edit_with(&mut self, kind: EditKind, current: impl FnOnce() -> T) {
if self.will_coalesce(kind) {
self.touch_coalesce();
return;
}
self.push(current(), kind);
}
pub fn break_coalesce(&mut self) {
self.last_edit = None;
}
pub fn undo(&mut self, current: T) -> Option<T> {
let prev = self.undo.pop_back()?;
self.redo.push_back(current);
self.break_coalesce();
Some(prev)
}
pub fn redo(&mut self, current: T) -> Option<T> {
let next = self.redo.pop_back()?;
self.undo.push_back(current);
self.break_coalesce();
Some(next)
}
pub fn can_redo(&self) -> bool {
!self.redo.is_empty()
}
pub fn for_each_mut(&mut self, mut update: impl FnMut(&mut T)) {
self.undo.iter_mut().for_each(&mut update);
self.redo.iter_mut().for_each(update);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn typing_coalesces_into_one_step() {
let mut h: History<String> = History::new();
h.before_edit_with(EditKind::Typing, || "a".into());
h.before_edit_with(EditKind::Typing, || "ab".into());
h.before_edit_with(EditKind::Typing, || "abc".into());
assert_eq!(h.undo.len(), 1);
let restored = h.undo("abc".into()).unwrap();
assert_eq!(restored, "a");
}
#[test]
fn atomic_always_splits() {
let mut h: History<String> = History::new();
h.before_edit_with(EditKind::Typing, || "a".into());
h.before_edit_with(EditKind::Atomic, || "ab".into());
h.before_edit_with(EditKind::Typing, || "abc".into());
assert_eq!(h.undo.len(), 3);
}
#[test]
fn redo_clears_on_new_edit() {
let mut h: History<String> = History::new();
h.before_edit_with(EditKind::Atomic, || "0".into());
let _ = h.undo("1".into());
assert!(h.can_redo());
h.before_edit_with(EditKind::Atomic, || "2".into());
assert!(!h.can_redo());
}
#[test]
fn coalesce_skips_snapshot_fn() {
let mut h: History<String> = History::new();
h.before_edit_with(EditKind::Typing, || "a".into());
let mut built = 0;
h.before_edit_with(EditKind::Typing, || {
built += 1;
"ab".into()
});
assert_eq!(built, 0);
assert_eq!(h.undo.len(), 1);
}
}