diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 8bff3e99..534d03aa 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -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 | diff --git a/packages/tui/src/components/editor.ts b/packages/tui/src/components/editor.ts index f880bef5..b7fc164a 100644 --- a/packages/tui/src/components/editor.ts +++ b/packages/tui/src/components/editor.ts @@ -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; diff --git a/packages/tui/src/keybindings.ts b/packages/tui/src/keybindings.ts index 30432e61..8859e135 100644 --- a/packages/tui/src/keybindings.ts +++ b/packages/tui/src/keybindings.ts @@ -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 = { deleteCharBackward: "backspace", deleteCharForward: "delete", deleteWordBackward: ["ctrl+w", "alt+backspace"], + deleteWordForward: "alt+d", deleteToLineStart: "ctrl+u", deleteToLineEnd: "ctrl+k", // Text input diff --git a/packages/tui/test/editor.test.ts b/packages/tui/test/editor.test.ts index dcf1ddcf..e46d0cea 100644 --- a/packages/tui/test/editor.test.ts +++ b/packages/tui/test/editor.test.ts @@ -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"); + }); }); });