use std::collections::VecDeque;
use std::time::Duration;
use crate::parser::CstDocument;
pub const DEFAULT_UNDO_COUNT_CAP: usize = 200;
pub const DEFAULT_UNDO_BYTE_CAP: usize = 64 * 1024 * 1024;
pub const DEFAULT_COALESCE_WINDOW: Duration = Duration::from_millis(500);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct UndoCap {
pub max_count: usize,
pub max_bytes: usize,
}
impl Default for UndoCap {
fn default() -> Self {
Self {
max_count: DEFAULT_UNDO_COUNT_CAP,
max_bytes: DEFAULT_UNDO_BYTE_CAP,
}
}
}
impl UndoCap {
#[must_use]
pub fn new(max_count: usize, max_bytes: usize) -> Self {
Self {
max_count: if max_count == 0 {
DEFAULT_UNDO_COUNT_CAP
} else {
max_count
},
max_bytes: if max_bytes == 0 {
DEFAULT_UNDO_BYTE_CAP
} else {
max_bytes
},
}
}
}
#[derive(Debug, Clone)]
pub struct UndoEntry<C> {
cst_snapshot: CstDocument,
source_text: String,
cursor: C,
}
impl<C> UndoEntry<C> {
#[must_use]
pub fn new(cst_snapshot: CstDocument, source_text: String, cursor: C) -> Self {
Self {
cst_snapshot,
source_text,
cursor,
}
}
#[must_use]
pub fn cst_snapshot(&self) -> &CstDocument {
&self.cst_snapshot
}
#[must_use]
pub fn source_text(&self) -> &str {
&self.source_text
}
#[must_use]
pub fn cursor(&self) -> &C {
&self.cursor
}
#[must_use]
pub fn byte_size(&self) -> usize {
self.source_text.len()
}
}
#[derive(Debug, Clone)]
pub struct UndoStack<C> {
current: Option<UndoEntry<C>>,
undo: VecDeque<UndoEntry<C>>,
redo: Vec<UndoEntry<C>>,
cap: UndoCap,
coalesce_window: Duration,
pending: bool,
}
impl<C> Default for UndoStack<C> {
fn default() -> Self {
Self::new()
}
}
impl<C> UndoStack<C> {
#[must_use]
pub fn new() -> Self {
Self::with_config(UndoCap::default(), DEFAULT_COALESCE_WINDOW)
}
#[must_use]
pub fn with_config(cap: UndoCap, coalesce_window: Duration) -> Self {
Self {
current: None,
undo: VecDeque::new(),
redo: Vec::new(),
cap,
coalesce_window,
pending: false,
}
}
#[must_use]
pub fn cap(&self) -> UndoCap {
self.cap
}
#[must_use]
pub fn coalesce_window(&self) -> Duration {
self.coalesce_window
}
#[must_use]
pub fn current(&self) -> Option<&UndoEntry<C>> {
self.current.as_ref()
}
#[must_use]
pub fn len(&self) -> usize {
self.undo.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.undo.is_empty()
}
#[must_use]
pub fn has_pending(&self) -> bool {
self.pending
}
#[must_use]
pub fn can_undo(&self) -> bool {
!self.undo.is_empty()
}
#[must_use]
pub fn can_redo(&self) -> bool {
!self.redo.is_empty()
}
#[must_use]
pub fn redo_len(&self) -> usize {
self.redo.len()
}
#[must_use]
pub fn retained_bytes(&self) -> usize {
self.undo.iter().map(UndoEntry::byte_size).sum()
}
#[must_use]
pub fn total_bytes(&self) -> usize {
self.undo.iter().map(UndoEntry::byte_size).sum::<usize>()
+ self.redo.iter().map(UndoEntry::byte_size).sum::<usize>()
+ self.current.as_ref().map_or(0, UndoEntry::byte_size)
}
pub fn record(&mut self, entry: UndoEntry<C>, coalesce: bool) {
self.redo.clear();
match self.current.take() {
None => {
self.current = Some(entry);
self.pending = false;
}
Some(prev) => {
if coalesce {
self.current = Some(entry);
} else {
self.push_boundary(prev);
self.current = Some(entry);
}
self.pending = true;
}
}
}
#[must_use]
pub fn undo(&mut self) -> Option<UndoEntry<C>>
where
C: Clone,
{
self.pending = false;
let prior = self.undo.pop_back()?;
if let Some(current) = self.current.take() {
self.redo.push(current);
}
self.current = Some(prior.clone());
Some(prior)
}
#[must_use]
pub fn redo(&mut self) -> Option<UndoEntry<C>>
where
C: Clone,
{
self.pending = false;
let next = self.redo.pop()?;
if let Some(current) = self.current.take() {
self.push_boundary(current);
}
self.current = Some(next.clone());
Some(next)
}
fn push_boundary(&mut self, entry: UndoEntry<C>) {
self.undo.push_back(entry);
while self.undo.len() > self.cap.max_count {
self.undo.pop_front();
}
while self.undo.len() > 1 && self.retained_bytes() > self.cap.max_bytes {
self.undo.pop_front();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn entry(src: &str, cur: usize) -> UndoEntry<usize> {
UndoEntry::new(crate::parse(src), src.to_string(), cur)
}
#[test]
fn cap_new_falls_back_to_default_on_zero() {
let cap = UndoCap::new(0, 0);
assert_eq!(cap.max_count, DEFAULT_UNDO_COUNT_CAP);
assert_eq!(cap.max_bytes, DEFAULT_UNDO_BYTE_CAP);
let cap = UndoCap::new(10, 0);
assert_eq!(cap.max_count, 10);
assert_eq!(cap.max_bytes, DEFAULT_UNDO_BYTE_CAP);
let cap = UndoCap::new(0, 1024);
assert_eq!(cap.max_count, DEFAULT_UNDO_COUNT_CAP);
assert_eq!(cap.max_bytes, 1024);
}
#[test]
fn cap_default_matches_constants() {
let cap = UndoCap::default();
assert_eq!(cap.max_count, 200);
assert_eq!(cap.max_bytes, 64 * 1024 * 1024);
}
#[test]
fn new_stack_is_empty_with_default_config() {
let stack: UndoStack<()> = UndoStack::new();
assert!(stack.is_empty());
assert_eq!(stack.len(), 0);
assert!(!stack.can_undo());
assert!(!stack.can_redo());
assert_eq!(stack.cap(), UndoCap::default());
assert_eq!(stack.coalesce_window(), DEFAULT_COALESCE_WINDOW);
assert_eq!(stack.retained_bytes(), 0);
assert!(stack.current().is_none());
}
#[test]
fn with_config_applies_cap_and_window() {
let cap = UndoCap::new(5, 4096);
let window = Duration::from_millis(120);
let stack: UndoStack<()> = UndoStack::with_config(cap, window);
assert_eq!(stack.cap(), cap);
assert_eq!(stack.coalesce_window(), window);
}
#[test]
fn undo_entry_exposes_snapshot_text_and_cursor() {
let src = "Foo(x: 1)\n";
let doc = crate::parse(src);
let entry = UndoEntry::new(doc, src.to_string(), 7usize);
assert_eq!(entry.source_text(), src);
assert_eq!(*entry.cursor(), 7usize);
assert_eq!(entry.byte_size(), src.len());
assert_eq!(crate::print(entry.cst_snapshot()), src);
}
#[test]
fn first_record_seeds_current_with_no_boundary() {
let mut stack: UndoStack<usize> = UndoStack::new();
stack.record(entry("(a: 1)\n", 0), false);
assert_eq!(stack.len(), 0, "seeding does not create an undo boundary");
assert!(!stack.can_undo());
assert_eq!(stack.current().unwrap().source_text(), "(a: 1)\n");
}
#[test]
fn undo_restores_exact_prior_bytes_and_redo_replays() {
let mut stack: UndoStack<usize> = UndoStack::new();
stack.record(entry("(a: 1)\n", 1), false);
stack.record(entry("(a: 12)\n", 2), false);
stack.record(entry("(a: 123)\n", 3), false);
let u1 = stack.undo().expect("undo 1");
assert_eq!(u1.source_text(), "(a: 12)\n");
assert_eq!(*u1.cursor(), 2);
assert_eq!(crate::print(u1.cst_snapshot()), "(a: 12)\n");
let u2 = stack.undo().expect("undo 2");
assert_eq!(u2.source_text(), "(a: 1)\n");
assert_eq!(*u2.cursor(), 1);
assert!(
!stack.can_undo(),
"back to the original; nothing more to undo"
);
let r1 = stack.redo().expect("redo 1");
assert_eq!(r1.source_text(), "(a: 12)\n");
assert_eq!(*r1.cursor(), 2);
let r2 = stack.redo().expect("redo 2");
assert_eq!(r2.source_text(), "(a: 123)\n");
assert_eq!(*r2.cursor(), 3);
assert!(!stack.can_redo());
}
#[test]
fn undo_on_empty_history_is_none() {
let mut stack: UndoStack<usize> = UndoStack::new();
assert!(stack.undo().is_none());
stack.record(entry("(a: 1)\n", 0), false);
assert!(stack.undo().is_none());
}
#[test]
fn new_edit_after_undo_clears_redo() {
let mut stack: UndoStack<usize> = UndoStack::new();
stack.record(entry("(a: 1)\n", 0), false);
stack.record(entry("(a: 2)\n", 0), false);
stack.record(entry("(a: 3)\n", 0), false);
let _ = stack.undo();
let _ = stack.undo();
assert!(stack.can_redo(), "two states are now redo-able");
assert_eq!(stack.redo_len(), 2);
stack.record(entry("(a: 9)\n", 0), false);
assert!(!stack.can_redo());
assert_eq!(stack.redo_len(), 0);
}
#[test]
fn coalesced_run_is_a_single_undo_unit() {
let mut stack: UndoStack<usize> = UndoStack::new();
stack.record(entry("", 0), false);
stack.record(entry("h", 1), false);
stack.record(entry("he", 2), true);
stack.record(entry("hel", 3), true);
stack.record(entry("hell", 4), true);
stack.record(entry("hello", 5), true);
assert_eq!(stack.len(), 1, "the whole run is a single undo unit");
let u = stack.undo().expect("undo the run");
assert_eq!(u.source_text(), "");
assert!(!stack.can_undo());
}
#[test]
fn coalesce_false_starts_a_new_unit() {
let mut stack: UndoStack<usize> = UndoStack::new();
stack.record(entry("a", 0), false); stack.record(entry("ab", 0), false); stack.record(entry("abc", 0), true); stack.record(entry("abc d", 0), false);
assert_eq!(stack.len(), 2);
assert_eq!(stack.undo().unwrap().source_text(), "abc");
assert_eq!(stack.undo().unwrap().source_text(), "a");
assert!(!stack.can_undo());
}
#[test]
fn coalesce_run_closed_by_undo_then_new_run() {
let mut stack: UndoStack<usize> = UndoStack::new();
stack.record(entry("x", 0), false); stack.record(entry("xy", 0), false); stack.record(entry("xyz", 0), true); assert!(stack.has_pending());
let _ = stack.undo();
assert!(!stack.has_pending(), "undo closes the coalescing run");
}
#[test]
fn count_cap_drops_oldest_boundary() {
let cap = UndoCap::new(3, DEFAULT_UNDO_BYTE_CAP);
let mut stack: UndoStack<usize> = UndoStack::with_config(cap, DEFAULT_COALESCE_WINDOW);
for i in 0..6 {
stack.record(entry(&format!("v{i}"), 0), false);
}
assert_eq!(stack.len(), 3, "count cap binds at 3 boundaries");
assert_eq!(stack.undo().unwrap().source_text(), "v4");
assert_eq!(stack.undo().unwrap().source_text(), "v3");
assert_eq!(stack.undo().unwrap().source_text(), "v2");
assert!(!stack.can_undo(), "oldest (v0, v1) were dropped");
}
#[test]
fn byte_cap_drops_oldest_boundary_independent_of_count() {
let cap = UndoCap::new(1000, 25);
let mut stack: UndoStack<usize> = UndoStack::with_config(cap, DEFAULT_COALESCE_WINDOW);
for i in 0..6 {
stack.record(entry(&format!("aaaaaaaaa{i}"), 0), false);
}
assert!(stack.retained_bytes() <= 25, "byte cap binds memory");
assert!(
stack.len() <= 2,
"size cap retains at most 2 ten-byte units"
);
}
#[test]
fn byte_cap_retains_at_least_one_oversize_boundary() {
let cap = UndoCap::new(1000, 4);
let mut stack: UndoStack<usize> = UndoStack::with_config(cap, DEFAULT_COALESCE_WINDOW);
stack.record(entry("aaaaaaaaaa", 0), false); stack.record(entry("bbbbbbbbbb", 0), false); assert_eq!(stack.len(), 1, "the single oversize boundary is retained");
assert_eq!(stack.undo().unwrap().source_text(), "aaaaaaaaaa");
}
#[test]
fn misconfigured_zero_cap_falls_back_to_default() {
let cap = UndoCap::new(0, 0);
let stack: UndoStack<usize> = UndoStack::with_config(cap, DEFAULT_COALESCE_WINDOW);
assert_eq!(stack.cap().max_count, DEFAULT_UNDO_COUNT_CAP);
assert_eq!(stack.cap().max_bytes, DEFAULT_UNDO_BYTE_CAP);
}
#[test]
fn total_bytes_tracks_full_footprint_and_stays_bounded() {
let cap = UndoCap::new(3, DEFAULT_UNDO_BYTE_CAP);
let mut stack: UndoStack<usize> = UndoStack::with_config(cap, DEFAULT_COALESCE_WINDOW);
for i in 0..10 {
stack.record(entry(&format!("value-{i:03}"), 0), false);
}
assert_eq!(stack.len(), 3);
let bound = 4 * 9; assert!(stack.total_bytes() <= bound);
}
}