mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-08 12:20:38 -07:00
feat (cli): Add command search using Ctrl+r (#5539)
Co-authored-by: Jacob Richman <jacob314@gmail.com>
This commit is contained in:
@@ -25,6 +25,7 @@ import type { UseReverseSearchCompletionReturn } from '../hooks/useReverseSearch
|
||||
import { useReverseSearchCompletion } from '../hooks/useReverseSearchCompletion.js';
|
||||
import * as clipboardUtils from '../utils/clipboardUtils.js';
|
||||
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
|
||||
import stripAnsi from 'strip-ansi';
|
||||
import chalk from 'chalk';
|
||||
|
||||
vi.mock('../hooks/useShellHistory.js');
|
||||
@@ -1789,6 +1790,144 @@ describe('InputPrompt', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('command search (Ctrl+R when not in shell)', () => {
|
||||
it('enters command search on Ctrl+R and shows suggestions', async () => {
|
||||
props.shellModeActive = false;
|
||||
|
||||
vi.mocked(useReverseSearchCompletion).mockImplementation(
|
||||
(buffer, data, isActive) => ({
|
||||
...mockReverseSearchCompletion,
|
||||
suggestions: isActive
|
||||
? [
|
||||
{ label: 'git commit -m "msg"', value: 'git commit -m "msg"' },
|
||||
{ label: 'git push', value: 'git push' },
|
||||
]
|
||||
: [],
|
||||
showSuggestions: !!isActive,
|
||||
activeSuggestionIndex: isActive ? 0 : -1,
|
||||
}),
|
||||
);
|
||||
|
||||
const { stdin, stdout, unmount } = renderWithProviders(
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
await wait();
|
||||
|
||||
act(() => {
|
||||
stdin.write('\x12'); // Ctrl+R
|
||||
});
|
||||
await wait();
|
||||
|
||||
const frame = stdout.lastFrame() ?? '';
|
||||
expect(frame).toContain('(r:)');
|
||||
expect(frame).toContain('git commit');
|
||||
expect(frame).toContain('git push');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('expands and collapses long suggestion via Right/Left arrows', async () => {
|
||||
props.shellModeActive = false;
|
||||
const longValue = 'l'.repeat(200);
|
||||
|
||||
vi.mocked(useReverseSearchCompletion).mockReturnValue({
|
||||
...mockReverseSearchCompletion,
|
||||
suggestions: [{ label: longValue, value: longValue, matchedIndex: 0 }],
|
||||
showSuggestions: true,
|
||||
activeSuggestionIndex: 0,
|
||||
visibleStartIndex: 0,
|
||||
isLoadingSuggestions: false,
|
||||
});
|
||||
|
||||
const { stdin, stdout, unmount } = renderWithProviders(
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
await wait();
|
||||
|
||||
stdin.write('\x12');
|
||||
await wait();
|
||||
|
||||
expect(clean(stdout.lastFrame())).toContain('→');
|
||||
|
||||
stdin.write('\u001B[C');
|
||||
await wait();
|
||||
expect(clean(stdout.lastFrame())).toContain('←');
|
||||
expect(stdout.lastFrame()).toMatchSnapshot(
|
||||
'command-search-expanded-match',
|
||||
);
|
||||
|
||||
stdin.write('\u001B[D');
|
||||
await wait();
|
||||
expect(clean(stdout.lastFrame())).toContain('→');
|
||||
expect(stdout.lastFrame()).toMatchSnapshot(
|
||||
'command-search-collapsed-match',
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders match window and expanded view (snapshots)', async () => {
|
||||
props.shellModeActive = false;
|
||||
props.buffer.setText('commit');
|
||||
|
||||
const label = 'git commit -m "feat: add search" in src/app';
|
||||
const matchedIndex = label.indexOf('commit');
|
||||
|
||||
vi.mocked(useReverseSearchCompletion).mockReturnValue({
|
||||
...mockReverseSearchCompletion,
|
||||
suggestions: [{ label, value: label, matchedIndex }],
|
||||
showSuggestions: true,
|
||||
activeSuggestionIndex: 0,
|
||||
visibleStartIndex: 0,
|
||||
isLoadingSuggestions: false,
|
||||
});
|
||||
|
||||
const { stdin, stdout, unmount } = renderWithProviders(
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
await wait();
|
||||
|
||||
stdin.write('\x12');
|
||||
await wait();
|
||||
expect(stdout.lastFrame()).toMatchSnapshot(
|
||||
'command-search-collapsed-match',
|
||||
);
|
||||
|
||||
stdin.write('\u001B[C');
|
||||
await wait();
|
||||
expect(stdout.lastFrame()).toMatchSnapshot(
|
||||
'command-search-expanded-match',
|
||||
);
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('does not show expand/collapse indicator for short suggestions', async () => {
|
||||
props.shellModeActive = false;
|
||||
const shortValue = 'echo hello';
|
||||
|
||||
vi.mocked(useReverseSearchCompletion).mockReturnValue({
|
||||
...mockReverseSearchCompletion,
|
||||
suggestions: [{ label: shortValue, value: shortValue }],
|
||||
showSuggestions: true,
|
||||
activeSuggestionIndex: 0,
|
||||
visibleStartIndex: 0,
|
||||
isLoadingSuggestions: false,
|
||||
});
|
||||
|
||||
const { stdin, stdout, unmount } = renderWithProviders(
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
await wait();
|
||||
|
||||
stdin.write('\x12');
|
||||
await wait();
|
||||
|
||||
const frame = clean(stdout.lastFrame());
|
||||
expect(frame).not.toContain('→');
|
||||
expect(frame).not.toContain('←');
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
describe('snapshots', () => {
|
||||
it('should render correctly in shell mode', async () => {
|
||||
props.shellModeActive = true;
|
||||
@@ -1821,3 +1960,8 @@ describe('InputPrompt', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
function clean(str: string | undefined): string {
|
||||
if (!str) return '';
|
||||
// Remove ANSI escape codes and trim whitespace
|
||||
return stripAnsi(str).trim();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user