{{ message }}
Repository navigation
perf(muya): skip a code block re-render that produces identical DOM - #4876
Merged
Merged
Conversation
niyo-oyin
added a commit
to niyo-oyin/marktext-ja
that referenced
this pull request
Jul 29, 2026
Four fixes, all the same failure class — editor code acting on keys or input events that belong to an open IME composition: - content.ts keydownHandler: guard Backspace/Delete with !isComposed, matching the existing Enter/arrows/Tab guards. Mid-composition they ran backspaceHandler/deleteHandler against a stale text snapshot and dislocated the cursor (upstream marktext#4956; reimplements PR marktext#4957). - codeBlockContent: apply upstream PR marktext#4876 (Jocs) verbatim — an IME commit no longer rebuilds domNode.innerHTML synchronously (which destroyed the text node the IME was anchored to, corrupting Japanese input from the 2nd character, marktext#4851); the syntax re-highlight runs behind a 300ms trailing debounce instead. State updates stay synchronous; structural edits still rebuild synchronously. - langInputContent: the code-fence language input had no composition handling at all — every mid-composition input event rebuilt innerHTML. Ignore inputs while composing and commit IME text without re-rendering, same mechanism as marktext#4876. - Float tools: handleContentKeydown, the BaseScrollFloat nav handler and the BaseFloat Escape handler now ignore keys pressed while a composition is open — Enter/Escape/Tab/arrows operate the IME's candidate list, not the float. All fixes are unit-tested with synthetic composition events; each new spec fails on the pre-fix code. Muya suite 1454/1454, lint/tsc/madge clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ef4yFuXFbyKNTazkwUtygY
niyo-oyin
added a commit
to niyo-oyin/marktext-ja
that referenced
this pull request
Jul 29, 2026
Four fixes, all the same failure class — editor code acting on keys or input events that belong to an open IME composition: - content.ts keydownHandler: guard Backspace/Delete with !isComposed, matching the existing Enter/arrows/Tab guards. Mid-composition they ran backspaceHandler/deleteHandler against a stale text snapshot and dislocated the cursor (upstream marktext#4956; reimplements PR marktext#4957). - codeBlockContent: apply upstream PR marktext#4876 (Jocs) verbatim — an IME commit no longer rebuilds domNode.innerHTML synchronously (which destroyed the text node the IME was anchored to, corrupting Japanese input from the 2nd character, marktext#4851); the syntax re-highlight runs behind a 300ms trailing debounce instead. State updates stay synchronous; structural edits still rebuild synchronously. - langInputContent: the code-fence language input had no composition handling at all — every mid-composition input event rebuilt innerHTML. Ignore inputs while composing and commit IME text without re-rendering, same mechanism as marktext#4876. - Float tools: handleContentKeydown, the BaseScrollFloat nav handler and the BaseFloat Escape handler now ignore keys pressed while a composition is open — Enter/Escape/Tab/arrows operate the IME's candidate list, not the float. All fixes are unit-tested with synthetic composition events; each new spec fails on the pre-fix code. Muya suite 1454/1454, lint/tsc/madge clean.
Jocs
force-pushed
the
fix/muya-ime-codeblock-rerender-4851
branch
from
September 22, 2026 03:31
f495eca to
f985c67
Compare
CodeBlockContent.update() ended every render in `domNode.innerHTML = code`, which destroys and recreates the block's child nodes even when the rebuilt string is byte-identical. Every keystroke in a code block therefore tore down the text node the caret was sitting in and built a new one, taking the native selection and any IME composition anchored to it along — and so did every render that changed nothing at all. Compare before writing. `Format.inputHandler` already has this guard by way of `checkNeedRender`, which is why paragraphs never paid the same cost. Both render paths go through the check, and the trailing-break span (#5114) is part of the compared string, so a `\n`-terminated block is idempotent too rather than rebuilding on every pass. `inputHandler`'s `if (needRender) … else …` had two identical branches and a `// TODO: throttle render` standing in for exactly this comparison; both go. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Node identity is what a needless re-render destroys, so that is what these tests assert — not the rendered values, which two freshly minted nodes compare equal on. The unit spec covers update() directly: an unchanged render keeps the existing nodes, changed text still replaces them, a `\n`-terminated block (which carries the trailing-break span) is idempotent too, and search highlights still render and still clear — the comparison must not swallow them. The e2e spec drives a real Chromium composition over CDP Input.imeSetComposition, the recipe ime-soft-line-break.spec.ts established, so the browser runs its own composition machinery instead of the test staging the DOM it expects. It asserts the code block's text node survives a run of IME commits, including with 500ms pauses between them (where a debounce-based guard would rebuild), in a Prism-highlighted block, and under plain typing. Both idempotence unit cases and all four e2e cases fail without the change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Jocs
force-pushed
the
fix/muya-ime-codeblock-rerender-4851
branch
from
September 22, 2026 13:47
f985c67 to
9f05ead
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.

Summary
CodeBlockContent.update()ended every render indomNode.innerHTML = code. AssigninginnerHTMLalways destroys and recreates the child nodes, even for a byte-identical string — so a code block tore down the text node the caret was sitting in and built a new one on every single keystroke, and on every render that changed nothing at all (a language switch resolving to the same highlight, a re-render triggered elsewhere, an undo landing on the same text).Paragraphs never paid this cost:
Format.inputHandlercallsupdate()only whencheckNeedRenderreports a token change, and otherwise moves the caret without touching the DOM.Change
Compare before writing:
\n-terminated block is idempotent too instead of rebuilding on every pass.update(undefined, highlights), which changes the output, so the comparison re-renders — and clears — exactly as before. There is a test for both directions.inputHandler'sif (needRender) … else …had two identical branches and a// TODO: throttle renderstanding in for exactly this comparison; both are gone.What this is not
An earlier revision of this PR claimed to fix #4851 (Japanese IME garbling in a code block) with a debounced re-highlight. That diagnosis was wrong, and the debounce was worse than useless — it only held within a 300 ms window, so ordinary IME typing (which pauses on every candidate selection) fell straight through it.
#4851 turned out to be #3468's mechanism: a code block whose text ends in a newline used to render that newline inside its own
<span class="mu-line-end">, the caret landed inside the newline's own text node, and Chromium will not sustain a composition there. #5286 moved the newline back into the main text node and it has been fixed since v0.20.0-rc.2 — verified by running a real CDP-driven composition against the 0.20.0-beta.5 source (garbles) and against current develop (clean). That issue is now closed.So this PR carries no bug fix. It removes work that was never needed, and the node identity it preserves is a precondition for anything composing against the live DOM.
Tests
Node identity is what a needless re-render destroys, so that is what the tests assert — not rendered values, which two freshly minted nodes compare equal on.
Unit (
renderIdempotence.spec.ts) — unchanged render keeps the existing nodes; changed text still replaces them; a\n-terminated block is idempotent; search highlights still render and still clear.E2E (
e2e/tests/typing/codeblock-node-identity.spec.ts) — a real Chromium composition over CDPInput.imeSetComposition. The code block's text node must survive a run of IME commits, including with 500 ms pauses between them, in a Prism-highlighted block, and under plain typing.Both idempotence unit cases and all four e2e cases fail without the change.
🤖 Generated with Claude Code