mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
fix(terminal): run the IME chord guard before the file-search matcher
matchFileSearchShortcut runs ahead of the shortcut resolver and takes the physical-code fallback itself. 'Process' is not a Latin key, so shouldUseNonLatinShortcutPhysicalFallback allows the fallback and a composing Ctrl+Shift+F matched sidebar.search.toggle on its code before the guard was reached. Hoisting the guard above it closes that, and the pending-composition lookup it needs is already available there. Verified: with the guard at its previous position the new test sees onSearchSelectedText called once; hoisted, it is not called. matchSearchNavigate is unaffected -- it compares e.key directly, so 'Process' never matches. Also reads the pending composition back after Escape in the xterm cancel test instead of asserting on a captured reference, so it states the intent whether the cancel path clears the field in place or detaches the object. Both found by CodeRabbit on stablyai/orca#12120.
This commit is contained in:
@@ -308,6 +308,36 @@ describe('Windows IME keyboard ownership', () => {
|
||||
harness.dispose()
|
||||
})
|
||||
|
||||
it('does not open file search for an IME-consumed Ctrl+Shift+F', () => {
|
||||
// Why: matchFileSearchShortcut runs before the shortcut resolver and takes the
|
||||
// physical-code fallback itself — `Process` is not a Latin key, so a composing
|
||||
// Ctrl+Shift+F matched `sidebar.search.toggle` on its `code`. The IME guard has
|
||||
// to sit above it, not just above the resolver.
|
||||
const harness = createHarness()
|
||||
// The shortcut only acts when there is a selection to search for, so give it one.
|
||||
const pane = harness.deps.managerRef.current?.getActivePane()
|
||||
vi.mocked(pane!.terminal.getSelection).mockReturnValue('needle')
|
||||
const hook = renderHook(() => useTerminalKeyboardShortcuts(harness.deps))
|
||||
harness.startComposition()
|
||||
const consumed = keyboardEvent('keydown', {
|
||||
key: 'Process',
|
||||
code: 'KeyF',
|
||||
keyCode: 229,
|
||||
timeStamp: 10,
|
||||
isComposing: true,
|
||||
ctrlKey: true,
|
||||
shiftKey: true
|
||||
})
|
||||
|
||||
harness.terminalInput.dispatchEvent(consumed)
|
||||
vi.runAllTimers()
|
||||
|
||||
expect(harness.deps.onSearchSelectedText).not.toHaveBeenCalled()
|
||||
expect(harness.sendInput).not.toHaveBeenCalled()
|
||||
hook.unmount()
|
||||
harness.dispose()
|
||||
})
|
||||
|
||||
it.each([
|
||||
{ label: 'Ctrl+KeyK', code: 'KeyK' },
|
||||
{ label: 'Ctrl+KeyW', code: 'KeyW' }
|
||||
|
||||
@@ -490,6 +490,26 @@ 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)
|
||||
) {
|
||||
// Why: Process has no logical key, so shortcut matching falls back to the physical code and
|
||||
// fires Ctrl+K/Ctrl+W here and in window-level handlers mid-composition. This must run before
|
||||
// matchFileSearchShortcut, which takes that same fallback and would claim a composing
|
||||
// Ctrl+Shift+F. xterm already ignores keyCode 229 while composing, so swallowing the chord
|
||||
// loses no input.
|
||||
e.stopImmediatePropagation()
|
||||
return
|
||||
}
|
||||
|
||||
if (matchFileSearchShortcut(e, shortcutPlatform, keybindings, terminalShortcutPolicy)) {
|
||||
const pane = manager.getActivePane() ?? manager.getPanes()[0]
|
||||
const selectedText = normalizeSelectedTextForFileSearch(pane?.terminal.getSelection())
|
||||
@@ -529,23 +549,6 @@ 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)
|
||||
) {
|
||||
// Why: Process has no logical key, so shortcut matching would fall back to the physical code and
|
||||
// fire Ctrl+K/Ctrl+W here and in window-level handlers mid-composition. xterm already ignores
|
||||
// keyCode 229 while composing, so swallowing the chord loses no input.
|
||||
e.stopImmediatePropagation()
|
||||
return
|
||||
}
|
||||
const shortcutEvent = imeProcessEnter
|
||||
? {
|
||||
key: 'Enter',
|
||||
|
||||
+5
-3
@@ -769,12 +769,14 @@ describe('xterm IME composition de-duplication', () => {
|
||||
await nextEventLoop()
|
||||
|
||||
textarea.dispatchEvent(new CompositionEvent('compositionend', { data: '한', bubbles: true }))
|
||||
const pending = getPendingComposition(terminal)
|
||||
expect(pending?.finalizerTimer).toBeDefined()
|
||||
// Read the state again after Escape rather than asserting on a captured
|
||||
// reference: the cancel path clears the field in place, but other paths
|
||||
// detach the pending object outright, and either way no timer must remain.
|
||||
expect(getPendingComposition(terminal)?.finalizerTimer).toBeDefined()
|
||||
dispatchKeydown(textarea, 'Escape', 'Escape', 229, true, 100)
|
||||
dispatchKeydown(textarea, 'Escape', 'Escape', 27, false, 100)
|
||||
|
||||
expect(pending?.finalizerTimer).toBeUndefined()
|
||||
expect(getPendingComposition(terminal)?.finalizerTimer).toBeUndefined()
|
||||
await nextEventLoop()
|
||||
|
||||
expect(emitted).toEqual([])
|
||||
|
||||
Reference in New Issue
Block a user