Files
reasampler/src/shell/undo/CLAUDE.md
T
daniel 3d95eb715c Undo-block review fixes: exception-safe depth, corrected docs, real test teeth
Fixes ctor exception safety, a ValidatePtr2 guard, false SDK-citation and
enforcement claims, the span-depth invariant, and vacuous fx_reclaim tests.
2026-08-07 17:07:56 -04:00

4.6 KiB

src/shell/undo — the one REAPER undo-block opener

Scope

One type, UndoBlock, and one query, undoBlockOpen(). Every extension-owned undo block in the tree is opened through this type; nothing here decides what a point is called or whether one is worth minting — each call site still owns its own label, mask, and discard-vs-mint condition. The FX reclaim span is NOT this directory's: shell/view/fx_reclaim owns it, and this directory only guarantees no block ever nests over it.

Invariants

  • Undo_BeginBlock2 is not callable outside this directory in every TU but one, and that is enforced by the compiler, not by review. Only undo_block.cpp and shell/view's fx_reclaim.cpp (which owns the span's own begin/end) define REAPERAPI_WANT_Undo_BeginBlock2 / ..._Undo_EndBlock2; every other TU compiles under REAPERAPI_MINIMAL, so a raw Undo_BeginBlock2 there is an undeclared identifier. app/main.cpp is the one exception — as the sole REAPERAPI_IMPLEMENT TU it never sets REAPERAPI_MINIMAL (it must allocate storage for whatever ANY TU wants), so the whole API, Undo_* included, compiles clean there too. That TU stays out of the undo business by discipline (src/app/CLAUDE.md: pointers + entry + dispatch only), not because the compiler stops it — do not add business logic there that would make this matter. Adding the WANT define back to a third TU is a second, easier way to defeat the enforcement that DOES exist — do not.
  • Ending the reclaim span is part of opening, and ONLY the outermost block does it. At depth 0 the span is the only block that can be open, so ending it mints the pending mode switch's point whole before this block exists. At depth > 0 an enclosing UndoBlock already ended it, and a close from the inner one would land at depth 2->1 — minting nothing and hijacking the enclosing block's label. That is the same criterion applyMode's reapply is gated on; it lives in the constructor so no call site has to make the judgment. Full reasoning for the rule itself: src/shell/view/CLAUDE.md.
  • The constructor's body is one try. fxReclaimEndUndoSpan copies a std::string and can throw; a throwing constructor runs no destructor, so an uncompensated depth increment would stick forever — undoBlockOpen() reads true for the rest of the session, fxReclaimTick refuses every tick, and no later UndoBlock is ever outermost. Any throw between the increment and the end of the constructor rolls the depth back before propagating.
  • Closing is a destructor. No throw between the open and the label decision can leave a block open in the user's project, which is why several call sites can allocate freely between the two.
  • No label recorded means no point. UndoBlock closes on ("", 0) unless recordPoint was called — the discard form, real from a hand-fired action (DAW-measured; src/shell/view/CLAUDE.md). A verb that mutated nothing therefore costs no undo entry simply by saying nothing.

Modules

  • undo_block — UndoBlock (RAII open/close plus recordPoint) and undoBlockOpen(). The latter is read by fxReclaimTick, which refuses a tick outright while any block is open: the drain's lazy openSpan would otherwise begin at depth 2, and the enclosing block's close would then mint under the span's label and destroy the user's own point. The refusal consumes nothing — no wakeup, no queue, no project pin — so the first tick after the block closes drains as if this one had never been due.

Gotchas

  • Undo_EndBlock2 is guarded by ValidatePtr2 only when project_ is a real, non-null pointer. nullptr means "the active project" — the same convention Undo_BeginBlock2 was handed — and ValidatePtr2 does not accept nullptr as a valid object of any type, so guarding it unconditionally would silently stop every UndoBlock undo(nullptr) site (most of them) from ever closing. A real pointer, by contrast, can go stale between open and close (several call sites allocate freely in between), so only that case is validated — the same asymmetry fx_reclaim.cpp's closeSpan already has, for the same reason: its span always holds a real pinned pointer, never nullptr for "active".
  • The depth counter deliberately does NOT count the reclaim span. The span is the thing this stands off, not a peer.
  • The counter drops AFTER Undo_EndBlock2, not before: the close that reaches depth 0 mints a point, and anything re-entered through that must still read a block as open rather than let the drain open a span underneath it.