use crate::compat::{Box, HashMap, String, Vec};
pub const DEFAULT_UNDO_LIMIT: usize = 256;
pub const GLOBAL_UNDO_CONTEXT: &str = "";
struct CommandStep {
undo: Box<dyn FnMut() + Send>,
redo: Box<dyn FnMut() + Send>,
}
impl CommandStep {
fn undo(&mut self) {
(self.undo)();
}
fn redo(&mut self) {
(self.redo)();
}
}
pub struct Command {
label: String,
steps: Vec<CommandStep>,
}
impl Command {
pub fn new<U, R>(label: impl Into<String>, undo: U, redo: R) -> Self
where
U: FnMut() + Send + 'static,
R: FnMut() + Send + 'static,
{
Self {
label: label.into(),
steps: alloc::vec![CommandStep { undo: Box::new(undo), redo: Box::new(redo) }],
}
}
fn blank(label: impl Into<String>) -> Self {
Self { label: label.into(), steps: Vec::new() }
}
pub fn label(&self) -> &str {
&self.label
}
pub fn is_empty(&self) -> bool {
self.steps.is_empty()
}
fn absorb(&mut self, other: Self) {
self.steps.extend(other.steps);
}
fn undo(&mut self) {
for step in self.steps.iter_mut().rev() {
step.undo();
}
}
fn redo(&mut self) {
for step in self.steps.iter_mut() {
step.redo();
}
}
}
impl core::fmt::Debug for Command {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Command")
.field("label", &self.label)
.field("steps", &self.steps.len())
.finish()
}
}
pub struct UndoStack {
limit: usize,
undo: Vec<Command>,
redo: Vec<Command>,
open_group: Option<Command>,
group_depth: usize,
dirty: bool,
}
impl UndoStack {
pub fn new() -> Self {
Self::with_limit(DEFAULT_UNDO_LIMIT)
}
pub fn with_limit(limit: usize) -> Self {
Self {
limit: limit.max(1),
undo: Vec::new(),
redo: Vec::new(),
open_group: None,
group_depth: 0,
dirty: false,
}
}
pub fn limit(&self) -> usize {
self.limit
}
pub fn set_limit(&mut self, limit: usize) {
self.limit = limit.max(1);
self.trim();
}
pub fn push(&mut self, command: Command) {
if command.is_empty() {
return;
}
self.dirty = true;
self.redo.clear();
if let Some(group) = &mut self.open_group {
group.absorb(command);
} else {
self.undo.push(command);
self.trim();
}
}
pub fn record<U, R>(&mut self, label: impl Into<String>, undo: U, redo: R)
where
U: FnMut() + Send + 'static,
R: FnMut() + Send + 'static,
{
self.push(Command::new(label, undo, redo));
}
pub fn begin_group(&mut self, label: impl Into<String>) {
if self.group_depth == 0 {
self.open_group = Some(Command::blank(label));
}
self.group_depth += 1;
}
pub fn end_group(&mut self) -> bool {
if self.group_depth == 0 {
return false;
}
self.group_depth -= 1;
if self.group_depth > 0 {
return false;
}
let Some(group) = self.open_group.take() else {
return false;
};
if group.is_empty() {
return false;
}
self.undo.push(group);
self.trim();
true
}
pub fn is_group_open(&self) -> bool {
self.group_depth > 0
}
pub fn undo(&mut self) -> Option<String> {
if self.is_group_open() {
return None;
}
let mut command = self.undo.pop()?;
command.undo();
let label = String::from(command.label());
self.redo.push(command);
self.dirty = true;
Some(label)
}
pub fn redo(&mut self) -> Option<String> {
if self.is_group_open() {
return None;
}
let mut command = self.redo.pop()?;
command.redo();
let label = String::from(command.label());
self.undo.push(command);
self.dirty = true;
Some(label)
}
pub fn can_undo(&self) -> bool {
!self.is_group_open() && !self.undo.is_empty()
}
pub fn can_redo(&self) -> bool {
!self.is_group_open() && !self.redo.is_empty()
}
pub fn undo_len(&self) -> usize {
self.undo.len()
}
pub fn redo_len(&self) -> usize {
self.redo.len()
}
pub fn is_dirty(&self) -> bool {
self.dirty
}
pub fn mark_saved(&mut self) {
self.dirty = false;
}
pub fn clear(&mut self) {
self.undo.clear();
self.redo.clear();
self.open_group = None;
self.group_depth = 0;
self.dirty = false;
}
fn trim(&mut self) {
while self.undo.len() > self.limit {
self.undo.remove(0);
}
}
}
impl Default for UndoStack {
fn default() -> Self {
Self::new()
}
}
impl core::fmt::Debug for UndoStack {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("UndoStack")
.field("undo", &self.undo.len())
.field("redo", &self.redo.len())
.field("limit", &self.limit)
.field("dirty", &self.dirty)
.finish()
}
}
pub struct UndoManager {
stacks: HashMap<String, UndoStack>,
active: String,
limit: usize,
}
impl UndoManager {
pub fn new() -> Self {
Self::with_limit(DEFAULT_UNDO_LIMIT)
}
pub fn with_limit(limit: usize) -> Self {
let mut stacks = HashMap::new();
stacks.insert(String::from(GLOBAL_UNDO_CONTEXT), UndoStack::with_limit(limit));
Self { stacks, active: String::from(GLOBAL_UNDO_CONTEXT), limit }
}
pub fn active_context(&self) -> &str {
&self.active
}
pub fn set_active_context(&mut self, context: impl Into<String>) -> String {
let context = context.into();
self.stacks.entry(context.clone()).or_insert_with(|| UndoStack::with_limit(self.limit));
core::mem::replace(&mut self.active, context)
}
pub fn stack_for(&self, context: &str) -> Option<&UndoStack> {
self.stacks.get(context)
}
pub fn stack(&self) -> &UndoStack {
self.stacks.get(&self.active).expect("active undo context always has a stack")
}
pub fn stack_mut(&mut self) -> &mut UndoStack {
self.stacks.get_mut(&self.active).expect("active undo context always has a stack")
}
pub fn contexts(&self) -> Vec<String> {
self.stacks.keys().cloned().collect()
}
pub fn push(&mut self, command: Command) {
self.stack_mut().push(command);
}
pub fn record<U, R>(&mut self, label: impl Into<String>, undo: U, redo: R)
where
U: FnMut() + Send + 'static,
R: FnMut() + Send + 'static,
{
self.stack_mut().record(label, undo, redo);
}
pub fn begin_group(&mut self, label: impl Into<String>) {
self.stack_mut().begin_group(label);
}
pub fn end_group(&mut self) -> bool {
self.stack_mut().end_group()
}
pub fn undo(&mut self) -> Option<String> {
self.stack_mut().undo()
}
pub fn redo(&mut self) -> Option<String> {
self.stack_mut().redo()
}
pub fn can_undo(&self) -> bool {
self.stack().can_undo()
}
pub fn can_redo(&self) -> bool {
self.stack().can_redo()
}
pub fn is_dirty(&self) -> bool {
self.stack().is_dirty()
}
pub fn mark_saved(&mut self) {
self.stack_mut().mark_saved();
}
pub fn clear(&mut self) {
self.stack_mut().clear();
}
}
impl Default for UndoManager {
fn default() -> Self {
Self::new()
}
}
impl core::fmt::Debug for UndoManager {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("UndoManager")
.field("active", &self.active)
.field("contexts", &self.stacks.len())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::compat::{lock, Mutex};
use alloc::sync::Arc;
use core::sync::atomic::{AtomicI32, Ordering};
fn counter() -> Arc<AtomicI32> {
Arc::new(AtomicI32::new(0))
}
#[test]
fn undo_and_redo_move_one_command_between_the_piles() {
let value = counter();
let mut stack = UndoStack::new();
let (set_value, undo_value) = (Arc::clone(&value), Arc::clone(&value));
stack.record(
"increment",
move || {
undo_value.fetch_sub(1, Ordering::SeqCst);
},
move || {
set_value.fetch_add(1, Ordering::SeqCst);
},
);
value.store(1, Ordering::SeqCst);
assert!(stack.can_undo());
assert_eq!(stack.undo().as_deref(), Some("increment"));
assert_eq!(value.load(Ordering::SeqCst), 0);
assert!(!stack.can_undo());
assert!(stack.can_redo());
assert_eq!(stack.redo().as_deref(), Some("increment"));
assert_eq!(value.load(Ordering::SeqCst), 1);
assert!(stack.can_undo());
assert!(!stack.can_redo());
}
#[test]
fn pushing_a_new_command_clears_redo() {
let mut stack = UndoStack::new();
stack.record("first", || {}, || {});
stack.undo();
assert_eq!(stack.redo_len(), 1);
stack.record("second", || {}, || {});
assert_eq!(stack.redo_len(), 0, "a new edit branches the history");
}
#[test]
fn a_group_is_one_undo_step_and_undoes_in_reverse_order() {
let order = Arc::new(Mutex::new(Vec::new()));
let mut stack = UndoStack::new();
stack.begin_group("typing");
for ch in ["a", "b", "c"] {
let undo_order = Arc::clone(&order);
let redo_order = Arc::clone(&order);
let recorded = String::from(ch);
stack.record(
recorded,
move || lock(&undo_order).push(-1),
move || lock(&redo_order).push(1),
);
}
assert!(stack.end_group());
assert_eq!(stack.undo_len(), 1, "the group is a single undo step");
stack.undo();
assert_eq!(*lock(&order), alloc::vec![-1, -1, -1]);
stack.redo();
assert_eq!(*lock(&order), alloc::vec![-1, -1, -1, 1, 1, 1]);
}
#[test]
fn undo_is_refused_while_a_group_is_open() {
let mut stack = UndoStack::new();
stack.begin_group("group");
stack.record("step", || {}, || {});
assert!(!stack.can_undo());
assert_eq!(stack.undo(), None);
assert!(stack.end_group());
assert_eq!(stack.undo().as_deref(), Some("group"));
}
#[test]
fn the_limit_drops_the_oldest_step() {
let mut stack = UndoStack::with_limit(2);
stack.record("one", || {}, || {});
stack.record("two", || {}, || {});
stack.record("three", || {}, || {});
assert_eq!(stack.undo_len(), 2);
assert_eq!(stack.undo().as_deref(), Some("three"));
assert_eq!(stack.undo().as_deref(), Some("two"));
assert_eq!(stack.undo(), None);
}
#[test]
fn the_dirty_flag_tracks_edits_until_saved() {
let mut stack = UndoStack::new();
assert!(!stack.is_dirty());
stack.record("edit", || {}, || {});
assert!(stack.is_dirty());
stack.mark_saved();
assert!(!stack.is_dirty());
stack.undo();
assert!(stack.is_dirty(), "undoing changes the document away from the saved state");
}
#[test]
fn contexts_keep_independent_histories() {
let mut manager = UndoManager::new();
manager.record("document", || {}, || {});
assert!(manager.can_undo());
let previous = manager.set_active_context("dialog");
assert_eq!(previous, GLOBAL_UNDO_CONTEXT);
assert!(!manager.can_undo(), "the dialog starts with its own empty stack");
manager.record("dialog", || {}, || {});
assert_eq!(manager.undo().as_deref(), Some("dialog"));
manager.set_active_context(GLOBAL_UNDO_CONTEXT);
assert!(manager.can_undo(), "the document stack was not touched");
assert_eq!(manager.undo().as_deref(), Some("document"));
}
}