From ca9562ceee7931ad6dc832becbaa76037cb8df92 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Mon, 3 Aug 2026 01:20:36 -0700 Subject: [PATCH] fix(terminal): block IME-owned Process shortcuts Adapted from #11616 commit 9f449d75b and #12120 commit 290f42a36. Co-authored-by: holdn2 --- .../keyboard-handlers-ime.test.tsx | 55 +++++++++++++++++++ .../terminal-pane/keyboard-handlers.ts | 22 ++++++-- .../terminal-ime-deferred-newline.ts | 7 ++- 3 files changed, 77 insertions(+), 7 deletions(-) diff --git a/src/renderer/src/components/terminal-pane/keyboard-handlers-ime.test.tsx b/src/renderer/src/components/terminal-pane/keyboard-handlers-ime.test.tsx index c033bb23d..eeb2eea36 100644 --- a/src/renderer/src/components/terminal-pane/keyboard-handlers-ime.test.tsx +++ b/src/renderer/src/components/terminal-pane/keyboard-handlers-ime.test.tsx @@ -31,6 +31,7 @@ function keyboardEvent( function createHarness(): { deps: KeyboardHandlersDeps editable: HTMLInputElement + sendInput: ReturnType startComposition: () => void terminalInput: HTMLTextAreaElement dispose: () => void @@ -97,6 +98,7 @@ function createHarness(): { return { deps, editable, + sendInput, terminalInput, startComposition: () => { terminalElement.dispatchEvent( @@ -309,4 +311,57 @@ describe('Windows IME keyboard ownership', () => { hook.unmount() harness.dispose() }) + + it.each([ + { code: 'KeyQ', shiftKey: true }, + { code: 'KeyW', ctrlKey: true } + ])('keeps an IME-consumed $code out of terminal shortcuts', (modifier) => { + const harness = createHarness() + const hook = renderHook(() => useTerminalKeyboardShortcuts(harness.deps)) + const laterWindowHandler = vi.fn() + window.addEventListener('keydown', laterWindowHandler) + harness.startComposition() + const consumed = keyboardEvent('keydown', { + key: 'Process', + keyCode: 229, + timeStamp: 10, + isComposing: true, + ...modifier + }) + + harness.terminalInput.dispatchEvent(consumed) + + expect(consumed.defaultPrevented).toBe(false) + expect(harness.sendInput).not.toHaveBeenCalled() + expect(harness.deps.onRequestClosePane).not.toHaveBeenCalled() + expect(laterWindowHandler).not.toHaveBeenCalled() + window.removeEventListener('keydown', laterWindowHandler) + hook.unmount() + harness.dispose() + }) + + it('keeps an IME-consumed Ctrl+Shift+F out of file search', () => { + const harness = createHarness() + const pane = harness.deps.managerRef.current?.getActivePane() + vi.mocked(pane!.terminal.getSelection).mockReturnValue('needle') + const hook = renderHook(() => useTerminalKeyboardShortcuts(harness.deps)) + harness.startComposition() + + harness.terminalInput.dispatchEvent( + keyboardEvent('keydown', { + key: 'Process', + code: 'KeyF', + keyCode: 229, + timeStamp: 10, + isComposing: true, + ctrlKey: true, + shiftKey: true + }) + ) + + expect(harness.deps.onSearchSelectedText).not.toHaveBeenCalled() + expect(harness.sendInput).not.toHaveBeenCalled() + hook.unmount() + harness.dispose() + }) }) diff --git a/src/renderer/src/components/terminal-pane/keyboard-handlers.ts b/src/renderer/src/components/terminal-pane/keyboard-handlers.ts index 987a2482e..2d28d8f6d 100644 --- a/src/renderer/src/components/terminal-pane/keyboard-handlers.ts +++ b/src/renderer/src/components/terminal-pane/keyboard-handlers.ts @@ -13,6 +13,7 @@ import { createTerminalImeDeferredNewlineSender, createTerminalImeModifiedEnterChordOwner, getTerminalImeModifiedEnterKind, + isTerminalImeConsumedKey, isTerminalImeEnterKeyUp, isTerminalImeProcessEnter } from './terminal-ime-deferred-newline' @@ -477,6 +478,22 @@ export function useTerminalKeyboardShortcuts({ return } + const terminalPaneForImeShortcut = manager.getActivePane() ?? manager.getPanes()[0] + const hasPendingImeComposition = hasPendingTerminalImeComposition( + terminalPaneForImeShortcut?.terminal.element + ) + const imeProcessEnter = isWindows && hasPendingImeComposition && isTerminalImeProcessEnter(e) + if ( + isWindows && + hasPendingImeComposition && + !imeProcessEnter && + isTerminalImeConsumedKey(e) + ) { + // Process has no logical key, so shortcut matching would fall back to its physical code. + e.stopImmediatePropagation() + return + } + if (matchFileSearchShortcut(e, shortcutPlatform, keybindings, terminalShortcutPolicy)) { const pane = manager.getActivePane() ?? manager.getPanes()[0] const selectedText = normalizeSelectedTextForFileSearch(pane?.terminal.getSelection()) @@ -516,11 +533,6 @@ export function useTerminalKeyboardShortcuts({ return } - const terminalPaneForImeShortcut = manager.getActivePane() ?? manager.getPanes()[0] - const hasPendingImeComposition = hasPendingTerminalImeComposition( - terminalPaneForImeShortcut?.terminal.element - ) - const imeProcessEnter = isWindows && hasPendingImeComposition && isTerminalImeProcessEnter(e) const shortcutEvent = imeProcessEnter ? { key: 'Enter', diff --git a/src/renderer/src/components/terminal-pane/terminal-ime-deferred-newline.ts b/src/renderer/src/components/terminal-pane/terminal-ime-deferred-newline.ts index 12b8a19cd..5c79bd641 100644 --- a/src/renderer/src/components/terminal-pane/terminal-ime-deferred-newline.ts +++ b/src/renderer/src/components/terminal-pane/terminal-ime-deferred-newline.ts @@ -106,6 +106,10 @@ export function getTerminalImeModifiedEnterKind( return null } +export function isTerminalImeConsumedKey(event: Pick): boolean { + return event.key === 'Process' && event.keyCode === 229 +} + export function isTerminalImeProcessEnter( event: Pick< KeyboardEvent, @@ -114,8 +118,7 @@ export function isTerminalImeProcessEnter( ): boolean { const { code } = event return ( - event.key === 'Process' && - event.keyCode === 229 && + isTerminalImeConsumedKey(event) && (!code || code === 'Unidentified' || code === 'Enter' || code === 'NumpadEnter') && getTerminalImeModifiedEnterKind(event) !== null )