Skip to content

fix(editor): Cmd+A on WebKit, and macOS Ctrl navigation chords (#27) - #39

Merged
do-md merged 1 commit into
mainfrom
fix/select-all-and-system-chords
Sep 13, 2026
Merged

do-md merged 1 commit into
mainfrom
fix/select-all-and-system-chords

Conversation

@do-md

@do-md do-md commented Sep 13, 2026

Copy link
Copy Markdown
Owner

Select-all only flagged the terminal state and left the DOM selection to the engine, and the kernel treated Ctrl as an alias of the command modifier. Both assumptions break on macOS WebKit:

  • WebKit's native SelectAll collapses to the document tail whenever the editable root's first child is a non-editable overlay -- exactly what hosts portal in (custom caret, remote-cursor layers). The user sees "Cmd+A flashes, then the selection vanishes". Blink is unaffected.
  • Ctrl+A/E/N/P/F/B are system text bindings (NSStandardKeyBindingResponding: paragraph start/end, next/previous line, forward/backward). Matching Ctrl+A as select-all hijacked paragraph-start into the terminal state, and the replayed whole-document range then teleported the caret to the top of the document. Ctrl+N/P landed unreliably and fought the arrow keys.

Changes:

  • commandKey: split out foreignCommandKey (Ctrl on macOS, Cmd/Win elsewhere). Kernel shortcuts (undo/redo, select-all) now require the primary modifier and opt out on the foreign one, mirroring the @do-md/commands keymap.
  • Build the whole-document selection ourselves (selectAllDom_) instead of trusting the engine command: endpoints on the first/last VISIBLE text leaf, because WebKit paints no highlight for a container-endpoint range and blind model coordinates resolve into hidden syntax runs. The DOM write is deferred one macrotask -- WebKit keeps a selection installed inside a preventDefaulted keydown as data but never paints it -- and bails if the terminal state was cancelled in the meantime.
  • preventDefault on select-all, which also stops the desktop menu's native selectAll: from reaching the same broken engine command.
  • Replay pass: for the terminal state, confirm coverage of the first/last render block and return, otherwise rebuild via selectAllDom_, instead of replaying (uuid, offset) through the leaf-coordinate machinery -- that rebuilt range fought the live selection every pass (the reported flicker).
  • Probe the first/last render block (data-render-id) rather than first/lastElementChild: overlay scaffolding is legitimately outside our own selection, so probing it made the echo guard demote the terminal state.
  • Flush pending speculative input on the system navigation chords too, like the bare cursor keys: otherwise the armed pending timer wakes up after the caret has moved and writes the stale cursor back.

What this changes

Which layer does this touch?

  • Application layer (macOS app, web app, plugins, helper libraries) — MIT
  • Editor kernel (.packages/@do-md/core/) — GPL-3.0

How it was verified

Contributor License Agreement

  • I have read the CLA and will sign it on this pull request when the
    automated check asks.

Select-all only flagged the terminal state and left the DOM selection to
the engine, and the kernel treated Ctrl as an alias of the command
modifier. Both assumptions break on macOS WebKit:

- WebKit's native SelectAll collapses to the document tail whenever the
  editable root's first child is a non-editable overlay -- exactly what
  hosts portal in (custom caret, remote-cursor layers). The user sees
  "Cmd+A flashes, then the selection vanishes". Blink is unaffected.
- Ctrl+A/E/N/P/F/B are system text bindings
  (NSStandardKeyBindingResponding: paragraph start/end, next/previous
  line, forward/backward). Matching Ctrl+A as select-all hijacked
  paragraph-start into the terminal state, and the replayed
  whole-document range then teleported the caret to the top of the
  document. Ctrl+N/P landed unreliably and fought the arrow keys.

Changes:

- commandKey: split out foreignCommandKey (Ctrl on macOS, Cmd/Win
  elsewhere). Kernel shortcuts (undo/redo, select-all) now require the
  primary modifier and opt out on the foreign one, mirroring the
  @do-md/commands keymap.
- Build the whole-document selection ourselves (selectAllDom_) instead of
  trusting the engine command: endpoints on the first/last VISIBLE text
  leaf, because WebKit paints no highlight for a container-endpoint range
  and blind model coordinates resolve into hidden syntax runs. The DOM
  write is deferred one macrotask -- WebKit keeps a selection installed
  inside a preventDefaulted keydown as data but never paints it -- and
  bails if the terminal state was cancelled in the meantime.
- preventDefault on select-all, which also stops the desktop menu's
  native selectAll: from reaching the same broken engine command.
- Replay pass: for the terminal state, confirm coverage of the first/last
  render block and return, otherwise rebuild via selectAllDom_, instead
  of replaying (uuid, offset) through the leaf-coordinate machinery --
  that rebuilt range fought the live selection every pass (the reported
  flicker).
- Probe the first/last *render* block (data-render-id) rather than
  first/lastElementChild: overlay scaffolding is legitimately outside our
  own selection, so probing it made the echo guard demote the terminal
  state.
- Flush pending speculative input on the system navigation chords too,
  like the bare cursor keys: otherwise the armed pending timer wakes up
  after the caret has moved and writes the stale cursor back.
@vercel

vercel Bot commented Sep 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
domd Ready Ready Preview Sep 13, 2026 9:53am UTC

@do-md
do-md merged commit fb3e407 into main Sep 13, 2026
3 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Sep 13, 2026

This branch was successfully deployed

1 active deployment
Preview — 06a69053 Deployed Sep 13, 2026 by vercel[bot]
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants