Skip to content

Drop the pending-drag button field that is now always primary - #730

Merged
nedtwigg merged 2 commits into
mainfrom
fix/drop-dead-pending-drag-button
Sep 21, 2026
Merged

nedtwigg merged 2 commits into
mainfrom
fix/drop-dead-pending-drag-button

Conversation

@dormouse-bot

@dormouse-bot dormouse-bot commented Sep 21, 2026

Copy link
Copy Markdown
Collaborator

pendingDrag.button in lib/src/lib/terminal-mouse-router.ts is dead state. beginPendingDrag returns before the assignment for any non-primary press — if (ev.button !== 0) return true;, added in #710 so a right-click leaves no pendingDrag behind — so the recorded button is always 0. That makes the guard in updatePendingOrActiveDrag (if (pendingDrag.button !== 0) return;) unreachable, and the mouseup check in finishPendingOrActiveDrag a comparison against 0 written indirectly. This drops the field, and leaves finishPendingOrActiveDrag with a single ev.button !== 0 guard hoisted above both of its paths, commented with where the invariant comes from.

Hoisting is what makes the second half of the change worth having: once the pendingDrag branch compares against a literal 0, it is identical to the guard that already sat just past it gating the active-drag path, and both are the first statement of their path. One guard above if (pendingDrag) is equivalent for every (pendingDrag, ev.button) pair, and the invariant comment sits once instead of over half the function.

No behavior change, so there is no test that would have failed before it: the removed guard could not fire, and the remaining check keeps the same truth value for every ev.button. lib/src/lib/terminal-mouse-router.test.ts and mouse-selection.test.ts pass unchanged (53 tests), and tsc --noEmit over lib is clean.

Equivalence across the four call sites of finishPendingOrActiveDrag

The hoist is a source transform, so it is worth recording that each caller reaches the guard with the same ev.button it reached the old pair of guards with:

Caller ev.button Before After
onWindowMouseUp the released button pendingDrag path !== pendingDrag.button (always 0); active path !== 0 !== 0
onWindowMouseMove backstop 0 — a mousemove reports no button transition both paths pass passes
onWindowPointerUp, mouse 0 for the captured primary release both paths pass passes
onWindowPointerUp, touch/pen 0 for contact removal both paths pass passes

beginDrag has exactly one non-test caller — inside if (pendingDrag) in updatePendingOrActiveDrag — which is what makes "an active drag only ever grows out of a pendingDrag" hold, and with it the claim that the active-drag path never sees a non-primary release either.

docs/specs/mouse-and-clipboard.md §2 ("Does not count: a non-primary click") and §3.1 are unaffected: neither states the field, and the non-primary mouseup still returns before clearTemporaryOverrideAfterMouseDispatch, which is what that rule pins.

beginPendingDrag returns before assigning pendingDrag for any non-primary press, so the recorded button was a constant 0: the guard on it was unreachable and the mouseup comparison was a comparison with 0.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 21, 2026

Copy link
Copy Markdown

Deploying mouseterm with  Cloudflare Pages  Cloudflare Pages

Latest commit: f79ba15
Status: ✅  Deploy successful!
Preview URL: https://1445c55d.mouseterm.pages.dev
Branch Preview URL: https://fix-drop-dead-pending-drag-b.mouseterm.pages.dev

View logs

@dormouse-bot dormouse-bot left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feedback on work in progress, not a merge verdict — mark ready when you want the full review.

The dead-state claim holds: pendingDrag has a single assignment site, downstream of beginPendingDrag's if (ev.button !== 0) return true;. What the change leaves behind is a duplicate — finishPendingOrActiveDrag now carries two identical ev.button !== 0 guards, the new one inside the if (pendingDrag) branch and the pre-existing one just past it gating the active-drag path. Neither branch does anything before its guard, so hoisting a single check above if (pendingDrag) { is exactly equivalent and lets the invariant comment sit once instead of over half the function. The second guard is outside the diff, so there's no inline suggestion for it; I can push the consolidation once this is out of draft.

@nedtwigg
nedtwigg marked this pull request as ready for review September 21, 2026 20:18
… paths

Dropping pendingDrag.button left finishPendingOrActiveDrag with two identical
`ev.button !== 0` guards, each the first statement of its path, so hoisting one
above `if (pendingDrag)` is equivalent for every (pendingDrag, ev.button) pair.
The invariant comment now sits once and covers both paths, and no longer claims
the event is a release — the window-mousemove backstop finalizes with a
mousemove, which also reports button 0.
@nedtwigg
nedtwigg merged commit c31289f into main Sep 21, 2026
14 checks passed
@nedtwigg
nedtwigg deleted the fix/drop-dead-pending-drag-button branch September 21, 2026 21:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants