Type: Bug
When the editor uses the native EditContext input path (editor.editContext: true, the default on Chromium), focusing the editor scrolls every natively-scrolling ancestor of the hidden edit context node. The textarea input path has guarded against exactly this for years; the native EditContext path is missing the same guard.
Root cause
FocusTracker.focus() in src/vs/editor/browser/controller/editContext/native/nativeEditContextUtils.ts calls this._domNode.focus() bare:
public focus(): void {
this._domNode.focus();
this.refreshFocusState();
}
The hidden .native-edit-context node is absolutely positioned at the editor's last cursor position. When the editor gains focus (e.g. on mouse down), the browser scrolls every scrollable ancestor to reveal that node. If any ancestor scrolls natively (overflow: auto), its scroll position jumps, shifting layout between Monaco's mouse-down hit test and the click resolution -- so the click can land the cursor on the wrong line, and the container visibly jumps.
The textarea path already handles this in writeNativeTextAreaContent (src/vs/editor/browser/controller/editContext/textArea/textAreaEditContextInput.ts):
// If the focus is outside the textarea, browsers will try really hard to reveal the textarea.
// Here, we try to undo the browser's desperate reveal.
try {
const scrollState = dom.saveParentsScrollTop(textArea);
this.setIgnoreSelectionChangeTime('setSelectionRange');
textArea.focus();
textArea.setSelectionRange(selectionStart, selectionEnd);
dom.restoreParentsScrollTop(textArea, scrollState);
} catch (e) {
Why VS Code itself doesn't show it
VS Code's own surfaces cannot reproduce this: workbench scrolling is synthetic, and ListView actively neutralizes browser-initiated scrolls (src/vs/base/browser/ui/list/listView.ts, scroll listener resetting element.scrollTop = 0). The bug only manifests for embedders of the Monaco editor whose ancestors scroll natively.
Downstream data point: Positron (Monaco embedder) hit this as posit-dev/positron#14085 (click places the cursor on the wrong line in an editor taller than its scroll container) and posit-dev/positron#14656 (click yanks the container scroll to the top), and carries this exact fix with a Playwright regression test in posit-dev/positron#14696.
Repro (Chromium, monaco-editor 0.55.1)
Editor inside a natively-scrolling container, editor taller than the container:
repro.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<style>
#scroller { height: 300px; width: 700px; overflow: auto; border: 2px solid #c00; }
#editor { height: 900px; width: 100%; }
</style>
</head>
<body>
<div id="scroller"><div id="editor"></div></div>
<script src="https://cdn.jsdelivr.net/npm/monaco-editor@0.55.1/min/vs/loader.js"></script>
<script>
const useEditContext = new URLSearchParams(location.search).get('editContext') !== 'false';
require.config({ paths: { vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.55.1/min/vs' } });
require(['vs/editor/editor.main'], function () {
monaco.editor.create(document.getElementById('editor'), {
value: Array.from({ length: 60 }, (_, i) => '// line ' + (i + 1)).join('\n'),
language: 'javascript',
editContext: useEditContext,
scrollBeyondLastLine: false,
minimap: { enabled: false }
});
});
</script>
</body>
</html>
- Open the page in Chromium (EditContext is Chromium-only). Do not focus the editor.
- Scroll the red container down (e.g.
scroller.scrollTop = 400).
- Click a visible line.
Actual: the container scroll jumps back to the top (scrollTop 400 -> 0) to reveal the hidden edit context node parked at the previous cursor position; with the layout shift the cursor can land on a different line than the one clicked.
Expected: cursor lands on the clicked line and the container does not scroll -- which is exactly what happens when you open the same page with ?editContext=false (textarea path): scrollTop stays at 400.
Measured with the harness above (same synthetic click at the container center, line 31 under the pointer):
| input path |
scrollTop before click |
scrollTop after click |
| EditContext (default) |
400 |
0 |
textarea (editContext: false) |
400 |
400 |
Proposed fix
Mirror the textarea guard in FocusTracker.focus() using the existing saveParentsScrollTop / restoreParentsScrollTop helpers from base/browser/dom.ts:
public focus(): void {
// If the focus is outside the dom node, browsers will try really hard to reveal it.
// Here, we try to undo the browser's desperate reveal.
const scrollState = saveParentsScrollTop(this._domNode);
this._domNode.focus();
restoreParentsScrollTop(this._domNode, scrollState);
this.refreshFocusState();
}
Verified: rebuilding monaco-editor-core from main (e8abd58) with this guard and pointing the same harness at it, the same click keeps scrollTop at 400, lands the cursor on the clicked line, and the editor is focused. PR to follow.
VS Code version: main @ e8abd58
Type: Bug
When the editor uses the native EditContext input path (
editor.editContext: true, the default on Chromium), focusing the editor scrolls every natively-scrolling ancestor of the hidden edit context node. The textarea input path has guarded against exactly this for years; the native EditContext path is missing the same guard.Root cause
FocusTracker.focus()insrc/vs/editor/browser/controller/editContext/native/nativeEditContextUtils.tscallsthis._domNode.focus()bare:The hidden
.native-edit-contextnode is absolutely positioned at the editor's last cursor position. When the editor gains focus (e.g. on mouse down), the browser scrolls every scrollable ancestor to reveal that node. If any ancestor scrolls natively (overflow: auto), its scroll position jumps, shifting layout between Monaco's mouse-down hit test and the click resolution -- so the click can land the cursor on the wrong line, and the container visibly jumps.The textarea path already handles this in
writeNativeTextAreaContent(src/vs/editor/browser/controller/editContext/textArea/textAreaEditContextInput.ts):Why VS Code itself doesn't show it
VS Code's own surfaces cannot reproduce this: workbench scrolling is synthetic, and
ListViewactively neutralizes browser-initiated scrolls (src/vs/base/browser/ui/list/listView.ts, scroll listener resettingelement.scrollTop = 0). The bug only manifests for embedders of the Monaco editor whose ancestors scroll natively.Downstream data point: Positron (Monaco embedder) hit this as posit-dev/positron#14085 (click places the cursor on the wrong line in an editor taller than its scroll container) and posit-dev/positron#14656 (click yanks the container scroll to the top), and carries this exact fix with a Playwright regression test in posit-dev/positron#14696.
Repro (Chromium, monaco-editor 0.55.1)
Editor inside a natively-scrolling container, editor taller than the container:
repro.html
scroller.scrollTop = 400).Actual: the container scroll jumps back to the top (
scrollTop400 -> 0) to reveal the hidden edit context node parked at the previous cursor position; with the layout shift the cursor can land on a different line than the one clicked.Expected: cursor lands on the clicked line and the container does not scroll -- which is exactly what happens when you open the same page with
?editContext=false(textarea path):scrollTopstays at 400.Measured with the harness above (same synthetic click at the container center, line 31 under the pointer):
editContext: false)Proposed fix
Mirror the textarea guard in
FocusTracker.focus()using the existingsaveParentsScrollTop/restoreParentsScrollTophelpers frombase/browser/dom.ts:Verified: rebuilding
monaco-editor-corefrom main (e8abd58) with this guard and pointing the same harness at it, the same click keepsscrollTopat 400, lands the cursor on the clicked line, and the editor is focused. PR to follow.VS Code version: main @ e8abd58