StatifierBlocks.Edit.History (StatifierBlocks v0.1.0)

Copy Markdown View Source

Undo and redo over StatifierBlocks.Edit.t() commands (ADR-0005 decision 3), and the one funnel every editor command actually goes through: commit/4 runs Edit.check_config/3 before Edit.apply/2, so "invalid config never reaches the document" is a property of this module, not a rule an editor shell has to remember to enforce itself.

The gate runs on undo and redo too

Edit.apply/2 is purely structural; it never asks a block type whether a config is valid. commit/4, undo/3 and redo/3 all route through the same private funnel, and that funnel always calls Edit.check_config/3 first - undo and redo included, not just the initial commit. This is deliberate, not an oversight: one code path means one thing to test, and it is the strict reading of decision 9 ("invalid config never reaches the document" is a property of every path that can produce a document, not only the first one).

It is also sound, not merely convenient. The config an inverse restores was already in the document at some earlier point, which means it already validated - under whatever palette was in effect when it got there. ADR-0005 decision 15 makes the editor single-session: one palette, built once, for the whole session. A host that swapped the palette out from under a live session could in principle see an undo refused by a rule that did not exist when the config was written, but that scenario is exactly what decision 15 puts out of scope. Within a single session the palette never changes underneath the history, so the config an undo or redo restores always validates the same way it did the first time.

Summary

Types

t()

undo and redo hold commands in the order they would be applied next: List.first(undo) is what a call to undo/3 runs, and List.first(redo) is what a call to redo/3 runs. limit bounds how many entries undo may carry; :infinity (the default) never drops one.

Functions

Whether redo/3 would have anything to do.

Whether undo/3 would have anything to do.

Applies command to document through the gate, pushes its inverse onto the undo stack, and clears the redo stack - a fresh commit invalidates whatever redo/3 would have replayed.

Builds an empty history. Options: :limit, a pos_integer() bounding the undo stack, or :infinity (the default - never drops an entry).

The mirror of undo/3: re-applies the top of the redo stack through the same gated path, and moves its inverse back onto the undo stack.

Applies the top of the undo stack's own inverse - the command already captured at commit time, so it is already the correct target - through the same gated path commit/4 uses. Moves the new inverse apply/2 hands back onto the redo stack, so redo/3 can replay the original forward command.

Types

t()

@type t() :: %StatifierBlocks.Edit.History{
  limit: pos_integer() | :infinity,
  redo: [StatifierBlocks.Edit.t()],
  undo: [StatifierBlocks.Edit.t()]
}

undo and redo hold commands in the order they would be applied next: List.first(undo) is what a call to undo/3 runs, and List.first(redo) is what a call to redo/3 runs. limit bounds how many entries undo may carry; :infinity (the default) never drops one.

Functions

can_redo?(history)

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

Whether redo/3 would have anything to do.

can_undo?(history)

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

Whether undo/3 would have anything to do.

commit(history, palette, document, command)

Applies command to document through the gate, pushes its inverse onto the undo stack, and clears the redo stack - a fresh commit invalidates whatever redo/3 would have replayed.

Funnel order (ADR-0005 decision 9): Edit.check_config/3, then Edit.apply/2, then push and clear. The trailing {:error, term()} arm is Edit.apply/2's own error union, propagated unchanged.

new(opts \\ [])

@spec new(keyword()) :: t()

Builds an empty history. Options: :limit, a pos_integer() bounding the undo stack, or :infinity (the default - never drops an entry).

redo(history, palette, document)

@spec redo(t(), StatifierBlocks.Palette.t(), StatifierBlocks.Document.t()) ::
  {:ok, t(), StatifierBlocks.Document.t()}
  | {:error, :nothing_to_redo}
  | {:error, term()}

The mirror of undo/3: re-applies the top of the redo stack through the same gated path, and moves its inverse back onto the undo stack.

undo(history, palette, document)

@spec undo(t(), StatifierBlocks.Palette.t(), StatifierBlocks.Document.t()) ::
  {:ok, t(), StatifierBlocks.Document.t()}
  | {:error, :nothing_to_undo}
  | {:error, term()}

Applies the top of the undo stack's own inverse - the command already captured at commit time, so it is already the correct target - through the same gated path commit/4 uses. Moves the new inverse apply/2 hands back onto the redo stack, so redo/3 can replay the original forward command.