mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-21 19:40:40 -07:00
feat(cli): Add W, B, E Vim motions and operator support (#16209)
Co-authored-by: Tommaso Sciortino <sciortino@gmail.com>
This commit is contained in:
@@ -156,6 +156,15 @@ describe('useVim hook', () => {
|
||||
vimMoveWordForward: vi.fn(),
|
||||
vimMoveWordBackward: vi.fn(),
|
||||
vimMoveWordEnd: vi.fn(),
|
||||
vimMoveBigWordForward: vi.fn(),
|
||||
vimMoveBigWordBackward: vi.fn(),
|
||||
vimMoveBigWordEnd: vi.fn(),
|
||||
vimDeleteBigWordForward: vi.fn(),
|
||||
vimDeleteBigWordBackward: vi.fn(),
|
||||
vimDeleteBigWordEnd: vi.fn(),
|
||||
vimChangeBigWordForward: vi.fn(),
|
||||
vimChangeBigWordBackward: vi.fn(),
|
||||
vimChangeBigWordEnd: vi.fn(),
|
||||
vimDeleteChar: vi.fn(),
|
||||
vimInsertAtCursor: vi.fn(),
|
||||
vimAppendAtCursor: vi.fn().mockImplementation(() => {
|
||||
@@ -570,6 +579,105 @@ describe('useVim hook', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Big Word movement', () => {
|
||||
it('should handle W (next big word)', () => {
|
||||
const testBuffer = createMockBuffer('hello world test');
|
||||
const { result } = renderVimHook(testBuffer);
|
||||
exitInsertMode(result);
|
||||
|
||||
act(() => {
|
||||
result.current.handleInput(createKey({ sequence: 'W' }));
|
||||
});
|
||||
|
||||
expect(testBuffer.vimMoveBigWordForward).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it('should handle B (previous big word)', () => {
|
||||
const testBuffer = createMockBuffer('hello world test', [0, 6]);
|
||||
const { result } = renderVimHook(testBuffer);
|
||||
exitInsertMode(result);
|
||||
|
||||
act(() => {
|
||||
result.current.handleInput(createKey({ sequence: 'B' }));
|
||||
});
|
||||
|
||||
expect(testBuffer.vimMoveBigWordBackward).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it('should handle E (end of big word)', () => {
|
||||
const testBuffer = createMockBuffer('hello world test');
|
||||
const { result } = renderVimHook(testBuffer);
|
||||
exitInsertMode(result);
|
||||
|
||||
act(() => {
|
||||
result.current.handleInput(createKey({ sequence: 'E' }));
|
||||
});
|
||||
|
||||
expect(testBuffer.vimMoveBigWordEnd).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it('should handle dW (delete big word forward)', () => {
|
||||
const testBuffer = createMockBuffer('hello.world test', [0, 0]);
|
||||
const { result } = renderVimHook(testBuffer);
|
||||
exitInsertMode(result);
|
||||
|
||||
act(() => {
|
||||
result.current.handleInput(createKey({ sequence: 'd' }));
|
||||
});
|
||||
act(() => {
|
||||
result.current.handleInput(createKey({ sequence: 'W' }));
|
||||
});
|
||||
|
||||
expect(testBuffer.vimDeleteBigWordForward).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it('should handle cW (change big word forward)', () => {
|
||||
const testBuffer = createMockBuffer('hello.world test', [0, 0]);
|
||||
const { result } = renderVimHook(testBuffer);
|
||||
exitInsertMode(result);
|
||||
|
||||
act(() => {
|
||||
result.current.handleInput(createKey({ sequence: 'c' }));
|
||||
});
|
||||
act(() => {
|
||||
result.current.handleInput(createKey({ sequence: 'W' }));
|
||||
});
|
||||
|
||||
expect(testBuffer.vimChangeBigWordForward).toHaveBeenCalledWith(1);
|
||||
expect(result.current.mode).toBe('INSERT');
|
||||
});
|
||||
|
||||
it('should handle dB (delete big word backward)', () => {
|
||||
const testBuffer = createMockBuffer('hello.world test', [0, 11]);
|
||||
const { result } = renderVimHook(testBuffer);
|
||||
exitInsertMode(result);
|
||||
|
||||
act(() => {
|
||||
result.current.handleInput(createKey({ sequence: 'd' }));
|
||||
});
|
||||
act(() => {
|
||||
result.current.handleInput(createKey({ sequence: 'B' }));
|
||||
});
|
||||
|
||||
expect(testBuffer.vimDeleteBigWordBackward).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it('should handle dE (delete big word end)', () => {
|
||||
const testBuffer = createMockBuffer('hello.world test', [0, 0]);
|
||||
const { result } = renderVimHook(testBuffer);
|
||||
exitInsertMode(result);
|
||||
|
||||
act(() => {
|
||||
result.current.handleInput(createKey({ sequence: 'd' }));
|
||||
});
|
||||
act(() => {
|
||||
result.current.handleInput(createKey({ sequence: 'E' }));
|
||||
});
|
||||
|
||||
expect(testBuffer.vimDeleteBigWordEnd).toHaveBeenCalledWith(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Disabled vim mode', () => {
|
||||
it('should not respond to vim commands when disabled', () => {
|
||||
mockVimContext.vimEnabled = false;
|
||||
|
||||
@@ -24,9 +24,15 @@ const CMD_TYPES = {
|
||||
DELETE_WORD_FORWARD: 'dw',
|
||||
DELETE_WORD_BACKWARD: 'db',
|
||||
DELETE_WORD_END: 'de',
|
||||
DELETE_BIG_WORD_FORWARD: 'dW',
|
||||
DELETE_BIG_WORD_BACKWARD: 'dB',
|
||||
DELETE_BIG_WORD_END: 'dE',
|
||||
CHANGE_WORD_FORWARD: 'cw',
|
||||
CHANGE_WORD_BACKWARD: 'cb',
|
||||
CHANGE_WORD_END: 'ce',
|
||||
CHANGE_BIG_WORD_FORWARD: 'cW',
|
||||
CHANGE_BIG_WORD_BACKWARD: 'cB',
|
||||
CHANGE_BIG_WORD_END: 'cE',
|
||||
DELETE_CHAR: 'x',
|
||||
DELETE_LINE: 'dd',
|
||||
CHANGE_LINE: 'cc',
|
||||
@@ -187,6 +193,21 @@ export function useVim(buffer: TextBuffer, onSubmit?: (value: string) => void) {
|
||||
break;
|
||||
}
|
||||
|
||||
case CMD_TYPES.DELETE_BIG_WORD_FORWARD: {
|
||||
buffer.vimDeleteBigWordForward(count);
|
||||
break;
|
||||
}
|
||||
|
||||
case CMD_TYPES.DELETE_BIG_WORD_BACKWARD: {
|
||||
buffer.vimDeleteBigWordBackward(count);
|
||||
break;
|
||||
}
|
||||
|
||||
case CMD_TYPES.DELETE_BIG_WORD_END: {
|
||||
buffer.vimDeleteBigWordEnd(count);
|
||||
break;
|
||||
}
|
||||
|
||||
case CMD_TYPES.CHANGE_WORD_FORWARD: {
|
||||
buffer.vimChangeWordForward(count);
|
||||
updateMode('INSERT');
|
||||
@@ -205,6 +226,24 @@ export function useVim(buffer: TextBuffer, onSubmit?: (value: string) => void) {
|
||||
break;
|
||||
}
|
||||
|
||||
case CMD_TYPES.CHANGE_BIG_WORD_FORWARD: {
|
||||
buffer.vimChangeBigWordForward(count);
|
||||
updateMode('INSERT');
|
||||
break;
|
||||
}
|
||||
|
||||
case CMD_TYPES.CHANGE_BIG_WORD_BACKWARD: {
|
||||
buffer.vimChangeBigWordBackward(count);
|
||||
updateMode('INSERT');
|
||||
break;
|
||||
}
|
||||
|
||||
case CMD_TYPES.CHANGE_BIG_WORD_END: {
|
||||
buffer.vimChangeBigWordEnd(count);
|
||||
updateMode('INSERT');
|
||||
break;
|
||||
}
|
||||
|
||||
case CMD_TYPES.DELETE_CHAR: {
|
||||
buffer.vimDeleteChar(count);
|
||||
break;
|
||||
@@ -371,7 +410,10 @@ export function useVim(buffer: TextBuffer, onSubmit?: (value: string) => void) {
|
||||
* @returns boolean indicating if command was handled
|
||||
*/
|
||||
const handleOperatorMotion = useCallback(
|
||||
(operator: 'd' | 'c', motion: 'w' | 'b' | 'e'): boolean => {
|
||||
(
|
||||
operator: 'd' | 'c',
|
||||
motion: 'w' | 'b' | 'e' | 'W' | 'B' | 'E',
|
||||
): boolean => {
|
||||
const count = getCurrentCount();
|
||||
|
||||
const commandMap = {
|
||||
@@ -379,11 +421,17 @@ export function useVim(buffer: TextBuffer, onSubmit?: (value: string) => void) {
|
||||
w: CMD_TYPES.DELETE_WORD_FORWARD,
|
||||
b: CMD_TYPES.DELETE_WORD_BACKWARD,
|
||||
e: CMD_TYPES.DELETE_WORD_END,
|
||||
W: CMD_TYPES.DELETE_BIG_WORD_FORWARD,
|
||||
B: CMD_TYPES.DELETE_BIG_WORD_BACKWARD,
|
||||
E: CMD_TYPES.DELETE_BIG_WORD_END,
|
||||
},
|
||||
c: {
|
||||
w: CMD_TYPES.CHANGE_WORD_FORWARD,
|
||||
b: CMD_TYPES.CHANGE_WORD_BACKWARD,
|
||||
e: CMD_TYPES.CHANGE_WORD_END,
|
||||
W: CMD_TYPES.CHANGE_BIG_WORD_FORWARD,
|
||||
B: CMD_TYPES.CHANGE_BIG_WORD_BACKWARD,
|
||||
E: CMD_TYPES.CHANGE_BIG_WORD_END,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -524,6 +572,21 @@ export function useVim(buffer: TextBuffer, onSubmit?: (value: string) => void) {
|
||||
return true;
|
||||
}
|
||||
|
||||
case 'W': {
|
||||
// Check if this is part of a delete or change command (dW/cW)
|
||||
if (state.pendingOperator === 'd') {
|
||||
return handleOperatorMotion('d', 'W');
|
||||
}
|
||||
if (state.pendingOperator === 'c') {
|
||||
return handleOperatorMotion('c', 'W');
|
||||
}
|
||||
|
||||
// Normal big word movement
|
||||
buffer.vimMoveBigWordForward(repeatCount);
|
||||
dispatch({ type: 'CLEAR_COUNT' });
|
||||
return true;
|
||||
}
|
||||
|
||||
case 'b': {
|
||||
// Check if this is part of a delete or change command (db/cb)
|
||||
if (state.pendingOperator === 'd') {
|
||||
@@ -539,6 +602,21 @@ export function useVim(buffer: TextBuffer, onSubmit?: (value: string) => void) {
|
||||
return true;
|
||||
}
|
||||
|
||||
case 'B': {
|
||||
// Check if this is part of a delete or change command (dB/cB)
|
||||
if (state.pendingOperator === 'd') {
|
||||
return handleOperatorMotion('d', 'B');
|
||||
}
|
||||
if (state.pendingOperator === 'c') {
|
||||
return handleOperatorMotion('c', 'B');
|
||||
}
|
||||
|
||||
// Normal backward big word movement
|
||||
buffer.vimMoveBigWordBackward(repeatCount);
|
||||
dispatch({ type: 'CLEAR_COUNT' });
|
||||
return true;
|
||||
}
|
||||
|
||||
case 'e': {
|
||||
// Check if this is part of a delete or change command (de/ce)
|
||||
if (state.pendingOperator === 'd') {
|
||||
@@ -554,6 +632,21 @@ export function useVim(buffer: TextBuffer, onSubmit?: (value: string) => void) {
|
||||
return true;
|
||||
}
|
||||
|
||||
case 'E': {
|
||||
// Check if this is part of a delete or change command (dE/cE)
|
||||
if (state.pendingOperator === 'd') {
|
||||
return handleOperatorMotion('d', 'E');
|
||||
}
|
||||
if (state.pendingOperator === 'c') {
|
||||
return handleOperatorMotion('c', 'E');
|
||||
}
|
||||
|
||||
// Normal big word end movement
|
||||
buffer.vimMoveBigWordEnd(repeatCount);
|
||||
dispatch({ type: 'CLEAR_COUNT' });
|
||||
return true;
|
||||
}
|
||||
|
||||
case 'x': {
|
||||
// Delete character under cursor
|
||||
buffer.vimDeleteChar(repeatCount);
|
||||
|
||||
Reference in New Issue
Block a user