Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
9.5 kB · 303 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304import { feature } from 'bun:bundle'import { useCallback, useEffect, useMemo, useRef, useState } from 'react'import { getModeFromInput, getValueFromInput,} from '../components/PromptInput/inputModes.js'import { makeHistoryReader } from '../history.js'import { KeyboardEvent } from '../ink/events/keyboard-event.js'// eslint-disable-next-line custom-rules/prefer-use-keybindings -- backward-compat bridge until consumers wire handleKeyDown to <Box onKeyDown>import { useInput } from '../ink.js'import { useKeybinding, useKeybindings } from '../keybindings/useKeybinding.js'import type { PromptInputMode } from '../types/textInputTypes.js'import type { HistoryEntry } from '../utils/config.js'
export function useHistorySearch( onAcceptHistory: (entry: HistoryEntry) => void, currentInput: string, onInputChange: (input: string) => void, onCursorChange: (cursorOffset: number) => void, currentCursorOffset: number, onModeChange: (mode: PromptInputMode) => void, currentMode: PromptInputMode, isSearching: boolean, setIsSearching: (isSearching: boolean) => void, setPastedContents: (pastedContents: HistoryEntry['pastedContents']) => void, currentPastedContents: HistoryEntry['pastedContents'],): { historyQuery: string setHistoryQuery: (query: string) => void historyMatch: HistoryEntry | undefined historyFailedMatch: boolean handleKeyDown: (e: KeyboardEvent) => void} { const [historyQuery, setHistoryQuery] = useState('') const [historyFailedMatch, setHistoryFailedMatch] = useState(false) const [originalInput, setOriginalInput] = useState('') const [originalCursorOffset, setOriginalCursorOffset] = useState(0) const [originalMode, setOriginalMode] = useState<PromptInputMode>('prompt') const [originalPastedContents, setOriginalPastedContents] = useState< HistoryEntry['pastedContents'] >({}) const [historyMatch, setHistoryMatch] = useState<HistoryEntry | undefined>( undefined, ) const historyReader = useRef<AsyncGenerator<HistoryEntry> | undefined>( undefined, ) const seenPrompts = useRef<Set<string>>(new Set()) const searchAbortController = useRef<AbortController | null>(null)
const closeHistoryReader = useCallback((): void => { if (historyReader.current) { // Must explicitly call .return() to trigger the finally block in readLinesReverse, // which closes the file handle. Without this, file descriptors leak. void historyReader.current.return(undefined) historyReader.current = undefined } }, [])
const reset = useCallback((): void => { setIsSearching(false) setHistoryQuery('') setHistoryFailedMatch(false) setOriginalInput('') setOriginalCursorOffset(0) setOriginalMode('prompt') setOriginalPastedContents({}) setHistoryMatch(undefined) closeHistoryReader() seenPrompts.current.clear() }, [setIsSearching, closeHistoryReader])
const searchHistory = useCallback( async (resume: boolean, signal?: AbortSignal): Promise<void> => { if (!isSearching) { return }
if (historyQuery.length === 0) { closeHistoryReader() seenPrompts.current.clear() setHistoryMatch(undefined) setHistoryFailedMatch(false) onInputChange(originalInput) onCursorChange(originalCursorOffset) onModeChange(originalMode) setPastedContents(originalPastedContents) return }
if (!resume) { closeHistoryReader() historyReader.current = makeHistoryReader() seenPrompts.current.clear() }
if (!historyReader.current) { return }
while (true) { if (signal?.aborted) { return }
const item = await historyReader.current.next() if (item.done) { // No match found - keep last match but mark as failed setHistoryFailedMatch(true) return }
const display = item.value.display
const matchPosition = display.lastIndexOf(historyQuery) if (matchPosition !== -1 && !seenPrompts.current.has(display)) { seenPrompts.current.add(display) setHistoryMatch(item.value) setHistoryFailedMatch(false) const mode = getModeFromInput(display) onModeChange(mode) onInputChange(display) setPastedContents(item.value.pastedContents)
// Position cursor relative to the clean value, not the display const value = getValueFromInput(display) const cleanMatchPosition = value.lastIndexOf(historyQuery) onCursorChange( cleanMatchPosition !== -1 ? cleanMatchPosition : matchPosition, ) return } } }, [ isSearching, historyQuery, closeHistoryReader, onInputChange, onCursorChange, onModeChange, setPastedContents, originalInput, originalCursorOffset, originalMode, originalPastedContents, ], )
// Handler: Start history search (when not searching) const handleStartSearch = useCallback(() => { setIsSearching(true) setOriginalInput(currentInput) setOriginalCursorOffset(currentCursorOffset) setOriginalMode(currentMode) setOriginalPastedContents(currentPastedContents) historyReader.current = makeHistoryReader() seenPrompts.current.clear() }, [ setIsSearching, currentInput, currentCursorOffset, currentMode, currentPastedContents, ])
// Handler: Find next match (when searching) const handleNextMatch = useCallback(() => { void searchHistory(true) }, [searchHistory])
// Handler: Accept current match and exit search const handleAccept = useCallback(() => { if (historyMatch) { const mode = getModeFromInput(historyMatch.display) const value = getValueFromInput(historyMatch.display) onInputChange(value) onModeChange(mode) setPastedContents(historyMatch.pastedContents) } else { // No match - restore original pasted contents setPastedContents(originalPastedContents) } reset() }, [ historyMatch, onInputChange, onModeChange, setPastedContents, originalPastedContents, reset, ])
// Handler: Cancel search and restore original input const handleCancel = useCallback(() => { onInputChange(originalInput) onCursorChange(originalCursorOffset) setPastedContents(originalPastedContents) reset() }, [ onInputChange, onCursorChange, setPastedContents, originalInput, originalCursorOffset, originalPastedContents, reset, ])
// Handler: Execute (accept and submit) const handleExecute = useCallback(() => { if (historyQuery.length === 0) { onAcceptHistory({ display: originalInput, pastedContents: originalPastedContents, }) } else if (historyMatch) { const mode = getModeFromInput(historyMatch.display) const value = getValueFromInput(historyMatch.display) onModeChange(mode) onAcceptHistory({ display: value, pastedContents: historyMatch.pastedContents, }) } reset() }, [ historyQuery, historyMatch, onAcceptHistory, onModeChange, originalInput, originalPastedContents, reset, ])
// Gated off under HISTORY_PICKER — the modal dialog owns ctrl+r there. useKeybinding('history:search', handleStartSearch, { context: 'Global', isActive: feature('HISTORY_PICKER') ? false : !isSearching, })
// History search context keybindings (only active when searching) const historySearchHandlers = useMemo( () => ({ 'historySearch:next': handleNextMatch, 'historySearch:accept': handleAccept, 'historySearch:cancel': handleCancel, 'historySearch:execute': handleExecute, }), [handleNextMatch, handleAccept, handleCancel, handleExecute], )
useKeybindings(historySearchHandlers, { context: 'HistorySearch', isActive: isSearching, })
// Handle backspace when query is empty (cancels search) // This is a conditional behavior that doesn't fit the keybinding model // well (backspace only cancels when query is empty) const handleKeyDown = (e: KeyboardEvent): void => { if (!isSearching) return if (e.key === 'backspace' && historyQuery === '') { e.preventDefault() handleCancel() } }
// Backward-compat bridge: PromptInput doesn't yet wire handleKeyDown to // <Box onKeyDown>. Subscribe via useInput and adapt InputEvent → // KeyboardEvent until the consumer is migrated (separate PR). // TODO(onKeyDown-migration): remove once PromptInput passes handleKeyDown. useInput( (_input, _key, event) => { handleKeyDown(new KeyboardEvent(event.keypress)) }, { isActive: isSearching }, )
// Keep a ref to searchHistory to avoid it being a dependency of useEffect const searchHistoryRef = useRef(searchHistory) searchHistoryRef.current = searchHistory
// Reset history search when query changes useEffect(() => { searchAbortController.current?.abort() const controller = new AbortController() searchAbortController.current = controller void searchHistoryRef.current(false, controller.signal) return () => { controller.abort() } }, [historyQuery])
return { historyQuery, setHistoryQuery, historyMatch, historyFailedMatch, handleKeyDown, }}