Files
claude-code/packages/@ant/ink/src/hooks/useSearchInput.ts
2026-05-01 21:55:51 +08:00

231 lines
5.8 KiB
TypeScript

/**
* Minimal stub of useSearchInput for the standalone @anthropic/ink package.
*
* Provides the same interface as the full implementation but without
* kill-ring / yank support. Suitable for FuzzyPicker and other theme
* components that need text input handling.
*/
import { useCallback, useState } from 'react'
import { KeyboardEvent } from '../core/events/keyboard-event.js'
import type { Key, InputEvent } from '../core/events/input-event.js'
import type { ParsedKey } from '../core/parse-keypress.js'
import useInput from './use-input.js'
import { useTerminalSize } from '../hooks/useTerminalSize.js'
type UseSearchInputOptions = {
isActive: boolean
onExit: () => void
onCancel?: () => void
onExitUp?: () => void
columns?: number
passthroughCtrlKeys?: string[]
initialQuery?: string
backspaceExitsOnEmpty?: boolean
}
type UseSearchInputReturn = {
query: string
setQuery: (q: string) => void
cursorOffset: number
handleKeyDown: (e: KeyboardEvent) => void
}
const UNHANDLED_SPECIAL_KEYS = new Set([
'pageup',
'pagedown',
'insert',
'wheelup',
'wheeldown',
'mouse',
'f1',
'f2',
'f3',
'f4',
'f5',
'f6',
'f7',
'f8',
'f9',
'f10',
'f11',
'f12',
])
export function useSearchInput({
isActive,
onExit,
onCancel,
onExitUp,
columns,
initialQuery = '',
backspaceExitsOnEmpty = true,
}: UseSearchInputOptions): UseSearchInputReturn {
const { columns: terminalColumns } = useTerminalSize()
const _effectiveColumns = columns ?? terminalColumns
const [query, setQueryState] = useState(initialQuery)
const [cursorOffset, setCursorOffset] = useState(initialQuery.length)
const setQuery = useCallback((q: string) => {
setQueryState(q)
setCursorOffset(q.length)
}, [])
const handleKeyDown = (e: KeyboardEvent): void => {
if (!isActive) return
if (e.key === 'return' || e.key === 'down') {
e.preventDefault()
onExit()
return
}
if (e.key === 'up') {
e.preventDefault()
onExitUp?.()
return
}
if (e.key === 'escape') {
e.preventDefault()
if (onCancel) {
onCancel()
} else if (query.length > 0) {
setQueryState('')
setCursorOffset(0)
} else {
onExit()
}
return
}
if (e.key === 'backspace') {
e.preventDefault()
if (query.length === 0) {
if (backspaceExitsOnEmpty) (onCancel ?? onExit)()
return
}
const newOffset = Math.max(0, cursorOffset - 1)
setQueryState(query.slice(0, newOffset) + query.slice(cursorOffset))
setCursorOffset(newOffset)
return
}
if (e.key === 'delete') {
e.preventDefault()
if (cursorOffset < query.length) {
setQueryState(
query.slice(0, cursorOffset) + query.slice(cursorOffset + 1),
)
}
return
}
if (e.key === 'left') {
e.preventDefault()
setCursorOffset(Math.max(0, cursorOffset - 1))
return
}
if (e.key === 'right') {
e.preventDefault()
setCursorOffset(Math.min(query.length, cursorOffset + 1))
return
}
if (e.key === 'home') {
e.preventDefault()
setCursorOffset(0)
return
}
if (e.key === 'end') {
e.preventDefault()
setCursorOffset(query.length)
return
}
if (e.ctrl) {
switch (e.key.toLowerCase()) {
case 'a':
e.preventDefault()
setCursorOffset(0)
return
case 'e':
e.preventDefault()
setCursorOffset(query.length)
return
case 'b':
e.preventDefault()
setCursorOffset(Math.max(0, cursorOffset - 1))
return
case 'f':
e.preventDefault()
setCursorOffset(Math.min(query.length, cursorOffset + 1))
return
case 'd': {
e.preventDefault()
if (query.length === 0) {
;(onCancel ?? onExit)()
return
}
if (cursorOffset < query.length) {
setQueryState(
query.slice(0, cursorOffset) + query.slice(cursorOffset + 1),
)
}
return
}
case 'h': {
e.preventDefault()
if (query.length === 0) {
if (backspaceExitsOnEmpty) (onCancel ?? onExit)()
return
}
const newOffset = Math.max(0, cursorOffset - 1)
setQueryState(query.slice(0, newOffset) + query.slice(cursorOffset))
setCursorOffset(newOffset)
return
}
case 'c':
e.preventDefault()
onCancel?.()
return
case 'u':
e.preventDefault()
setQueryState(query.slice(cursorOffset))
setCursorOffset(0)
return
case 'k':
e.preventDefault()
setQueryState(query.slice(0, cursorOffset))
return
case 'w': {
e.preventDefault()
// Delete word before cursor
const before = query.slice(0, cursorOffset)
const after = query.slice(cursorOffset)
const trimmed = before.replace(/\S+\s*$/, '')
setQueryState(trimmed + after)
setCursorOffset(trimmed.length)
return
}
}
return
}
if (e.key === 'tab') {
return
}
// Regular character input
if (e.key.length >= 1 && !UNHANDLED_SPECIAL_KEYS.has(e.key)) {
e.preventDefault()
setQueryState(
query.slice(0, cursorOffset) + e.key + query.slice(cursorOffset),
)
setCursorOffset(cursorOffset + 1)
}
}
// Bridge: subscribe via useInput and adapt to KeyboardEvent
useInput(
(_input: string, _key: Key, event: InputEvent) => {
handleKeyDown(new KeyboardEvent(event.keypress as ParsedKey))
},
{ isActive },
)
return { query, setQuery, cursorOffset, handleKeyDown }
}