diff --git a/docs/cli/keyboard-shortcuts.md b/docs/cli/keyboard-shortcuts.md
index e6960bcde5..54defec914 100644
--- a/docs/cli/keyboard-shortcuts.md
+++ b/docs/cli/keyboard-shortcuts.md
@@ -15,19 +15,28 @@ available combinations.
#### Cursor Movement
-| Action | Keys |
-| ----------------------------------------- | ---------------------- |
-| Move the cursor to the start of the line. | `Ctrl + A`
`Home` |
-| Move the cursor to the end of the line. | `Ctrl + E`
`End` |
+| Action | Keys |
+| ------------------------------------------- | ------------------------------------------------------------ |
+| Move the cursor to the start of the line. | `Ctrl + A`
`Home` |
+| Move the cursor to the end of the line. | `Ctrl + E`
`End` |
+| Move the cursor one character to the left. | `Left Arrow (no Ctrl, no Cmd)`
`Ctrl + B` |
+| Move the cursor one character to the right. | `Right Arrow (no Ctrl, no Cmd)`
`Ctrl + F` |
+| Move the cursor one word to the left. | `Ctrl + Left Arrow`
`Cmd + Left Arrow`
`Cmd + B` |
+| Move the cursor one word to the right. | `Ctrl + Right Arrow`
`Cmd + Right Arrow`
`Cmd + F` |
#### Editing
-| Action | Keys |
-| ------------------------------------------------ | ----------------------------------------- |
-| Delete from the cursor to the end of the line. | `Ctrl + K` |
-| Delete from the cursor to the start of the line. | `Ctrl + U` |
-| Clear all text in the input field. | `Ctrl + C` |
-| Delete the previous word. | `Ctrl + Backspace`
`Cmd + Backspace` |
+| Action | Keys |
+| ------------------------------------------------ | -------------------------------------------------------------------------------------------- |
+| Delete from the cursor to the end of the line. | `Ctrl + K` |
+| Delete from the cursor to the start of the line. | `Ctrl + U` |
+| Clear all text in the input field. | `Ctrl + C` |
+| Delete the previous word. | `Ctrl + Backspace`
`Cmd + Backspace`
`Ctrl + ""`
`Cmd + ""`
`Ctrl + W` |
+| Delete the next word. | `Ctrl + Delete`
`Cmd + Delete` |
+| Delete the character to the left. | `Backspace`
`""`
`Ctrl + H` |
+| Delete the character to the right. | `Delete`
`Ctrl + D` |
+| Undo the most recent text edit. | `Ctrl + Z (no Shift)` |
+| Redo the most recent undone text edit. | `Ctrl + Shift + Z` |
#### Screen Control
@@ -115,27 +124,11 @@ available combinations.
## Additional context-specific shortcuts
-- `Option+M` (macOS): Entering `ยต` with Option+M also toggles Markdown
- rendering, matching `Cmd+M`.
+- `Option+B/F/M` (macOS only): Are interpreted as `Cmd+B/F/M` even if your
+ terminal isn't configured to send Meta with Option.
- `!` on an empty prompt: Enter or exit shell mode.
- `\` (at end of a line) + `Enter`: Insert a newline without leaving single-line
mode.
-- `Ctrl+Delete` / `Meta+Delete`: Delete the word to the right of the cursor.
-- `Ctrl+B` or `Left Arrow`: Move the cursor one character to the left while
- editing text.
-- `Ctrl+F` or `Right Arrow`: Move the cursor one character to the right.
-- `Ctrl+D` or `Delete`: Remove the character immediately to the right of the
- cursor.
-- `Ctrl+H` or `Backspace`: Remove the character immediately to the left of the
- cursor.
-- `Ctrl+Left Arrow` / `Meta+Left Arrow` / `Meta+B`: Move one word to the left.
-- `Ctrl+Right Arrow` / `Meta+Right Arrow` / `Meta+F`: Move one word to the
- right.
-- `Ctrl+W`: Delete the word to the left of the cursor (in addition to
- `Ctrl+Backspace` / `Cmd+Backspace`).
-- `Ctrl+Z` / `Ctrl+Shift+Z`: Undo or redo the most recent text edit.
-- `Meta+Enter`: Open the current input in an external editor (alias for
- `Ctrl+X`).
- `Esc` pressed twice quickly: Clear the current input buffer.
- `Up Arrow` / `Down Arrow`: When the cursor is at the top or bottom of a
single-line input, navigate backward or forward through prompt history.
diff --git a/packages/cli/src/config/keyBindings.ts b/packages/cli/src/config/keyBindings.ts
index 06819e382a..ba7b2e10a3 100644
--- a/packages/cli/src/config/keyBindings.ts
+++ b/packages/cli/src/config/keyBindings.ts
@@ -64,6 +64,15 @@ export enum Command {
TOGGLE_COPY_MODE = 'toggleCopyMode',
TOGGLE_YOLO = 'toggleYolo',
TOGGLE_AUTO_EDIT = 'toggleAutoEdit',
+ UNDO = 'undo',
+ REDO = 'redo',
+ MOVE_LEFT = 'moveLeft',
+ MOVE_RIGHT = 'moveRight',
+ MOVE_WORD_LEFT = 'moveWordLeft',
+ MOVE_WORD_RIGHT = 'moveWordRight',
+ DELETE_CHAR_LEFT = 'deleteCharLeft',
+ DELETE_CHAR_RIGHT = 'deleteCharRight',
+ DELETE_WORD_FORWARD = 'deleteWordForward',
QUIT = 'quit',
EXIT = 'exit',
SHOW_MORE_LINES = 'showMoreLines',
@@ -126,6 +135,37 @@ export const defaultKeyBindings: KeyBindingConfig = {
[Command.DELETE_WORD_BACKWARD]: [
{ key: 'backspace', ctrl: true },
{ key: 'backspace', command: true },
+ { sequence: '\x7f', ctrl: true },
+ { sequence: '\x7f', command: true },
+ { key: 'w', ctrl: true },
+ ],
+ [Command.MOVE_LEFT]: [
+ { key: 'left', ctrl: false, command: false },
+ { key: 'b', ctrl: true },
+ ],
+ [Command.MOVE_RIGHT]: [
+ { key: 'right', ctrl: false, command: false },
+ { key: 'f', ctrl: true },
+ ],
+ [Command.MOVE_WORD_LEFT]: [
+ { key: 'left', ctrl: true },
+ { key: 'left', command: true },
+ { key: 'b', command: true },
+ ],
+ [Command.MOVE_WORD_RIGHT]: [
+ { key: 'right', ctrl: true },
+ { key: 'right', command: true },
+ { key: 'f', command: true },
+ ],
+ [Command.DELETE_CHAR_LEFT]: [
+ { key: 'backspace' },
+ { sequence: '\x7f' },
+ { key: 'h', ctrl: true },
+ ],
+ [Command.DELETE_CHAR_RIGHT]: [{ key: 'delete' }, { key: 'd', ctrl: true }],
+ [Command.DELETE_WORD_FORWARD]: [
+ { key: 'delete', ctrl: true },
+ { key: 'delete', command: true },
],
// Screen control
@@ -208,6 +248,8 @@ export const defaultKeyBindings: KeyBindingConfig = {
[Command.TOGGLE_COPY_MODE]: [{ key: 's', ctrl: true }],
[Command.TOGGLE_YOLO]: [{ key: 'y', ctrl: true }],
[Command.TOGGLE_AUTO_EDIT]: [{ key: 'tab', shift: true }],
+ [Command.UNDO]: [{ key: 'z', ctrl: true, shift: false }],
+ [Command.REDO]: [{ key: 'z', ctrl: true, shift: true }],
[Command.QUIT]: [{ key: 'c', ctrl: true }],
[Command.EXIT]: [{ key: 'd', ctrl: true }],
[Command.SHOW_MORE_LINES]: [{ key: 's', ctrl: true }],
@@ -242,7 +284,14 @@ export const commandCategories: readonly CommandCategory[] = [
},
{
title: 'Cursor Movement',
- commands: [Command.HOME, Command.END],
+ commands: [
+ Command.HOME,
+ Command.END,
+ Command.MOVE_LEFT,
+ Command.MOVE_RIGHT,
+ Command.MOVE_WORD_LEFT,
+ Command.MOVE_WORD_RIGHT,
+ ],
},
{
title: 'Editing',
@@ -251,6 +300,11 @@ export const commandCategories: readonly CommandCategory[] = [
Command.KILL_LINE_LEFT,
Command.CLEAR_INPUT,
Command.DELETE_WORD_BACKWARD,
+ Command.DELETE_WORD_FORWARD,
+ Command.DELETE_CHAR_LEFT,
+ Command.DELETE_CHAR_RIGHT,
+ Command.UNDO,
+ Command.REDO,
],
},
{
@@ -334,10 +388,19 @@ export const commandDescriptions: Readonly> = {
[Command.ESCAPE]: 'Dismiss dialogs or cancel the current focus.',
[Command.HOME]: 'Move the cursor to the start of the line.',
[Command.END]: 'Move the cursor to the end of the line.',
+ [Command.MOVE_LEFT]: 'Move the cursor one character to the left.',
+ [Command.MOVE_RIGHT]: 'Move the cursor one character to the right.',
+ [Command.MOVE_WORD_LEFT]: 'Move the cursor one word to the left.',
+ [Command.MOVE_WORD_RIGHT]: 'Move the cursor one word to the right.',
[Command.KILL_LINE_RIGHT]: 'Delete from the cursor to the end of the line.',
[Command.KILL_LINE_LEFT]: 'Delete from the cursor to the start of the line.',
[Command.CLEAR_INPUT]: 'Clear all text in the input field.',
[Command.DELETE_WORD_BACKWARD]: 'Delete the previous word.',
+ [Command.DELETE_WORD_FORWARD]: 'Delete the next word.',
+ [Command.DELETE_CHAR_LEFT]: 'Delete the character to the left.',
+ [Command.DELETE_CHAR_RIGHT]: 'Delete the character to the right.',
+ [Command.UNDO]: 'Undo the most recent text edit.',
+ [Command.REDO]: 'Redo the most recent undone text edit.',
[Command.CLEAR_SCREEN]: 'Clear the terminal screen and redraw the UI.',
[Command.SCROLL_UP]: 'Scroll content up.',
[Command.SCROLL_DOWN]: 'Scroll content down.',
diff --git a/packages/cli/src/ui/components/Help.tsx b/packages/cli/src/ui/components/Help.tsx
index 385f7edfa3..c32726475c 100644
--- a/packages/cli/src/ui/components/Help.tsx
+++ b/packages/cli/src/ui/components/Help.tsx
@@ -144,7 +144,7 @@ export const Help: React.FC = ({ commands }) => (
- {process.platform === 'darwin' ? 'Ctrl+X / Meta+Enter' : 'Ctrl+X'}
+ Ctrl+X
{' '}
- Open input in external editor
diff --git a/packages/cli/src/ui/components/shared/text-buffer.ts b/packages/cli/src/ui/components/shared/text-buffer.ts
index abcdbdc420..cdf689c24f 100644
--- a/packages/cli/src/ui/components/shared/text-buffer.ts
+++ b/packages/cli/src/ui/components/shared/text-buffer.ts
@@ -25,6 +25,7 @@ import {
} from '../../utils/textUtils.js';
import { parsePastedPaths } from '../../utils/clipboardUtils.js';
import type { Key } from '../../contexts/KeypressContext.js';
+import { keyMatchers, Command } from '../../keyMatchers.js';
import type { VimAction } from './vim-buffer-actions.js';
import { handleVimAction } from './vim-buffer-actions.js';
@@ -2220,38 +2221,20 @@ export function useTextBuffer({
input === '\\r') // VSCode terminal represents shift + enter this way
)
newline();
- else if (key.name === 'left' && !key.meta && !key.ctrl) move('left');
- else if (key.ctrl && key.name === 'b') move('left');
- else if (key.name === 'right' && !key.meta && !key.ctrl) move('right');
- else if (key.ctrl && key.name === 'f') move('right');
+ else if (keyMatchers[Command.MOVE_LEFT](key)) move('left');
+ else if (keyMatchers[Command.MOVE_RIGHT](key)) move('right');
else if (key.name === 'up') move('up');
else if (key.name === 'down') move('down');
- else if ((key.ctrl || key.meta) && key.name === 'left') move('wordLeft');
- else if (key.meta && key.name === 'b') move('wordLeft');
- else if ((key.ctrl || key.meta) && key.name === 'right')
- move('wordRight');
- else if (key.meta && key.name === 'f') move('wordRight');
- else if (key.name === 'home') move('home');
- else if (key.ctrl && key.name === 'a') move('home');
- else if (key.name === 'end') move('end');
- else if (key.ctrl && key.name === 'e') move('end');
- else if (key.ctrl && key.name === 'w') deleteWordLeft();
- else if (
- (key.meta || key.ctrl) &&
- (key.name === 'backspace' || input === '\x7f')
- )
- deleteWordLeft();
- else if ((key.meta || key.ctrl) && key.name === 'delete')
- deleteWordRight();
- else if (
- key.name === 'backspace' ||
- input === '\x7f' ||
- (key.ctrl && key.name === 'h')
- )
- backspace();
- else if (key.name === 'delete' || (key.ctrl && key.name === 'd')) del();
- else if (key.ctrl && !key.shift && key.name === 'z') undo();
- else if (key.ctrl && key.shift && key.name === 'z') redo();
+ else if (keyMatchers[Command.MOVE_WORD_LEFT](key)) move('wordLeft');
+ else if (keyMatchers[Command.MOVE_WORD_RIGHT](key)) move('wordRight');
+ else if (keyMatchers[Command.HOME](key)) move('home');
+ else if (keyMatchers[Command.END](key)) move('end');
+ else if (keyMatchers[Command.DELETE_WORD_BACKWARD](key)) deleteWordLeft();
+ else if (keyMatchers[Command.DELETE_WORD_FORWARD](key)) deleteWordRight();
+ else if (keyMatchers[Command.DELETE_CHAR_LEFT](key)) backspace();
+ else if (keyMatchers[Command.DELETE_CHAR_RIGHT](key)) del();
+ else if (keyMatchers[Command.UNDO](key)) undo();
+ else if (keyMatchers[Command.REDO](key)) redo();
else if (key.insertable) {
insert(input, { paste: key.paste });
}
diff --git a/packages/cli/src/ui/constants/tips.ts b/packages/cli/src/ui/constants/tips.ts
index a18205ff36..7322718d06 100644
--- a/packages/cli/src/ui/constants/tips.ts
+++ b/packages/cli/src/ui/constants/tips.ts
@@ -112,7 +112,7 @@ export const INFORMATIVE_TIPS = [
'Paste from your clipboard with Ctrl+V...',
'Undo text edits in the input with Ctrl+Z...',
'Redo undone text edits with Ctrl+Shift+Z...',
- 'Open the current prompt in an external editor with Ctrl+X or Meta+Enter...',
+ 'Open the current prompt in an external editor with Ctrl+X...',
'In menus, move up/down with k/j or the arrow keys...',
'In menus, select an item by typing its number...',
"If you're using an IDE, see the context with Ctrl+G...",
diff --git a/packages/cli/src/ui/keyMatchers.test.ts b/packages/cli/src/ui/keyMatchers.test.ts
index 8ddfd0371d..2cf98b7b9c 100644
--- a/packages/cli/src/ui/keyMatchers.test.ts
+++ b/packages/cli/src/ui/keyMatchers.test.ts
@@ -39,7 +39,7 @@ describe('keyMatchers', () => {
// Cursor movement
{
command: Command.HOME,
- positive: [createKey('a', { ctrl: true })],
+ positive: [createKey('a', { ctrl: true }), createKey('home')],
negative: [
createKey('a'),
createKey('a', { shift: true }),
@@ -48,13 +48,41 @@ describe('keyMatchers', () => {
},
{
command: Command.END,
- positive: [createKey('e', { ctrl: true })],
+ positive: [createKey('e', { ctrl: true }), createKey('end')],
negative: [
createKey('e'),
createKey('e', { shift: true }),
createKey('a', { ctrl: true }),
],
},
+ {
+ command: Command.MOVE_LEFT,
+ positive: [createKey('left'), createKey('b', { ctrl: true })],
+ negative: [createKey('left', { ctrl: true }), createKey('b')],
+ },
+ {
+ command: Command.MOVE_RIGHT,
+ positive: [createKey('right'), createKey('f', { ctrl: true })],
+ negative: [createKey('right', { ctrl: true }), createKey('f')],
+ },
+ {
+ command: Command.MOVE_WORD_LEFT,
+ positive: [
+ createKey('left', { ctrl: true }),
+ createKey('left', { meta: true }),
+ createKey('b', { meta: true }),
+ ],
+ negative: [createKey('left'), createKey('b', { ctrl: true })],
+ },
+ {
+ command: Command.MOVE_WORD_RIGHT,
+ positive: [
+ createKey('right', { ctrl: true }),
+ createKey('right', { meta: true }),
+ createKey('f', { meta: true }),
+ ],
+ negative: [createKey('right'), createKey('f', { ctrl: true })],
+ },
// Text deletion
{
@@ -72,14 +100,49 @@ describe('keyMatchers', () => {
positive: [createKey('c', { ctrl: true })],
negative: [createKey('c'), createKey('k', { ctrl: true })],
},
+ {
+ command: Command.DELETE_CHAR_LEFT,
+ positive: [
+ createKey('backspace'),
+ { ...createKey('\x7f'), sequence: '\x7f' },
+ createKey('h', { ctrl: true }),
+ ],
+ negative: [createKey('h'), createKey('x', { ctrl: true })],
+ },
+ {
+ command: Command.DELETE_CHAR_RIGHT,
+ positive: [createKey('delete'), createKey('d', { ctrl: true })],
+ negative: [createKey('d'), createKey('x', { ctrl: true })],
+ },
{
command: Command.DELETE_WORD_BACKWARD,
positive: [
createKey('backspace', { ctrl: true }),
createKey('backspace', { meta: true }),
+ { ...createKey('\x7f', { ctrl: true }), sequence: '\x7f' },
+ { ...createKey('\x7f', { meta: true }), sequence: '\x7f' },
+ createKey('w', { ctrl: true }),
],
negative: [createKey('backspace'), createKey('delete', { ctrl: true })],
},
+ {
+ command: Command.DELETE_WORD_FORWARD,
+ positive: [
+ createKey('delete', { ctrl: true }),
+ createKey('delete', { meta: true }),
+ ],
+ negative: [createKey('delete'), createKey('backspace', { ctrl: true })],
+ },
+ {
+ command: Command.UNDO,
+ positive: [createKey('z', { ctrl: true, shift: false })],
+ negative: [createKey('z'), createKey('z', { ctrl: true, shift: true })],
+ },
+ {
+ command: Command.REDO,
+ positive: [createKey('z', { ctrl: true, shift: true })],
+ negative: [createKey('z'), createKey('z', { ctrl: true, shift: false })],
+ },
// Screen control
{