mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-14 22:02:59 -07:00
fix(cli): implement platform-specific undo/redo and smart bubbling
This commit is contained in:
@@ -63,7 +63,7 @@ gemini --delete-session 1
|
||||
|
||||
## How to rewind time (Undo mistakes)
|
||||
|
||||
Gemini CLI's **Rewind** feature is like `Ctrl+Z` for your workflow.
|
||||
Gemini CLI's **Rewind** feature is like Undo for your workflow.
|
||||
|
||||
### Scenario: Triggering rewind
|
||||
|
||||
|
||||
@@ -499,12 +499,13 @@ the dedicated [Custom Commands documentation](../cli/custom-commands.md).
|
||||
These shortcuts apply directly to the input prompt for text manipulation.
|
||||
|
||||
- **Undo:**
|
||||
- **Keyboard shortcut:** Press **Alt+z** or **Cmd+z** to undo the last action
|
||||
in the input prompt.
|
||||
- **Keyboard shortcut:** Press **Ctrl+z** (Windows), **Cmd+z** (macOS), or
|
||||
**Alt+z** (Linux) to undo the last action in the input prompt.
|
||||
|
||||
- **Redo:**
|
||||
- **Keyboard shortcut:** Press **Shift+Alt+Z** or **Shift+Cmd+Z** to redo the
|
||||
last undone action in the input prompt.
|
||||
- **Keyboard shortcut:** Press **Ctrl+y** (Windows), **Shift+Cmd+Z** (macOS),
|
||||
or **Shift+Alt+Z** (Linux) to redo the last undone action in the input
|
||||
prompt.
|
||||
|
||||
## At commands (`@`)
|
||||
|
||||
|
||||
@@ -39,8 +39,8 @@ available combinations.
|
||||
| `edit.deleteWordRight` | Delete the next word. | `Ctrl+Delete`<br />`Alt+Delete`<br />`Alt+D` |
|
||||
| `edit.deleteLeft` | Delete the character to the left. | `Backspace`<br />`Ctrl+H` |
|
||||
| `edit.deleteRight` | Delete the character to the right. | `Delete`<br />`Ctrl+D` |
|
||||
| `edit.undo` | Undo the most recent text edit. | `Cmd/Win+Z`<br />`Alt+Z` |
|
||||
| `edit.redo` | Redo the most recent undone text edit. | `Ctrl+Shift+Z`<br />`Shift+Cmd/Win+Z`<br />`Alt+Shift+Z` |
|
||||
| `edit.undo` | Undo the most recent text edit. | `Ctrl+Z` (Windows)<br />`Cmd+Z` (macOS)<br />`Alt+Z` (Linux) |
|
||||
| `edit.redo` | Redo the most recent undone text edit. | `Ctrl+Y` (Windows)<br />`Ctrl+Shift+Z`<br />`Shift+Cmd+Z`<br />`Alt+Shift+Z` |
|
||||
|
||||
#### Scrolling
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user