feat(tui): add Alt+D to delete word forward (kill)

Adds deleteWordForward action bound to Alt+D, which deletes from cursor
to the end of the current word and saves to kill ring for later yanking.
Consecutive forward kills append to the same kill ring entry.
This commit is contained in:
Sviatoslav Abakumov 2026-01-17 18:54:23 +04:00 committed by Mario Zechner
parent 9fb7434a06
commit 505894f4ea
4 changed files with 80 additions and 0 deletions

View file

@ -3437,6 +3437,7 @@ export class InteractiveMode {
const submit = this.getEditorKeyDisplay("submit");
const newLine = this.getEditorKeyDisplay("newLine");
const deleteWordBackward = this.getEditorKeyDisplay("deleteWordBackward");
const deleteWordForward = this.getEditorKeyDisplay("deleteWordForward");
const deleteToLineStart = this.getEditorKeyDisplay("deleteToLineStart");
const deleteToLineEnd = this.getEditorKeyDisplay("deleteToLineEnd");
const yank = this.getEditorKeyDisplay("yank");
@ -3471,6 +3472,7 @@ export class InteractiveMode {
| \`${submit}\` | Send message |
| \`${newLine}\` | New line${process.platform === "win32" ? " (Ctrl+Enter on Windows Terminal)" : ""} |
| \`${deleteWordBackward}\` | Delete word backwards |
| \`${deleteWordForward}\` | Delete word forwards |
| \`${deleteToLineStart}\` | Delete to start of line |
| \`${deleteToLineEnd}\` | Delete to end of line |
| \`${yank}\` | Paste the most-recently-deleted text |

View file

@ -646,6 +646,10 @@ export class Editor implements Component, Focusable {
this.deleteWordBackwards();
return;
}
if (kb.matches(data, "deleteWordForward")) {
this.deleteWordForward();
return;
}
if (kb.matches(data, "deleteCharBackward") || matchesKey(data, "shift+backspace")) {
this.handleBackspace();
return;
@ -1242,6 +1246,46 @@ export class Editor implements Component, Focusable {
}
}
private deleteWordForward(): void {
this.historyIndex = -1; // Exit history browsing mode
const currentLine = this.state.lines[this.state.cursorLine] || "";
// If at end of line, merge with next line (delete the newline)
if (this.state.cursorCol >= currentLine.length) {
if (this.state.cursorLine < this.state.lines.length - 1) {
// Treat newline as deleted text (forward deletion = append)
this.addToKillRing("\n", false);
this.lastAction = "kill";
const nextLine = this.state.lines[this.state.cursorLine + 1] || "";
this.state.lines[this.state.cursorLine] = currentLine + nextLine;
this.state.lines.splice(this.state.cursorLine + 1, 1);
}
} else {
// Save lastAction before cursor movement (moveWordForwards resets it)
const wasKill = this.lastAction === "kill";
const oldCursorCol = this.state.cursorCol;
this.moveWordForwards();
const deleteTo = this.state.cursorCol;
this.state.cursorCol = oldCursorCol;
// Restore kill state for accumulation check, then save to kill ring
this.lastAction = wasKill ? "kill" : null;
const deletedText = currentLine.slice(this.state.cursorCol, deleteTo);
this.addToKillRing(deletedText, false);
this.lastAction = "kill";
this.state.lines[this.state.cursorLine] =
currentLine.slice(0, this.state.cursorCol) + currentLine.slice(deleteTo);
}
if (this.onChange) {
this.onChange(this.getText());
}
}
private handleForwardDelete(): void {
this.historyIndex = -1; // Exit history browsing mode
this.lastAction = null;

View file

@ -19,6 +19,7 @@ export type EditorAction =
| "deleteCharBackward"
| "deleteCharForward"
| "deleteWordBackward"
| "deleteWordForward"
| "deleteToLineStart"
| "deleteToLineEnd"
// Text input
@ -69,6 +70,7 @@ export const DEFAULT_EDITOR_KEYBINDINGS: Required<EditorKeybindingsConfig> = {
deleteCharBackward: "backspace",
deleteCharForward: "delete",
deleteWordBackward: ["ctrl+w", "alt+backspace"],
deleteWordForward: "alt+d",
deleteToLineStart: "ctrl+u",
deleteToLineEnd: "ctrl+k",
// Text input

View file

@ -1062,5 +1062,37 @@ describe("Editor component", () => {
editor.handleInput("\x1by"); // Alt+Y
assert.strictEqual(editor.getText(), "hello SINGLEworld");
});
it("Alt+D deletes word forward and saves to kill ring", () => {
const editor = new Editor(createTestTUI(), defaultEditorTheme);
editor.setText("hello world test");
editor.handleInput("\x01"); // Ctrl+A - go to start
editor.handleInput("\x1bd"); // Alt+D - deletes "hello"
assert.strictEqual(editor.getText(), " world test");
editor.handleInput("\x1bd"); // Alt+D - deletes " world" (skips whitespace, then word)
assert.strictEqual(editor.getText(), " test");
// Yank should get accumulated text
editor.handleInput("\x19"); // Ctrl+Y
assert.strictEqual(editor.getText(), "hello world test");
});
it("Alt+D at end of line deletes newline", () => {
const editor = new Editor(createTestTUI(), defaultEditorTheme);
editor.setText("line1\nline2");
// Move to start of document, then to end of first line
editor.handleInput("\x1b[A"); // Up arrow - go to first line
editor.handleInput("\x05"); // Ctrl+E - end of line
editor.handleInput("\x1bd"); // Alt+D - deletes newline (merges lines)
assert.strictEqual(editor.getText(), "line1line2");
editor.handleInput("\x19"); // Ctrl+Y
assert.strictEqual(editor.getText(), "line1\nline2");
});
});
});