Skip to content

fix(editor): Escape can't collapse an all-empty multi-caret when a stored mark is present#981

Merged
xiaolai merged 2 commits into
mainfrom
fix/escape-multicursor-mark-swallow
May 30, 2026
Merged

fix(editor): Escape can't collapse an all-empty multi-caret when a stored mark is present#981
xiaolai merged 2 commits into
mainfrom
fix/escape-multicursor-mark-swallow

Conversation

@xiaolai
Copy link
Copy Markdown
Owner

@xiaolai xiaolai commented May 30, 2026

Scope correction

This started as an attempt to fix "Escape sometimes can't clear a selection over a list," but the real-state regression test (added per a Codex audit) disproved that root cause: Selection.empty is true only when all ranges are empty, so a multi-cursor selection with any non-empty (visible) range has empty === false and escapeMarkBoundary already bails via !empty. Escape already works for the visible-selection case.

What this PR actually fixes is a narrower, real bug.

Bug

With multiple empty carets (multi-cursor, no selection) and a stored mark / mark at the primary caret, pressing Escape was swallowed: the editor keymap (priority 1000) runs first, collapseNonEmptySelection skips (empty), and escapeMarkBoundary — guarding only !empty — cleared the stored marks and returned true. That preempted the multi-cursor keymap's collapseMultiSelection, so the extra carets were never reduced.

Fix

escapeMarkBoundary now bails on multi-range selections (selection.ranges.length > 1), mirroring the guard collapseNonEmptySelection already has. The editor keymap's Escape then returns false and the multi-cursor handler collapses the carets.

Test

keymapUtils.test.ts:

  • A mock-shaped unit test (multi-range + stored marks → not handled).
  • A real-EditorState integration test: a MultiSelection of two empty carets + a stored mark → escapeMarkBoundary returns false (no swallow), then collapseMultiSelection reduces it to a single caret. (The existing single-cursor mocks gained a realistic ranges: [{}].)

Not covered (separate issue)

The originally-reported "visible multi-cursor selection over a list won't clear with one Escape" is a different behavior — collapseMultiSelection collapses to the primary range (not a caret), so a non-empty primary leaves a selection after one Escape. Needs its own investigation/decision.

Verification

  • pnpm test src/plugins/editorPlugins/keymapUtils.test.ts + multiCursor suites pass
  • pnpm check:all green

xiaolai added 2 commits May 31, 2026 01:42
…or selections

A MultiSelection whose primary range is a caret reports empty===true, so it
slips past collapseNonEmptySelection's empty-guard and reaches escapeMarkBoundary.
That helper only guarded !empty, so for a multi-cursor selection with a mark (or
stored marks) at the primary caret it cleared the marks and returned true —
swallowing Escape before the multi-cursor keymap's own Escape handler could
collapse the secondary ranges. The leftover secondary selection (e.g. over a
list) then could not be dismissed with Escape. Intermittent because it depended
on mark/stored-mark state at the caret.

Bail from escapeMarkBoundary on multi-range selections (mirroring the guard
collapseNonEmptySelection already has) so editorKeymap's Escape returns false and
the multi-cursor handler reduces the selection. Regression test added.
…aret (audit follow-up)

Codex audit-fix flagged the regression test as mock-only. Adding a real
EditorState + MultiSelection integration test revealed the original premise was
wrong: Selection.empty is true only when ALL ranges are empty, so a multi-cursor
selection with any non-empty range has empty===false and escapeMarkBoundary
already bails via !empty. The swallow only affects the all-empty multi-caret
case. Test and comments corrected to that true scenario; the production guard is
unchanged and correct for it.
@xiaolai xiaolai changed the title fix(editor): Escape can't clear multi-cursor selection when a mark sits at the caret fix(editor): Escape can't collapse an all-empty multi-caret when a stored mark is present May 30, 2026
@xiaolai xiaolai merged commit 3ef466b into main May 30, 2026
8 checks passed
@xiaolai xiaolai deleted the fix/escape-multicursor-mark-swallow branch May 30, 2026 18:25
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.

1 participant