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.
This commit is contained in:
DeWitt Clinton 2025-05-20 10:12:07 -07:00 committed by GitHub
parent 6ca446bded
commit ee702c3139
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 12 additions and 45 deletions

View File

@ -375,7 +375,6 @@ export const App = ({
navigateSuggestionUp={completion.navigateUp} navigateSuggestionUp={completion.navigateUp}
navigateSuggestionDown={completion.navigateDown} navigateSuggestionDown={completion.navigateDown}
resetCompletion={completion.resetCompletionState} resetCompletion={completion.resetCompletionState}
setEditorState={setEditorState}
onClearScreen={handleClearScreen} // Added onClearScreen prop onClearScreen={handleClearScreen} // Added onClearScreen prop
shellModeActive={shellModeActive} shellModeActive={shellModeActive}
setShellModeActive={setShellModeActive} setShellModeActive={setShellModeActive}

View File

@ -24,7 +24,6 @@ interface InputPromptProps {
userMessages: readonly string[]; userMessages: readonly string[];
navigateSuggestionUp: () => void; navigateSuggestionUp: () => void;
navigateSuggestionDown: () => void; navigateSuggestionDown: () => void;
setEditorState: (updater: (prevState: EditorState) => EditorState) => void;
onClearScreen: () => void; onClearScreen: () => void;
shellModeActive: boolean; shellModeActive: boolean;
setShellModeActive: (value: boolean) => void; setShellModeActive: (value: boolean) => void;
@ -48,7 +47,6 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
navigateSuggestionUp, navigateSuggestionUp,
navigateSuggestionDown, navigateSuggestionDown,
resetCompletion, resetCompletion,
setEditorState,
onClearScreen, onClearScreen,
shellModeActive, shellModeActive,
setShellModeActive, setShellModeActive,
@ -114,12 +112,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
); );
const inputPreprocessor = useCallback( const inputPreprocessor = useCallback(
( (input: string, key: Key) => {
input: string,
key: Key,
_currentText?: string,
_cursorOffset?: number,
) => {
if (input === '!' && query === '' && !showSuggestions) { if (input === '!' && query === '' && !showSuggestions) {
setShellModeActive(!shellModeActive); setShellModeActive(!shellModeActive);
onChangeAndMoveCursor(''); // Clear the '!' from input onChangeAndMoveCursor(''); // Clear the '!' from input
@ -156,17 +149,6 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
} }
} else { } else {
// Keybindings when suggestions are not shown // 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') { if (key.ctrl && input === 'l') {
onClearScreen(); onClearScreen();
return true; return true;
@ -193,7 +175,6 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
activeSuggestionIndex, activeSuggestionIndex,
handleSubmit, handleSubmit,
inputHistory, inputHistory,
setEditorState,
onClearScreen, onClearScreen,
shellModeActive, shellModeActive,
setShellModeActive, setShellModeActive,

View File

@ -44,12 +44,7 @@ export interface MultilineTextEditorProps {
// Called on all key events to allow the caller. Returns true if the // Called on all key events to allow the caller. Returns true if the
// event was handled and should not be passed to the editor. // event was handled and should not be passed to the editor.
readonly inputPreprocessor?: ( readonly inputPreprocessor?: (input: string, key: Key) => boolean;
input: string,
key: Key,
currentText: string,
cursorOffset: number,
) => boolean;
// Optional initial cursor position (character offset) // Optional initial cursor position (character offset)
readonly initialCursorOffset?: number; readonly initialCursorOffset?: number;
@ -98,14 +93,7 @@ export const MultilineTextEditor = ({
return; return;
} }
// Calculate cursorOffset for inputPreprocessor if (inputPreprocessor?.(input, key) === true) {
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) {
return; return;
} }
@ -121,14 +109,7 @@ export const MultilineTextEditor = ({
const isCtrlX = const isCtrlX =
(key.ctrl && (input === 'x' || input === '\x18')) || input === '\x18'; (key.ctrl && (input === 'x' || input === '\x18')) || input === '\x18';
const isCtrlE = if (isCtrlX) {
(key.ctrl && (input === 'e' || input === '\x05')) ||
input === '\x05' ||
(!key.ctrl &&
input === 'e' &&
input.length === 1 &&
input.charCodeAt(0) === 5);
if (isCtrlX || isCtrlE) {
buffer.openInExternalEditor(); buffer.openInExternalEditor();
return; return;
} }

View File

@ -1104,16 +1104,22 @@ export function useTextBuffer({
if (key['return'] || input === '\r' || input === '\n') newline(); if (key['return'] || input === '\r' || input === '\n') newline();
else if (key['leftArrow'] && !key['meta'] && !key['ctrl'] && !key['alt']) else if (key['leftArrow'] && !key['meta'] && !key['ctrl'] && !key['alt'])
move('left'); move('left');
else if (key['ctrl'] && input === 'b') move('left');
else if (key['rightArrow'] && !key['meta'] && !key['ctrl'] && !key['alt']) else if (key['rightArrow'] && !key['meta'] && !key['ctrl'] && !key['alt'])
move('right'); move('right');
else if (key['ctrl'] && input === 'f') move('right');
else if (key['upArrow']) move('up'); else if (key['upArrow']) move('up');
else if (key['downArrow']) move('down'); 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'); 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'); move('wordRight');
else if (key['meta'] && input === 'f') move('wordRight');
else if (key['home']) move('home'); else if (key['home']) move('home');
else if (key['ctrl'] && input === 'a') move('home');
else if (key['end']) move('end'); else if (key['end']) move('end');
else if (key['ctrl'] && input === 'e') move('end');
else if ( else if (
(key['meta'] || key['ctrl'] || key['alt']) && (key['meta'] || key['ctrl'] || key['alt']) &&
(key['backspace'] || input === '\x7f') (key['backspace'] || input === '\x7f')