diff --git a/packages/tui/src/components/editor.ts b/packages/tui/src/components/editor.ts index 03ec7243..f19b7288 100644 --- a/packages/tui/src/components/editor.ts +++ b/packages/tui/src/components/editor.ts @@ -232,6 +232,7 @@ export class Editor implements Component { // Bracketed paste mode buffering private pasteBuffer: string = ""; private isInPaste: boolean = false; + private pendingShiftEnter: boolean = false; // Prompt history for up/down navigation private history: string[] = []; @@ -422,6 +423,21 @@ export class Editor implements Component { return; } + if (this.pendingShiftEnter) { + if (data === "\r") { + this.pendingShiftEnter = false; + this.addNewLine(); + return; + } + this.pendingShiftEnter = false; + this.insertCharacter("\\"); + } + + if (data === "\\") { + this.pendingShiftEnter = true; + return; + } + // Ctrl+C - let parent handle (exit/clear) if (kb.matches(data, "copy")) { return; diff --git a/packages/tui/src/components/input.ts b/packages/tui/src/components/input.ts index 8ca7723a..642f83c1 100644 --- a/packages/tui/src/components/input.ts +++ b/packages/tui/src/components/input.ts @@ -16,6 +16,7 @@ export class Input implements Component { // Bracketed paste mode buffering private pasteBuffer: string = ""; private isInPaste: boolean = false; + private pendingShiftEnter: boolean = false; getValue(): string { return this.value; @@ -63,6 +64,23 @@ export class Input implements Component { } return; } + + if (this.pendingShiftEnter) { + if (data === "\r") { + this.pendingShiftEnter = false; + if (this.onSubmit) this.onSubmit(this.value); + return; + } + this.pendingShiftEnter = false; + this.value = `${this.value.slice(0, this.cursor)}\\${this.value.slice(this.cursor)}`; + this.cursor += 1; + } + + if (data === "\\") { + this.pendingShiftEnter = true; + return; + } + const kb = getEditorKeybindings(); // Escape/Cancel diff --git a/packages/tui/test/editor.test.ts b/packages/tui/test/editor.test.ts index b48f2628..3cbcbc00 100644 --- a/packages/tui/test/editor.test.ts +++ b/packages/tui/test/editor.test.ts @@ -275,6 +275,26 @@ describe("Editor component", () => { }); }); + describe("Shift+Enter handling", () => { + it("treats split VS Code Shift+Enter as a newline", () => { + const editor = new Editor(defaultEditorTheme); + + editor.handleInput("\\"); + editor.handleInput("\r"); + + assert.strictEqual(editor.getText(), "\n"); + }); + + it("inserts a literal backslash when not followed by Enter", () => { + const editor = new Editor(defaultEditorTheme); + + editor.handleInput("\\"); + editor.handleInput("x"); + + assert.strictEqual(editor.getText(), "\\x"); + }); + }); + describe("Unicode text editing behavior", () => { it("inserts mixed ASCII, umlauts, and emojis as literal text", () => { const editor = new Editor(defaultEditorTheme); diff --git a/packages/tui/test/input.test.ts b/packages/tui/test/input.test.ts new file mode 100644 index 00000000..98bd71d4 --- /dev/null +++ b/packages/tui/test/input.test.ts @@ -0,0 +1,30 @@ +import assert from "node:assert"; +import { describe, it } from "node:test"; +import { Input } from "../src/components/input.js"; + +describe("Input component", () => { + it("treats split VS Code Shift+Enter as submit", () => { + const input = new Input(); + let submitted: string | undefined; + + input.setValue("hello"); + input.onSubmit = (value) => { + submitted = value; + }; + + input.handleInput("\\"); + input.handleInput("\r"); + + assert.strictEqual(submitted, "hello"); + assert.strictEqual(input.getValue(), "hello"); + }); + + it("inserts a literal backslash when not followed by Enter", () => { + const input = new Input(); + + input.handleInput("\\"); + input.handleInput("x"); + + assert.strictEqual(input.getValue(), "\\x"); + }); +});