summaryrefslogtreecommitdiff
path: root/packages/cli/src/ui/components
diff options
context:
space:
mode:
Diffstat (limited to 'packages/cli/src/ui/components')
-rw-r--r--packages/cli/src/ui/components/InputPrompt.test.tsx91
-rw-r--r--packages/cli/src/ui/components/InputPrompt.tsx124
2 files changed, 57 insertions, 158 deletions
diff --git a/packages/cli/src/ui/components/InputPrompt.test.tsx b/packages/cli/src/ui/components/InputPrompt.test.tsx
index bad29f10..a1894002 100644
--- a/packages/cli/src/ui/components/InputPrompt.test.tsx
+++ b/packages/cli/src/ui/components/InputPrompt.test.tsx
@@ -121,6 +121,15 @@ describe('InputPrompt', () => {
openInExternalEditor: vi.fn(),
newline: vi.fn(),
backspace: vi.fn(),
+ preferredCol: null,
+ selectionAnchor: null,
+ insert: vi.fn(),
+ del: vi.fn(),
+ undo: vi.fn(),
+ redo: vi.fn(),
+ replaceRange: vi.fn(),
+ deleteWordLeft: vi.fn(),
+ deleteWordRight: vi.fn(),
} as unknown as TextBuffer;
mockShellHistory = {
@@ -137,12 +146,14 @@ describe('InputPrompt', () => {
isLoadingSuggestions: false,
showSuggestions: false,
visibleStartIndex: 0,
+ isPerfectMatch: false,
navigateUp: vi.fn(),
navigateDown: vi.fn(),
resetCompletionState: vi.fn(),
setActiveSuggestionIndex: vi.fn(),
setShowSuggestions: vi.fn(),
- } as unknown as UseCompletionReturn;
+ handleAutocomplete: vi.fn(),
+ };
mockedUseCompletion.mockReturnValue(mockCompletion);
mockInputHistory = {
@@ -465,7 +476,7 @@ describe('InputPrompt', () => {
stdin.write('\t'); // Press Tab
await wait();
- expect(props.buffer.setText).toHaveBeenCalledWith('/memory');
+ expect(mockCompletion.handleAutocomplete).toHaveBeenCalledWith(0);
unmount();
});
@@ -488,7 +499,7 @@ describe('InputPrompt', () => {
stdin.write('\t'); // Press Tab
await wait();
- expect(props.buffer.setText).toHaveBeenCalledWith('/memory add');
+ expect(mockCompletion.handleAutocomplete).toHaveBeenCalledWith(1);
unmount();
});
@@ -513,7 +524,7 @@ describe('InputPrompt', () => {
await wait();
// It should NOT become '/show'. It should correctly become '/memory show'.
- expect(props.buffer.setText).toHaveBeenCalledWith('/memory show');
+ expect(mockCompletion.handleAutocomplete).toHaveBeenCalledWith(0);
unmount();
});
@@ -533,7 +544,7 @@ describe('InputPrompt', () => {
stdin.write('\t'); // Press Tab
await wait();
- expect(props.buffer.setText).toHaveBeenCalledWith('/chat resume fix-foo');
+ expect(mockCompletion.handleAutocomplete).toHaveBeenCalledWith(0);
unmount();
});
@@ -553,7 +564,7 @@ describe('InputPrompt', () => {
await wait();
// The app should autocomplete the text, NOT submit.
- expect(props.buffer.setText).toHaveBeenCalledWith('/memory');
+ expect(mockCompletion.handleAutocomplete).toHaveBeenCalledWith(0);
expect(props.onSubmit).not.toHaveBeenCalled();
unmount();
@@ -583,7 +594,7 @@ describe('InputPrompt', () => {
stdin.write('\t'); // Press Tab for autocomplete
await wait();
- expect(props.buffer.setText).toHaveBeenCalledWith('/help');
+ expect(mockCompletion.handleAutocomplete).toHaveBeenCalledWith(0);
unmount();
});
@@ -600,10 +611,29 @@ describe('InputPrompt', () => {
unmount();
});
+ it('should submit directly on Enter when isPerfectMatch is true', async () => {
+ mockedUseCompletion.mockReturnValue({
+ ...mockCompletion,
+ showSuggestions: false,
+ isPerfectMatch: true,
+ });
+ props.buffer.setText('/clear');
+
+ const { stdin, unmount } = render(<InputPrompt {...props} />);
+ await wait();
+
+ stdin.write('\r');
+ await wait();
+
+ expect(props.onSubmit).toHaveBeenCalledWith('/clear');
+ unmount();
+ });
+
it('should submit directly on Enter when a complete leaf command is typed', async () => {
mockedUseCompletion.mockReturnValue({
...mockCompletion,
showSuggestions: false,
+ isPerfectMatch: false, // Added explicit isPerfectMatch false
});
props.buffer.setText('/clear');
@@ -632,7 +662,7 @@ describe('InputPrompt', () => {
stdin.write('\r');
await wait();
- expect(props.buffer.replaceRangeByOffset).toHaveBeenCalled();
+ expect(mockCompletion.handleAutocomplete).toHaveBeenCalledWith(0);
expect(props.onSubmit).not.toHaveBeenCalled();
unmount();
});
@@ -697,11 +727,10 @@ describe('InputPrompt', () => {
const { unmount } = render(<InputPrompt {...props} />);
await wait();
- // Verify useCompletion was called with true (should show completion)
+ // Verify useCompletion was called with correct signature
expect(mockedUseCompletion).toHaveBeenCalledWith(
- '@src/components',
+ mockBuffer,
path.join('test', 'project', 'src'),
- true, // shouldShowCompletion should be true
mockSlashCommands,
mockCommandContext,
expect.any(Object),
@@ -725,9 +754,8 @@ describe('InputPrompt', () => {
await wait();
expect(mockedUseCompletion).toHaveBeenCalledWith(
- '/memory',
+ mockBuffer,
path.join('test', 'project', 'src'),
- true, // shouldShowCompletion should be true
mockSlashCommands,
mockCommandContext,
expect.any(Object),
@@ -751,9 +779,8 @@ describe('InputPrompt', () => {
await wait();
expect(mockedUseCompletion).toHaveBeenCalledWith(
- '@src/file.ts hello',
+ mockBuffer,
path.join('test', 'project', 'src'),
- false, // shouldShowCompletion should be false
mockSlashCommands,
mockCommandContext,
expect.any(Object),
@@ -777,9 +804,8 @@ describe('InputPrompt', () => {
await wait();
expect(mockedUseCompletion).toHaveBeenCalledWith(
- '/memory add',
+ mockBuffer,
path.join('test', 'project', 'src'),
- false, // shouldShowCompletion should be false
mockSlashCommands,
mockCommandContext,
expect.any(Object),
@@ -803,9 +829,8 @@ describe('InputPrompt', () => {
await wait();
expect(mockedUseCompletion).toHaveBeenCalledWith(
- 'hello world',
+ mockBuffer,
path.join('test', 'project', 'src'),
- false, // shouldShowCompletion should be false
mockSlashCommands,
mockCommandContext,
expect.any(Object),
@@ -828,10 +853,10 @@ describe('InputPrompt', () => {
const { unmount } = render(<InputPrompt {...props} />);
await wait();
+ // Verify useCompletion was called with the buffer
expect(mockedUseCompletion).toHaveBeenCalledWith(
- 'first line\n/memory',
+ mockBuffer,
path.join('test', 'project', 'src'),
- false, // shouldShowCompletion should be false (isSlashCommand returns false because text doesn't start with /)
mockSlashCommands,
mockCommandContext,
expect.any(Object),
@@ -855,9 +880,8 @@ describe('InputPrompt', () => {
await wait();
expect(mockedUseCompletion).toHaveBeenCalledWith(
- '/memory',
+ mockBuffer,
path.join('test', 'project', 'src'),
- true, // shouldShowCompletion should be true (isSlashCommand returns true AND cursor is after / without space)
mockSlashCommands,
mockCommandContext,
expect.any(Object),
@@ -882,9 +906,8 @@ describe('InputPrompt', () => {
await wait();
expect(mockedUseCompletion).toHaveBeenCalledWith(
- '@src/file๐Ÿ‘.txt',
+ mockBuffer,
path.join('test', 'project', 'src'),
- true, // shouldShowCompletion should be true
mockSlashCommands,
mockCommandContext,
expect.any(Object),
@@ -909,9 +932,8 @@ describe('InputPrompt', () => {
await wait();
expect(mockedUseCompletion).toHaveBeenCalledWith(
- '@src/file๐Ÿ‘.txt hello',
+ mockBuffer,
path.join('test', 'project', 'src'),
- false, // shouldShowCompletion should be false
mockSlashCommands,
mockCommandContext,
expect.any(Object),
@@ -936,9 +958,8 @@ describe('InputPrompt', () => {
await wait();
expect(mockedUseCompletion).toHaveBeenCalledWith(
- '@src/my\\ file.txt',
+ mockBuffer,
path.join('test', 'project', 'src'),
- true, // shouldShowCompletion should be true
mockSlashCommands,
mockCommandContext,
expect.any(Object),
@@ -963,9 +984,8 @@ describe('InputPrompt', () => {
await wait();
expect(mockedUseCompletion).toHaveBeenCalledWith(
- '@path/my\\ file.txt hello',
+ mockBuffer,
path.join('test', 'project', 'src'),
- false, // shouldShowCompletion should be false
mockSlashCommands,
mockCommandContext,
expect.any(Object),
@@ -992,9 +1012,8 @@ describe('InputPrompt', () => {
await wait();
expect(mockedUseCompletion).toHaveBeenCalledWith(
- '@docs/my\\ long\\ file\\ name.md',
+ mockBuffer,
path.join('test', 'project', 'src'),
- true, // shouldShowCompletion should be true
mockSlashCommands,
mockCommandContext,
expect.any(Object),
@@ -1019,9 +1038,8 @@ describe('InputPrompt', () => {
await wait();
expect(mockedUseCompletion).toHaveBeenCalledWith(
- '/memory\\ test',
+ mockBuffer,
path.join('test', 'project', 'src'),
- true, // shouldShowCompletion should be true
mockSlashCommands,
mockCommandContext,
expect.any(Object),
@@ -1048,9 +1066,8 @@ describe('InputPrompt', () => {
await wait();
expect(mockedUseCompletion).toHaveBeenCalledWith(
- '@' + path.join('files', 'emoji\\ ๐Ÿ‘\\ test.txt'),
+ mockBuffer,
path.join('test', 'project', 'src'),
- true, // shouldShowCompletion should be true
mockSlashCommands,
mockCommandContext,
expect.any(Object),
diff --git a/packages/cli/src/ui/components/InputPrompt.tsx b/packages/cli/src/ui/components/InputPrompt.tsx
index 6192fb8c..9f15b56d 100644
--- a/packages/cli/src/ui/components/InputPrompt.tsx
+++ b/packages/cli/src/ui/components/InputPrompt.tsx
@@ -10,13 +10,12 @@ import { Colors } from '../colors.js';
import { SuggestionsDisplay } from './SuggestionsDisplay.js';
import { useInputHistory } from '../hooks/useInputHistory.js';
import { TextBuffer } from './shared/text-buffer.js';
-import { cpSlice, cpLen, toCodePoints } from '../utils/textUtils.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 { useKeypress, Key } from '../hooks/useKeypress.js';
-import { isAtCommand, isSlashCommand } from '../utils/commandUtils.js';
import { CommandContext, SlashCommand } from '../commands/types.js';
import { Config } from '@google/gemini-cli-core';
import {
@@ -59,53 +58,9 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
}) => {
const [justNavigatedHistory, setJustNavigatedHistory] = useState(false);
- // Check if cursor is after @ or / without unescaped spaces
- const isCursorAfterCommandWithoutSpace = useCallback(() => {
- const [row, col] = buffer.cursor;
- const currentLine = buffer.lines[row] || '';
-
- // Convert current line to code points for Unicode-aware processing
- const codePoints = toCodePoints(currentLine);
-
- // Search backwards from cursor position within the current line only
- for (let i = col - 1; i >= 0; i--) {
- const char = codePoints[i];
-
- if (char === ' ') {
- // Check if this space is escaped by counting backslashes before it
- let backslashCount = 0;
- for (let j = i - 1; j >= 0 && codePoints[j] === '\\'; j--) {
- backslashCount++;
- }
-
- // If there's an odd number of backslashes, the space is escaped
- const isEscaped = backslashCount % 2 === 1;
-
- if (!isEscaped) {
- // Found unescaped space before @ or /, return false
- return false;
- }
- // If escaped, continue searching backwards
- } else if (char === '@' || char === '/') {
- // Found @ or / without unescaped space in between
- return true;
- }
- }
-
- return false;
- }, [buffer.cursor, buffer.lines]);
-
- const shouldShowCompletion = useCallback(
- () =>
- (isAtCommand(buffer.text) || isSlashCommand(buffer.text)) &&
- isCursorAfterCommandWithoutSpace(),
- [buffer.text, isCursorAfterCommandWithoutSpace],
- );
-
const completion = useCompletion(
- buffer.text,
+ buffer,
config.getTargetDir(),
- shouldShowCompletion(),
slashCommands,
commandContext,
config,
@@ -159,78 +114,6 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
setJustNavigatedHistory,
]);
- const completionSuggestions = completion.suggestions;
- const handleAutocomplete = useCallback(
- (indexToUse: number) => {
- if (indexToUse < 0 || indexToUse >= completionSuggestions.length) {
- return;
- }
- const query = buffer.text;
- const suggestion = completionSuggestions[indexToUse].value;
-
- if (query.trimStart().startsWith('/')) {
- const hasTrailingSpace = query.endsWith(' ');
- const parts = query
- .trimStart()
- .substring(1)
- .split(/\s+/)
- .filter(Boolean);
-
- let isParentPath = false;
- // If there's no trailing space, we need to check if the current query
- // is already a complete path to a parent command.
- if (!hasTrailingSpace) {
- let currentLevel: readonly SlashCommand[] | undefined = slashCommands;
- for (let i = 0; i < parts.length; i++) {
- const part = parts[i];
- const found: SlashCommand | undefined = currentLevel?.find(
- (cmd) => cmd.name === part || cmd.altNames?.includes(part),
- );
-
- if (found) {
- if (i === parts.length - 1 && found.subCommands) {
- isParentPath = true;
- }
- currentLevel = found.subCommands as
- | readonly SlashCommand[]
- | undefined;
- } else {
- // Path is invalid, so it can't be a parent path.
- currentLevel = undefined;
- break;
- }
- }
- }
-
- // Determine the base path of the command.
- // - If there's a trailing space, the whole command is the base.
- // - If it's a known parent path, the whole command is the base.
- // - Otherwise, the base is everything EXCEPT the last partial part.
- const basePath =
- hasTrailingSpace || isParentPath ? parts : parts.slice(0, -1);
- const newValue = `/${[...basePath, suggestion].join(' ')}`;
-
- buffer.setText(newValue);
- } else {
- const atIndex = query.lastIndexOf('@');
- if (atIndex === -1) return;
- const pathPart = query.substring(atIndex + 1);
- const lastSlashIndexInPath = pathPart.lastIndexOf('/');
- let autoCompleteStartIndex = atIndex + 1;
- if (lastSlashIndexInPath !== -1) {
- autoCompleteStartIndex += lastSlashIndexInPath + 1;
- }
- buffer.replaceRangeByOffset(
- autoCompleteStartIndex,
- buffer.text.length,
- suggestion,
- );
- }
- resetCompletionState();
- },
- [resetCompletionState, buffer, completionSuggestions, slashCommands],
- );
-
// Handle clipboard image pasting with Ctrl+V
const handleClipboardImage = useCallback(async () => {
try {
@@ -337,7 +220,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
? 0 // Default to the first if none is active
: completion.activeSuggestionIndex;
if (targetIndex < completion.suggestions.length) {
- handleAutocomplete(targetIndex);
+ completion.handleAutocomplete(targetIndex);
}
}
return;
@@ -459,7 +342,6 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
setShellModeActive,
onClearScreen,
inputHistory,
- handleAutocomplete,
handleSubmitAndClear,
shellHistory,
handleClipboardImage,