use std::time::Instant;
use serde::{Deserialize, Serialize};
use super::cursor::CursorSet;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HistoryEntry {
pub content: String,
pub cursors: CursorSet,
#[serde(skip)]
pub timestamp: Option<Instant>,
}
impl HistoryEntry {
#[must_use]
pub fn new(content: String, cursors: CursorSet) -> Self {
Self {
content,
cursors,
timestamp: Some(Instant::now()),
}
}
}
#[derive(Debug, Clone)]
pub struct HistoryConfig {
pub max_entries: usize,
pub coalesce_window_ms: u64,
}
impl Default for HistoryConfig {
fn default() -> Self {
Self {
max_entries: 1000,
coalesce_window_ms: 500,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct History {
undo_stack: Vec<HistoryEntry>,
redo_stack: Vec<HistoryEntry>,
config: HistoryConfig,
is_undoing: bool,
}
impl History {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_config(config: HistoryConfig) -> Self {
Self {
config,
..Default::default()
}
}
pub fn push(&mut self, content: String, cursors: CursorSet) {
if self.is_undoing {
return;
}
let entry = HistoryEntry::new(content, cursors);
if let Some(last) = self.undo_stack.last()
&& let (Some(last_ts), Some(entry_ts)) = (last.timestamp, entry.timestamp)
{
let elapsed =
u64::try_from(entry_ts.duration_since(last_ts).as_millis()).unwrap_or(u64::MAX);
if elapsed < self.config.coalesce_window_ms {
return;
}
}
self.undo_stack.push(entry);
self.redo_stack.clear();
if self.undo_stack.len() > self.config.max_entries {
self.undo_stack.remove(0);
}
}
pub fn push_checkpoint(&mut self, content: String, cursors: CursorSet) {
if self.is_undoing {
return;
}
let mut entry = HistoryEntry::new(content, cursors);
entry.timestamp = None;
self.undo_stack.push(entry);
self.redo_stack.clear();
if self.undo_stack.len() > self.config.max_entries {
self.undo_stack.remove(0);
}
}
pub fn undo(
&mut self,
current_content: &str,
current_cursors: &CursorSet,
) -> Option<HistoryEntry> {
let entry = self.undo_stack.pop()?;
self.redo_stack.push(HistoryEntry::new(
current_content.to_string(),
current_cursors.clone(),
));
Some(entry)
}
pub fn redo(
&mut self,
current_content: &str,
current_cursors: &CursorSet,
) -> Option<HistoryEntry> {
let entry = self.redo_stack.pop()?;
self.undo_stack.push(HistoryEntry::new(
current_content.to_string(),
current_cursors.clone(),
));
Some(entry)
}
#[must_use]
pub fn can_undo(&self) -> bool {
!self.undo_stack.is_empty()
}
#[must_use]
pub fn can_redo(&self) -> bool {
!self.redo_stack.is_empty()
}
pub fn clear(&mut self) {
self.undo_stack.clear();
self.redo_stack.clear();
}
#[must_use]
pub fn undo_count(&self) -> usize {
self.undo_stack.len()
}
#[must_use]
pub fn redo_count(&self) -> usize {
self.redo_stack.len()
}
pub fn begin_undo(&mut self) {
self.is_undoing = true;
}
pub fn end_undo(&mut self) {
self.is_undoing = false;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::components::editor::cursor::{Cursor, CursorPosition};
fn test_cursors() -> CursorSet {
CursorSet::new(Cursor::new(CursorPosition::zero()))
}
#[test]
fn test_undo_redo() {
let mut history = History::new();
history.push("state1".to_string(), test_cursors());
std::thread::sleep(std::time::Duration::from_millis(600));
history.push("state2".to_string(), test_cursors());
let entry = history.undo("state3", &test_cursors());
assert!(entry.is_some());
assert_eq!(entry.unwrap().content, "state2");
let entry = history.redo("state2", &test_cursors());
assert!(entry.is_some());
assert_eq!(entry.unwrap().content, "state3");
}
#[test]
fn test_redo_cleared_on_new_edit() {
let mut history = History::new();
history.push("state1".to_string(), test_cursors());
std::thread::sleep(std::time::Duration::from_millis(600));
history.push("state2".to_string(), test_cursors());
history.undo("state3", &test_cursors());
assert!(history.can_redo());
std::thread::sleep(std::time::Duration::from_millis(600));
history.push("state4".to_string(), test_cursors());
assert!(!history.can_redo());
}
}