Skip to content

editor: hide mouse cursor while typing - #332287

Draft
Aiday Marlen Kyzy (aiday-mar) wants to merge 5 commits into
microsoft:mainfrom
aiday-mar:fix/29351-hide-cursor-while-typing
Draft

editor: hide mouse cursor while typing#332287
Aiday Marlen Kyzy (aiday-mar) wants to merge 5 commits into
microsoft:mainfrom
aiday-mar:fix/29351-hide-cursor-while-typing

Conversation

@aiday-mar

@aiday-mar Aiday Marlen Kyzy (aiday-mar) commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Adds an opt-in editor.hideMouseCursorOnTyping setting (default false) that hides the mouse
pointer after keyboard text input in the editor and reveals it again on the next pointer or focus
activity.

Hiding is driven from the semantic text input path (_type), not from raw key events, so
navigation keys, modifiers, paste, and programmatic edits leave the pointer alone.

Revealing happens on pointer move (guarded so that a re-render under a resting pointer does not
count as movement), pointer leave of the editor container, pointer down, wheel, context menu, editor
blur, and owner-window blur. The state is also cleared when the setting is turned off, when the model
is replaced, and when the editor is disposed.

IME composition deliberately keeps the pointer visible. Composition candidate windows are
positioned relative to the pointer on several platforms, and hiding it mid-composition makes the
candidate window hard to track, so _startComposition reveals the pointer and _compositionType
keeps it revealed for the duration of the composition.

Scope is the editor only. The integrated terminal and the rest of the workbench are unaffected — see
the follow-ups below.

The Monaco editor API declarations (monaco.d.ts, standaloneEnums.ts) are regenerated, and the
diff editor gets a targeted CSS override because its pre-existing !important resize cursors would
otherwise beat the cursor: none rule.

Fixes #29351

Follow-ups

Deliberately out of scope for this PR:

  • Integrated terminal. The terminal draws its own cursor and does not go through
    CodeEditorWidget, so it needs a separate hook; the setting description says so explicitly.
  • Shared overflow widget containers. Suggest/hover widgets rendered into a workbench-level
    overflow container live outside the editor's DOM subtree, so the cursor: none class does not
    reach them. Handling that means either hiding at a shared ancestor or propagating the state, both
    of which affect non-editor UI.
  • Chromium kHideCursorWhileTyping. Chromium can do this natively at the compositor level.
    Once Electron ships a version exposing it, this implementation could delegate instead of managing
    the class in JS.

Testing

  • node build/next/index.ts transpile
  • npx tsc --noEmit -p src/tsconfig.json
  • npm run eslint -- src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts src/vs/editor/common/config/editorOptions.ts src/vs/editor/test/browser/widget/codeEditorWidget.test.ts
  • npm run stylelint -- src/vs/editor/browser/widget/codeEditor/editor.css src/vs/editor/browser/widget/diffEditor/style.css
  • npm run test-browser -- --browser chromium --run src/vs/editor/test/browser/widget/codeEditorWidget.test.ts

The browser tests cover: default-off, hiding on text input, key events and paste not hiding, each
reveal source, the no-movement and descendant-pointerleave guards, IME (including cancelled and
interrupted compositions), config change, model replacement, disposal, and the computed
cursor: none on the editor and its descendants.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings August 24, 2026 10:41
@aiday-mar
Aiday Marlen Kyzy (aiday-mar) marked this pull request as draft August 24, 2026 10:42

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an opt-in editor setting that hides the mouse pointer during keyboard input and restores it on pointer movement, disablement, or disposal.

Changes:

  • Registers and exposes editor.hideMouseCursorOnTyping.
  • Implements pointer visibility handling and styling.
  • Adds browser coverage for typing, movement, and disabling.
Show a summary per file
File Description
src/vs/monaco.d.ts Exposes the Monaco API option.
src/vs/editor/test/browser/widget/codeEditorWidget.test.ts Tests cursor visibility transitions.
src/vs/editor/common/standalone/standaloneEnums.ts Adds the standalone option identifier.
src/vs/editor/common/config/editorOptions.ts Defines and registers the setting.
src/vs/editor/browser/widget/codeEditor/editor.css Applies hidden-pointer styling.
src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts Manages cursor visibility from editor events.

