mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-26 05:50:56 -07:00
feat (cli): Add command search using Ctrl+r (#5539)
Co-authored-by: Jacob Richman <jacob314@gmail.com>
This commit is contained in:
@@ -4,11 +4,20 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { useEffect, useCallback } from 'react';
|
||||
import { useState, useEffect, useMemo, useCallback, useRef } from 'react';
|
||||
import { useCompletion } from './useCompletion.js';
|
||||
import type { TextBuffer } from '../components/shared/text-buffer.js';
|
||||
import type { Suggestion } from '../components/SuggestionsDisplay.js';
|
||||
|
||||
function useDebouncedValue<T>(value: T, delay = 200): T {
|
||||
const [debounced, setDebounced] = useState(value);
|
||||
useEffect(() => {
|
||||
const handle = setTimeout(() => setDebounced(value), delay);
|
||||
return () => clearTimeout(handle);
|
||||
}, [value, delay]);
|
||||
return debounced;
|
||||
}
|
||||
|
||||
export interface UseReverseSearchCompletionReturn {
|
||||
suggestions: Suggestion[];
|
||||
activeSuggestionIndex: number;
|
||||
@@ -23,7 +32,7 @@ export interface UseReverseSearchCompletionReturn {
|
||||
|
||||
export function useReverseSearchCompletion(
|
||||
buffer: TextBuffer,
|
||||
shellHistory: readonly string[],
|
||||
history: readonly string[],
|
||||
reverseSearchActive: boolean,
|
||||
): UseReverseSearchCompletionReturn {
|
||||
const {
|
||||
@@ -32,45 +41,95 @@ export function useReverseSearchCompletion(
|
||||
visibleStartIndex,
|
||||
showSuggestions,
|
||||
isLoadingSuggestions,
|
||||
|
||||
setSuggestions,
|
||||
setShowSuggestions,
|
||||
setActiveSuggestionIndex,
|
||||
resetCompletionState,
|
||||
navigateUp,
|
||||
navigateDown,
|
||||
setVisibleStartIndex,
|
||||
} = useCompletion();
|
||||
|
||||
const debouncedQuery = useDebouncedValue(buffer.text, 100);
|
||||
|
||||
// incremental search
|
||||
const prevQueryRef = useRef<string>('');
|
||||
const prevMatchesRef = useRef<Suggestion[]>([]);
|
||||
|
||||
// Clear incremental cache when activating reverse search
|
||||
useEffect(() => {
|
||||
if (reverseSearchActive) {
|
||||
prevQueryRef.current = '';
|
||||
prevMatchesRef.current = [];
|
||||
}
|
||||
}, [reverseSearchActive]);
|
||||
|
||||
// Also clear cache when history changes so new items are considered
|
||||
useEffect(() => {
|
||||
prevQueryRef.current = '';
|
||||
prevMatchesRef.current = [];
|
||||
}, [history]);
|
||||
|
||||
const searchHistory = useCallback(
|
||||
(query: string, items: readonly string[]) => {
|
||||
const out: Suggestion[] = [];
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const cmd = items[i];
|
||||
const idx = cmd.toLowerCase().indexOf(query);
|
||||
if (idx !== -1) {
|
||||
out.push({ label: cmd, value: cmd, matchedIndex: idx });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const matches = useMemo<Suggestion[]>(() => {
|
||||
if (!reverseSearchActive) return [];
|
||||
if (debouncedQuery.length === 0)
|
||||
return history.map((cmd) => ({
|
||||
label: cmd,
|
||||
value: cmd,
|
||||
matchedIndex: -1,
|
||||
}));
|
||||
|
||||
const query = debouncedQuery.toLowerCase();
|
||||
const canUseCache =
|
||||
prevQueryRef.current &&
|
||||
query.startsWith(prevQueryRef.current) &&
|
||||
prevMatchesRef.current.length > 0;
|
||||
|
||||
const source = canUseCache
|
||||
? prevMatchesRef.current.map((m) => m.value)
|
||||
: history;
|
||||
|
||||
return searchHistory(query, source);
|
||||
}, [debouncedQuery, history, reverseSearchActive, searchHistory]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!reverseSearchActive) {
|
||||
resetCompletionState();
|
||||
}
|
||||
}, [reverseSearchActive, resetCompletionState]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!reverseSearchActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
const q = buffer.text.toLowerCase();
|
||||
const matches = shellHistory.reduce<Suggestion[]>((acc, cmd) => {
|
||||
const idx = cmd.toLowerCase().indexOf(q);
|
||||
if (idx !== -1) {
|
||||
acc.push({ label: cmd, value: cmd, matchedIndex: idx });
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
setSuggestions(matches);
|
||||
setShowSuggestions(matches.length > 0);
|
||||
setActiveSuggestionIndex(matches.length > 0 ? 0 : -1);
|
||||
const hasAny = matches.length > 0;
|
||||
setShowSuggestions(hasAny);
|
||||
setActiveSuggestionIndex(hasAny ? 0 : -1);
|
||||
setVisibleStartIndex(0);
|
||||
|
||||
prevQueryRef.current = debouncedQuery.toLowerCase();
|
||||
prevMatchesRef.current = matches;
|
||||
}, [
|
||||
buffer.text,
|
||||
shellHistory,
|
||||
debouncedQuery,
|
||||
matches,
|
||||
reverseSearchActive,
|
||||
setActiveSuggestionIndex,
|
||||
setShowSuggestions,
|
||||
setSuggestions,
|
||||
setShowSuggestions,
|
||||
setActiveSuggestionIndex,
|
||||
setVisibleStartIndex,
|
||||
resetCompletionState,
|
||||
]);
|
||||
|
||||
const handleAutocomplete = useCallback(
|
||||
|
||||
Reference in New Issue
Block a user