Add readline-like keybindings to the input prompts. (#354)
New keybindings in the main input prompt (when auto-suggestions are not active): - `Ctrl+L`: Clears the entire screen. - `Ctrl+A`: Moves the cursor to the beginning of the current input line. - `Ctrl+E`: Moves the cursor to the end of the current input line. - `Ctrl+P`: Navigates to the previous command in the input history. - `Ctrl+N`: Navigates to the next command in the input history. In the multiline text editor (e.g., when editing a previous message): - `Ctrl+K`: Deletes text from the current cursor position to the end of the line ("kill line right").
This commit is contained in:
parent
ff36c93733
commit
aec6c0861e
|
@ -180,6 +180,11 @@ export const App = ({
|
||||||
[setQuery, setEditorState],
|
[setQuery, setEditorState],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleClearScreen = useCallback(() => {
|
||||||
|
clearItems();
|
||||||
|
refreshStatic();
|
||||||
|
}, [clearItems, refreshStatic]);
|
||||||
|
|
||||||
const completion = useCompletion(
|
const completion = useCompletion(
|
||||||
query,
|
query,
|
||||||
config.getTargetDir(),
|
config.getTargetDir(),
|
||||||
|
@ -305,6 +310,8 @@ 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
|
||||||
/>
|
/>
|
||||||
{completion.showSuggestions && (
|
{completion.showSuggestions && (
|
||||||
<Box>
|
<Box>
|
||||||
|
|
|
@ -24,6 +24,8 @@ interface InputPromptProps {
|
||||||
userMessages: readonly string[];
|
userMessages: readonly string[];
|
||||||
navigateSuggestionUp: () => void;
|
navigateSuggestionUp: () => void;
|
||||||
navigateSuggestionDown: () => void;
|
navigateSuggestionDown: () => void;
|
||||||
|
setEditorState: (updater: (prevState: EditorState) => EditorState) => void;
|
||||||
|
onClearScreen: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EditorState {
|
export interface EditorState {
|
||||||
|
@ -44,6 +46,8 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||||
navigateSuggestionUp,
|
navigateSuggestionUp,
|
||||||
navigateSuggestionDown,
|
navigateSuggestionDown,
|
||||||
resetCompletion,
|
resetCompletion,
|
||||||
|
setEditorState,
|
||||||
|
onClearScreen,
|
||||||
}) => {
|
}) => {
|
||||||
const handleSubmit = useCallback(
|
const handleSubmit = useCallback(
|
||||||
(submittedValue: string) => {
|
(submittedValue: string) => {
|
||||||
|
@ -106,7 +110,12 @@ 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 (showSuggestions) {
|
if (showSuggestions) {
|
||||||
if (key.upArrow) {
|
if (key.upArrow) {
|
||||||
navigateSuggestionUp();
|
navigateSuggestionUp();
|
||||||
|
@ -136,6 +145,31 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||||
resetCompletion();
|
resetCompletion();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
} 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;
|
||||||
|
}
|
||||||
|
if (key.ctrl && input === 'p') {
|
||||||
|
inputHistory.navigateUp();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (key.ctrl && input === 'n') {
|
||||||
|
inputHistory.navigateDown();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
@ -149,6 +183,9 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||||
resetCompletion,
|
resetCompletion,
|
||||||
activeSuggestionIndex,
|
activeSuggestionIndex,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
|
inputHistory,
|
||||||
|
setEditorState,
|
||||||
|
onClearScreen,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -43,7 +43,12 @@ 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?: (input: string, key: Key) => boolean;
|
readonly inputPreprocessor?: (
|
||||||
|
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;
|
||||||
|
@ -92,7 +97,24 @@ export const MultilineTextEditor = ({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inputPreprocessor?.(input, key) === true) {
|
// 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) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key.ctrl && input === 'k') {
|
||||||
|
buffer.killLineRight();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key.ctrl && input === 'u') {
|
||||||
|
buffer.killLineLeft();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -372,9 +372,9 @@ export function useTextBuffer({
|
||||||
pushUndo,
|
pushUndo,
|
||||||
cursorRow,
|
cursorRow,
|
||||||
cursorCol,
|
cursorCol,
|
||||||
lines,
|
|
||||||
currentLine,
|
currentLine,
|
||||||
currentLineLen,
|
currentLineLen,
|
||||||
|
lines.length,
|
||||||
setPreferredCol,
|
setPreferredCol,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -521,12 +521,58 @@ export function useTextBuffer({
|
||||||
pushUndo,
|
pushUndo,
|
||||||
cursorRow,
|
cursorRow,
|
||||||
cursorCol,
|
cursorCol,
|
||||||
lines,
|
|
||||||
currentLine,
|
currentLine,
|
||||||
del,
|
del,
|
||||||
|
lines.length,
|
||||||
setPreferredCol,
|
setPreferredCol,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const killLineRight = useCallback((): void => {
|
||||||
|
const lineContent = currentLine(cursorRow);
|
||||||
|
if (cursorCol < currentLineLen(cursorRow)) {
|
||||||
|
// Cursor is before the end of the line's content, delete text to the right
|
||||||
|
pushUndo();
|
||||||
|
setLines((prevLines) => {
|
||||||
|
const newLines = [...prevLines];
|
||||||
|
newLines[cursorRow] = cpSlice(lineContent, 0, cursorCol);
|
||||||
|
return newLines;
|
||||||
|
});
|
||||||
|
// Cursor position and preferredCol do not change in this case
|
||||||
|
} else if (
|
||||||
|
cursorCol === currentLineLen(cursorRow) &&
|
||||||
|
cursorRow < lines.length - 1
|
||||||
|
) {
|
||||||
|
// Cursor is at the end of the line's content (or line is empty),
|
||||||
|
// and it's not the last line. Delete the newline.
|
||||||
|
// `del()` handles pushUndo and setPreferredCol.
|
||||||
|
del();
|
||||||
|
}
|
||||||
|
// If cursor is at the end of the line and it's the last line, do nothing.
|
||||||
|
}, [
|
||||||
|
pushUndo,
|
||||||
|
cursorRow,
|
||||||
|
cursorCol,
|
||||||
|
currentLine,
|
||||||
|
currentLineLen,
|
||||||
|
lines.length,
|
||||||
|
del,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const killLineLeft = useCallback((): void => {
|
||||||
|
const lineContent = currentLine(cursorRow);
|
||||||
|
// Only act if the cursor is not at the beginning of the line
|
||||||
|
if (cursorCol > 0) {
|
||||||
|
pushUndo();
|
||||||
|
setLines((prevLines) => {
|
||||||
|
const newLines = [...prevLines];
|
||||||
|
newLines[cursorRow] = cpSlice(lineContent, cursorCol);
|
||||||
|
return newLines;
|
||||||
|
});
|
||||||
|
setCursorCol(0);
|
||||||
|
setPreferredCol(null);
|
||||||
|
}
|
||||||
|
}, [pushUndo, cursorRow, cursorCol, currentLine, setPreferredCol]);
|
||||||
|
|
||||||
const move = useCallback(
|
const move = useCallback(
|
||||||
(dir: Direction): void => {
|
(dir: Direction): void => {
|
||||||
const before = [cursorRow, cursorCol];
|
const before = [cursorRow, cursorCol];
|
||||||
|
@ -772,6 +818,8 @@ export function useTextBuffer({
|
||||||
replaceRange,
|
replaceRange,
|
||||||
deleteWordLeft,
|
deleteWordLeft,
|
||||||
deleteWordRight,
|
deleteWordRight,
|
||||||
|
killLineRight,
|
||||||
|
killLineLeft,
|
||||||
handleInput,
|
handleInput,
|
||||||
openInExternalEditor,
|
openInExternalEditor,
|
||||||
|
|
||||||
|
@ -872,6 +920,14 @@ export interface TextBuffer {
|
||||||
* follows the caret and the next contiguous run of word characters.
|
* follows the caret and the next contiguous run of word characters.
|
||||||
*/
|
*/
|
||||||
deleteWordRight: () => void;
|
deleteWordRight: () => void;
|
||||||
|
/**
|
||||||
|
* Deletes text from the cursor to the end of the current line.
|
||||||
|
*/
|
||||||
|
killLineRight: () => void;
|
||||||
|
/**
|
||||||
|
* Deletes text from the start of the current line to the cursor.
|
||||||
|
*/
|
||||||
|
killLineLeft: () => void;
|
||||||
/**
|
/**
|
||||||
* High level "handleInput" – receives what Ink gives us.
|
* High level "handleInput" – receives what Ink gives us.
|
||||||
*/
|
*/
|
||||||
|
|
Loading…
Reference in New Issue