From 8d3e93cdb0d7cec5ab62f8afaa5cf9b7797f00d5 Mon Sep 17 00:00:00 2001 From: Tommaso Sciortino Date: Mon, 12 Jan 2026 16:28:10 -0800 Subject: [PATCH] Migrate keybindings (#16460) --- docs/cli/keyboard-shortcuts.md | 49 ++++++-------- packages/cli/src/config/keyBindings.ts | 65 +++++++++++++++++- packages/cli/src/ui/components/Help.tsx | 2 +- .../src/ui/components/shared/text-buffer.ts | 43 ++++-------- packages/cli/src/ui/constants/tips.ts | 2 +- packages/cli/src/ui/keyMatchers.test.ts | 67 ++++++++++++++++++- 6 files changed, 165 insertions(+), 63 deletions(-) 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 {