summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDeWitt Clinton <[email protected]>2025-05-14 17:33:37 -0700
committerGitHub <[email protected]>2025-05-14 17:33:37 -0700
commitaec6c0861e8244cdaefda14840844e922705c8fa (patch)
tree040c8cc37513d05ec72d855c918ebeccce3a9787
parentff36c937338d534d7d0ee87944c60f130aa10d23 (diff)
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").
-rw-r--r--packages/cli/src/ui/App.tsx7
-rw-r--r--packages/cli/src/ui/components/InputPrompt.tsx39
-rw-r--r--packages/cli/src/ui/components/shared/multiline-editor.tsx26
-rw-r--r--packages/cli/src/ui/components/shared/text-buffer.ts60
4 files changed, 127 insertions, 5 deletions
diff --git a/packages/cli/src/ui/App.tsx b/packages/cli/src/ui/App.tsx
index e1ae8da3..e4cd5de9 100644
--- a/packages/cli/src/ui/App.tsx
+++ b/packages/cli/src/ui/App.tsx
@@ -180,6 +180,11 @@ export const App = ({
[setQuery, setEditorState],
);
+ const handleClearScreen = useCallback(() => {
+ clearItems();
+ refreshStatic();
+ }, [clearItems, refreshStatic]);
+
const completion = useCompletion(
query,
config.getTargetDir(),
@@ -305,6 +310,8 @@ export const App = ({
navigateSuggestionUp={completion.navigateUp}
navigateSuggestionDown={completion.navigateDown}
resetCompletion={completion.resetCompletionState}
+ setEditorState={setEditorState}
+ onClearScreen={handleClearScreen} // Added onClearScreen prop
/>
{completion.showSuggestions && (
<Box>
diff --git a/packages/cli/src/ui/components/InputPrompt.tsx b/packages/cli/src/ui/components/InputPrompt.tsx
index 1c3d2a07..b1e05554 100644
--- a/packages/cli/src/ui/components/InputPrompt.tsx
+++ b/packages/cli/src/ui/components/InputPrompt.tsx
@@ -24,6 +24,8 @@ interface InputPromptProps {
userMessages: readonly string[];
navigateSuggestionUp: () => void;
navigateSuggestionDown: () => void;
+ setEditorState: (updater: (prevState: EditorState) => EditorState) => void;
+ onClearScreen: () => void;
}
export interface EditorState {
@@ -44,6 +46,8 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
navigateSuggestionUp,
navigateSuggestionDown,
resetCompletion,
+ setEditorState,
+ onClearScreen,
}) => {
const handleSubmit = useCallback(
(submittedValue: string) => {
@@ -106,7 +110,12 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
);
const inputPreprocessor = useCallback(
- (input: string, key: Key) => {
+ (
+ input: string,
+ key: Key,
+ _currentText?: string,
+ _cursorOffset?: number,
+ ) => {
if (showSuggestions) {
if (key.upArrow) {
navigateSuggestionUp();
@@ -136,6 +145,31 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
resetCompletion();
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;
},
@@ -149,6 +183,9 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
resetCompletion,
activeSuggestionIndex,
handleSubmit,
+ inputHistory,
+ setEditorState,
+ onClearScreen,
],
);
diff --git a/packages/cli/src/ui/components/shared/multiline-editor.tsx b/packages/cli/src/ui/components/shared/multiline-editor.tsx
index bd49efcb..e1e21fff 100644
--- a/packages/cli/src/ui/components/shared/multiline-editor.tsx
+++ b/packages/cli/src/ui/components/shared/multiline-editor.tsx
@@ -43,7 +43,12 @@ 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) => boolean;
+ readonly inputPreprocessor?: (
+ input: string,
+ key: Key,
+ currentText: string,
+ cursorOffset: number,
+ ) => boolean;
// Optional initial cursor position (character offset)
readonly initialCursorOffset?: number;
@@ -92,7 +97,24 @@ export const MultilineTextEditor = ({
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;
}
diff --git a/packages/cli/src/ui/components/shared/text-buffer.ts b/packages/cli/src/ui/components/shared/text-buffer.ts
index f2cb1ae2..661df70c 100644
--- a/packages/cli/src/ui/components/shared/text-buffer.ts
+++ b/packages/cli/src/ui/components/shared/text-buffer.ts
@@ -372,9 +372,9 @@ export function useTextBuffer({
pushUndo,
cursorRow,
cursorCol,
- lines,
currentLine,
currentLineLen,
+ lines.length,
setPreferredCol,
]);
@@ -521,12 +521,58 @@ export function useTextBuffer({
pushUndo,
cursorRow,
cursorCol,
- lines,
currentLine,
del,
+ lines.length,
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(
(dir: Direction): void => {
const before = [cursorRow, cursorCol];
@@ -772,6 +818,8 @@ export function useTextBuffer({
replaceRange,
deleteWordLeft,
deleteWordRight,
+ killLineRight,
+ killLineLeft,
handleInput,
openInExternalEditor,
@@ -873,6 +921,14 @@ export interface TextBuffer {
*/
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.
*/
handleInput: (