fix: input keyboard shortcut opt+delete (#685)
This commit is contained in:
@@ -525,6 +525,22 @@ export default class TextBuffer {
|
||||
end++;
|
||||
}
|
||||
|
||||
/*
|
||||
* After consuming the actual word we also want to swallow any immediate
|
||||
* separator run that *follows* it so that a forward word-delete mirrors
|
||||
* the behaviour of common shells/editors (and matches the expectations
|
||||
* encoded in our test-suite).
|
||||
*
|
||||
* Example – given the text "foo bar baz" and the caret placed at the
|
||||
* beginning of "bar" (index 4) we want Alt+Delete to turn the string
|
||||
* into "foo␠baz" (single space). Without this extra loop we would stop
|
||||
* right before the separating space, producing "foo␠␠baz".
|
||||
*/
|
||||
|
||||
while (end < arr.length && !isWordChar(arr[end])) {
|
||||
end++;
|
||||
}
|
||||
|
||||
this.lines[this.cursorRow] =
|
||||
cpSlice(line, 0, this.cursorCol) + cpSlice(line, end);
|
||||
// caret stays in place
|
||||
@@ -859,12 +875,42 @@ export default class TextBuffer {
|
||||
// no `key.backspace` flag set. Treat that byte exactly like an ordinary
|
||||
// Backspace for parity with textarea.rs and to make interactive tests
|
||||
// feedable through the simpler `(ch, {}, vp)` path.
|
||||
// ------------------------------------------------------------------
|
||||
// Word-wise deletions
|
||||
//
|
||||
// macOS (and many terminals on Linux/BSD) map the physical “Delete” key
|
||||
// to a *backspace* operation – emitting either the raw DEL (0x7f) byte
|
||||
// or setting `key.backspace = true` in Ink’s parsed event. Holding the
|
||||
// Option/Alt modifier therefore *also* sends backspace semantics even
|
||||
// though users colloquially refer to the shortcut as “⌥+Delete”.
|
||||
//
|
||||
// Historically we treated **modifier + Delete** as a *forward* word
|
||||
// deletion. This behaviour, however, diverges from the default found
|
||||
// in shells (zsh, bash, fish, etc.) and native macOS text fields where
|
||||
// ⌥+Delete removes the word *to the left* of the caret. Update the
|
||||
// mapping so that both
|
||||
//
|
||||
// • ⌥/Alt/Meta + Backspace and
|
||||
// • ⌥/Alt/Meta + Delete
|
||||
//
|
||||
// perform a **backward** word deletion. We keep the ability to delete
|
||||
// the *next* word by requiring an additional Shift modifier – a common
|
||||
// binding on full-size keyboards that expose a dedicated Forward Delete
|
||||
// key.
|
||||
// ------------------------------------------------------------------
|
||||
else if (
|
||||
// ⌥/Alt/Meta + (Backspace|Delete|DEL byte) → backward word delete
|
||||
(key["meta"] || key["ctrl"] || key["alt"]) &&
|
||||
(key["backspace"] || input === "\x7f")
|
||||
!key["shift"] &&
|
||||
(key["backspace"] || input === "\x7f" || key["delete"])
|
||||
) {
|
||||
this.deleteWordLeft();
|
||||
} else if ((key["meta"] || key["ctrl"] || key["alt"]) && key["delete"]) {
|
||||
} else if (
|
||||
// ⇧+⌥/Alt/Meta + (Backspace|Delete|DEL byte) → forward word delete
|
||||
(key["meta"] || key["ctrl"] || key["alt"]) &&
|
||||
key["shift"] &&
|
||||
(key["backspace"] || input === "\x7f" || key["delete"])
|
||||
) {
|
||||
this.deleteWordRight();
|
||||
} else if (
|
||||
key["backspace"] ||
|
||||
|
||||
@@ -43,18 +43,17 @@ describe("TextBuffer – word‑wise navigation & deletion", () => {
|
||||
expect(tb.getText()).toBe("foo bar ");
|
||||
});
|
||||
|
||||
test("Option/Alt+Delete deletes next word", () => {
|
||||
test("Option/Alt+Delete deletes previous word (matches shells)", () => {
|
||||
const tb = new TextBuffer("foo bar baz");
|
||||
const vp = { height: 10, width: 80 } as const;
|
||||
|
||||
// Move caret between first and second word (after space)
|
||||
tb.move("wordRight"); // after foo
|
||||
tb.move("right"); // skip space -> start of bar
|
||||
// Place caret at end so we can test backward deletion.
|
||||
tb.move("end");
|
||||
|
||||
// Option+Delete
|
||||
// Simulate Option+Delete (parsed as alt-modified Delete on some terminals)
|
||||
tb.handleInput(undefined, { delete: true, alt: true }, vp);
|
||||
|
||||
expect(tb.getText()).toBe("foo baz"); // note double space removed later maybe
|
||||
expect(tb.getText()).toBe("foo bar ");
|
||||
});
|
||||
|
||||
test("wordLeft eventually reaches column 0", () => {
|
||||
@@ -121,4 +120,18 @@ describe("TextBuffer – word‑wise navigation & deletion", () => {
|
||||
const [, col] = tb.getCursor();
|
||||
expect(col).toBe(6);
|
||||
});
|
||||
|
||||
test("Shift+Option/Alt+Delete deletes next word", () => {
|
||||
const tb = new TextBuffer("foo bar baz");
|
||||
const vp = { height: 10, width: 80 } as const;
|
||||
|
||||
// Move caret between first and second word (after space)
|
||||
tb.move("wordRight"); // after foo
|
||||
tb.move("right"); // skip space -> start of bar
|
||||
|
||||
// Shift+Option+Delete should now remove "bar "
|
||||
tb.handleInput(undefined, { delete: true, alt: true, shift: true }, vp);
|
||||
|
||||
expect(tb.getText()).toBe("foo baz");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user