From c8dcf1cdae42be7e8e8506eeb74c5ea9c7ba76eb Mon Sep 17 00:00:00 2001 From: Mario Rodler Date: Sun, 16 Nov 2025 18:09:17 +0100 Subject: [PATCH 1/4] feat(tui): Editor now handles extended character input (e.g., umlauts) and updates key binding documentation --- packages/tui/README.md | 2 +- packages/tui/src/components/editor.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/tui/README.md b/packages/tui/README.md index 5ae89f3d..ef02d244 100644 --- a/packages/tui/README.md +++ b/packages/tui/README.md @@ -115,7 +115,7 @@ editor.setAutocompleteProvider(provider); **Key Bindings:** - `Enter` - Submit -- `Shift+Enter` or `Ctrl+Enter` - New line +- `Shift+Enter`, `Ctrl+Enter`, or `Alt+Enter` - New line (terminal-dependent, Alt+Enter most reliable) - `Tab` - Autocomplete - `Ctrl+K` - Delete line - `Ctrl+A` / `Ctrl+E` - Line start/end diff --git a/packages/tui/src/components/editor.ts b/packages/tui/src/components/editor.ts index f9457649..99b04b6e 100644 --- a/packages/tui/src/components/editor.ts +++ b/packages/tui/src/components/editor.ts @@ -334,7 +334,7 @@ export class Editor implements Component { this.moveCursor(0, -1); } // Regular characters (printable ASCII) - else if (data.charCodeAt(0) >= 32 && data.charCodeAt(0) <= 126) { + else { this.insertCharacter(data); } } From ce0c42f539a355fdcde770ef81299505d21af79d Mon Sep 17 00:00:00 2001 From: Mario Rodler Date: Sun, 16 Nov 2025 18:15:21 +0100 Subject: [PATCH 2/4] test(editor): Add tests for handling extended characters, emojis, and special key inputs --- packages/tui/src/components/editor.ts | 2 +- packages/tui/test/editor.test.ts | 171 ++++++++++++++++++++++++++ 2 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 packages/tui/test/editor.test.ts diff --git a/packages/tui/src/components/editor.ts b/packages/tui/src/components/editor.ts index 99b04b6e..67ea7e52 100644 --- a/packages/tui/src/components/editor.ts +++ b/packages/tui/src/components/editor.ts @@ -472,7 +472,7 @@ export class Editor implements Component { // Filter out non-printable characters except newlines const filteredText = tabExpandedText .split("") - .filter((char) => char === "\n" || (char >= " " && char <= "~")) + .filter((char) => char === "\n" || char.length > 0) .join(""); // Split into lines diff --git a/packages/tui/test/editor.test.ts b/packages/tui/test/editor.test.ts new file mode 100644 index 00000000..971fbecc --- /dev/null +++ b/packages/tui/test/editor.test.ts @@ -0,0 +1,171 @@ +import assert from "node:assert"; +import { describe, it } from "node:test"; +import { Editor } from "../src/components/editor.js"; + +describe("Editor component", () => { + describe("Unicode character input", () => { + it("should handle German umlauts correctly", () => { + const editor = new Editor(); + + // Simulate typing umlauts + editor.handleInput("ä"); + editor.handleInput("ö"); + editor.handleInput("ü"); + editor.handleInput("Ä"); + editor.handleInput("Ö"); + editor.handleInput("Ü"); + editor.handleInput("ß"); + + const text = editor.getText(); + assert.strictEqual(text, "äöüÄÖÜß"); + }); + + it("should handle emojis correctly", () => { + const editor = new Editor(); + + // Simulate typing emojis + editor.handleInput("😀"); + editor.handleInput("👍"); + editor.handleInput("🎉"); + + const text = editor.getText(); + assert.strictEqual(text, "😀👍🎉"); + }); + + it("should handle mixed ASCII, umlauts, and emojis", () => { + const editor = new Editor(); + + editor.handleInput("H"); + editor.handleInput("e"); + editor.handleInput("l"); + editor.handleInput("l"); + editor.handleInput("o"); + editor.handleInput(" "); + editor.handleInput("ä"); + editor.handleInput("ö"); + editor.handleInput("ü"); + editor.handleInput(" "); + editor.handleInput("😀"); + + const text = editor.getText(); + assert.strictEqual(text, "Hello äöü 😀"); + }); + + it("should handle backspace with umlauts correctly", () => { + const editor = new Editor(); + + editor.handleInput("ä"); + editor.handleInput("ö"); + editor.handleInput("ü"); + + // Delete the last character (ü) + editor.handleInput("\x7f"); // Backspace + + const text = editor.getText(); + assert.strictEqual(text, "äö"); + }); + + it("should handle backspace with emojis correctly", () => { + const editor = new Editor(); + + editor.handleInput("😀"); + editor.handleInput("👍"); + + // Delete the last emoji (👍) - requires 2 backspaces since emojis are 2 code units + editor.handleInput("\x7f"); // Backspace + editor.handleInput("\x7f"); // Backspace + + const text = editor.getText(); + assert.strictEqual(text, "😀"); + }); + + it("should handle cursor movement with umlauts", () => { + const editor = new Editor(); + + editor.handleInput("ä"); + editor.handleInput("ö"); + editor.handleInput("ü"); + + // Move cursor left twice + editor.handleInput("\x1b[D"); // Left arrow + editor.handleInput("\x1b[D"); // Left arrow + + // Insert 'x' in the middle + editor.handleInput("x"); + + const text = editor.getText(); + assert.strictEqual(text, "äxöü"); + }); + + it("should handle cursor movement with emojis", () => { + const editor = new Editor(); + + editor.handleInput("😀"); + editor.handleInput("👍"); + editor.handleInput("🎉"); + + // Move cursor left twice (should skip the emoji) + editor.handleInput("\x1b[D"); // Left arrow + editor.handleInput("\x1b[D"); // Left arrow + + // Note: Emojis are 2 code units, so we need to move left twice per emoji + // But cursor position is in code units, not visual columns + editor.handleInput("\x1b[D"); + editor.handleInput("\x1b[D"); + + // Insert 'x' + editor.handleInput("x"); + + const text = editor.getText(); + assert.strictEqual(text, "😀x👍🎉"); + }); + + it("should handle multi-line text with umlauts", () => { + const editor = new Editor(); + + editor.handleInput("ä"); + editor.handleInput("ö"); + editor.handleInput("ü"); + editor.handleInput("\n"); // Shift+Enter (new line) + editor.handleInput("Ä"); + editor.handleInput("Ö"); + editor.handleInput("Ü"); + + const text = editor.getText(); + assert.strictEqual(text, "äöü\nÄÖÜ"); + }); + + it("should handle paste with umlauts", () => { + const editor = new Editor(); + + // Simulate bracketed paste by calling handlePaste directly + // (Bracketed paste is async and doesn't work well in sync tests) + editor.setText("äöüÄÖÜß"); + + const text = editor.getText(); + assert.strictEqual(text, "äöüÄÖÜß"); + }); + + it("should handle special control keys", () => { + const editor = new Editor(); + + // Ctrl+A moves cursor to start + editor.handleInput("a"); + editor.handleInput("b"); + editor.handleInput("\x01"); // Ctrl+A (move to start) + editor.handleInput("x"); // Insert at start + + const text = editor.getText(); + assert.strictEqual(text, "xab"); + }); + + it("should handle setText with umlauts", () => { + const editor = new Editor(); + + editor.setText("Hällö Wörld! 😀"); + + const text = editor.getText(); + assert.strictEqual(text, "Hällö Wörld! 😀"); + }); + }); +}); From b37d601317b7d3ad78021067b3354d0b70b95091 Mon Sep 17 00:00:00 2001 From: Mario Rodler Date: Sun, 16 Nov 2025 21:05:49 +0100 Subject: [PATCH 3/4] test(editor): Simplify and refine Unicode handling tests for clarity and consistency --- packages/tui/test/editor.test.ts | 72 +++++++------------------------- 1 file changed, 16 insertions(+), 56 deletions(-) diff --git a/packages/tui/test/editor.test.ts b/packages/tui/test/editor.test.ts index 971fbecc..ab9b3af2 100644 --- a/packages/tui/test/editor.test.ts +++ b/packages/tui/test/editor.test.ts @@ -3,36 +3,8 @@ import { describe, it } from "node:test"; import { Editor } from "../src/components/editor.js"; describe("Editor component", () => { - describe("Unicode character input", () => { - it("should handle German umlauts correctly", () => { - const editor = new Editor(); - - // Simulate typing umlauts - editor.handleInput("ä"); - editor.handleInput("ö"); - editor.handleInput("ü"); - editor.handleInput("Ä"); - editor.handleInput("Ö"); - editor.handleInput("Ü"); - editor.handleInput("ß"); - - const text = editor.getText(); - assert.strictEqual(text, "äöüÄÖÜß"); - }); - - it("should handle emojis correctly", () => { - const editor = new Editor(); - - // Simulate typing emojis - editor.handleInput("😀"); - editor.handleInput("👍"); - editor.handleInput("🎉"); - - const text = editor.getText(); - assert.strictEqual(text, "😀👍🎉"); - }); - - it("should handle mixed ASCII, umlauts, and emojis", () => { + describe("Unicode text editing behavior", () => { + it("inserts mixed ASCII, umlauts, and emojis as literal text", () => { const editor = new Editor(); editor.handleInput("H"); @@ -51,7 +23,7 @@ describe("Editor component", () => { assert.strictEqual(text, "Hello äöü 😀"); }); - it("should handle backspace with umlauts correctly", () => { + it("deletes single-code-unit unicode characters (umlauts) with Backspace", () => { const editor = new Editor(); editor.handleInput("ä"); @@ -65,7 +37,7 @@ describe("Editor component", () => { assert.strictEqual(text, "äö"); }); - it("should handle backspace with emojis correctly", () => { + it("deletes multi-code-unit emojis with repeated Backspace", () => { const editor = new Editor(); editor.handleInput("😀"); @@ -79,7 +51,7 @@ describe("Editor component", () => { assert.strictEqual(text, "😀"); }); - it("should handle cursor movement with umlauts", () => { + it("inserts characters at the correct position after cursor movement over umlauts", () => { const editor = new Editor(); editor.handleInput("ä"); @@ -97,36 +69,35 @@ describe("Editor component", () => { assert.strictEqual(text, "äxöü"); }); - it("should handle cursor movement with emojis", () => { + it("moves cursor in code units across multi-code-unit emojis before insertion", () => { const editor = new Editor(); editor.handleInput("😀"); editor.handleInput("👍"); editor.handleInput("🎉"); - // Move cursor left twice (should skip the emoji) + // Move cursor left over last emoji (🎉) editor.handleInput("\x1b[D"); // Left arrow editor.handleInput("\x1b[D"); // Left arrow - // Note: Emojis are 2 code units, so we need to move left twice per emoji - // But cursor position is in code units, not visual columns + // Move cursor left over second emoji (👍) editor.handleInput("\x1b[D"); editor.handleInput("\x1b[D"); - // Insert 'x' + // Insert 'x' between first and second emoji editor.handleInput("x"); const text = editor.getText(); assert.strictEqual(text, "😀x👍🎉"); }); - it("should handle multi-line text with umlauts", () => { + it("preserves umlauts across line breaks", () => { const editor = new Editor(); editor.handleInput("ä"); editor.handleInput("ö"); editor.handleInput("ü"); - editor.handleInput("\n"); // Shift+Enter (new line) + editor.handleInput("\n"); // new line editor.handleInput("Ä"); editor.handleInput("Ö"); editor.handleInput("Ü"); @@ -135,21 +106,19 @@ describe("Editor component", () => { assert.strictEqual(text, "äöü\nÄÖÜ"); }); - it("should handle paste with umlauts", () => { + it("replaces the entire document with unicode text via setText (paste simulation)", () => { const editor = new Editor(); - // Simulate bracketed paste by calling handlePaste directly - // (Bracketed paste is async and doesn't work well in sync tests) - editor.setText("äöüÄÖÜß"); + // Simulate bracketed paste / programmatic replacement + editor.setText("Hällö Wörld! 😀 äöüÄÖÜß"); const text = editor.getText(); - assert.strictEqual(text, "äöüÄÖÜß"); + assert.strictEqual(text, "Hällö Wörld! 😀 äöüÄÖÜß"); }); - it("should handle special control keys", () => { + it("moves cursor to document start on Ctrl+A and inserts at the beginning", () => { const editor = new Editor(); - // Ctrl+A moves cursor to start editor.handleInput("a"); editor.handleInput("b"); editor.handleInput("\x01"); // Ctrl+A (move to start) @@ -158,14 +127,5 @@ describe("Editor component", () => { const text = editor.getText(); assert.strictEqual(text, "xab"); }); - - it("should handle setText with umlauts", () => { - const editor = new Editor(); - - editor.setText("Hällö Wörld! 😀"); - - const text = editor.getText(); - assert.strictEqual(text, "Hällö Wörld! 😀"); - }); }); }); From 3b6ef6cf57aa2b2aa5dabeda93ec84ce1c7f93f7 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Sun, 16 Nov 2025 23:05:19 +0100 Subject: [PATCH 4/4] refactor(tui): Use explicit charCode check for unicode filtering - Use 'charCodeAt(0) >= 32' instead of open 'else' in handleInput() - Use same filter in handlePaste() for consistency - Prevents control characters while allowing all unicode - More explicit and safer than accepting everything --- packages/tui/src/components/editor.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/tui/src/components/editor.ts b/packages/tui/src/components/editor.ts index 67ea7e52..4374775d 100644 --- a/packages/tui/src/components/editor.ts +++ b/packages/tui/src/components/editor.ts @@ -333,8 +333,8 @@ export class Editor implements Component { // Left this.moveCursor(0, -1); } - // Regular characters (printable ASCII) - else { + // Regular characters (printable characters and unicode, but not control characters) + else if (data.charCodeAt(0) >= 32) { this.insertCharacter(data); } } @@ -472,7 +472,7 @@ export class Editor implements Component { // Filter out non-printable characters except newlines const filteredText = tabExpandedText .split("") - .filter((char) => char === "\n" || char.length > 0) + .filter((char) => char === "\n" || char.charCodeAt(0) >= 32) .join(""); // Split into lines