fix(cli): implement platform-specific undo/redo and smart bubbling

This commit is contained in:
Coco Sheng
2026-04-29 11:58:45 -04:00
parent f8603e990b
commit 85b46a640a
7 changed files with 236 additions and 13 deletions
@@ -41,6 +41,7 @@ import {
getTransformedImagePath,
} from './text-buffer.js';
import { cpLen } from '../../utils/textUtils.js';
import { type Key } from '../../hooks/useKeypress.js';
import { escapePath } from '@google/gemini-cli-core';
const defaultVisualLayout: VisualLayout = {
@@ -1799,6 +1800,180 @@ describe('useTextBuffer', () => {
expect(getBufferState(result).text).toBe('');
});
it('should only handle Undo if there is something to undo', async () => {
const { result } = await renderHook(() => useTextBuffer({ viewport }));
// Platform-specific undo key
const undoKey: Key =
process.platform === 'win32'
? {
name: 'z',
ctrl: true,
shift: false,
alt: false,
cmd: false,
insertable: false,
sequence: '\x1a',
}
: process.platform === 'darwin'
? {
name: 'z',
ctrl: false,
shift: false,
alt: false,
cmd: true,
insertable: false,
sequence: '\u001b[122;D',
}
: {
name: 'z',
ctrl: false,
shift: false,
alt: true,
cmd: false,
insertable: false,
sequence: '\u001bz',
};
// 1. Initial state: nothing to undo
let handled = true;
act(() => {
handled = result.current.handleInput(undoKey);
});
expect(handled).toBe(false);
// 2. Insert something
act(() => {
result.current.handleInput({
name: 'a',
shift: false,
alt: false,
ctrl: false,
cmd: false,
insertable: true,
sequence: 'a',
});
});
expect(getBufferState(result).text).toBe('a');
// 3. Now undo should work
act(() => {
handled = result.current.handleInput(undoKey);
});
expect(handled).toBe(true);
expect(getBufferState(result).text).toBe('');
// 4. Undo again: nothing left to undo
act(() => {
handled = result.current.handleInput(undoKey);
});
expect(handled).toBe(false);
});
it('should only handle Redo if there is something to redo', async () => {
const { result } = await renderHook(() => useTextBuffer({ viewport }));
// Platform-specific redo key (first in list)
const redoKey: Key =
process.platform === 'win32'
? {
name: 'y',
ctrl: true,
shift: false,
alt: false,
cmd: false,
insertable: false,
sequence: '\x19',
}
: process.platform === 'darwin'
? {
name: 'z',
ctrl: false,
shift: true,
alt: false,
cmd: true,
insertable: false,
sequence: '\u001b[122;2D',
}
: {
name: 'z',
ctrl: false,
shift: true,
alt: true,
cmd: false,
insertable: false,
sequence: '\u001bZ',
};
const undoKey: Key =
process.platform === 'win32'
? {
name: 'z',
ctrl: true,
shift: false,
alt: false,
cmd: false,
insertable: false,
sequence: '\x1a',
}
: process.platform === 'darwin'
? {
name: 'z',
ctrl: false,
shift: false,
alt: false,
cmd: true,
insertable: false,
sequence: '\u001b[122;D',
}
: {
name: 'z',
ctrl: false,
shift: false,
alt: true,
cmd: false,
insertable: false,
sequence: '\u001bz',
};
// 1. Initial state: nothing to redo
let handled = true;
act(() => {
handled = result.current.handleInput(redoKey);
});
expect(handled).toBe(false);
// 2. Insert and Undo
act(() => {
result.current.handleInput({
name: 'a',
shift: false,
alt: false,
ctrl: false,
cmd: false,
insertable: true,
sequence: 'a',
});
});
act(() => {
result.current.handleInput(undoKey);
});
expect(getBufferState(result).text).toBe('');
// 3. Now redo should work
act(() => {
handled = result.current.handleInput(redoKey);
});
expect(handled).toBe(true);
expect(getBufferState(result).text).toBe('a');
// 4. Redo again: nothing left to redo
act(() => {
handled = result.current.handleInput(redoKey);
});
expect(handled).toBe(false);
});
it('should handle multiple delete characters in one input', async () => {
const { result } = await renderHook(() =>
useTextBuffer({
@@ -2889,6 +2889,8 @@ export function useTextBuffer({
transformationsByLine,
pastedContent,
expandedPaste,
undoStack,
redoStack,
} = state;
const text = useMemo(() => lines.join('\n'), [lines]);
@@ -3454,10 +3456,16 @@ export function useTextBuffer({
return true;
}
if (keyMatchers[Command.UNDO](key)) {
if (undoStack.length === 0) {
return false;
}
undo();
return true;
}
if (keyMatchers[Command.REDO](key)) {
if (redoStack.length === 0) {
return false;
}
redo();
return true;
}
@@ -3486,6 +3494,8 @@ export function useTextBuffer({
visualCursor,
visualLines,
keyMatchers,
undoStack.length,
redoStack.length,
],
);
@@ -108,6 +108,36 @@ describe('keyBindings config', () => {
}
});
it('should have platform-specific UNDO bindings', () => {
const undoBindings = defaultKeyBindingConfig.get(Command.UNDO);
if (process.platform === 'win32') {
expect(undoBindings?.[0].name).toBe('z');
expect(undoBindings?.[0].ctrl).toBe(true);
} else if (process.platform === 'darwin') {
expect(undoBindings?.[0].name).toBe('z');
expect(undoBindings?.[0].cmd).toBe(true);
} else {
expect(undoBindings?.[0].name).toBe('z');
expect(undoBindings?.[0].alt).toBe(true);
}
});
it('should have platform-specific REDO bindings', () => {
const redoBindings = defaultKeyBindingConfig.get(Command.REDO);
if (process.platform === 'win32') {
expect(redoBindings?.[0].name).toBe('y');
expect(redoBindings?.[0].ctrl).toBe(true);
} else if (process.platform === 'darwin') {
expect(redoBindings?.[0].name).toBe('z');
expect(redoBindings?.[0].shift).toBe(true);
expect(redoBindings?.[0].cmd).toBe(true);
} else {
expect(redoBindings?.[0].name).toBe('z');
expect(redoBindings?.[0].shift).toBe(true);
expect(redoBindings?.[0].alt).toBe(true);
}
});
describe('command metadata', () => {
const commandValues = Object.values(Command);
+13 -6
View File
@@ -312,14 +312,21 @@ export const defaultKeyBindingConfig: KeyBindingConfig = new Map([
Command.DELETE_CHAR_RIGHT,
[new KeyBinding('delete'), new KeyBinding('ctrl+d')],
],
[Command.UNDO, [new KeyBinding('cmd+z'), new KeyBinding('alt+z')]],
[
Command.UNDO,
process.platform === 'win32'
? [new KeyBinding('ctrl+z'), new KeyBinding('alt+z')]
: process.platform === 'darwin'
? [new KeyBinding('cmd+z'), new KeyBinding('alt+z')]
: [new KeyBinding('alt+z'), new KeyBinding('cmd+z')],
],
[
Command.REDO,
[
new KeyBinding('ctrl+shift+z'),
new KeyBinding('cmd+shift+z'),
new KeyBinding('alt+shift+z'),
],
process.platform === 'win32'
? [new KeyBinding('ctrl+y'), new KeyBinding('ctrl+shift+z')]
: process.platform === 'darwin'
? [new KeyBinding('cmd+shift+z'), new KeyBinding('alt+shift+z')]
: [new KeyBinding('alt+shift+z'), new KeyBinding('cmd+shift+z')],
],
// Scrolling