Review details

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

  • Files reviewed: 5/6 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread src/vs/editor/browser/widget/codeEditor/editor.css
`pointerleave` was registered with `capture: true` alongside the other
reveal events, so it also fired for descendants. Browsers dispatch
boundary events when the element under a resting pointer is re-rendered,
which is exactly what typing does to the lines below the pointer. The
cursor was therefore revealed on the first keystroke whenever the pointer
happened to rest over the text being edited - the main scenario in microsoft#29351.

Give `pointerleave` its own non-capturing listener on the editor container
so only leaving the editor itself reveals the cursor. This mirrors the
movement guard already applied to `pointermove` for the same reason.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@aiday-mar

Copy link
Copy Markdown
Contributor Author

Manual test checklist

A checklist for verifying editor.hideMouseCursorOnTyping end-to-end. The automated tests in codeEditorWidget.test.ts already cover the state machine; this list focuses on what only a real browser/OS can show: actual pointer rendering, real IME, real focus changes, and every editor surface the CSS class can land on.

Terminology below: hidden = the OS mouse pointer is invisible over the editor; revealed = it is visible again.


1. Setting registration and defaults

  • editor.hideMouseCursorOnTyping appears in the Settings UI (search "hide mouse"), under Text Editor, as a checkbox, unchecked by default.
  • The description renders correctly and reads sensibly.
  • Setting it in settings.json gets IntelliSense/validation, and an invalid value (e.g. "yes") is flagged.
  • It can be scoped per language ("[markdown]": { "editor.hideMouseCursorOnTyping": true }) and the scoped value wins in that language's editors.
  • It can be set per workspace/folder and the effective value is used.

2. Regression check — default off

With the setting unset/false, confirm nothing changed:

  • Typing in a normal editor never hides the pointer.
  • The pointer over the editor still shows the I-beam over text, the arrow over the gutter/minimap/scrollbar, the hand over Ctrl/Cmd+hovered links, and the resize cursors over diff-editor unchanged-region controls and the sash.
  • editor.mouseStyle set to default and to copy still behaves as before.

3. Basic hide behavior (setting on)

  • Type a printable character with the mouse resting over the text area → pointer hides immediately.
  • Keep typing → pointer stays hidden for the whole burst.
  • Type while the pointer rests over the gutter, minimap, scrollbar, folding controls, sticky scroll, code lens, and the ruler → hidden in all of these.
  • Type Enter / Tab / Space / Backspace-then-a-character → hidden (they produce text).
  • Type with multiple cursors (Alt+click several spots, then type) → hidden.
  • Type into a snippet placeholder → hidden.
  • Type in an untitled/empty file, a large file (e.g. 10k+ lines), and a read-only editor (no text produced there → stays revealed).

4. Actions that must NOT hide the pointer

With the pointer resting over the editor, each of these should leave it visible:

  • Arrow keys, Home/End, PageUp/PageDown, Ctrl/Cmd+arrow navigation.
  • Pressing and releasing modifiers alone (Shift, Ctrl, Alt, Cmd), and Escape / function keys.
  • Keyboard shortcuts that don't insert text (Cmd/Ctrl+S, Cmd/Ctrl+P, Cmd/Ctrl+F, Cmd/Ctrl+B, Cmd/Ctrl+/ comment toggle).
  • Paste via keyboard (Cmd/Ctrl+V) and paste via the context menu.
  • Undo / redo (Cmd/Ctrl+Z, Cmd/Ctrl+Shift+Z).
  • Accepting a suggestion with Enter/Tab, accepting an inline (ghost-text) suggestion.
  • Format Document / Organize Imports / other programmatic edits.
  • Find & Replace: typing in the find input, and Replace All.
  • An extension or Copilot writing into the document while you don't type.
  • Cut/delete lines (Cmd/Ctrl+X, Cmd/Ctrl+Shift+K).
  • Typing into the Find widget input, the Quick Open box, or a tree filter — the editor's pointer stays visible.

5. Reveal triggers

