Skip to content

Typing with an IME no longer makes the line sink and snap back - #746

Merged
PathGao merged 1 commit into
masterfrom
fix/ime-composition-jitter
Sep 2, 2026
Merged

Typing with an IME no longer makes the line sink and snap back#746
PathGao merged 1 commit into
masterfrom
fix/ime-composition-jitter

Conversation

@PathGao

@PathGao PathGao commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

What this is

Closes #724. Typing Chinese with the macOS IME made the line being composed sink a few pixels and snap back on every keystroke — on every line except the first. Reported by @PathGao, who also ran every experiment below on the builds that led here.

Mechanism

Not the block patcher, not fonts, not the editor's scroll. Two things had to be established first, both against the real app: no layout number the editor exposes changes during composition (content height, the caret line's top, the editor's scroll offset all hold still), and the jitter is gone with accessibilitySupport: 'off' but unchanged with a one-line accessibility page.

The moving part is Monaco's hidden textarea. In a browser without EditContext — WKWebView, so every macOS build — the editor takes input through it, and during IME composition it becomes a one-row overlay on the composed line. It holds a page of document text, so Monaco scrolls it to the caret's row (scrollTop = newlinesBefore × lineHeight). Logged from inside the app, one keystroke:

WRITE  top 21 → kept 21      [max 21]   Monaco's write, not clamped
SCROLL top 18  (off by 3)    [max 21]   WebKit scrolls the focused textarea to "caret just visible"
WRITE  top 21 → kept 21                 the next render scrolls it forward again

102 self-scrolls over three composed lines, every one exactly 3px short; on line 3 it is 42 → 39. The first line of the page never jitters because its target offset is 0. Chromium never runs this code path (editContext defaults on there), which is why the upstream report says "only in Safari".

Monaco already treats accessibilitySupport: 'auto' in a browser as "no screen reader" when deciding whether to write into the textarea — it skips render-time writes unless a screen reader is confirmed (vscode#192278) — but not when deciding what to write; that path hands the same state a page. This makes the two agree. The overlay then holds one short line, has no overflow, and there is nothing for the browser to scroll.

Proposed upstream as microsoft/vscode#333909, against microsoft/monaco-editor#4796. Until a Monaco release carries it, scripts/monacoImePatch.mjs applies the same two-line change to the bundled Monaco at build time.

Scope

A build-time source patch rather than an editor option: accessibilitySupport: 'off' also fixes it but would take VoiceOver support away from every user; the patch keeps 'on' behaving exactly as before. Monaco is still 0.55.1 — 0.56 does not carry the fix either (checked), and #599 keeps it off anyway.

Things tried and rejected on the way, each measured: reordering Monaco's two writes (no effect — the write is never clamped, the offset is moved afterwards), re-asserting the offset every frame (works, but keeps the alignment depending on a value the browser owns), moving the overlay to follow the browser's scroll (worse: moving a focused editable makes WebKit reveal its caret through the ancestors, and the whole editor jumps), and giving the overlay its full height with clip-path (broke rendering; _doRender couples height and line-height).

Tests

scripts/monacoImePatch.test.ts pins the two anchors to the installed Monaco: each matches exactly once, the patch replaces exactly those two lines and nothing else, and a Monaco without the anchors throws instead of building unpatched. These are text assertions against node_modules, and that is the point — the contract is "these two lines still exist in this file". The Monaco bump that finally carries the upstream change turns them red, which is the signal to delete the patch. They read the file through readSource, per the convention test.

Verification

npm audit           0 vulnerabilities
npm run check       830 files, 0 errors
npm test            1028 pass
npm run test:vitest 434 pass
cargo test          164 pass

Built the app from this branch and confirmed both replaced conditions are in the bundle. On the reporter's machine (macOS 26, system Chinese IME): composition on any line no longer moves, ASCII typing, select-all, copy, paste, undo and cross-line cursor movement unchanged.

Not verified: Windows and Linux builds (WebView2 is Chromium, so the textarea path is not used there; Linux WebKitGTK is, and should benefit the same way).

🤖 Generated with Claude Code

…p back (#724)

Monaco's hidden textarea becomes a one-row overlay on the composed line
during IME composition, but it holds a page of text, so Monaco scrolls it to
the caret's row. WebKit scrolls it back to "caret just visible" — 3px short
of the row — on every composition update, and the next render scrolls it
forward again. That is the jitter: the composed text sinks and snaps back
on every keystroke, on every line but the first of the page.

Monaco already treats `accessibilitySupport: 'auto'` in a browser as "no
screen reader" when deciding whether to write into the textarea
(vscode#192278), but not when deciding what to write; it hands that state
a page. A build-time patch makes the two decisions agree, so the overlay
holds one short line, has no overflow, and gives the browser nothing to
scroll. Proposed upstream as microsoft/vscode#333909; the patch and its
test go when a Monaco release carries it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@PathGao
PathGao merged commit 30af2ec into master Sep 2, 2026
4 checks passed
@PathGao
PathGao deleted the fix/ime-composition-jitter branch September 2, 2026 00:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug-typing jitter

1 participant