fix(terminal): block IME-owned Process shortcuts

Adapted from #11616 commit 9f449d75b and #12120 commit 290f42a36.

Co-authored-by: holdn2 <club.makersfarm@gmail.com>
This commit is contained in:
Neil 2026-08-03 01:20:36 -07:00 committed by OrcaWin
parent 8af6876510
commit ca9562ceee
3 changed files with 77 additions and 7 deletions

View File

@ -31,6 +31,7 @@ function keyboardEvent(
function createHarness(): {
deps: KeyboardHandlersDeps
editable: HTMLInputElement
sendInput: ReturnType<typeof vi.fn>
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()
})
})

View File

@ -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',

View File

@ -106,6 +106,10 @@ export function getTerminalImeModifiedEnterKind(
return null
}
export function isTerminalImeConsumedKey(event: Pick<KeyboardEvent, 'key' | 'keyCode'>): 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
)