Plushie.Undo (Plushie v0.7.1)

Copy Markdown View Source

Undo/redo stack for reversible commands. Pure data structure, no processes.

Each command provides an apply function and an undo function. The stack tracks entries as {apply_fn, undo_fn} pairs so that undo moves an entry to the redo stack (calling undo_fn) and redo moves it back (calling apply_fn).

Max size

The undo stack is bounded by :max_size (default 100). When a push exceeds the limit, the oldest entries are dropped. The redo stack is unbounded (it can only shrink or be cleared, never grow past the undo stack size).

Coalescing

Commands with the same :coalesce key that arrive within :coalesce_window_ms of each other are merged into a single undo entry. The merged entry keeps the original undo function (so one undo reverses all coalesced changes) and composes the apply functions.

Example

iex> u = Plushie.Undo.new(0)
iex> cmd = %{apply: &(&1 + 1), undo: &(&1 - 1)}
iex> u = Plushie.Undo.push(u, cmd)
iex> Plushie.Undo.current(u)
1
iex> u = Plushie.Undo.undo(u)
iex> Plushie.Undo.current(u)
0

Summary

Functions

Return true if there are entries on the redo stack.

Return true if there are entries on the undo stack.

Return the current model.

Return the labels from the undo stack, most recent first.

Create a new undo stack with model as the initial state.

Push a command onto the undo stack, updating the current model. Clears the redo stack.

Redo the last undone command. Returns unchanged if the redo stack is empty.

Undo the last command. Returns unchanged if the undo stack is empty.

Types

command()

@type command() :: %{
  :apply => (term() -> term()),
  :undo => (term() -> term()),
  optional(:label) => String.t(),
  optional(:coalesce) => term(),
  optional(:coalesce_window_ms) => non_neg_integer()
}

entry()

@type entry() :: %{
  apply_fn: (term() -> term()),
  undo_fn: (term() -> term()),
  label: String.t() | nil,
  coalesce: term() | nil,
  timestamp: integer()
}

t()

@type t() :: %Plushie.Undo{
  current: term(),
  max_size: pos_integer(),
  redo_stack: [entry()],
  undo_size: non_neg_integer(),
  undo_stack: [entry()]
}

Functions

can_redo?(undo)

@spec can_redo?(undo :: t()) :: boolean()

Return true if there are entries on the redo stack.

can_undo?(undo)

@spec can_undo?(undo :: t()) :: boolean()

Return true if there are entries on the undo stack.

current(undo)

@spec current(undo :: t()) :: term()

Return the current model.

history(undo)

@spec history(undo :: t()) :: [String.t() | nil]

Return the labels from the undo stack, most recent first.

new(model, opts \\ [])

@spec new(model :: term(), opts :: keyword()) :: t()

Create a new undo stack with model as the initial state.

Options

  • :max_size - maximum number of undo entries (default 100). When exceeded, the oldest entries are dropped silently.

push(u, command)

@spec push(undo :: t(), command :: command()) :: t()

Push a command onto the undo stack, updating the current model. Clears the redo stack.

The command must be a map with :apply and :undo keys (both single-arity functions). Optional keys: :label, :coalesce, :coalesce_window_ms.

If the command carries a :coalesce key that matches the top of the undo stack and the time delta is within :coalesce_window_ms, the entry is merged rather than pushed.

When the undo stack exceeds :max_size, the oldest entries are dropped.

redo(u)

@spec redo(undo :: t()) :: t()

Redo the last undone command. Returns unchanged if the redo stack is empty.

undo(u)

@spec undo(undo :: t()) :: t()

Undo the last command. Returns unchanged if the undo stack is empty.