mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-15 16:41:11 -07:00
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:
@@ -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.",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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(['']);
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user