diff options
| author | Ayesha Shafique <[email protected]> | 2025-08-04 00:53:24 +0500 |
|---|---|---|
| committer | GitHub <[email protected]> | 2025-08-03 19:53:24 +0000 |
| commit | 072d8ba2899f2601dad6d4b0333fdcb80555a7dd (patch) | |
| tree | a8333f75184889929b844c115c5fb93555abdf62 /packages/cli/src/ui/components/InputPrompt.tsx | |
| parent | 03ed37d0dc2b5e2077b53073517abaab3d24d9c2 (diff) | |
feat: Add reverse search capability for shell commands (#4793)
Diffstat (limited to 'packages/cli/src/ui/components/InputPrompt.tsx')
| -rw-r--r-- | packages/cli/src/ui/components/InputPrompt.tsx | 126 |
1 files changed, 118 insertions, 8 deletions
diff --git a/packages/cli/src/ui/components/InputPrompt.tsx b/packages/cli/src/ui/components/InputPrompt.tsx index 5a7b6353..db4eec1b 100644 --- a/packages/cli/src/ui/components/InputPrompt.tsx +++ b/packages/cli/src/ui/components/InputPrompt.tsx @@ -9,12 +9,13 @@ import { Box, Text } from 'ink'; import { Colors } from '../colors.js'; import { SuggestionsDisplay } from './SuggestionsDisplay.js'; import { useInputHistory } from '../hooks/useInputHistory.js'; -import { TextBuffer } from './shared/text-buffer.js'; +import { TextBuffer, logicalPosToOffset } from './shared/text-buffer.js'; import { cpSlice, cpLen } from '../utils/textUtils.js'; import chalk from 'chalk'; import stringWidth from 'string-width'; import { useShellHistory } from '../hooks/useShellHistory.js'; -import { useCompletion } from '../hooks/useCompletion.js'; +import { useReverseSearchCompletion } from '../hooks/useReverseSearchCompletion.js'; +import { useSlashCompletion } from '../hooks/useSlashCompletion.js'; import { useKeypress, Key } from '../hooks/useKeypress.js'; import { CommandContext, SlashCommand } from '../commands/types.js'; import { Config } from '@google/gemini-cli-core'; @@ -69,18 +70,32 @@ export const InputPrompt: React.FC<InputPromptProps> = ({ setDirs(dirsChanged); } }, [dirs.length, dirsChanged]); + const [reverseSearchActive, setReverseSearchActive] = useState(false); + const [textBeforeReverseSearch, setTextBeforeReverseSearch] = useState(''); + const [cursorPosition, setCursorPosition] = useState<[number, number]>([ + 0, 0, + ]); + const shellHistory = useShellHistory(config.getProjectRoot()); + const historyData = shellHistory.history; - const completion = useCompletion( + const completion = useSlashCompletion( buffer, dirs, config.getTargetDir(), slashCommands, commandContext, + reverseSearchActive, config, ); + const reverseSearchCompletion = useReverseSearchCompletion( + buffer, + historyData, + reverseSearchActive, + ); const resetCompletionState = completion.resetCompletionState; - const shellHistory = useShellHistory(config.getProjectRoot()); + const resetReverseSearchCompletionState = + reverseSearchCompletion.resetCompletionState; const handleSubmitAndClear = useCallback( (submittedValue: string) => { @@ -92,8 +107,16 @@ export const InputPrompt: React.FC<InputPromptProps> = ({ buffer.setText(''); onSubmit(submittedValue); resetCompletionState(); + resetReverseSearchCompletionState(); }, - [onSubmit, buffer, resetCompletionState, shellModeActive, shellHistory], + [ + onSubmit, + buffer, + resetCompletionState, + shellModeActive, + shellHistory, + resetReverseSearchCompletionState, + ], ); const customSetTextAndResetCompletionSignal = useCallback( @@ -118,6 +141,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({ useEffect(() => { if (justNavigatedHistory) { resetCompletionState(); + resetReverseSearchCompletionState(); setJustNavigatedHistory(false); } }, [ @@ -125,6 +149,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({ buffer.text, resetCompletionState, setJustNavigatedHistory, + resetReverseSearchCompletionState, ]); // Handle clipboard image pasting with Ctrl+V @@ -197,6 +222,19 @@ export const InputPrompt: React.FC<InputPromptProps> = ({ } if (key.name === 'escape') { + if (reverseSearchActive) { + setReverseSearchActive(false); + reverseSearchCompletion.resetCompletionState(); + buffer.setText(textBeforeReverseSearch); + const offset = logicalPosToOffset( + buffer.lines, + cursorPosition[0], + cursorPosition[1], + ); + buffer.moveToOffset(offset); + return; + } + if (shellModeActive) { setShellModeActive(false); return; @@ -208,11 +246,61 @@ export const InputPrompt: React.FC<InputPromptProps> = ({ } } + if (shellModeActive && key.ctrl && key.name === 'r') { + setReverseSearchActive(true); + setTextBeforeReverseSearch(buffer.text); + setCursorPosition(buffer.cursor); + return; + } + if (key.ctrl && key.name === 'l') { onClearScreen(); return; } + if (reverseSearchActive) { + const { + activeSuggestionIndex, + navigateUp, + navigateDown, + showSuggestions, + suggestions, + } = reverseSearchCompletion; + + if (showSuggestions) { + if (key.name === 'up') { + navigateUp(); + return; + } + if (key.name === 'down') { + navigateDown(); + return; + } + if (key.name === 'tab') { + reverseSearchCompletion.handleAutocomplete(activeSuggestionIndex); + reverseSearchCompletion.resetCompletionState(); + setReverseSearchActive(false); + return; + } + } + + if (key.name === 'return' && !key.ctrl) { + const textToSubmit = + showSuggestions && activeSuggestionIndex > -1 + ? suggestions[activeSuggestionIndex].value + : buffer.text; + handleSubmitAndClear(textToSubmit); + reverseSearchCompletion.resetCompletionState(); + setReverseSearchActive(false); + return; + } + + // Prevent up/down from falling through to regular history navigation + if (key.name === 'up' || key.name === 'down') { + return; + } + } + // If the command is a perfect match, pressing enter should execute it. if (completion.isPerfectMatch && key.name === 'return') { handleSubmitAndClear(buffer.text); @@ -272,7 +360,6 @@ export const InputPrompt: React.FC<InputPromptProps> = ({ return; } } else { - // Shell History Navigation if (key.name === 'up') { const prevCommand = shellHistory.getPreviousCommand(); if (prevCommand !== null) buffer.setText(prevCommand); @@ -284,7 +371,6 @@ export const InputPrompt: React.FC<InputPromptProps> = ({ return; } } - if (key.name === 'return' && !key.ctrl && !key.meta && !key.paste) { if (buffer.text.trim()) { const [row, col] = buffer.cursor; @@ -362,9 +448,13 @@ export const InputPrompt: React.FC<InputPromptProps> = ({ inputHistory, handleSubmitAndClear, shellHistory, + reverseSearchCompletion, handleClipboardImage, resetCompletionState, vimHandleInput, + reverseSearchActive, + textBeforeReverseSearch, + cursorPosition, ], ); @@ -385,7 +475,15 @@ export const InputPrompt: React.FC<InputPromptProps> = ({ <Text color={shellModeActive ? Colors.AccentYellow : Colors.AccentPurple} > - {shellModeActive ? '! ' : '> '} + {shellModeActive ? ( + reverseSearchActive ? ( + <Text color={Colors.AccentCyan}>(r:) </Text> + ) : ( + '! ' + ) + ) : ( + '> ' + )} </Text> <Box flexGrow={1} flexDirection="column"> {buffer.text.length === 0 && placeholder ? ( @@ -449,6 +547,18 @@ export const InputPrompt: React.FC<InputPromptProps> = ({ /> </Box> )} + {reverseSearchActive && ( + <Box> + <SuggestionsDisplay + suggestions={reverseSearchCompletion.suggestions} + activeIndex={reverseSearchCompletion.activeSuggestionIndex} + isLoading={reverseSearchCompletion.isLoadingSuggestions} + width={suggestionsWidth} + scrollOffset={reverseSearchCompletion.visibleStartIndex} + userInput={buffer.text} + /> + </Box> + )} </> ); }; |
