ux(polish) autocomplete in the input prompt (#18181)

This commit is contained in:
Jacob Richman
2026-02-05 12:38:29 -08:00
committed by GitHub
parent 9ca7300c90
commit 8efae719ee
11 changed files with 927 additions and 210 deletions
@@ -114,6 +114,7 @@ describe('useCommandCompletion', () => {
initialText: string,
cursorOffset?: number,
shellModeActive = false,
active = true,
) => {
let hookResult: ReturnType<typeof useCommandCompletion> & {
textBuffer: ReturnType<typeof useTextBuffer>;
@@ -121,15 +122,16 @@ describe('useCommandCompletion', () => {
function TestComponent() {
const textBuffer = useTextBufferForTest(initialText, cursorOffset);
const completion = useCommandCompletion(
textBuffer,
testRootDir,
[],
mockCommandContext,
false,
const completion = useCommandCompletion({
buffer: textBuffer,
cwd: testRootDir,
slashCommands: [],
commandContext: mockCommandContext,
reverseSearchActive: false,
shellModeActive,
mockConfig,
);
config: mockConfig,
active,
});
hookResult = { ...completion, textBuffer };
return null;
}
@@ -197,7 +199,6 @@ describe('useCommandCompletion', () => {
act(() => {
result.current.setActiveSuggestionIndex(5);
result.current.setShowSuggestions(true);
});
act(() => {
@@ -509,22 +510,25 @@ describe('useCommandCompletion', () => {
function TestComponent() {
const textBuffer = useTextBufferForTest('// This is a line comment');
const completion = useCommandCompletion(
textBuffer,
testRootDir,
[],
mockCommandContext,
false,
false,
mockConfig,
);
const completion = useCommandCompletion({
buffer: textBuffer,
cwd: testRootDir,
slashCommands: [],
commandContext: mockCommandContext,
reverseSearchActive: false,
shellModeActive: false,
config: mockConfig,
active: true,
});
hookResult = { ...completion, textBuffer };
return null;
}
renderWithProviders(<TestComponent />);
// Should not trigger prompt completion for comments
expect(hookResult!.suggestions.length).toBe(0);
await waitFor(() => {
expect(hookResult!.suggestions.length).toBe(0);
});
});
it('should not trigger prompt completion for block comments', async () => {
@@ -541,22 +545,25 @@ describe('useCommandCompletion', () => {
const textBuffer = useTextBufferForTest(
'/* This is a block comment */',
);
const completion = useCommandCompletion(
textBuffer,
testRootDir,
[],
mockCommandContext,
false,
false,
mockConfig,
);
const completion = useCommandCompletion({
buffer: textBuffer,
cwd: testRootDir,
slashCommands: [],
commandContext: mockCommandContext,
reverseSearchActive: false,
shellModeActive: false,
config: mockConfig,
active: true,
});
hookResult = { ...completion, textBuffer };
return null;
}
renderWithProviders(<TestComponent />);
// Should not trigger prompt completion for comments
expect(hookResult!.suggestions.length).toBe(0);
await waitFor(() => {
expect(hookResult!.suggestions.length).toBe(0);
});
});
it('should trigger prompt completion for regular text when enabled', async () => {
@@ -573,24 +580,27 @@ describe('useCommandCompletion', () => {
const textBuffer = useTextBufferForTest(
'This is regular text that should trigger completion',
);
const completion = useCommandCompletion(
textBuffer,
testRootDir,
[],
mockCommandContext,
false,
false,
mockConfig,
);
const completion = useCommandCompletion({
buffer: textBuffer,
cwd: testRootDir,
slashCommands: [],
commandContext: mockCommandContext,
reverseSearchActive: false,
shellModeActive: false,
config: mockConfig,
active: true,
});
hookResult = { ...completion, textBuffer };
return null;
}
renderWithProviders(<TestComponent />);
// This test verifies that comments are filtered out while regular text is not
expect(hookResult!.textBuffer.text).toBe(
'This is regular text that should trigger completion',
);
await waitFor(() => {
expect(hookResult!.textBuffer.text).toBe(
'This is regular text that should trigger completion',
);
});
});
});
@@ -36,7 +36,6 @@ export interface UseCommandCompletionReturn {
isLoadingSuggestions: boolean;
isPerfectMatch: boolean;
setActiveSuggestionIndex: React.Dispatch<React.SetStateAction<number>>;
setShowSuggestions: React.Dispatch<React.SetStateAction<boolean>>;
resetCompletionState: () => void;
navigateUp: () => void;
navigateDown: () => void;
@@ -58,25 +57,35 @@ export interface UseCommandCompletionReturn {
completionMode: CompletionMode;
}
export function useCommandCompletion(
buffer: TextBuffer,
cwd: string,
slashCommands: readonly SlashCommand[],
commandContext: CommandContext,
reverseSearchActive: boolean = false,
shellModeActive: boolean,
config?: Config,
): UseCommandCompletionReturn {
export interface UseCommandCompletionOptions {
buffer: TextBuffer;
cwd: string;
slashCommands: readonly SlashCommand[];
commandContext: CommandContext;
reverseSearchActive?: boolean;
shellModeActive: boolean;
config?: Config;
active: boolean;
}
export function useCommandCompletion({
buffer,
cwd,
slashCommands,
commandContext,
reverseSearchActive = false,
shellModeActive,
config,
active,
}: UseCommandCompletionOptions): UseCommandCompletionReturn {
const {
suggestions,
activeSuggestionIndex,
visibleStartIndex,
showSuggestions,
isLoadingSuggestions,
isPerfectMatch,
setSuggestions,
setShowSuggestions,
setActiveSuggestionIndex,
setIsLoadingSuggestions,
setIsPerfectMatch,
@@ -173,7 +182,7 @@ export function useCommandCompletion(
}, [cursorRow, cursorCol, buffer.lines, buffer.text, config]);
useAtCompletion({
enabled: completionMode === CompletionMode.AT,
enabled: active && completionMode === CompletionMode.AT,
pattern: query || '',
config,
cwd,
@@ -182,7 +191,8 @@ export function useCommandCompletion(
});
const slashCompletionRange = useSlashCompletion({
enabled: completionMode === CompletionMode.SLASH && !shellModeActive,
enabled:
active && completionMode === CompletionMode.SLASH && !shellModeActive,
query,
slashCommands,
commandContext,
@@ -194,29 +204,46 @@ export function useCommandCompletion(
const promptCompletion = usePromptCompletion({
buffer,
config,
enabled: completionMode === CompletionMode.PROMPT,
enabled: active && completionMode === CompletionMode.PROMPT,
});
useEffect(() => {
setActiveSuggestionIndex(suggestions.length > 0 ? 0 : -1);
setVisibleStartIndex(0);
}, [suggestions, setActiveSuggestionIndex, setVisibleStartIndex]);
// Generic perfect match detection for non-slash modes or as a fallback
if (completionMode !== CompletionMode.SLASH) {
if (suggestions.length > 0) {
const firstSuggestion = suggestions[0];
setIsPerfectMatch(firstSuggestion.value === query);
} else {
setIsPerfectMatch(false);
}
}
}, [
suggestions,
setActiveSuggestionIndex,
setVisibleStartIndex,
completionMode,
query,
setIsPerfectMatch,
]);
useEffect(() => {
if (completionMode === CompletionMode.IDLE || reverseSearchActive) {
if (
!active ||
completionMode === CompletionMode.IDLE ||
reverseSearchActive
) {
resetCompletionState();
return;
}
// Show suggestions if we are loading OR if there are results to display.
setShowSuggestions(isLoadingSuggestions || suggestions.length > 0);
}, [
completionMode,
suggestions.length,
isLoadingSuggestions,
reverseSearchActive,
resetCompletionState,
setShowSuggestions,
]);
}, [active, completionMode, reverseSearchActive, resetCompletionState]);
const showSuggestions =
active &&
completionMode !== CompletionMode.IDLE &&
!reverseSearchActive &&
(isLoadingSuggestions || suggestions.length > 0);
/**
* Gets the completed text by replacing the completion range with the suggestion value.
@@ -333,7 +360,6 @@ export function useCommandCompletion(
isLoadingSuggestions,
isPerfectMatch,
setActiveSuggestionIndex,
setShowSuggestions,
resetCompletionState,
navigateUp,
navigateDown,
@@ -13,7 +13,6 @@ export interface UseCompletionReturn {
suggestions: Suggestion[];
activeSuggestionIndex: number;
visibleStartIndex: number;
showSuggestions: boolean;
isLoadingSuggestions: boolean;
isPerfectMatch: boolean;
setSuggestions: React.Dispatch<React.SetStateAction<Suggestion[]>>;
@@ -21,7 +20,6 @@ export interface UseCompletionReturn {
setVisibleStartIndex: React.Dispatch<React.SetStateAction<number>>;
setIsLoadingSuggestions: React.Dispatch<React.SetStateAction<boolean>>;
setIsPerfectMatch: React.Dispatch<React.SetStateAction<boolean>>;
setShowSuggestions: React.Dispatch<React.SetStateAction<boolean>>;
resetCompletionState: () => void;
navigateUp: () => void;
navigateDown: () => void;
@@ -32,7 +30,6 @@ export function useCompletion(): UseCompletionReturn {
const [activeSuggestionIndex, setActiveSuggestionIndex] =
useState<number>(-1);
const [visibleStartIndex, setVisibleStartIndex] = useState<number>(0);
const [showSuggestions, setShowSuggestions] = useState<boolean>(false);
const [isLoadingSuggestions, setIsLoadingSuggestions] =
useState<boolean>(false);
const [isPerfectMatch, setIsPerfectMatch] = useState<boolean>(false);
@@ -41,7 +38,6 @@ export function useCompletion(): UseCompletionReturn {
setSuggestions([]);
setActiveSuggestionIndex(-1);
setVisibleStartIndex(0);
setShowSuggestions(false);
setIsLoadingSuggestions(false);
setIsPerfectMatch(false);
}, []);
@@ -108,12 +104,10 @@ export function useCompletion(): UseCompletionReturn {
suggestions,
activeSuggestionIndex,
visibleStartIndex,
showSuggestions,
isLoadingSuggestions,
isPerfectMatch,
setSuggestions,
setShowSuggestions,
setActiveSuggestionIndex,
setVisibleStartIndex,
setIsLoadingSuggestions,
+234 -13
View File
@@ -25,6 +25,7 @@ describe('useInputHistory', () => {
onSubmit: mockOnSubmit,
isActive: true,
currentQuery: '',
currentCursorOffset: 0,
onChange: mockOnChange,
}),
);
@@ -45,6 +46,7 @@ describe('useInputHistory', () => {
onSubmit: mockOnSubmit,
isActive: true,
currentQuery: ' test query ',
currentCursorOffset: 0,
onChange: mockOnChange,
}),
);
@@ -68,6 +70,7 @@ describe('useInputHistory', () => {
onSubmit: mockOnSubmit,
isActive: true,
currentQuery: '',
currentCursorOffset: 0,
onChange: mockOnChange,
}),
);
@@ -88,6 +91,7 @@ describe('useInputHistory', () => {
onSubmit: mockOnSubmit,
isActive: false,
currentQuery: 'current',
currentCursorOffset: 0,
onChange: mockOnChange,
}),
);
@@ -105,6 +109,7 @@ describe('useInputHistory', () => {
onSubmit: mockOnSubmit,
isActive: true,
currentQuery: 'current',
currentCursorOffset: 0,
onChange: mockOnChange,
}),
);
@@ -123,6 +128,7 @@ describe('useInputHistory', () => {
onSubmit: mockOnSubmit,
isActive: true,
currentQuery,
currentCursorOffset: 0,
onChange: mockOnChange,
}),
);
@@ -131,17 +137,19 @@ describe('useInputHistory', () => {
result.current.navigateUp();
});
expect(mockOnChange).toHaveBeenCalledWith(userMessages[2]); // Last message
expect(mockOnChange).toHaveBeenCalledWith(userMessages[2], 'start'); // Last message
});
it('should store currentQuery as originalQueryBeforeNav on first navigateUp', () => {
it('should store currentQuery and currentCursorOffset as original state on first navigateUp', () => {
const currentQuery = 'original user input';
const currentCursorOffset = 5;
const { result } = renderHook(() =>
useInputHistory({
userMessages,
onSubmit: mockOnSubmit,
isActive: true,
currentQuery,
currentCursorOffset,
onChange: mockOnChange,
}),
);
@@ -149,13 +157,16 @@ describe('useInputHistory', () => {
act(() => {
result.current.navigateUp(); // historyIndex becomes 0
});
expect(mockOnChange).toHaveBeenCalledWith(userMessages[2]);
expect(mockOnChange).toHaveBeenCalledWith(userMessages[2], 'start');
// Navigate down to restore original query
// Navigate down to restore original query and cursor position
act(() => {
result.current.navigateDown(); // historyIndex becomes -1
});
expect(mockOnChange).toHaveBeenCalledWith(currentQuery);
expect(mockOnChange).toHaveBeenCalledWith(
currentQuery,
currentCursorOffset,
);
});
it('should navigate through history messages on subsequent navigateUp calls', () => {
@@ -165,6 +176,7 @@ describe('useInputHistory', () => {
onSubmit: mockOnSubmit,
isActive: true,
currentQuery: '',
currentCursorOffset: 0,
onChange: mockOnChange,
}),
);
@@ -172,17 +184,17 @@ describe('useInputHistory', () => {
act(() => {
result.current.navigateUp(); // Navigates to 'message 3'
});
expect(mockOnChange).toHaveBeenCalledWith(userMessages[2]);
expect(mockOnChange).toHaveBeenCalledWith(userMessages[2], 'start');
act(() => {
result.current.navigateUp(); // Navigates to 'message 2'
});
expect(mockOnChange).toHaveBeenCalledWith(userMessages[1]);
expect(mockOnChange).toHaveBeenCalledWith(userMessages[1], 'start');
act(() => {
result.current.navigateUp(); // Navigates to 'message 1'
});
expect(mockOnChange).toHaveBeenCalledWith(userMessages[0]);
expect(mockOnChange).toHaveBeenCalledWith(userMessages[0], 'start');
});
});
@@ -193,6 +205,7 @@ describe('useInputHistory', () => {
onSubmit: mockOnSubmit,
isActive: true, // Start active to allow setup navigation
currentQuery: 'current',
currentCursorOffset: 0,
onChange: mockOnChange,
};
const { result, rerender } = renderHook(
@@ -225,6 +238,7 @@ describe('useInputHistory', () => {
onSubmit: mockOnSubmit,
isActive: true,
currentQuery: 'current',
currentCursorOffset: 0,
onChange: mockOnChange,
}),
);
@@ -235,28 +249,235 @@ describe('useInputHistory', () => {
expect(mockOnChange).not.toHaveBeenCalled();
});
it('should restore originalQueryBeforeNav when navigating down to initial state', () => {
it('should restore cursor offset only when in middle of compose prompt', () => {
const originalQuery = 'my original input';
const originalCursorOffset = 5; // Middle
const { result } = renderHook(() =>
useInputHistory({
userMessages,
onSubmit: mockOnSubmit,
isActive: true,
currentQuery: originalQuery,
currentCursorOffset: originalCursorOffset,
onChange: mockOnChange,
}),
);
act(() => {
result.current.navigateUp(); // Navigates to 'message 3', stores 'originalQuery'
result.current.navigateUp();
});
expect(mockOnChange).toHaveBeenCalledWith(userMessages[2]);
mockOnChange.mockClear();
act(() => {
result.current.navigateDown(); // Navigates back to original query
result.current.navigateDown();
});
expect(mockOnChange).toHaveBeenCalledWith(originalQuery);
// Should restore middle offset
expect(mockOnChange).toHaveBeenCalledWith(
originalQuery,
originalCursorOffset,
);
});
it('should NOT restore cursor offset if it was at start or end of compose prompt', () => {
const originalQuery = 'my original input';
const { result, rerender } = renderHook(
(props) => useInputHistory(props),
{
initialProps: {
userMessages,
onSubmit: mockOnSubmit,
isActive: true,
currentQuery: originalQuery,
currentCursorOffset: 0, // Start
onChange: mockOnChange,
},
},
);
// Case 1: Start
act(() => {
result.current.navigateUp();
});
mockOnChange.mockClear();
act(() => {
result.current.navigateDown();
});
// Should use 'end' default instead of 0
expect(mockOnChange).toHaveBeenCalledWith(originalQuery, 'end');
// Case 2: End
rerender({
userMessages,
onSubmit: mockOnSubmit,
isActive: true,
currentQuery: originalQuery,
currentCursorOffset: originalQuery.length, // End
onChange: mockOnChange,
});
act(() => {
result.current.navigateUp();
});
mockOnChange.mockClear();
act(() => {
result.current.navigateDown();
});
// Should use 'end' default
expect(mockOnChange).toHaveBeenCalledWith(originalQuery, 'end');
});
it('should remember text edits but use default cursor when navigating between history items', () => {
const originalQuery = 'my original input';
const originalCursorOffset = 5;
const { result, rerender } = renderHook(
(props) => useInputHistory(props),
{
initialProps: {
userMessages,
onSubmit: mockOnSubmit,
isActive: true,
currentQuery: originalQuery,
currentCursorOffset: originalCursorOffset,
onChange: mockOnChange,
},
},
);
// 1. Navigate UP from compose prompt (-1 -> 0)
act(() => {
result.current.navigateUp();
});
expect(mockOnChange).toHaveBeenCalledWith(userMessages[2], 'start');
mockOnChange.mockClear();
// Simulate being at History[0] ('message 3') and editing it
const editedHistoryText = 'message 3 edited';
const editedHistoryOffset = 5;
rerender({
userMessages,
onSubmit: mockOnSubmit,
isActive: true,
currentQuery: editedHistoryText,
currentCursorOffset: editedHistoryOffset,
onChange: mockOnChange,
});
// 2. Navigate UP to next history item (0 -> 1)
act(() => {
result.current.navigateUp();
});
expect(mockOnChange).toHaveBeenCalledWith(userMessages[1], 'start');
mockOnChange.mockClear();
// 3. Navigate DOWN back to History[0] (1 -> 0)
act(() => {
result.current.navigateDown();
});
// Should restore edited text AND the offset because we just came from History[0]
expect(mockOnChange).toHaveBeenCalledWith(
editedHistoryText,
editedHistoryOffset,
);
mockOnChange.mockClear();
// Simulate being at History[0] (restored) and navigating DOWN to compose prompt (0 -> -1)
rerender({
userMessages,
onSubmit: mockOnSubmit,
isActive: true,
currentQuery: editedHistoryText,
currentCursorOffset: editedHistoryOffset,
onChange: mockOnChange,
});
// 4. Navigate DOWN to compose prompt
act(() => {
result.current.navigateDown();
});
// Level -1 should ALWAYS restore its offset if it was in the middle
expect(mockOnChange).toHaveBeenCalledWith(
originalQuery,
originalCursorOffset,
);
});
it('should restore offset for history items ONLY if returning from them immediately', () => {
const originalQuery = 'my original input';
const initialProps = {
userMessages,
onSubmit: mockOnSubmit,
isActive: true,
currentQuery: originalQuery,
currentCursorOffset: 5,
onChange: mockOnChange,
};
const { result, rerender } = renderHook(
(props) => useInputHistory(props),
{
initialProps,
},
);
// -1 -> 0 ('message 3')
act(() => {
result.current.navigateUp();
});
expect(mockOnChange).toHaveBeenCalledWith(userMessages[2], 'start');
const historyOffset = 4;
// Manually update props to reflect current level
rerender({
...initialProps,
currentQuery: userMessages[2],
currentCursorOffset: historyOffset,
});
// 0 -> 1 ('message 2')
act(() => {
result.current.navigateUp();
});
expect(mockOnChange).toHaveBeenCalledWith(userMessages[1], 'start');
rerender({
...initialProps,
currentQuery: userMessages[1],
currentCursorOffset: 0,
});
// 1 -> 2 ('message 1')
act(() => {
result.current.navigateUp();
});
expect(mockOnChange).toHaveBeenCalledWith(userMessages[0], 'start');
rerender({
...initialProps,
currentQuery: userMessages[0],
currentCursorOffset: 0,
});
mockOnChange.mockClear();
// 2 -> 1 ('message 2')
act(() => {
result.current.navigateDown();
});
// 2 -> 1 is immediate back-and-forth.
// But Level 1 offset was 0 (not in middle), so use 'end' default.
expect(mockOnChange).toHaveBeenCalledWith(userMessages[1], 'end');
mockOnChange.mockClear();
// Rerender to reflect Level 1 state
rerender({
...initialProps,
currentQuery: userMessages[1],
currentCursorOffset: userMessages[1].length,
});
// 1 -> 0 ('message 3')
act(() => {
result.current.navigateDown();
});
// 1 -> 0 is NOT immediate (Level 2 was the last jump point).
// So Level 0 SHOULD use default 'end' even though it has a middle offset saved.
expect(mockOnChange).toHaveBeenCalledWith(userMessages[2], 'end');
});
});
});
+70 -47
View File
@@ -4,14 +4,16 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { useState, useCallback } from 'react';
import { useState, useCallback, useRef } from 'react';
import { cpLen } from '../utils/textUtils.js';
interface UseInputHistoryProps {
userMessages: readonly string[];
onSubmit: (value: string) => void;
isActive: boolean;
currentQuery: string; // Renamed from query to avoid confusion
onChange: (value: string) => void;
currentCursorOffset: number;
onChange: (value: string, cursorPosition?: 'start' | 'end' | number) => void;
}
export interface UseInputHistoryReturn {
@@ -25,15 +27,25 @@ export function useInputHistory({
onSubmit,
isActive,
currentQuery,
currentCursorOffset,
onChange,
}: UseInputHistoryProps): UseInputHistoryReturn {
const [historyIndex, setHistoryIndex] = useState<number>(-1);
const [originalQueryBeforeNav, setOriginalQueryBeforeNav] =
useState<string>('');
// previousHistoryIndexRef tracks the index we occupied *immediately before* the current historyIndex.
// This allows us to detect when we are "returning" to a level we just left.
const previousHistoryIndexRef = useRef<number | undefined>(undefined);
// Cache stores text and cursor offset for each history index level.
// Level -1 is the current unsubmitted prompt.
const historyCacheRef = useRef<
Record<number, { text: string; offset: number }>
>({});
const resetHistoryNav = useCallback(() => {
setHistoryIndex(-1);
setOriginalQueryBeforeNav('');
previousHistoryIndexRef.current = undefined;
historyCacheRef.current = {};
}, []);
const handleSubmit = useCallback(
@@ -47,61 +59,72 @@ export function useInputHistory({
[onSubmit, resetHistoryNav],
);
const navigateTo = useCallback(
(nextIndex: number, defaultCursor: 'start' | 'end') => {
const prevIndexBeforeMove = historyIndex;
// 1. Save current state to cache before moving
historyCacheRef.current[prevIndexBeforeMove] = {
text: currentQuery,
offset: currentCursorOffset,
};
// 2. Update index
setHistoryIndex(nextIndex);
// 3. Restore next state
const saved = historyCacheRef.current[nextIndex];
// We robustly restore the cursor position IF:
// 1. We are returning to the compose prompt (-1)
// 2. OR we are returning to the level we occupied *just before* the current one.
// AND in both cases, the cursor was not at the very first or last character.
const isReturningToPrevious =
nextIndex === -1 || nextIndex === previousHistoryIndexRef.current;
if (
isReturningToPrevious &&
saved &&
saved.offset > 0 &&
saved.offset < cpLen(saved.text)
) {
onChange(saved.text, saved.offset);
} else if (nextIndex === -1) {
onChange(saved ? saved.text : '', defaultCursor);
} else {
// For regular history browsing, use default cursor position.
if (saved) {
onChange(saved.text, defaultCursor);
} else {
const newValue = userMessages[userMessages.length - 1 - nextIndex];
onChange(newValue, defaultCursor);
}
}
// Record the level we just came from for the next navigation
previousHistoryIndexRef.current = prevIndexBeforeMove;
},
[historyIndex, currentQuery, currentCursorOffset, userMessages, onChange],
);
const navigateUp = useCallback(() => {
if (!isActive) return false;
if (userMessages.length === 0) return false;
let nextIndex = historyIndex;
if (historyIndex === -1) {
// Store the current query from the parent before navigating
setOriginalQueryBeforeNav(currentQuery);
nextIndex = 0;
} else if (historyIndex < userMessages.length - 1) {
nextIndex = historyIndex + 1;
} else {
return false; // Already at the oldest message
}
if (nextIndex !== historyIndex) {
setHistoryIndex(nextIndex);
const newValue = userMessages[userMessages.length - 1 - nextIndex];
onChange(newValue);
if (historyIndex < userMessages.length - 1) {
navigateTo(historyIndex + 1, 'start');
return true;
}
return false;
}, [
historyIndex,
setHistoryIndex,
onChange,
userMessages,
isActive,
currentQuery, // Use currentQuery from props
setOriginalQueryBeforeNav,
]);
}, [historyIndex, userMessages, isActive, navigateTo]);
const navigateDown = useCallback(() => {
if (!isActive) return false;
if (historyIndex === -1) return false; // Not currently navigating history
const nextIndex = historyIndex - 1;
setHistoryIndex(nextIndex);
if (nextIndex === -1) {
// Reached the end of history navigation, restore original query
onChange(originalQueryBeforeNav);
} else {
const newValue = userMessages[userMessages.length - 1 - nextIndex];
onChange(newValue);
}
navigateTo(historyIndex - 1, 'end');
return true;
}, [
historyIndex,
setHistoryIndex,
originalQueryBeforeNav,
onChange,
userMessages,
isActive,
]);
}, [historyIndex, isActive, navigateTo]);
return {
handleSubmit,
@@ -39,10 +39,8 @@ export function useReverseSearchCompletion(
suggestions,
activeSuggestionIndex,
visibleStartIndex,
showSuggestions,
isLoadingSuggestions,
setSuggestions,
setShowSuggestions,
setActiveSuggestionIndex,
resetCompletionState,
navigateUp,
@@ -115,7 +113,6 @@ export function useReverseSearchCompletion(
setSuggestions(matches);
const hasAny = matches.length > 0;
setShowSuggestions(hasAny);
setActiveSuggestionIndex(hasAny ? 0 : -1);
setVisibleStartIndex(0);
@@ -126,12 +123,14 @@ export function useReverseSearchCompletion(
matches,
reverseSearchActive,
setSuggestions,
setShowSuggestions,
setActiveSuggestionIndex,
setVisibleStartIndex,
resetCompletionState,
]);
const showSuggestions =
reverseSearchActive && (isLoadingSuggestions || suggestions.length > 0);
const handleAutocomplete = useCallback(
(i: number) => {
if (i < 0 || i >= suggestions.length) return;