mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
fix(terminal): keep a blank Windows IME code out of the shifted-jamo rejection
#11293 records the Windows Process sequence arriving with a blank `code` (see "owns one Windows Process sequence across changing timestamps and blank codes"), so requiring code Enter/NumpadEnter would have dropped a real committing Enter on that path. An undeterminable code now keeps its previous verdict; only a known non-Enter physical code, which is what a shifted jamo carries, is refused. Also pins the two guards this branch relies on and had left untested: the Windows chord slot stays free for a Ctrl+Enter behind a deferred Ctrl+Backspace, and the composition marker is never read as its own re-dispatch. Both were verified to fail without the guard they cover.
This commit is contained in:
@@ -298,16 +298,13 @@ describe('Windows IME keyboard ownership', () => {
|
||||
|
||||
// Windows gives a shifted jamo the same Process/229/shiftKey shape as the committing
|
||||
// Enter, so treating it as Enter injects a Shift+Enter newline into ordinary Hangul.
|
||||
it.each([
|
||||
{ code: 'KeyQ', jamo: 'ㅃ' },
|
||||
{ code: 'KeyE', jamo: 'ㄸ' }
|
||||
])('does not read a shifted $jamo as the committing Enter', ({ code, jamo }) => {
|
||||
it('does not read a shifted jamo as the committing Enter', () => {
|
||||
const harness = createHarness()
|
||||
const hook = renderHook(() => useTerminalKeyboardShortcuts(harness.deps))
|
||||
harness.startComposition()
|
||||
const shiftedJamo = keyboardEvent('keydown', {
|
||||
key: 'Process',
|
||||
code,
|
||||
code: 'KeyQ',
|
||||
keyCode: 229,
|
||||
timeStamp: 10,
|
||||
isComposing: true,
|
||||
@@ -315,14 +312,49 @@ describe('Windows IME keyboard ownership', () => {
|
||||
})
|
||||
|
||||
harness.terminalInput.dispatchEvent(shiftedJamo)
|
||||
harness.endComposition(jamo)
|
||||
harness.endComposition('ㅃ')
|
||||
vi.advanceTimersByTime(250)
|
||||
|
||||
expect(harness.ptyWrites).toEqual([jamo])
|
||||
expect(harness.ptyWrites).toEqual(['ㅃ'])
|
||||
expect(shiftedJamo.defaultPrevented).toBe(false)
|
||||
hook.unmount()
|
||||
harness.dispose()
|
||||
})
|
||||
|
||||
// The chord owner keys off the held modifier alone, so a deferred Ctrl+Backspace must not
|
||||
// claim its single slot — the real Ctrl+Enter behind it would then fail to claim and vanish.
|
||||
it('leaves the Enter chord slot free for a Ctrl+Enter behind a deferred Ctrl+Backspace', () => {
|
||||
const harness = createHarness()
|
||||
const hook = renderHook(() => useTerminalKeyboardShortcuts(harness.deps))
|
||||
harness.startComposition()
|
||||
|
||||
harness.terminalInput.dispatchEvent(
|
||||
keyboardEvent('keydown', {
|
||||
key: 'Backspace',
|
||||
code: 'Backspace',
|
||||
keyCode: 8,
|
||||
timeStamp: 10,
|
||||
isComposing: true,
|
||||
ctrlKey: true
|
||||
})
|
||||
)
|
||||
harness.terminalInput.dispatchEvent(
|
||||
keyboardEvent('keydown', {
|
||||
key: 'Process',
|
||||
code: 'Enter',
|
||||
keyCode: 229,
|
||||
timeStamp: 20,
|
||||
isComposing: true,
|
||||
ctrlKey: true
|
||||
})
|
||||
)
|
||||
harness.endComposition('녕')
|
||||
vi.advanceTimersByTime(250)
|
||||
|
||||
expect(harness.ptyWrites).toEqual(['녕', '\x17', '\x1b[13;5u'])
|
||||
hook.unmount()
|
||||
harness.dispose()
|
||||
})
|
||||
})
|
||||
|
||||
// The committing press repeats and the direct shortcut write races xterm's flush for
|
||||
|
||||
@@ -444,16 +444,13 @@ export function useTerminalKeyboardShortcuts({
|
||||
return
|
||||
}
|
||||
|
||||
const modifiedEnterChord = isWindows ? getModifiedEnterChord(e) : null
|
||||
// Why: the chord owner tracks Enter chords only, while the deferral covers every
|
||||
// key the IME re-dispatches — Ctrl/Cmd+Backspace, Cmd+Delete and the word-motion
|
||||
// arrows double the same way Enter does.
|
||||
const isRedispatchedEnter = e.key === 'Enter' && e.keyCode === 13
|
||||
// Why: the chord owner tracks Enter chords only, while the deferral covers every key
|
||||
// the IME re-dispatches.
|
||||
const modifiedEnterChord =
|
||||
isWindows && e.key === 'Enter' && e.keyCode === 13 ? getModifiedEnterChord(e) : null
|
||||
if (
|
||||
!e.isComposing &&
|
||||
((isRedispatchedEnter &&
|
||||
modifiedEnterChord &&
|
||||
modifiedEnterChordOwner.absorb(modifiedEnterChord)) ||
|
||||
((modifiedEnterChord && modifiedEnterChordOwner.absorb(modifiedEnterChord)) ||
|
||||
(isTerminalImeRedispatchableKey(e) && deferredNewlineSender.absorbRedispatchedEnter(e)))
|
||||
) {
|
||||
// Chromium can drop the modifier when re-dispatching the committing press.
|
||||
@@ -757,6 +754,9 @@ export function useTerminalKeyboardShortcuts({
|
||||
heldImeEnterModifiers.delete(kind)
|
||||
modifiedEnterChordOwner.release({ kind, code: e.code, timeStamp: e.timeStamp })
|
||||
}
|
||||
// Why: only Enter releases its absorb credit here. Releasing on the other IME-owned
|
||||
// keys risks clearing a credit before the re-dispatch that needs it, and a stale one
|
||||
// self-heals — clearCreditsForCode drops it on the next press of the same code.
|
||||
if (e.key !== 'Enter') {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
createTerminalImeModifiedEnterChordOwner,
|
||||
isTerminalImeEnterKeyUp,
|
||||
isTerminalImeProcessEnter,
|
||||
isTerminalImeRedispatchableKey,
|
||||
sendTerminalInputAfterComposition
|
||||
} from './terminal-ime-deferred-newline'
|
||||
import {
|
||||
@@ -334,12 +335,17 @@ describe('isTerminalImeProcessEnter', () => {
|
||||
...overrides
|
||||
}) as KeyboardEvent
|
||||
|
||||
it.each([{ shiftKey: true }, { shiftKey: false, ctrlKey: true }, { code: 'NumpadEnter' }])(
|
||||
'recognizes a Windows IME modifier Enter reported as Process',
|
||||
(modifiers) => {
|
||||
expect(isTerminalImeProcessEnter(event(modifiers))).toBe(true)
|
||||
}
|
||||
)
|
||||
it.each([
|
||||
{ shiftKey: true },
|
||||
{ shiftKey: false, ctrlKey: true },
|
||||
{ code: 'NumpadEnter' },
|
||||
// Windows sends the Process sequence without a usable code; refusing it would drop a
|
||||
// real newline, so an undeterminable code keeps the pre-existing verdict.
|
||||
{ code: '' },
|
||||
{ code: 'Unidentified' }
|
||||
])('recognizes a Windows IME modifier Enter reported as Process', (modifiers) => {
|
||||
expect(isTerminalImeProcessEnter(event(modifiers))).toBe(true)
|
||||
})
|
||||
|
||||
it.each([
|
||||
{ key: 'Enter' },
|
||||
@@ -349,14 +355,29 @@ describe('isTerminalImeProcessEnter', () => {
|
||||
{ altKey: true },
|
||||
// Windows hands every IME-consumed keydown the same Process/229 shape, so a shifted
|
||||
// jamo (ㅃ on KeyQ) differs from the committing Enter only by `code`.
|
||||
{ code: 'KeyQ' },
|
||||
{ code: 'Digit3' },
|
||||
{ code: '' }
|
||||
{ code: 'KeyQ' }
|
||||
])('rejects a non-IME or ambiguous Process key', (override) => {
|
||||
expect(isTerminalImeProcessEnter(event(override))).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isTerminalImeRedispatchableKey', () => {
|
||||
it('accepts an IME-owned key carrying its real keyCode', () => {
|
||||
expect(isTerminalImeRedispatchableKey({ key: 'Backspace', keyCode: 8 })).toBe(true)
|
||||
})
|
||||
|
||||
it.each([
|
||||
// The committing press itself: macOS marks it 229 and it must not be read as the
|
||||
// re-dispatch it is about to trigger.
|
||||
{ key: 'Enter', keyCode: 229 },
|
||||
{ key: 'Backspace', keyCode: 229 },
|
||||
// Not a key the IME consumes mid-composition.
|
||||
{ key: 'a', keyCode: 65 }
|
||||
])('rejects the composition marker and non-owned keys', (event) => {
|
||||
expect(isTerminalImeRedispatchableKey(event)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isTerminalImeEnterKeyUp', () => {
|
||||
it('recognizes the balancing Enter keyup when Chromium drops its modifier', () => {
|
||||
expect(isTerminalImeEnterKeyUp({ key: 'Enter', keyCode: 13 })).toBe(true)
|
||||
|
||||
@@ -114,11 +114,14 @@ export function isTerminalImeProcessEnter(
|
||||
>
|
||||
): boolean {
|
||||
// Why: Windows reports every IME-consumed keydown as Process/229, so a shifted jamo
|
||||
// (ㅃ, ㄲ) matches the modifier check too; only `code` tells it apart from Enter.
|
||||
// (ㅃ, ㄲ) matches the modifier check too; `code` is the only field telling them apart.
|
||||
// An absent code stays accepted — Windows sends this sequence with a blank one, per the
|
||||
// chord owner's "blank codes" case in this module's test.
|
||||
const { code } = event
|
||||
return (
|
||||
event.key === 'Process' &&
|
||||
event.keyCode === 229 &&
|
||||
(event.code === 'Enter' || event.code === 'NumpadEnter') &&
|
||||
(!code || code === 'Unidentified' || code === 'Enter' || code === 'NumpadEnter') &&
|
||||
getTerminalImeModifiedEnterKind(event) !== null
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user