Search documentation of Plushie

# Plushie.Undo (Plushie v0.7.2)

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

### Types

command()
entry()
t()

### Functions

can_redo?(undo)

Return `true` if there are entries on the redo stack.

can_undo?(undo)

Return `true` if there are entries on the undo stack.

current(undo)

Return the current model.

history(undo)

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

new(model, opts \\ [])

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

push(u, command)

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

redo(u)

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

undo(u)

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.