From ee702c3139a2fe966407379891cae0e3a148891e Mon Sep 17 00:00:00 2001 From: DeWitt Clinton Date: Tue, 20 May 2025 10:12:07 -0700 Subject: [PATCH] Implement additional readline-like keybindings, including alt-left arrow and alt-right arrow. (#443) This change adds keybinding support for: - `Ctrl+B`: Moves the cursor backward one character. - `Ctrl+F`: Moves the cursor forward one character. - `Alt+Left Arrow`: Moves the cursor backward one word. - `Alt+Right Arrow`: Moves the cursor forward one word. Closes b/411469305. --- packages/cli/src/ui/App.tsx | 1 - .../cli/src/ui/components/InputPrompt.tsx | 21 +--------------- .../ui/components/shared/multiline-editor.tsx | 25 +++---------------- .../src/ui/components/shared/text-buffer.ts | 10 ++++++-- 4 files changed, 12 insertions(+), 45 deletions(-) diff --git a/packages/cli/src/ui/App.tsx b/packages/cli/src/ui/App.tsx index 972f4522..67aee9b5 100644 --- a/packages/cli/src/ui/App.tsx +++ b/packages/cli/src/ui/App.tsx @@ -375,7 +375,6 @@ export const App = ({ navigateSuggestionUp={completion.navigateUp} navigateSuggestionDown={completion.navigateDown} resetCompletion={completion.resetCompletionState} - setEditorState={setEditorState} onClearScreen={handleClearScreen} // Added onClearScreen prop shellModeActive={shellModeActive} setShellModeActive={setShellModeActive} diff --git a/packages/cli/src/ui/components/InputPrompt.tsx b/packages/cli/src/ui/components/InputPrompt.tsx index f77ac4d6..38af2a8c 100644 --- a/packages/cli/src/ui/components/InputPrompt.tsx +++ b/packages/cli/src/ui/components/InputPrompt.tsx @@ -24,7 +24,6 @@ interface InputPromptProps { userMessages: readonly string[]; navigateSuggestionUp: () => void; navigateSuggestionDown: () => void; - setEditorState: (updater: (prevState: EditorState) => EditorState) => void; onClearScreen: () => void; shellModeActive: boolean; setShellModeActive: (value: boolean) => void; @@ -48,7 +47,6 @@ export const InputPrompt: React.FC = ({ navigateSuggestionUp, navigateSuggestionDown, resetCompletion, - setEditorState, onClearScreen, shellModeActive, setShellModeActive, @@ -114,12 +112,7 @@ export const InputPrompt: React.FC = ({ ); const inputPreprocessor = useCallback( - ( - input: string, - key: Key, - _currentText?: string, - _cursorOffset?: number, - ) => { + (input: string, key: Key) => { if (input === '!' && query === '' && !showSuggestions) { setShellModeActive(!shellModeActive); onChangeAndMoveCursor(''); // Clear the '!' from input @@ -156,17 +149,6 @@ export const InputPrompt: React.FC = ({ } } else { // Keybindings when suggestions are not shown - if (key.ctrl && input === 'a') { - setEditorState((s) => ({ key: s.key + 1, initialCursorOffset: 0 })); - return true; - } - if (key.ctrl && input === 'e') { - setEditorState((s) => ({ - key: s.key + 1, - initialCursorOffset: query.length, - })); - return true; - } if (key.ctrl && input === 'l') { onClearScreen(); return true; @@ -193,7 +175,6 @@ export const InputPrompt: React.FC = ({ activeSuggestionIndex, handleSubmit, inputHistory, - setEditorState, onClearScreen, shellModeActive, setShellModeActive, diff --git a/packages/cli/src/ui/components/shared/multiline-editor.tsx b/packages/cli/src/ui/components/shared/multiline-editor.tsx index a89acfd1..890a9b47 100644 --- a/packages/cli/src/ui/components/shared/multiline-editor.tsx +++ b/packages/cli/src/ui/components/shared/multiline-editor.tsx @@ -44,12 +44,7 @@ export interface MultilineTextEditorProps { // Called on all key events to allow the caller. Returns true if the // event was handled and should not be passed to the editor. - readonly inputPreprocessor?: ( - input: string, - key: Key, - currentText: string, - cursorOffset: number, - ) => boolean; + readonly inputPreprocessor?: (input: string, key: Key) => boolean; // Optional initial cursor position (character offset) readonly initialCursorOffset?: number; @@ -98,14 +93,7 @@ export const MultilineTextEditor = ({ return; } - // Calculate cursorOffset for inputPreprocessor - let charOffset = 0; - for (let i = 0; i < buffer.cursor[0]; i++) { - charOffset += buffer.lines[i].length + 1; // +1 for newline - } - charOffset += buffer.cursor[1]; - - if (inputPreprocessor?.(input, key, buffer.text, charOffset) === true) { + if (inputPreprocessor?.(input, key) === true) { return; } @@ -121,14 +109,7 @@ export const MultilineTextEditor = ({ const isCtrlX = (key.ctrl && (input === 'x' || input === '\x18')) || input === '\x18'; - const isCtrlE = - (key.ctrl && (input === 'e' || input === '\x05')) || - input === '\x05' || - (!key.ctrl && - input === 'e' && - input.length === 1 && - input.charCodeAt(0) === 5); - if (isCtrlX || isCtrlE) { + if (isCtrlX) { buffer.openInExternalEditor(); return; } diff --git a/packages/cli/src/ui/components/shared/text-buffer.ts b/packages/cli/src/ui/components/shared/text-buffer.ts index b13a57c6..f84d83bc 100644 --- a/packages/cli/src/ui/components/shared/text-buffer.ts +++ b/packages/cli/src/ui/components/shared/text-buffer.ts @@ -1104,16 +1104,22 @@ export function useTextBuffer({ if (key['return'] || input === '\r' || input === '\n') newline(); else if (key['leftArrow'] && !key['meta'] && !key['ctrl'] && !key['alt']) move('left'); + else if (key['ctrl'] && input === 'b') move('left'); else if (key['rightArrow'] && !key['meta'] && !key['ctrl'] && !key['alt']) move('right'); + else if (key['ctrl'] && input === 'f') move('right'); else if (key['upArrow']) move('up'); else if (key['downArrow']) move('down'); - else if ((key['meta'] || key['ctrl'] || key['alt']) && key['leftArrow']) + else if ((key['ctrl'] || key['alt']) && key['leftArrow']) move('wordLeft'); - else if ((key['meta'] || key['ctrl'] || key['alt']) && key['rightArrow']) + else if (key['meta'] && input === 'b') move('wordLeft'); + else if ((key['ctrl'] || key['alt']) && key['rightArrow']) move('wordRight'); + else if (key['meta'] && input === 'f') move('wordRight'); else if (key['home']) move('home'); + else if (key['ctrl'] && input === 'a') move('home'); else if (key['end']) move('end'); + else if (key['ctrl'] && input === 'e') move('end'); else if ( (key['meta'] || key['ctrl'] || key['alt']) && (key['backspace'] || input === '\x7f')