Each: type to hide, then perform the action, and confirm the pointer comes back.

  • Move the mouse a tiny amount (mouse) → revealed.
  • Move via trackpad (single-finger drag) → revealed.
  • Left click / right click / middle click → revealed (and the click still works: caret moves, context menu opens, middle-click paste/scroll behaves as before).
  • Mouse wheel scroll → revealed.
  • Trackpad two-finger scroll → revealed.
  • Move the pointer out of the editor (to tabs, side bar, status bar, panel) → visible outside, and visible again on re-entry without needing an extra jiggle.
  • Click into another editor group / another tab → revealed.
  • Click into the terminal, the Explorer, or the Chat input → revealed.
  • Switch apps (Cmd+Tab / Alt+Tab) and come back → revealed.
  • Switch to another VS Code window → revealed in the window you left.
  • Open the command palette / a quick pick over the editor → revealed.
  • Open a modal dialog or a notification toast → revealed.
  • Touchpad tap-to-click, pen/stylus tap, and touchscreen tap (if you have the hardware) → revealed.
  • Toggle the setting off while the pointer is hidden → revealed immediately, and typing no longer hides.
  • Toggle it back on → typing hides again.

6. The "phantom pointer event" case (core of this PR)

This is the behavior the pointermove/pointerleave filtering exists for. Test on each OS, and at multiple zoom levels:

  • Rest the pointer over the text you are typing (i.e. on the same line, right where new glyphs appear) and type a long sentence without moving the mouse at all → pointer stays hidden the whole time; it must not flicker back on individual keystrokes.
  • Rest the pointer over a line below the caret and type enough to reflow/scroll those lines under it → stays hidden.
  • Rest the pointer exactly on a token boundary / on the caret itself and type → stays hidden.
  • Rest the pointer over the minimap or over a decoration (squiggly, breakpoint, git gutter) that changes as you type → stays hidden.
  • Type until the editor auto-scrolls (hold Enter to push the caret past the bottom) with the pointer resting still → stays hidden.
  • Trigger an inline suggestion / hover-decoration change under the resting pointer → stays hidden.
  • After any of the above, move the mouse by one pixel → revealed. It must not require a big movement.

7. IME and composition

Test with at least one of: Japanese, Chinese Pinyin, Korean.

  • Start a composition → pointer is revealed and stays visible for the whole composition, so the candidate window is usable with the mouse.
  • Pick a candidate with the mouse → works normally.
  • Commit the composition, then type plain ASCII → hides again.
  • Cancel a composition with Escape, then type → hides again.
  • Composition followed immediately by switching editors/files mid-composition → no stuck-hidden pointer, and typing afterwards still hides.
  • macOS dead keys / press-and-hold accent popup (hold e → accent picker) → pointer stays usable, popup selectable by mouse.
  • macOS emoji picker (Ctrl+Cmd+Space) inserting an emoji → pointer usable; behavior sane afterwards.
  • Dictation / voice typing (if available) → does not leave the pointer stuck hidden.

8. Focus and window lifecycle

  • Type to hide, then close the tab / close the editor group → pointer visible; no leftover monaco-editor-hide-mouse-cursor class in the DOM (check in the DevTools element inspector).
  • Type to hide, then switch to a different file in the same group → revealed.
  • Type to hide in a split, then type in the other split → the first editor is revealed and only the second hides.
  • Type to hide, then "Move Editor into New Window" → pointer is correct in both windows; typing in the auxiliary window hides only there.
  • In the auxiliary window: blur it by clicking the main window → revealed in the auxiliary window.
  • Reload window (Developer: Reload Window) while hidden → pointer visible after reload.
  • Enter/exit full screen and Zen Mode while hidden → pointer state is sane.
  • Type to hide, then trigger a layout change (toggle the side bar/panel, resize the window) → pointer state is sane.

9. CSS scope — where the pointer is and isn't hidden

  • While hidden, verify the hidden region is exactly the editor's own body; the pointer stays visible over tabs, breadcrumbs, the side bar, panel, and status bar.
  • While hidden, the pointer is also hidden over widgets rendered inside the editor: the Find widget, sticky scroll, the scrollbars, the minimap, inline chat / inline diff widgets, and peek views.
  • While hidden, widgets rendered outside the editor DOM (suggest widget, hover, parameter hints, context menu — workbench editors use fixedOverflowWidgets) still show a normal pointer. Confirm this looks intentional rather than broken, especially when the suggest widget pops open under a resting pointer.
  • While hidden, Ctrl/Cmd+hover over a link does not flash the hand cursor back.
  • After reveal, every one of the above cursors is restored to what it was (I-beam over text, arrow over the gutter, hand over links, resize over the sash).

