diff --git a/src/renderer/src/components/terminal-pane/keyboard-handlers.test.ts b/src/renderer/src/components/terminal-pane/keyboard-handlers.test.ts index 0fd53d42c73..8281975bcf8 100644 --- a/src/renderer/src/components/terminal-pane/keyboard-handlers.test.ts +++ b/src/renderer/src/components/terminal-pane/keyboard-handlers.test.ts @@ -1,7 +1,11 @@ // src/renderer/src/components/terminal-pane/keyboard-handlers.test.ts -import { describe, it, expect } from 'vitest' +import { describe, it, expect, vi } from 'vitest' import { FIND_QUERY_MAX_BYTES } from '@/lib/find-query-bounds' -import { matchFileSearchShortcut, matchSearchNavigate } from './keyboard-handlers' +import { + matchFileSearchShortcut, + matchSearchNavigate, + runTerminalSearchNavigation +} from './keyboard-handlers' function makeKeyEvent( overrides: Partial<{ @@ -87,6 +91,46 @@ describe('matchSearchNavigate', () => { }) }) +describe('runTerminalSearchNavigation', () => { + const searchState = { query: 'hello', caseSensitive: true, regex: false } + + it('runs the next search through the guarded xterm path', () => { + const findNext = vi.fn(() => true) + const findPrevious = vi.fn(() => false) + const pane = { searchAddon: { findNext, findPrevious } } as unknown as Parameters< + typeof runTerminalSearchNavigation + >[0] + + expect(runTerminalSearchNavigation(pane, 'next', searchState)).toBe(true) + expect(findNext).toHaveBeenCalledWith('hello', { caseSensitive: true, regex: false }) + expect(findPrevious).not.toHaveBeenCalled() + }) + + it('runs the previous search through the guarded xterm path', () => { + const findNext = vi.fn(() => false) + const findPrevious = vi.fn(() => true) + const pane = { searchAddon: { findNext, findPrevious } } as unknown as Parameters< + typeof runTerminalSearchNavigation + >[0] + + expect(runTerminalSearchNavigation(pane, 'previous', searchState)).toBe(true) + expect(findPrevious).toHaveBeenCalledWith('hello', { caseSensitive: true, regex: false }) + expect(findNext).not.toHaveBeenCalled() + }) + + it('contains the xterm decoration positive-integer crash from shortcut navigation', () => { + const findNext = vi.fn(() => { + throw new Error('This API only accepts positive integers') + }) + const pane = { searchAddon: { findNext } } as unknown as Parameters< + typeof runTerminalSearchNavigation + >[0] + + expect(() => runTerminalSearchNavigation(pane, 'next', searchState)).not.toThrow() + expect(runTerminalSearchNavigation(pane, 'next', searchState)).toBe(false) + }) +}) + describe('matchFileSearchShortcut', () => { it('matches Cmd+Shift+F on macOS', () => { expect( diff --git a/src/renderer/src/components/terminal-pane/keyboard-handlers.ts b/src/renderer/src/components/terminal-pane/keyboard-handlers.ts index 02b97be5c8f..35cd9f82fcd 100644 --- a/src/renderer/src/components/terminal-pane/keyboard-handlers.ts +++ b/src/renderer/src/components/terminal-pane/keyboard-handlers.ts @@ -4,6 +4,7 @@ import { useEffect } from 'react' import type { ManagedPane, PaneManager } from '@/lib/pane-manager/pane-manager' import type { PtyTransport } from './pty-transport' +import { safeFind } from '../terminal-search-safe-find' import { resolveTerminalShortcutAction } from './terminal-shortcut-policy' import type { MacOptionAsAlt } from './terminal-shortcut-policy' import { @@ -65,6 +66,8 @@ export type SearchState = { regex: boolean } +export type SearchNavigationDirection = 'next' | 'previous' + /** * Pure decision function for Cmd+G / Cmd+Shift+G search navigation. * Returns 'next', 'previous', or null (no match). @@ -75,7 +78,7 @@ export function matchSearchNavigate( isMac: boolean, searchOpen: boolean, searchState: SearchState -): 'next' | 'previous' | null { +): SearchNavigationDirection | null { if (e.altKey) { return null } @@ -98,6 +101,25 @@ export function matchSearchNavigate( return e.shiftKey ? 'previous' : 'next' } +export function runTerminalSearchNavigation( + pane: Pick, + direction: SearchNavigationDirection, + searchState: SearchState +): boolean { + const { query, caseSensitive, regex } = searchState + const options = { caseSensitive, regex } + + // Why: Cmd/Ctrl+G hits the same xterm decoration path as the search panel, + // so narrow-viewport highlight failures need the same containment. + return direction === 'next' + ? safeFind((term, findOptions) => pane.searchAddon.findNext(term, findOptions), query, options) + : safeFind( + (term, findOptions) => pane.searchAddon.findPrevious(term, findOptions), + query, + options + ) +} + export function matchFileSearchShortcut( e: Pick, platform: KeybindingPlatform, @@ -233,12 +255,7 @@ export function useTerminalKeyboardShortcuts({ if (!pane) { return } - const { query, caseSensitive, regex } = searchStateRef.current - if (direction === 'next') { - pane.searchAddon.findNext(query, { caseSensitive, regex }) - } else { - pane.searchAddon.findPrevious(query, { caseSensitive, regex }) - } + runTerminalSearchNavigation(pane, direction, searchStateRef.current) pane.terminal.focus() return }