fix(super-editor): defer layout rerender and track-changes rewriting during IME composition (SD-2368)#3711
Open
luccas-harbour wants to merge 9 commits into
Conversation
Chrome aborts a native IME composition whenever the composing DOM node is restructured mid-preedit, which made CJK input lose committed characters (e.g. 你好) in both editing and suggesting modes. Address every source of mid-composition DOM churn: - Editor: defer tracked-transaction rewriting while a composition is in flight; track the composed range through applied transactions and convert it into a single tracked insertion after compositionend (blur as fallback), surfacing the new change id to the sidebar bubble pipeline via meta. - PresentationEditor: defer visible layout repaints while composing and flush the pending rerender once on compositionend/blur/focusout, with non-composing input as a lost-compositionend recovery path. - keymap: decline Backspace/Delete/Enter while composing so prosemirror-view's synthesized key events from composition commits fall through and the DOM change applies as-is. - HiddenHost: force `.sd-paragraph-content` to display: block in hidden hosts so Blink's empty-inline cleanup can't remove the contentDOM when the preedit is rewritten in an empty paragraph. Adds behavior specs covering committed text, no visible repaints mid-composition, deferred tracking, and the tracked-change sidebar bubble.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
…tion undo event The post-composition flush applies trackInsert marks in a separate transaction whose mark-only steps have empty step maps, so prosemirror-history's adjacency check failed and the flush became its own undo event. A first undo then only stripped the suggestion marks, leaving the composed text behind as an untracked edit, and the unified history coordinator recorded a spurious extra entry. Stamp the flush with the deferred transactions' composition id so prosemirror-history groups it with the composed text: one undo removes text and marks together, and the coordinator never sees a second event. Caught by header-footer-undo-cross-container.spec.ts on Firefox, where Playwright's keyboard.insertText is synthesized as an IME commit.
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
CJK (and other IME-based) input was broken in the presentation editor, especially with track changes enabled: Chrome aborted the native composition on nearly every keystroke, dropping or mangling preedit text. This PR makes IME composition survive end-to-end by deferring everything that restructures the composing DOM or repaints layout until
compositionend, then reconciling afterwards.Root causes fixed
trackedTransactionrewrites each preedit update into tracked-insert mark spans plus decorations, which restructures the DOM text node Chrome is composing into — aborting the composition on every keystroke.PresentationEditorrerendered the visible layout on every doc change, including mid-composition preedit updates.span.sd-paragraph-content; Blink removes the "redundant" empty span, ProseMirror loses its contentDOM and redraws, and the first composed keystrokes vanish.How it works
Deferred composition tracking (
Editor.ts)trackedTransactionrewrite). The inserted range is tracked in#deferredCompositionRangeand mapped through every subsequently applied transaction.compositionend(microtask-deferred, with a forceddomObserverflush so the trailing DOM read lands first),#flushDeferredCompositionTrackingconverts the composed range into a tracked insertion in one transaction. Text composed inside an existing suggestion is left alone (mark inheritance already covers it).replacementGroupId) unlesstrackedChanges.replacements: 'independent'is set. Deferral falls back to immediate tracking when the deleted content can't be faithfully restored (tables, non-text leaves).blurforce-flushes so compositions abandoned withoutcompositionenddon't leak raw untracked text; chained compositions (common with CJK IMEs) keep deferring until the last one ends.compositionTrackingFlushmeta, whichtrackedTransactionnow skips to avoid re-rewriting the marks it carries.Deferred layout repaint (
PresentationEditor.ts)compositionstart/compositionendlisteners on the visible host and the active hidden editor target set an#isComposingflag;#flushRerenderQueuekeeps#pendingDocChange/#pendingMappingintact while composing and a single rerender is scheduled when the composition ends. Listeners are re-pointed when the active hidden target changes (body ↔ header/footer ↔ story), withblur/focusout/non-composing input as escape hatches.Hidden-host DOM survival (
HiddenHost.ts).sd-paragraph-contenttodisplay: blockinside hidden hosts only, so Blink gives the emptied container a placeholder<br>instead of removing it. Visible editors are unaffected.Keymap guards (
keymap.js)handleEnter/handleBackspace/handleDeletedecline whileview.composing, restoring vanilla ProseMirror's fall-through for synthesized mid-composition key events.Tests
tests/comments/chinese-ime-composition.spec.tssimulating real Chrome composition event sequences (preedit updates, commit, empty-paragraph composition, tracked-changes mode).track-changes-extension.test.js(deferred range mapping, replacement pairing, blur flush, skip-meta) andPresentationEditor.test.ts(composition deferral lifecycle, target swapping, rerender resume).HiddenHost.test.tscovers the injected stylesheet;keymap-backspace-chain.test.jscovers the composing decline.