From 2fdedadea8aabd5a6897cf7ba930f6c6c2ac52e2 Mon Sep 17 00:00:00 2001 From: PathGao <42336971+PathGao@users.noreply.github.com> Date: Wed, 2 Sep 2026 08:06:43 +0800 Subject: [PATCH 1/3] Editor textarea: short content unless a screen reader is attached `writeNativeTextAreaContent` already treats AccessibilitySupport.Unknown as "no screen reader": it skips render-time writes unless isScreenReaderOptimized() (#192278). `getScreenReaderContent` disagreed and handed Unknown the paged content, so a one-row textarea carried several rows of text and had to be scrolled to the caret's row. WebKit scrolls a focused textarea to keep its caret visible on every IME composition update, landing a few pixels short of the row-aligned offset `_render` writes; the next render writes it back, and the composed text jitters on every keystroke (microsoft/monaco-editor#4796). With the short content there is no overflow to scroll and nothing to fight over. Behaviour with a screen reader attached (Enabled) is unchanged. Co-Authored-By: Claude Fable 5.1 --- .../editContext/textArea/textAreaEditContext.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContext.ts b/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContext.ts index 1afbfb12b855a8..73db192fec2870 100644 --- a/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContext.ts +++ b/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContext.ts @@ -207,8 +207,12 @@ export class TextAreaEditContext extends AbstractEditContext { const textAreaInputHost: ITextAreaInputHost = { context: this._context, getScreenReaderContent: (): TextAreaState => { - if (this._accessibilitySupport === AccessibilitySupport.Disabled) { - // We know for a fact that a screen reader is not attached + if (this._accessibilitySupport !== AccessibilitySupport.Enabled) { + // No screen reader is known to be attached. `writeNativeTextAreaContent` already + // treats this case as "no screen reader" and skips render-time writes (#192278); + // handing it the paged content anyway put several rows of text into a one-row + // textarea, and during IME composition WebKit scrolls that textarea to keep its + // caret visible, fighting the row alignment `_render` maintains (monaco-editor#4796). // On OSX, we write the character before the cursor to allow for "long-press" composition // Also on OSX, we write the word before the cursor to allow for the Accessibility Keyboard to give good hints const selection = this._selections[0]; @@ -559,13 +563,15 @@ export class TextAreaEditContext extends AbstractEditContext { this._accessibilityPageSize = accessibilityPageSize; } - // When wrapping is enabled and a screen reader might be attached, + // When wrapping is enabled and a screen reader is attached, // we will size the textarea to match the width used for wrapping points computation (see `domLineBreaksComputer.ts`). // This is because screen readers will read the text in the textarea and we'd like that the // wrapping points in the textarea match the wrapping points in the editor. + // Without one, the textarea only ever holds the short content `getScreenReaderContent` writes, + // and there is nothing to wrap. const layoutInfo = options.get(EditorOption.layoutInfo); const wrappingColumn = layoutInfo.wrappingColumn; - if (wrappingColumn !== -1 && this._accessibilitySupport !== AccessibilitySupport.Disabled) { + if (wrappingColumn !== -1 && this._accessibilitySupport === AccessibilitySupport.Enabled) { const fontInfo = options.get(EditorOption.fontInfo); this._textAreaWrapping = true; this._textAreaWidth = Math.round(wrappingColumn * fontInfo.typicalHalfwidthCharacterWidth); From 9172a4d1fc5f60c871a540f57f8dac60ef6a6e0a Mon Sep 17 00:00:00 2001 From: PathGao <42336971+PathGao@users.noreply.github.com> Date: Wed, 2 Sep 2026 08:11:42 +0800 Subject: [PATCH 2/3] Editor textarea: write the composition overlay's scroll offsets after sizing it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The composition branch of `_render` wrote `scrollTop`/`scrollLeft` before `_doRender` gave the textarea its size for the row, while the non-composition branch a few lines down does it the other way round. A scroll offset is clamped against the element's size at the time it is written, so the order the sibling branch uses is the one that cannot lose part of the write. No behavioural difference was measured for the jitter in monaco-editor#4796 — with the short content there is nothing to scroll — this only makes the two branches agree. Co-Authored-By: Claude Fable 5.1 --- .../editContext/textArea/textAreaEditContext.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContext.ts b/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContext.ts index 73db192fec2870..4fc4f822ce07bb 100644 --- a/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContext.ts +++ b/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContext.ts @@ -754,9 +754,6 @@ export class TextAreaEditContext extends AbstractEditContext { (textareaSpansSingleToken ? viewLineData.tokens.getPresentation(startTokenIndex) : null) ); - this.textArea.domNode.scrollTop = lineCount * lineHeight; - this.textArea.domNode.scrollLeft = scrollLeft; - this._doRender({ lastRenderPosition: null, top: top, @@ -771,6 +768,11 @@ export class TextAreaEditContext extends AbstractEditContext { strikethrough: presentation.strikethrough, fontSize }); + // After `_doRender`, the way the non-composition branch below orders it: a scroll + // offset is clamped against the textarea's size at the time it is written, and it is + // `_doRender` that gives the textarea its size for this row. + this.textArea.domNode.scrollTop = lineCount * lineHeight; + this.textArea.domNode.scrollLeft = scrollLeft; } return; } From 9b39c2ce50c97ce857a4a81bb57fd48ce65f9708 Mon Sep 17 00:00:00 2001 From: PathGao <42336971+PathGao@users.noreply.github.com> Date: Wed, 2 Sep 2026 08:19:16 +0800 Subject: [PATCH 3/3] Editor textarea: keep the Android exception, one-line comments Android + Unknown went through the short-content branch and returned EMPTY before reaching the Android word handling that composition diffing relies on. The Android cases now behave exactly as before; only the non-Android Unknown case takes the short content. Co-Authored-By: Claude Fable 5.1 --- .../editContext/textArea/textAreaEditContext.ts | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContext.ts b/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContext.ts index 4fc4f822ce07bb..6d1ab16559b75f 100644 --- a/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContext.ts +++ b/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContext.ts @@ -207,12 +207,8 @@ export class TextAreaEditContext extends AbstractEditContext { const textAreaInputHost: ITextAreaInputHost = { context: this._context, getScreenReaderContent: (): TextAreaState => { - if (this._accessibilitySupport !== AccessibilitySupport.Enabled) { - // No screen reader is known to be attached. `writeNativeTextAreaContent` already - // treats this case as "no screen reader" and skips render-time writes (#192278); - // handing it the paged content anyway put several rows of text into a one-row - // textarea, and during IME composition WebKit scrolls that textarea to keep its - // caret visible, fighting the row alignment `_render` maintains (monaco-editor#4796). + if (this._accessibilitySupport === AccessibilitySupport.Disabled || (this._accessibilitySupport === AccessibilitySupport.Unknown && !browser.isAndroid)) { + // No screen reader is known to be attached; a page here gives WebKit an overflow to scroll during IME composition (monaco-editor#4796) // On OSX, we write the character before the cursor to allow for "long-press" composition // Also on OSX, we write the word before the cursor to allow for the Accessibility Keyboard to give good hints const selection = this._selections[0]; @@ -567,8 +563,6 @@ export class TextAreaEditContext extends AbstractEditContext { // we will size the textarea to match the width used for wrapping points computation (see `domLineBreaksComputer.ts`). // This is because screen readers will read the text in the textarea and we'd like that the // wrapping points in the textarea match the wrapping points in the editor. - // Without one, the textarea only ever holds the short content `getScreenReaderContent` writes, - // and there is nothing to wrap. const layoutInfo = options.get(EditorOption.layoutInfo); const wrappingColumn = layoutInfo.wrappingColumn; if (wrappingColumn !== -1 && this._accessibilitySupport === AccessibilitySupport.Enabled) { @@ -768,9 +762,7 @@ export class TextAreaEditContext extends AbstractEditContext { strikethrough: presentation.strikethrough, fontSize }); - // After `_doRender`, the way the non-composition branch below orders it: a scroll - // offset is clamped against the textarea's size at the time it is written, and it is - // `_doRender` that gives the textarea its size for this row. + // After `_doRender`, as the branch below does: the offsets are clamped against the size it just set this.textArea.domNode.scrollTop = lineCount * lineHeight; this.textArea.domNode.scrollLeft = scrollLeft; }