From d1cb5678c7885c33eeddbffe9681db9e90738968 Mon Sep 17 00:00:00 2001 From: maoxin1234 <875408344@qq.com> Date: Sun, 13 Sep 2026 21:22:38 +0800 Subject: [PATCH 1/2] fix(cli): support physical delete and home/end navigation (#13265) --- extensions/cli/src/ui/TextBuffer.test.ts | 60 ++++++++++++++++++++++-- extensions/cli/src/ui/TextBuffer.ts | 50 +++++++++++++++++++- 2 files changed, 105 insertions(+), 5 deletions(-) diff --git a/extensions/cli/src/ui/TextBuffer.test.ts b/extensions/cli/src/ui/TextBuffer.test.ts index c6cc18cda53..1a3651d3b7e 100644 --- a/extensions/cli/src/ui/TextBuffer.test.ts +++ b/extensions/cli/src/ui/TextBuffer.test.ts @@ -664,11 +664,11 @@ describe("TextBuffer", () => { expect(buffer.cursor).toBe(2); }); - it("should handle delete key", () => { + it("should handle delete key as forward deletion", () => { const result = buffer.handleInput("", { delete: true } as any); expect(result).toBe(true); - expect(buffer.text).toBe("hllo"); - expect(buffer.cursor).toBe(1); + expect(buffer.text).toBe("helo"); + expect(buffer.cursor).toBe(2); }); it("should handle backspace key", () => { @@ -678,6 +678,60 @@ describe("TextBuffer", () => { expect(buffer.cursor).toBe(1); }); + it("should handle home and end keys on single-line text", () => { + let result = buffer.handleInput("", { home: true } as any); + expect(result).toBe(true); + expect(buffer.cursor).toBe(0); + + result = buffer.handleInput("", { end: true } as any); + expect(result).toBe(true); + expect(buffer.cursor).toBe(5); + }); + + it("should handle home and end keys on multi-line text", () => { + const text = "first line\nsecond line\nthird line"; + buffer.setText(text); + const secondLineStart = text.indexOf("second line"); + buffer.setCursor(secondLineStart + 4); + + let result = buffer.handleInput("", { home: true } as any); + expect(result).toBe(true); + expect(buffer.cursor).toBe(secondLineStart); + + result = buffer.handleInput("", { end: true } as any); + expect(result).toBe(true); + expect(buffer.cursor).toBe(secondLineStart + "second line".length); + }); + + it("should handle escape sequences for home, end, and delete", () => { + // Home sequence \u001b[1~ + let result = buffer.handleInput("\u001b[1~", {} as any); + expect(result).toBe(true); + expect(buffer.cursor).toBe(0); + + // End sequence \u001b[4~ + result = buffer.handleInput("\u001b[4~", {} as any); + expect(result).toBe(true); + expect(buffer.cursor).toBe(5); + + // Alternative Home sequence \u001b[H + result = buffer.handleInput("\u001b[H", {} as any); + expect(result).toBe(true); + expect(buffer.cursor).toBe(0); + + // Alternative End sequence \u001b[F + result = buffer.handleInput("\u001b[F", {} as any); + expect(result).toBe(true); + expect(buffer.cursor).toBe(5); + + // Forward delete sequence \u001b[3~ + buffer.setCursor(2); + result = buffer.handleInput("\u001b[3~", {} as any); + expect(result).toBe(true); + expect(buffer.text).toBe("helo"); + expect(buffer.cursor).toBe(2); + }); + it("should handle meta key combinations", () => { const result = buffer.handleInput("", { meta: true, diff --git a/extensions/cli/src/ui/TextBuffer.ts b/extensions/cli/src/ui/TextBuffer.ts index 768ab0b4b60..6be2fbee21d 100644 --- a/extensions/cli/src/ui/TextBuffer.ts +++ b/extensions/cli/src/ui/TextBuffer.ts @@ -177,6 +177,16 @@ export class TextBuffer { this._cursor = this._text.length; } + moveToLineStart(): void { + const lastNewline = this._text.lastIndexOf("\n", this._cursor - 1); + this._cursor = lastNewline === -1 ? 0 : lastNewline + 1; + } + + moveToLineEnd(): void { + const nextNewline = this._text.indexOf("\n", this._cursor); + this._cursor = nextNewline === -1 ? this._text.length : nextNewline; + } + moveLeft(): void { this._cursor = Math.max(0, this._cursor - 1); } @@ -420,6 +430,19 @@ export class TextBuffer { this.deleteWordBackward(); return true; } + // Standard terminal escape sequences for navigation / editing + if (sequence === "[1~" || sequence === "[H" || sequence === "OH") { + this.moveToLineStart(); + return true; + } + if (sequence === "[4~" || sequence === "[F" || sequence === "OF") { + this.moveToLineEnd(); + return true; + } + if (sequence === "[3~") { + this.deleteForward(); + return true; + } return true; // Consume other option sequences } @@ -477,6 +500,18 @@ export class TextBuffer { return false; } + private handleNavigationKeys(key: Key): boolean { + if (key.home && !key.meta) { + this.moveToLineStart(); + return true; + } + if (key.end && !key.meta) { + this.moveToLineEnd(); + return true; + } + return false; + } + private handleArrowKeys(key: Key): boolean { if (key.leftArrow && !key.meta) { this.moveLeft(); @@ -490,8 +525,14 @@ export class TextBuffer { } private handleDeleteKeys(key: Key): boolean { - // On Mac, backspace key registers as key.delete, so treat it as backward deletion - if ((key.delete || key.backspace) && !key.meta) { + if (key.meta) { + return false; + } + if (key.delete) { + this.deleteForward(); + return true; + } + if (key.backspace) { this.deleteBackward(); return true; } @@ -517,6 +558,11 @@ export class TextBuffer { return true; } + // Handle home / end navigation keys + if (this.handleNavigationKeys(key)) { + return true; + } + // Handle arrow keys if (this.handleArrowKeys(key)) { return true; From b7d98fdc0bed74e982cf9870a8cd3e6e34a9e414 Mon Sep 17 00:00:00 2001 From: maoxin1234 <875408344@qq.com> Date: Sun, 13 Sep 2026 22:07:12 +0800 Subject: [PATCH 2/2] fix(cli): cast Key to support optional home/end navigation properties --- extensions/cli/src/ui/TextBuffer.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/extensions/cli/src/ui/TextBuffer.ts b/extensions/cli/src/ui/TextBuffer.ts index 6be2fbee21d..be50022e91f 100644 --- a/extensions/cli/src/ui/TextBuffer.ts +++ b/extensions/cli/src/ui/TextBuffer.ts @@ -501,11 +501,12 @@ export class TextBuffer { } private handleNavigationKeys(key: Key): boolean { - if (key.home && !key.meta) { + const navKey = key as Key & { home?: boolean; end?: boolean }; + if (navKey.home && !key.meta) { this.moveToLineStart(); return true; } - if (key.end && !key.meta) { + if (navKey.end && !key.meta) { this.moveToLineEnd(); return true; }