Fix Ctrl+W to use standard readline word deletion behavior (#306)

- Skip trailing whitespace before deleting word (readline behavior)
- Make word navigation grapheme-aware using Intl.Segmenter
- Add Ctrl+Left/Right and Alt+Left/Right word navigation to Input
- Accept full Unicode input while rejecting control characters (C0/C1/DEL)
- Extract shared utilities to utils.ts (getSegmenter, isWhitespaceChar, isPunctuationChar)
- Fix unsafe cast in Editor.forceFileAutocomplete with runtime type check
- Add comprehensive tests for word deletion and navigation
This commit is contained in:
Ahmed Kamal 2025-12-25 05:09:47 +02:00 committed by GitHub
parent 65cbc22d7c
commit 0427445242
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 250 additions and 86 deletions

View file

@ -396,6 +396,84 @@ describe("Editor component", () => {
const text = editor.getText();
assert.strictEqual(text, "xab");
});
it("deletes words correctly with Ctrl+W and Alt+Backspace", () => {
const editor = new Editor(defaultEditorTheme);
// Basic word deletion
editor.setText("foo bar baz");
editor.handleInput("\x17"); // Ctrl+W
assert.strictEqual(editor.getText(), "foo bar ");
// Trailing whitespace
editor.setText("foo bar ");
editor.handleInput("\x17");
assert.strictEqual(editor.getText(), "foo ");
// Punctuation run
editor.setText("foo bar...");
editor.handleInput("\x17");
assert.strictEqual(editor.getText(), "foo bar");
// Delete across multiple lines
editor.setText("line one\nline two");
editor.handleInput("\x17");
assert.strictEqual(editor.getText(), "line one\nline ");
// Delete empty line (merge)
editor.setText("line one\n");
editor.handleInput("\x17");
assert.strictEqual(editor.getText(), "line one");
// Grapheme safety (emoji as a word)
editor.setText("foo 😀😀 bar");
editor.handleInput("\x17");
assert.strictEqual(editor.getText(), "foo 😀😀 ");
editor.handleInput("\x17");
assert.strictEqual(editor.getText(), "foo ");
// Alt+Backspace
editor.setText("foo bar");
editor.handleInput("\x1b\x7f"); // Alt+Backspace (legacy)
assert.strictEqual(editor.getText(), "foo ");
});
it("navigates words correctly with Ctrl+Left/Right", () => {
const editor = new Editor(defaultEditorTheme);
editor.setText("foo bar... baz");
// Cursor at end
// Move left over baz
editor.handleInput("\x1b[1;5D"); // Ctrl+Left
assert.deepStrictEqual(editor.getCursor(), { line: 0, col: 11 }); // after '...'
// Move left over punctuation
editor.handleInput("\x1b[1;5D"); // Ctrl+Left
assert.deepStrictEqual(editor.getCursor(), { line: 0, col: 7 }); // after 'bar'
// Move left over bar
editor.handleInput("\x1b[1;5D"); // Ctrl+Left
assert.deepStrictEqual(editor.getCursor(), { line: 0, col: 4 }); // after 'foo '
// Move right over bar
editor.handleInput("\x1b[1;5C"); // Ctrl+Right
assert.deepStrictEqual(editor.getCursor(), { line: 0, col: 7 }); // at end of 'bar'
// Move right over punctuation run
editor.handleInput("\x1b[1;5C"); // Ctrl+Right
assert.deepStrictEqual(editor.getCursor(), { line: 0, col: 10 }); // after '...'
// Move right skips space and lands after baz
editor.handleInput("\x1b[1;5C"); // Ctrl+Right
assert.deepStrictEqual(editor.getCursor(), { line: 0, col: 14 }); // end of line
// Test forward from start with leading whitespace
editor.setText(" foo bar");
editor.handleInput("\x01"); // Ctrl+A to go to start
editor.handleInput("\x1b[1;5C"); // Ctrl+Right
assert.deepStrictEqual(editor.getCursor(), { line: 0, col: 6 }); // after 'foo'
});
});
describe("Grapheme-aware text wrapping", () => {