3d95eb715c
Fixes ctor exception safety, a ValidatePtr2 guard, false SDK-citation and enforcement claims, the span-depth invariant, and vacuous fx_reclaim tests.
4.6 KiB
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_BeginBlock2is not callable outside this directory in every TU but one, and that is enforced by the compiler, not by review. Onlyundo_block.cppandshell/view'sfx_reclaim.cpp(which owns the span's own begin/end) defineREAPERAPI_WANT_Undo_BeginBlock2/..._Undo_EndBlock2; every other TU compiles underREAPERAPI_MINIMAL, so a rawUndo_BeginBlock2there is an undeclared identifier.app/main.cppis the one exception — as the soleREAPERAPI_IMPLEMENTTU it never setsREAPERAPI_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
UndoBlockalready 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 criterionapplyMode'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.fxReclaimEndUndoSpancopies astd::stringand 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,fxReclaimTickrefuses every tick, and no laterUndoBlockis 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.
UndoBlockcloses on("", 0)unlessrecordPointwas 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 plusrecordPoint) andundoBlockOpen(). The latter is read byfxReclaimTick, which refuses a tick outright while any block is open: the drain's lazyopenSpanwould 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_EndBlock2is guarded byValidatePtr2only whenproject_is a real, non-null pointer.nullptrmeans "the active project" — the same conventionUndo_BeginBlock2was handed — andValidatePtr2does not acceptnullptras a valid object of any type, so guarding it unconditionally would silently stop everyUndoBlock 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 asymmetryfx_reclaim.cpp'scloseSpanalready has, for the same reason: its span always holds a real pinned pointer, nevernullptrfor "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.