bug(ux) vim mode fixes. Start in insert mode. Fix bug blocking F12 and ctrl-X in vim mode. (#17938)

This commit is contained in:
Jacob Richman
2026-01-29 23:31:47 -08:00
committed by GitHub
parent 137080da45
commit 32cfce16bb
8 changed files with 276 additions and 75 deletions

View File

@@ -19,7 +19,7 @@ import { SettingsContext } from '../contexts/SettingsContext.js';
vi.mock('../contexts/VimModeContext.js', () => ({
useVimMode: vi.fn(() => ({
vimEnabled: false,
vimMode: 'NORMAL',
vimMode: 'INSERT',
})),
}));
import { ApprovalMode } from '@google/gemini-cli-core';
@@ -54,7 +54,9 @@ vi.mock('./DetailedMessagesDisplay.js', () => ({
}));
vi.mock('./InputPrompt.js', () => ({
InputPrompt: () => <Text>InputPrompt</Text>,
InputPrompt: ({ placeholder }: { placeholder?: string }) => (
<Text>InputPrompt: {placeholder}</Text>
),
calculatePromptWidths: vi.fn(() => ({
inputWidth: 80,
suggestionsWidth: 40,
@@ -487,4 +489,40 @@ describe('Composer', () => {
expect(lastFrame()).not.toContain('DetailedMessagesDisplay');
});
});
describe('Vim Mode Placeholders', () => {
it('shows correct placeholder in INSERT mode', async () => {
const uiState = createMockUIState({ isInputActive: true });
const { useVimMode } = await import('../contexts/VimModeContext.js');
vi.mocked(useVimMode).mockReturnValue({
vimEnabled: true,
vimMode: 'INSERT',
toggleVimEnabled: vi.fn(),
setVimMode: vi.fn(),
});
const { lastFrame } = renderComposer(uiState);
expect(lastFrame()).toContain(
"InputPrompt: Press 'Esc' for NORMAL mode.",
);
});
it('shows correct placeholder in NORMAL mode', async () => {
const uiState = createMockUIState({ isInputActive: true });
const { useVimMode } = await import('../contexts/VimModeContext.js');
vi.mocked(useVimMode).mockReturnValue({
vimEnabled: true,
vimMode: 'NORMAL',
toggleVimEnabled: vi.fn(),
setVimMode: vi.fn(),
});
const { lastFrame } = renderComposer(uiState);
expect(lastFrame()).toContain(
"InputPrompt: Press 'i' for INSERT mode.",
);
});
});
});

View File

@@ -35,7 +35,7 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
const isScreenReaderEnabled = useIsScreenReaderEnabled();
const uiState = useUIState();
const uiActions = useUIActions();
const { vimEnabled } = useVimMode();
const { vimEnabled, vimMode } = useVimMode();
const terminalWidth = process.stdout.columns;
const isNarrow = isNarrowWidth(terminalWidth);
const debugConsoleMaxHeight = Math.floor(Math.max(terminalWidth * 0.2, 5));
@@ -143,7 +143,9 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
popAllMessages={uiActions.popAllMessages}
placeholder={
vimEnabled
? " Press 'i' for INSERT mode and 'Esc' for NORMAL mode."
? vimMode === 'INSERT'
? " Press 'Esc' for NORMAL mode."
: " Press 'i' for INSERT mode."
: uiState.shellModeActive
? ' Type your shell command'
: ' Type your message or @path/to/file'

View File

@@ -1418,7 +1418,7 @@ describe('useTextBuffer', () => {
const { result } = renderHook(() =>
useTextBuffer({ viewport, isValidPath: () => false }),
);
act(() =>
act(() => {
result.current.handleInput({
name: 'h',
shift: false,
@@ -1427,9 +1427,9 @@ describe('useTextBuffer', () => {
cmd: false,
insertable: true,
sequence: 'h',
}),
);
act(() =>
});
});
void act(() =>
result.current.handleInput({
name: 'i',
shift: false,
@@ -1447,7 +1447,7 @@ describe('useTextBuffer', () => {
const { result } = renderHook(() =>
useTextBuffer({ viewport, isValidPath: () => false }),
);
act(() =>
act(() => {
result.current.handleInput({
name: 'return',
shift: false,
@@ -1456,8 +1456,8 @@ describe('useTextBuffer', () => {
cmd: false,
insertable: true,
sequence: '\r',
}),
);
});
});
expect(getBufferState(result).lines).toEqual(['', '']);
});
@@ -1465,7 +1465,7 @@ describe('useTextBuffer', () => {
const { result } = renderHook(() =>
useTextBuffer({ viewport, isValidPath: () => false }),
);
act(() =>
act(() => {
result.current.handleInput({
name: 'j',
shift: false,
@@ -1474,8 +1474,8 @@ describe('useTextBuffer', () => {
cmd: false,
insertable: false,
sequence: '\n',
}),
);
});
});
expect(getBufferState(result).lines).toEqual(['', '']);
});
@@ -1483,7 +1483,7 @@ describe('useTextBuffer', () => {
const { result } = renderHook(() =>
useTextBuffer({ viewport, isValidPath: () => false }),
);
act(() =>
act(() => {
result.current.handleInput({
name: 'tab',
shift: false,
@@ -1492,8 +1492,8 @@ describe('useTextBuffer', () => {
cmd: false,
insertable: false,
sequence: '\t',
}),
);
});
});
expect(getBufferState(result).text).toBe('');
});
@@ -1501,7 +1501,7 @@ describe('useTextBuffer', () => {
const { result } = renderHook(() =>
useTextBuffer({ viewport, isValidPath: () => false }),
);
act(() =>
act(() => {
result.current.handleInput({
name: 'tab',
shift: true,
@@ -1510,8 +1510,8 @@ describe('useTextBuffer', () => {
cmd: false,
insertable: false,
sequence: '\u001b[9;2u',
}),
);
});
});
expect(getBufferState(result).text).toBe('');
});
@@ -1524,7 +1524,7 @@ describe('useTextBuffer', () => {
}),
);
act(() => result.current.move('end'));
act(() =>
act(() => {
result.current.handleInput({
name: 'backspace',
shift: false,
@@ -1533,8 +1533,8 @@ describe('useTextBuffer', () => {
cmd: false,
insertable: false,
sequence: '\x7f',
}),
);
});
});
expect(getBufferState(result).text).toBe('');
});
@@ -1627,7 +1627,7 @@ describe('useTextBuffer', () => {
}),
);
act(() => result.current.move('end')); // cursor [0,2]
act(() =>
act(() => {
result.current.handleInput({
name: 'left',
shift: false,
@@ -1636,10 +1636,10 @@ describe('useTextBuffer', () => {
cmd: false,
insertable: false,
sequence: '\x1b[D',
}),
);
});
});
expect(getBufferState(result).cursor).toEqual([0, 1]);
act(() =>
act(() => {
result.current.handleInput({
name: 'right',
shift: false,
@@ -1648,8 +1648,8 @@ describe('useTextBuffer', () => {
cmd: false,
insertable: false,
sequence: '\x1b[C',
}),
);
});
});
expect(getBufferState(result).cursor).toEqual([0, 2]);
});
@@ -1659,7 +1659,7 @@ describe('useTextBuffer', () => {
);
const textWithAnsi = '\x1B[31mHello\x1B[0m \x1B[32mWorld\x1B[0m';
// Simulate pasting by calling handleInput with a string longer than 1 char
act(() =>
act(() => {
result.current.handleInput({
name: '',
shift: false,
@@ -1668,8 +1668,8 @@ describe('useTextBuffer', () => {
cmd: false,
insertable: true,
sequence: textWithAnsi,
}),
);
});
});
expect(getBufferState(result).text).toBe('Hello World');
});
@@ -1677,7 +1677,7 @@ describe('useTextBuffer', () => {
const { result } = renderHook(() =>
useTextBuffer({ viewport, isValidPath: () => false }),
);
act(() =>
act(() => {
result.current.handleInput({
name: 'return',
shift: true,
@@ -1686,8 +1686,8 @@ describe('useTextBuffer', () => {
cmd: false,
insertable: true,
sequence: '\r',
}),
); // Simulates Shift+Enter in VSCode terminal
});
}); // Simulates Shift+Enter in VSCode terminal
expect(getBufferState(result).lines).toEqual(['', '']);
});
@@ -1927,7 +1927,9 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots
const { result } = renderHook(() =>
useTextBuffer({ viewport, isValidPath: () => false }),
);
act(() => result.current.handleInput(createInput(input)));
act(() => {
result.current.handleInput(createInput(input));
});
expect(getBufferState(result).text).toBe(expected);
});
@@ -1936,7 +1938,9 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots
useTextBuffer({ viewport, isValidPath: () => false }),
);
const validText = 'Hello World\nThis is a test.';
act(() => result.current.handleInput(createInput(validText)));
act(() => {
result.current.handleInput(createInput(validText));
});
expect(getBufferState(result).text).toBe(validText);
});
@@ -1950,7 +1954,7 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots
expect(largeTextWithUnsafe.length).toBeGreaterThan(5000);
act(() =>
act(() => {
result.current.handleInput({
name: '',
shift: false,
@@ -1959,8 +1963,8 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots
cmd: false,
insertable: true,
sequence: largeTextWithUnsafe,
}),
);
});
});
const resultText = getBufferState(result).text;
expect(resultText).not.toContain('\x07');
@@ -1985,7 +1989,7 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots
expect(largeTextWithAnsi.length).toBeGreaterThan(5000);
act(() =>
act(() => {
result.current.handleInput({
name: '',
shift: false,
@@ -1994,8 +1998,8 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots
cmd: false,
insertable: true,
sequence: largeTextWithAnsi,
}),
);
});
});
const resultText = getBufferState(result).text;
expect(resultText).not.toContain('\x1B[31m');
@@ -2010,7 +2014,7 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots
useTextBuffer({ viewport, isValidPath: () => false }),
);
const emojis = '🐍🐳🦀🦄';
act(() =>
act(() => {
result.current.handleInput({
name: '',
shift: false,
@@ -2019,8 +2023,8 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots
cmd: false,
insertable: true,
sequence: emojis,
}),
);
});
});
expect(getBufferState(result).text).toBe(emojis);
});
});
@@ -2202,7 +2206,7 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots
singleLine: true,
}),
);
act(() =>
act(() => {
result.current.handleInput({
name: 'return',
shift: false,
@@ -2211,8 +2215,8 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots
cmd: false,
insertable: true,
sequence: '\r',
}),
);
});
});
expect(getBufferState(result).lines).toEqual(['']);
});
@@ -2224,7 +2228,7 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots
singleLine: true,
}),
);
act(() =>
act(() => {
result.current.handleInput({
name: 'f1',
shift: false,
@@ -2233,8 +2237,8 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots
cmd: false,
insertable: false,
sequence: '\u001bOP',
}),
);
});
});
expect(getBufferState(result).lines).toEqual(['']);
});

View File

@@ -3419,7 +3419,7 @@ export interface TextBuffer {
/**
* High level "handleInput" receives what Ink gives us.
*/
handleInput: (key: Key) => void;
handleInput: (key: Key) => boolean;
/**
* Opens the current buffer contents in the user's preferred terminal text
* editor ($VISUAL or $EDITOR, falling back to "vi"). The method blocks