diff options
| author | Lee Won Jun <[email protected]> | 2025-08-09 16:03:17 +0900 |
|---|---|---|
| committer | GitHub <[email protected]> | 2025-08-09 07:03:17 +0000 |
| commit | b8084ba8158b89facd49fd78a51abb80b1db54da (patch) | |
| tree | 5fd41e255b5118d53798c29d9fad95478a1ed582 /packages/cli/src/config | |
| parent | 6487cc16895976ef6c983f8beca08a64addb6688 (diff) | |
Centralize Key Binding Logic and Refactor (Reopen) (#5356)
Co-authored-by: Lee-WonJun <[email protected]>
Diffstat (limited to 'packages/cli/src/config')
| -rw-r--r-- | packages/cli/src/config/keyBindings.test.ts | 62 | ||||
| -rw-r--r-- | packages/cli/src/config/keyBindings.ts | 180 |
2 files changed, 242 insertions, 0 deletions
diff --git a/packages/cli/src/config/keyBindings.test.ts b/packages/cli/src/config/keyBindings.test.ts new file mode 100644 index 00000000..2e89e421 --- /dev/null +++ b/packages/cli/src/config/keyBindings.test.ts @@ -0,0 +1,62 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect } from 'vitest'; +import { + Command, + KeyBindingConfig, + defaultKeyBindings, +} from './keyBindings.js'; + +describe('keyBindings config', () => { + describe('defaultKeyBindings', () => { + it('should have bindings for all commands', () => { + const commands = Object.values(Command); + + for (const command of commands) { + expect(defaultKeyBindings[command]).toBeDefined(); + expect(Array.isArray(defaultKeyBindings[command])).toBe(true); + } + }); + + it('should have valid key binding structures', () => { + for (const [_, bindings] of Object.entries(defaultKeyBindings)) { + for (const binding of bindings) { + // Each binding should have either key or sequence, but not both + const hasKey = binding.key !== undefined; + const hasSequence = binding.sequence !== undefined; + + expect(hasKey || hasSequence).toBe(true); + expect(hasKey && hasSequence).toBe(false); + + // Modifier properties should be boolean or undefined + if (binding.ctrl !== undefined) { + expect(typeof binding.ctrl).toBe('boolean'); + } + if (binding.shift !== undefined) { + expect(typeof binding.shift).toBe('boolean'); + } + if (binding.command !== undefined) { + expect(typeof binding.command).toBe('boolean'); + } + if (binding.paste !== undefined) { + expect(typeof binding.paste).toBe('boolean'); + } + } + } + }); + + it('should export all required types', () => { + // Basic type checks + expect(typeof Command.HOME).toBe('string'); + expect(typeof Command.END).toBe('string'); + + // Config should be readonly + const config: KeyBindingConfig = defaultKeyBindings; + expect(config[Command.HOME]).toBeDefined(); + }); + }); +}); diff --git a/packages/cli/src/config/keyBindings.ts b/packages/cli/src/config/keyBindings.ts new file mode 100644 index 00000000..f6ba52e2 --- /dev/null +++ b/packages/cli/src/config/keyBindings.ts @@ -0,0 +1,180 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Command enum for all available keyboard shortcuts + */ +export enum Command { + // Basic bindings + RETURN = 'return', + ESCAPE = 'escape', + + // Cursor movement + HOME = 'home', + END = 'end', + + // Text deletion + KILL_LINE_RIGHT = 'killLineRight', + KILL_LINE_LEFT = 'killLineLeft', + CLEAR_INPUT = 'clearInput', + + // Screen control + CLEAR_SCREEN = 'clearScreen', + + // History navigation + HISTORY_UP = 'historyUp', + HISTORY_DOWN = 'historyDown', + NAVIGATION_UP = 'navigationUp', + NAVIGATION_DOWN = 'navigationDown', + + // Auto-completion + ACCEPT_SUGGESTION = 'acceptSuggestion', + + // Text input + SUBMIT = 'submit', + NEWLINE = 'newline', + + // External tools + OPEN_EXTERNAL_EDITOR = 'openExternalEditor', + PASTE_CLIPBOARD_IMAGE = 'pasteClipboardImage', + + // App level bindings + SHOW_ERROR_DETAILS = 'showErrorDetails', + TOGGLE_TOOL_DESCRIPTIONS = 'toggleToolDescriptions', + TOGGLE_IDE_CONTEXT_DETAIL = 'toggleIDEContextDetail', + QUIT = 'quit', + EXIT = 'exit', + SHOW_MORE_LINES = 'showMoreLines', + + // Shell commands + REVERSE_SEARCH = 'reverseSearch', + SUBMIT_REVERSE_SEARCH = 'submitReverseSearch', + ACCEPT_SUGGESTION_REVERSE_SEARCH = 'acceptSuggestionReverseSearch', +} + +/** + * Data-driven key binding structure for user configuration + */ +export interface KeyBinding { + /** The key name (e.g., 'a', 'return', 'tab', 'escape') */ + key?: string; + /** The key sequence (e.g., '\x18' for Ctrl+X) - alternative to key name */ + sequence?: string; + /** Control key requirement: true=must be pressed, false=must not be pressed, undefined=ignore */ + ctrl?: boolean; + /** Shift key requirement: true=must be pressed, false=must not be pressed, undefined=ignore */ + shift?: boolean; + /** Command/meta key requirement: true=must be pressed, false=must not be pressed, undefined=ignore */ + command?: boolean; + /** Paste operation requirement: true=must be paste, false=must not be paste, undefined=ignore */ + paste?: boolean; +} + +/** + * Configuration type mapping commands to their key bindings + */ +export type KeyBindingConfig = { + readonly [C in Command]: readonly KeyBinding[]; +}; + +/** + * Default key binding configuration + * Matches the original hard-coded logic exactly + */ +export const defaultKeyBindings: KeyBindingConfig = { + // Basic bindings + [Command.RETURN]: [{ key: 'return' }], + // Original: key.name === 'escape' + [Command.ESCAPE]: [{ key: 'escape' }], + + // Cursor movement + // Original: key.ctrl && key.name === 'a' + [Command.HOME]: [{ key: 'a', ctrl: true }], + // Original: key.ctrl && key.name === 'e' + [Command.END]: [{ key: 'e', ctrl: true }], + + // Text deletion + // Original: key.ctrl && key.name === 'k' + [Command.KILL_LINE_RIGHT]: [{ key: 'k', ctrl: true }], + // Original: key.ctrl && key.name === 'u' + [Command.KILL_LINE_LEFT]: [{ key: 'u', ctrl: true }], + // Original: key.ctrl && key.name === 'c' + [Command.CLEAR_INPUT]: [{ key: 'c', ctrl: true }], + + // Screen control + // Original: key.ctrl && key.name === 'l' + [Command.CLEAR_SCREEN]: [{ key: 'l', ctrl: true }], + + // History navigation + // Original: key.ctrl && key.name === 'p' + [Command.HISTORY_UP]: [{ key: 'p', ctrl: true }], + // Original: key.ctrl && key.name === 'n' + [Command.HISTORY_DOWN]: [{ key: 'n', ctrl: true }], + // Original: key.name === 'up' + [Command.NAVIGATION_UP]: [{ key: 'up' }], + // Original: key.name === 'down' + [Command.NAVIGATION_DOWN]: [{ key: 'down' }], + + // Auto-completion + // Original: key.name === 'tab' || (key.name === 'return' && !key.ctrl) + [Command.ACCEPT_SUGGESTION]: [{ key: 'tab' }, { key: 'return', ctrl: false }], + + // Text input + // Original: key.name === 'return' && !key.ctrl && !key.meta && !key.paste + [Command.SUBMIT]: [ + { + key: 'return', + ctrl: false, + command: false, + paste: false, + }, + ], + // Original: key.name === 'return' && (key.ctrl || key.meta || key.paste) + // Split into multiple data-driven bindings + [Command.NEWLINE]: [ + { key: 'return', ctrl: true }, + { key: 'return', command: true }, + { key: 'return', paste: true }, + ], + + // External tools + // Original: key.ctrl && (key.name === 'x' || key.sequence === '\x18') + [Command.OPEN_EXTERNAL_EDITOR]: [ + { key: 'x', ctrl: true }, + { sequence: '\x18', ctrl: true }, + ], + // Original: key.ctrl && key.name === 'v' + [Command.PASTE_CLIPBOARD_IMAGE]: [{ key: 'v', ctrl: true }], + + // App level bindings + // Original: key.ctrl && key.name === 'o' + [Command.SHOW_ERROR_DETAILS]: [{ key: 'o', ctrl: true }], + // Original: key.ctrl && key.name === 't' + [Command.TOGGLE_TOOL_DESCRIPTIONS]: [{ key: 't', ctrl: true }], + // Original: key.ctrl && key.name === 'e' + [Command.TOGGLE_IDE_CONTEXT_DETAIL]: [{ key: 'e', ctrl: true }], + // Original: key.ctrl && (key.name === 'c' || key.name === 'C') + [Command.QUIT]: [ + { key: 'c', ctrl: true }, + { key: 'C', ctrl: true }, + ], + // Original: key.ctrl && (key.name === 'd' || key.name === 'D') + [Command.EXIT]: [ + { key: 'd', ctrl: true }, + { key: 'D', ctrl: true }, + ], + // Original: key.ctrl && key.name === 's' + [Command.SHOW_MORE_LINES]: [{ key: 's', ctrl: true }], + + // Shell commands + // Original: key.ctrl && key.name === 'r' + [Command.REVERSE_SEARCH]: [{ key: 'r', ctrl: true }], + // Original: key.name === 'return' && !key.ctrl + // Note: original logic ONLY checked ctrl=false, ignored meta/shift/paste + [Command.SUBMIT_REVERSE_SEARCH]: [{ key: 'return', ctrl: false }], + // Original: key.name === 'tab' + [Command.ACCEPT_SUGGESTION_REVERSE_SEARCH]: [{ key: 'tab' }], +}; |