10. Diff editor (the style.css change)

  • Side-by-side diff: type in the editable side → pointer hides on that side; check the other side's behavior is sane.
  • Inline diff: same check.
  • While hidden, hover the "unchanged region" expand/collapse controls (the canMoveTop / canMoveBottom chevrons) → pointer stays hidden (this is the !important override the PR adds).
  • While hidden, hover the region during an unchanged-region drag (draggingUnchangedRegion) → stays hidden.
  • After revealing, the n-resize/s-resize/ns-resize cursors on those controls and on the sash come back exactly as before.
  • Multi-file diff editor and the SCM "Working Tree" diff → same behavior.

11. Other editor surfaces

Each is a CodeEditorWidget, so the option applies. Check that typing behaves sensibly (or at least does nothing broken) in:

  • Notebook cells (code and markdown), including editing several cells in a row.
  • Notebook diff editor.
  • The Chat input box and Chat code blocks.
  • Inline chat.
  • The SCM commit message input.
  • Comment/PR comment editors.
  • Peek views (Go to References, call hierarchy, type hierarchy) — the nested editor.
  • The Settings editor JSON view and the Keybindings editor "when" input.
  • The Debug Console / REPL input and Debug watch/hover expression inputs.
  • Search & Replace editor (Search Editor).
  • Interactive Playground / walkthrough embedded snippets.
  • The output/log viewer and the test results output (read-only — nothing should hide).
  • Confirm the integrated terminal is unaffected (the setting explicitly does not cover it).

12. Platform and target coverage

  • macOS.
  • Windows.
  • Linux (X11 and/or Wayland — cursor: none is the most likely to differ here).
  • Web (vscode.dev / ./scripts/code-server.sh) in Chrome, Firefox, and Safari — cursor: none support and the pointermove movementX/Y values differ across engines, so re-run §5 and §6 on each.
  • High-DPI / scaled displays, and after changing window.zoomLevel.
  • External mouse vs. built-in trackpad.
  • Multi-monitor: type to hide, then move the pointer to a second monitor → revealed.

13. Accessibility

  • With a screen reader on (or editor.accessibilitySupport: on, which switches the input element), typing still hides and revealing still works.
  • Screencast Mode on → typing hides the pointer; confirm this doesn't defeat the purpose of screencast mode in a surprising way.
  • With OS "shake to locate pointer" / pointer-size accessibility features on, the pointer still comes back on move.
  • Confirm the hidden pointer never blocks a mouse-driven workflow: you can always get it back with a single small movement.

14. Performance and leaks

  • Type a long burst (hold a key, or paste-then-retype a large block) with the setting on → no perceptible typing latency vs. the setting off; compare in Developer: Startup Performance / a profile if unsure.
  • Open and close ~50 editors while typing in each → check the heap snapshot / Developer: Toggle Developer Tools for retained listeners or growing listener counts.
  • Developer: Open Process Explorer / a CPU profile while typing with the pointer hidden → no busy pointermove handling.
  • Check the DOM after each reveal: the monaco-editor-hide-mouse-cursor class is removed and not left on any element.

15. Automated checks before merge

  • npm run compile clean.
  • ./scripts/test.sh --grep CodeEditorWidget (the new hideMouseCursorOnTyping suite) passes.
  • Full editor unit tests pass, including in the browser runner (npm run test-browser) since these tests use real computed styles.
  • npm run monaco-compile-check / the Monaco API surface check passes — monaco.d.ts and standaloneEnums.ts were regenerated for the new EditorOption.hideMouseCursorOnTyping = 174.
  • npm run hygiene / eslint clean.
  • Confirm the new enum value is appended (not inserted), so existing EditorOption numbering is unchanged.

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.

[Feature request] Hide cursor while typing

2 participants