Repository navigation
feat(muya): add cursorCoords + active formats to selection-change - #4387
Conversation
The desktop editor relies on two extras the legacy muyajs put on its selectionChange payload: cursorCoords (the caret's screen rect, used for typewriter-mode scrolling via changes.cursorCoords.y) and the active inline formats at the cursor (to light up the format toolbar). Add both to the selection-change event emitted by Selection.setSelection. Formats are duck-typed off the anchor block's getFormatsInRange to avoid a selection -> format circular import. Adds happy-dom coverage. Part of the muyajs -> @muyajs/core engine migration (stage A8). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR extends Muya’s selection-change event payload to better match MarkText’s legacy selectionChange surface during the muyajs → @muyajs/core migration, enabling desktop features that depend on cursor coordinates and active inline formats.
Changes:
- Emit
cursorCoords(fromSelection.getCursorCoords()) onselection-change. - Emit
formats(from the active block’sgetFormatsInRange().formatswhen selection is within a single formattable block; otherwise[]). - Add a happy-dom Vitest spec to assert the new payload keys are present.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Backport of marktext's `selectionChange` payload extras the desktop | ||
| // relies on: `cursorCoords` for typewriter-mode scrolling and the | ||
| // active inline formats at the cursor for lighting up the toolbar. | ||
| const cursorCoords = Selection.getCursorCoords(); | ||
| // Duck-type the Format block — a value import of Format here would |
There was a problem hiding this comment.
Fixed in bf9b578. getCursorCoords now takes a preferEnd flag (defaulted false, so existing callers like getCursorYOffset are unchanged), and setSelection passes direction === 'forward'. For a forward range selection it now returns the last client rect (the caret/focus end) so typewriter scrolling tracks the cursor instead of the selection start; collapsed and backward selections keep using the first rect.
There was a problem hiding this comment.
Good call — strengthened in bf9b578. Added a case that places the selection inside **bold** and asserts formats contains a strong token, so the test now fails if active-format detection regresses to always-empty.
- cursorCoords now follows the caret/focus end for forward range selections (getCursorCoords gains a `preferEnd` param, defaulted false so existing callers are unchanged; setSelection passes `direction === 'forward'`). getClientRects returns the first rect by default, which is the selection start — typewriter scrolling should track the caret, not the start. - Strengthen the spec with a non-empty case: cursor inside `**bold**` now asserts a `strong` token is emitted in `formats`, not just that it is an array. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Background
Part of the muyajs → @muyajs/core engine migration. The desktop editor
relies on two extras the legacy engine put on its
selectionChangeeventpayload, which
@muyajs/core'sselection-changedid not carry:cursorCoords— the caret's screen rect. The desktop typewriter modescrolls using
changes.cursorCoords.y.formats— the active inline formats at the cursor, used to light upthe desktop format toolbar (legacy emitted these via a
selectionFormatsevent).
What this adds
In
Selection.setSelection, the emittedselection-changepayload now alsoincludes:
cursorCoords: Selection.getCursorCoords()(aDOMRect | null).formats: the active inline-format tokens at the cursor when the selectionis within a single formattable block, else
[].formatsis computed by duck-typing the anchor block'sgetFormatsInRange(a value import of
Formatintoselection/index.tswould create aselection → formatcircular dependency — verified viacheck-circular).heading-copy-linkandformat-click/preview-image(other legacyinteraction events) are tracked separately and will land in a follow-up.
Tests
New happy-dom spec
src/selection/__tests__/selectionChange.spec.tsassertingthe payload carries
cursorCoordsand aformatsarray.Verification
pnpm -C packages/muya lint→ 0 errorspnpm -C packages/muya lint:types✓pnpm -C packages/muya check-circular✓ (noselection → formatcycle)pnpm -C packages/muya test→ 404 passedpnpm -C packages/muya test:spec→ 1347 passed (conformance unchanged)🤖 Generated with Claude Code