fix: input keyboard shortcut opt+delete (#685)
This commit is contained in:
@@ -525,6 +525,22 @@ export default class TextBuffer {
|
|||||||
end++;
|
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] =
|
this.lines[this.cursorRow] =
|
||||||
cpSlice(line, 0, this.cursorCol) + cpSlice(line, end);
|
cpSlice(line, 0, this.cursorCol) + cpSlice(line, end);
|
||||||
// caret stays in place
|
// caret stays in place
|
||||||
@@ -859,12 +875,42 @@ export default class TextBuffer {
|
|||||||
// no `key.backspace` flag set. Treat that byte exactly like an ordinary
|
// no `key.backspace` flag set. Treat that byte exactly like an ordinary
|
||||||
// Backspace for parity with textarea.rs and to make interactive tests
|
// Backspace for parity with textarea.rs and to make interactive tests
|
||||||
// feedable through the simpler `(ch, {}, vp)` path.
|
// 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 (
|
else if (
|
||||||
|
// ⌥/Alt/Meta + (Backspace|Delete|DEL byte) → backward word delete
|
||||||
(key["meta"] || key["ctrl"] || key["alt"]) &&
|
(key["meta"] || key["ctrl"] || key["alt"]) &&
|
||||||
(key["backspace"] || input === "\x7f")
|
!key["shift"] &&
|
||||||
|
(key["backspace"] || input === "\x7f" || key["delete"])
|
||||||
) {
|
) {
|
||||||
this.deleteWordLeft();
|
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();
|
this.deleteWordRight();
|
||||||
} else if (
|
} else if (
|
||||||
key["backspace"] ||
|
key["backspace"] ||
|
||||||
|
|||||||
@@ -43,18 +43,17 @@ describe("TextBuffer – word‑wise navigation & deletion", () => {
|
|||||||
expect(tb.getText()).toBe("foo bar ");
|
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 tb = new TextBuffer("foo bar baz");
|
||||||
const vp = { height: 10, width: 80 } as const;
|
const vp = { height: 10, width: 80 } as const;
|
||||||
|
|
||||||
// Move caret between first and second word (after space)
|
// Place caret at end so we can test backward deletion.
|
||||||
tb.move("wordRight"); // after foo
|
tb.move("end");
|
||||||
tb.move("right"); // skip space -> start of bar
|
|
||||||
|
|
||||||
// Option+Delete
|
// Simulate Option+Delete (parsed as alt-modified Delete on some terminals)
|
||||||
tb.handleInput(undefined, { delete: true, alt: true }, vp);
|
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", () => {
|
test("wordLeft eventually reaches column 0", () => {
|
||||||
@@ -121,4 +120,18 @@ describe("TextBuffer – word‑wise navigation & deletion", () => {
|
|||||||
const [, col] = tb.getCursor();
|
const [, col] = tb.getCursor();
|
||||||
expect(col).toBe(6);
|
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