{{ message }}
Repository navigation
fix(muya): cut a cross-block selection from beforeinput too (#5415) - #5536
Merged
Merged
Conversation
Jocs
force-pushed
the
fix/cross-block-modifier-edit-5415
branch
from
September 23, 2026 14:24
309c30f to
abe25da
Compare
`cutSelection`'s same-block branch replaces the selected range with nothing. Lift that splice into `replaceRange` and add `insertTextAtCursor` on top of it, so replacing a selection with text and replacing it with nothing stay one implementation. No caller for the insert half yet; the cross-block `beforeinput` path added next needs it to finish an edit whose native insertion it cancelled. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
muya cut a selection spanning two blocks only from its document `keydown` handler, and `shouldCrossBlockCut` refuses every chord carrying Cmd or Ctrl so that Ctrl+C would stop deleting the selection (#3491). A key alone cannot say whether a modifier combo edits, though: on macOS Cmd+Backspace, Ctrl+K, Ctrl+D and Ctrl+H are editing commands, Ctrl+Backspace is Chromium's delete-word binding everywhere, and input committed without a key press at all (emoji picker, dictation) never reaches a keydown handler. Those edits ran in the browser instead, which deleted across the block elements: the second block left the screen but stayed in muya's tree with a detached DOM node and in the saved markdown, and the next structural edit near it threw "Failed to execute 'insertBefore' on 'Node'". Add a `beforeinput` handler that catches what keydown missed. It asks `inputType`, which says whether the event edits at all, so non-editing shortcuts stay untouched — they fire no `beforeinput` — and every route into an edit is covered however it was produced. Anything the keydown handler did take over reaches it with the selection already cut, or not at all because that handler cancelled the event, so the two never act on the same edit. The rule is one sentence: over a cross-block selection muya owns every editing `beforeinput` except the ones another handler already owns. An input type nobody has enumerated therefore degrades to "selection replaced" rather than to the corruption above, and the exclusion set does not have to grow a row per bug report. `classifyInputKind` already splits insert from delete for undo grouping, so that stays one definition too. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`detachedBlocks`, `expectTreeMatchesJson` and the Enter-twice-on-the-list probe together are one assertion — "the browser did not leave a block behind" — and #5035's spec is the only place that owns it. The next spec to make that assertion (#5415) would copy all three verbatim, so move them next to the other shared helpers first and switch #5035 over. `.mu-list-item` joins `helpers/selectors.ts`, which is where specs are meant to read class names from. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…#5415) Drives the real chords over a selection running from a bullet into the paragraph below it, and asserts all three symptoms are gone: the markdown loses the second block, no block is left with a detached DOM node, and Enter, Enter at the end of the list no longer throws. Ctrl+Backspace is Chromium's own delete-word binding and needs no AppKit command, so it covers this on CI; Playwright forwards the macOS editing commands (Cmd+Backspace, Ctrl+K/D/H, Ctrl+Y yank) only when the host is macOS, so those are gated on the platform. Ctrl+Y is the one insertion route a keyboard can reach, standing in for the emoji picker and dictation, which no test can drive. Ctrl+C and a plain printable key are pinned alongside: both go through the keydown handler, and neither behaviour may change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Jocs
force-pushed
the
fix/cross-block-modifier-edit-5415
branch
from
September 23, 2026 15:24
abe25da to
5f50a84
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Fixes #5415.
The bug
Deleting a selection that spans two blocks with Cmd+Backspace removed the second block from the screen but not from the document. The block stayed in muya's tree with its DOM node detached, so the next structural edit nearby — Enter, Enter at the end of a bullet list — threw
NotFoundError: Failed to execute 'insertBefore' on 'Node'. One of the triggers behind #5035.Root cause
muya cuts a cross-block selection only from its document
keydownhandler, andshouldCrossBlockCut()refuses every chord carrying Cmd or Ctrl — a guard added so Ctrl+C would stop silently deleting the selection (#3491).A key alone cannot say whether a modifier combo edits. Several of the refused chords are editing commands, and some edits carry no keydown at all:
deleteToBeginningOfLine:)Chromium then applied the edit across the block elements and removed the ones in between, leaving muya's tree and json state holding a block whose element is gone.
Fix
Add a
beforeinputhandler alongside the existingkeydownone.beforeinputfires for every editing intent whatever produced it, and itsinputTypesays whether the event edits at all — so non-editing shortcuts need no blacklist (they fire nobeforeinput), and the modifier guard stays exactly as it is for #3491.The two handlers never act on the same edit: anything
keydowntook over arrives atbeforeinputwith the selection already cut, or does not arrive becausekeydowncancelled the event. So the new handler only ever sees what the old one missed.crossBlockReplacement()is one rule — over a cross-block selection muya owns every editingbeforeinputexcept the ones another handler already owns:deleteByCut/deleteByDrag/insertFromPaste/insertFromPasteAsQuotation/insertFromDrop/insertParagraph/insertLineBreak/ the two composition types are excluded — thecut,pasteand Enter paths own them, and composition is not cancelable.Stating it as an exclusion set rather than an allowlist matters: an
inputTypenobody has enumerated (a future Chromium addition, a route no one reported yet) then degrades to "selection replaced" instead of to the corruption above, and the set does not grow a row per bug report.classifyInputKind()already splits insert from delete for undo grouping, so that stays one definition.Why not just widen the keydown guard?
Measured, same spec, three implementations:
develop- tw- twt- txtTwo reasons the key layer cannot get there:
- tw/- twt/- txt. That needs a second hand-maintained table.Ctrl+K,Ctrl+D,Ctrl+HandCtrl+Yare MarkText's own shortcuts (view.toggle-toc,format.strike,paragraph.bullet-list,format.inline-code— seekeybindingsLinux.ts), so allowlisting them by key would reintroduce In Typewriter Mode, when I selected text, and somehow text would be disppeared #3491 on a different key.beforeinputneeds no table: those chords fire nobeforeinputon Linux, and arrive asdeleteContentForwardetc. on macOS.The keydown path stays, and must: IME composition reaches
beforeinputasinsertCompositionText, which iscancelable: false. The keydown fallthrough (cut withoutpreventDefault, beforecompositionstart) is the only hook that can collapse the selection in time. The two handlers are complementary, not redundant.Verification
Measured in Chromium via the muya e2e host. Document
- one/- two/ blank /text, selection fromtw|ointote|xt:beforeinputdeleteContentBackward- one/- twxt/``/text+ detached paragraph- one/- twxtdeleteHardLineForward- one/- twxtdeleteContentForward- one/- twxtdeleteContentBackward- one/- twxtdeleteContentBackward- one/- twxtinsertFromYank- one/- twtextxtEach case then runs Enter, Enter at the end of the list and asserts no
pageerror, no detached block, and that the block tree still matchesgetState(). Without the fix that step throws the reportedinsertBeforeerror.Tests
e2e/tests/editing/cross-block-modifier-edit-5415.spec.ts— the chords above, plus two guards that must not change: Ctrl+C still leaves the selection alone (In Typewriter Mode, when I selected text, and somehow text would be disppeared #3491) and a printable key still replaces it. Playwright only forwards macOS AppKit editing commands when the host is macOS, so the mac-only chords are gated on the platform; Ctrl+Backspace needs no such command and is what covers this on CI.src/clipboard/__tests__/crossBlockReplacement.spec.ts— theinputTypedecision table, including that formatting and history commands never delete, and that an unrecognised type is still claimed.Suite results: muya clipboard units 171 passed; muya e2e
editing/111 passed. The one red spec there,cross-block-enter.spec.ts:36, fails identically on unmodifieddevelopin this environment and is unrelated.Also in this PR
replaceRange()incut.ts—cutSelection's same-block branch and the newinsertTextAtCursor()were the same splice; they are one implementation now.e2e/tests/helpers/tree.ts—detachedBlocks/expectTreeMatchesJson/ the Enter-twice probe are a single "the browser left a block behind" assertion that Unexpected error with message Failed to execute 'insertBefore' on 'Node': The node before which the new node is to be inserted is not a child of this node. #5035's spec owned privately. Shared, and Unexpected error with message Failed to execute 'insertBefore' on 'Node': The node before which the new node is to be inserted is not a child of this node. #5035 switched over.beforeinputhandler short-circuits on the native selection being collapsed before resolving muya's, so ordinary typing does not pay forgetSelection()(which materialises the block list per endpoint viaLinkedList.offset).Not covered
The emoji picker / dictation / handwriting routes named in the issue go through the same insert branch, but I could not verify them:
document.execCommand('insertText')fires nobeforeinputat all, so it is not a faithful stand-in, and those input sources cannot be driven from a test. Ctrl+Y is the one insertion route reachable from a keyboard, and it is what the spec exercises.🤖 Generated with Claude Code