mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-23 12:44:30 +00:00
fix(cli): Prevent unmapped keys in Vim Normal mode from inserting text into prompt Input. (#25139)
Co-authored-by: Tommaso Sciortino <sciortino@gmail.com>
This commit is contained in:
@@ -154,6 +154,8 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||
onEscapePromptChange={uiActions.onEscapePromptChange}
|
||||
focus={isFocused}
|
||||
vimHandleInput={uiActions.vimHandleInput}
|
||||
vimEnabled={vimEnabled}
|
||||
vimMode={vimMode}
|
||||
isEmbeddedShellFocused={uiState.embeddedShellFocused}
|
||||
popAllMessages={uiActions.popAllMessages}
|
||||
onQueueMessage={uiActions.addMessage}
|
||||
|
||||
@@ -4898,6 +4898,60 @@ describe('InputPrompt', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should NOT open shortcuts help with ? in vim NORMAL mode', async () => {
|
||||
const setShortcutsHelpVisible = vi.fn();
|
||||
const vimHandleInput = vi.fn().mockReturnValue(true);
|
||||
|
||||
const { stdin, unmount } = await renderWithProviders(
|
||||
<TestInputPrompt
|
||||
{...props}
|
||||
vimEnabled={true}
|
||||
vimMode="NORMAL"
|
||||
vimHandleInput={vimHandleInput}
|
||||
/>,
|
||||
{
|
||||
uiActions: { setShortcutsHelpVisible },
|
||||
},
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('?');
|
||||
});
|
||||
|
||||
expect(setShortcutsHelpVisible).not.toHaveBeenCalled();
|
||||
expect(vimHandleInput).toHaveBeenCalled();
|
||||
expect(mockBuffer.handleInput).not.toHaveBeenCalled();
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should open shortcuts help with ? in vim INSERT mode', async () => {
|
||||
const setShortcutsHelpVisible = vi.fn();
|
||||
const vimHandleInput = vi.fn().mockReturnValue(false);
|
||||
|
||||
const { stdin, unmount } = await renderWithProviders(
|
||||
<TestInputPrompt
|
||||
{...props}
|
||||
vimEnabled={true}
|
||||
vimMode="INSERT"
|
||||
vimHandleInput={vimHandleInput}
|
||||
/>,
|
||||
{
|
||||
uiActions: { setShortcutsHelpVisible },
|
||||
},
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('?');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(setShortcutsHelpVisible).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: 'terminal paste event occurs',
|
||||
|
||||
@@ -92,6 +92,7 @@ import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js';
|
||||
import { useIsHelpDismissKey } from '../utils/shortcutsHelp.js';
|
||||
import { useRepeatedKeyPress } from '../hooks/useRepeatedKeyPress.js';
|
||||
import { useKeyMatchers } from '../hooks/useKeyMatchers.js';
|
||||
import type { VimMode } from '../contexts/VimModeContext.js';
|
||||
|
||||
const SCROLLBAR_GUTTER_WIDTH = 1;
|
||||
|
||||
@@ -126,6 +127,8 @@ export interface InputPromptProps {
|
||||
onEscapePromptChange?: (showPrompt: boolean) => void;
|
||||
onSuggestionsVisibilityChange?: (visible: boolean) => void;
|
||||
vimHandleInput?: (key: Key) => boolean;
|
||||
vimEnabled?: boolean;
|
||||
vimMode?: VimMode;
|
||||
isEmbeddedShellFocused?: boolean;
|
||||
setQueueErrorMessage: (message: string | null) => void;
|
||||
streamingState: StreamingState;
|
||||
@@ -214,6 +217,8 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
onEscapePromptChange,
|
||||
onSuggestionsVisibilityChange,
|
||||
vimHandleInput,
|
||||
vimEnabled,
|
||||
vimMode,
|
||||
isEmbeddedShellFocused,
|
||||
setQueueErrorMessage,
|
||||
streamingState,
|
||||
@@ -859,7 +864,11 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
}
|
||||
|
||||
if (shortcutsHelpVisible) {
|
||||
if (key.sequence === '?' && key.insertable) {
|
||||
if (
|
||||
key.sequence === '?' &&
|
||||
key.insertable &&
|
||||
(!vimEnabled || vimMode === 'INSERT')
|
||||
) {
|
||||
setShortcutsHelpVisible(false);
|
||||
buffer.handleInput(key);
|
||||
return true;
|
||||
@@ -879,7 +888,8 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
key.sequence === '?' &&
|
||||
key.insertable &&
|
||||
!shortcutsHelpVisible &&
|
||||
buffer.text.length === 0
|
||||
buffer.text.length === 0 &&
|
||||
(!vimEnabled || vimMode === 'INSERT')
|
||||
) {
|
||||
setShortcutsHelpVisible(true);
|
||||
return true;
|
||||
@@ -1374,6 +1384,8 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
resetCompletionState,
|
||||
resetEscapeState,
|
||||
vimHandleInput,
|
||||
vimEnabled,
|
||||
vimMode,
|
||||
reverseSearchActive,
|
||||
textBeforeReverseSearch,
|
||||
cursorPosition,
|
||||
|
||||
@@ -2258,6 +2258,80 @@ describe('useVim hook', async () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('should handle unmapped keys in Normal mode', () => {
|
||||
type UnmappedKeyCase = {
|
||||
char: string;
|
||||
insertable: boolean;
|
||||
};
|
||||
it.each<UnmappedKeyCase>([
|
||||
{ char: 'm', insertable: true },
|
||||
{ char: 'n', insertable: true },
|
||||
{ char: 'p', insertable: true },
|
||||
{ char: 'q', insertable: true },
|
||||
{ char: 's', insertable: true },
|
||||
{ char: 'v', insertable: true },
|
||||
{ char: 'y', insertable: true },
|
||||
{ char: 'z', insertable: true },
|
||||
{ char: 'H', insertable: true },
|
||||
{ char: 'J', insertable: true },
|
||||
{ char: 'K', insertable: true },
|
||||
{ char: 'L', insertable: true },
|
||||
{ char: 'M', insertable: true },
|
||||
{ char: 'N', insertable: true },
|
||||
{ char: 'P', insertable: true },
|
||||
{ char: 'Q', insertable: true },
|
||||
{ char: 'R', insertable: true },
|
||||
{ char: 'S', insertable: true },
|
||||
{ char: 'U', insertable: true },
|
||||
{ char: 'V', insertable: true },
|
||||
{ char: 'Y', insertable: true },
|
||||
{ char: 'Z', insertable: true },
|
||||
{ char: '/', insertable: true },
|
||||
{ char: '#', insertable: true },
|
||||
{ char: '%', insertable: true },
|
||||
{ char: '&', insertable: true },
|
||||
{ char: "'", insertable: true },
|
||||
{ char: '(', insertable: true },
|
||||
{ char: ')', insertable: true },
|
||||
{ char: '*', insertable: true },
|
||||
{ char: '+', insertable: true },
|
||||
{ char: '-', insertable: true },
|
||||
{ char: '/', insertable: true },
|
||||
{ char: ':', insertable: true },
|
||||
{ char: '<', insertable: true },
|
||||
{ char: '=', insertable: true },
|
||||
{ char: '>', insertable: true },
|
||||
{ char: '@', insertable: true },
|
||||
{ char: '[', insertable: true },
|
||||
{ char: '\\', insertable: true },
|
||||
{ char: ']', insertable: true },
|
||||
{ char: '_', insertable: true },
|
||||
{ char: '`', insertable: true },
|
||||
{ char: '{', insertable: true },
|
||||
{ char: '|', insertable: true },
|
||||
{ char: '}', insertable: true },
|
||||
])(
|
||||
'$char: should be swallowed and do nothing in Normal mode',
|
||||
async ({ char, insertable }) => {
|
||||
const { result } = await renderVimHook();
|
||||
exitInsertMode(result);
|
||||
|
||||
let handled = false;
|
||||
act(() => {
|
||||
handled = result.current.handleInput(
|
||||
createKey({ sequence: char, name: char, insertable }),
|
||||
);
|
||||
});
|
||||
|
||||
expect(handled).toBe(true);
|
||||
expect(mockVimContext.setVimMode).not.toHaveBeenCalledWith('INSERT');
|
||||
|
||||
expect(mockBuffer.vimFindCharForward).not.toHaveBeenCalled();
|
||||
expect(mockBuffer.vimFindCharBackward).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('Operator + find motions (df, dt, dF, dT, cf, ct, cF, cT)', async () => {
|
||||
it('df{char}: executes delete-to-char, not a dangling operator', async () => {
|
||||
const { result } = await renderVimHook();
|
||||
|
||||
@@ -1486,6 +1486,11 @@ export function useVim(buffer: TextBuffer, onSubmit?: (value: string) => void) {
|
||||
// Unknown command, clear count and pending states
|
||||
dispatch({ type: 'CLEAR_PENDING_STATES' });
|
||||
|
||||
// Ignore any Insertable key in Normal Mode
|
||||
if (normalizedKey.insertable) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Not handled by vim so allow other handlers to process it.
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user