дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-08 19:02:17 +02:00
fix(terminal): deliver the whole text of a multi-character key event (#2122)
With a backspace-and-retype input method such as GoNhanh, Vietnamese letters were lost in the terminal: `không` arrived as `khô`. These tools post a retyped string such as `ông` as one key event, but xterm builds its keypress text from `charCode`, which holds only the first character, and cancels the event. Such events are now delivered from the input event's own `data`. IME ownership is now reset on the next keydown, so the key after a multi-character event is not duplicated. Terminal sessions and the Run console share this handling. Refs #1325 Co-authored-by: hiennq12 <[email protected]>
Цей коміт міститься в:
1 джерело
2d6394f203
коміт
49a113c875
2 files changed
+70
-3
No files matched your search
@@ -28,7 +28,7 @@ function keydown(target: EventTarget, init: { key: string; keyCode: number; isCo
|
||||
}
|
||||
|
||||
function keypress(target: EventTarget, key: string) {
|
||||
const e = new KeyboardEvent("keypress", { key, bubbles: true, cancelable: true });
|
||||
const e = new KeyboardEvent("keypress", { key, charCode: key.charCodeAt(0), bubbles: true, cancelable: true });
|
||||
target.dispatchEvent(e);
|
||||
return e;
|
||||
}
|
||||
@@ -98,6 +98,26 @@ describe("ownTerminalInput", () => {
|
||||
expect(sent).toEqual(["123"]);
|
||||
});
|
||||
|
||||
it("sends the whole text of one key event carrying several characters", () => {
|
||||
// Telex tools on macOS retype `ông` as one synthetic key; xterm's keypress sends only `ô`.
|
||||
const { textarea, sent, reached } = setup();
|
||||
keydown(textarea, { key: "ông", keyCode: 65 });
|
||||
keypress(textarea, "ông");
|
||||
input(textarea, { inputType: "insertText", data: "ông" });
|
||||
keyup(textarea, 65);
|
||||
expect(sent).toEqual(["ông"]);
|
||||
expect(reached).toEqual(["keydown"]);
|
||||
});
|
||||
|
||||
it("leaves a single-character keypress to xterm", () => {
|
||||
const { textarea, sent, reached } = setup();
|
||||
keydown(textarea, { key: "ô", keyCode: 65 });
|
||||
keypress(textarea, "ô");
|
||||
input(textarea, { inputType: "insertText", data: "ô" });
|
||||
expect(sent).toEqual([]);
|
||||
expect(reached).toEqual(["keydown", "keypress", "insertText"]);
|
||||
});
|
||||
|
||||
it("sends a composition commit reported without compositionstart", () => {
|
||||
const { textarea, sent } = setup();
|
||||
input(textarea, { inputType: "insertFromComposition", data: "," });
|
||||
@@ -202,6 +222,7 @@ describe("ownTerminalInput", () => {
|
||||
describe("ownTerminalInput with xterm", () => {
|
||||
type Step =
|
||||
| ["keydown" | "keyup", string, number, boolean?]
|
||||
| ["keypress", string]
|
||||
| ["compositionstart" | "compositionupdate" | "compositionend", string]
|
||||
| ["input", string, string | null, string];
|
||||
|
||||
@@ -221,6 +242,8 @@ describe("ownTerminalInput with xterm", () => {
|
||||
const e = keydown(textarea, { key: step[1], keyCode: step[2], isComposing: step[3] });
|
||||
// A browser fires no keypress for a keydown xterm cancelled.
|
||||
if (!step[3] && !e.defaultPrevented && step[1].length === 1) keypress(textarea, step[1]);
|
||||
} else if (step[0] === "keypress") {
|
||||
keypress(textarea, step[1]);
|
||||
} else if (step[0] === "keyup") {
|
||||
keyup(textarea, step[2]);
|
||||
} else if (step[0] === "input") {
|
||||
@@ -236,6 +259,47 @@ describe("ownTerminalInput with xterm", () => {
|
||||
return sent;
|
||||
}
|
||||
|
||||
it.each([true, false])("sends a retyped word and following plain keys once (keyup first: %s)", async (release) => {
|
||||
// Uppercase typing reaches both keypress and input; lowercase keydown is cancelled by xterm.
|
||||
const sent = await replay([
|
||||
["keydown", "ông", 65],
|
||||
["keypress", "ông"],
|
||||
["input", "insertText", "ông", "ông"],
|
||||
...(release ? ([["keyup", "ông", 65]] as Step[]) : []),
|
||||
["keydown", "A", 65],
|
||||
["input", "insertText", "A", "A"],
|
||||
["keyup", "A", 65],
|
||||
["keydown", "b", 66],
|
||||
["input", "insertText", "b", "Ab"],
|
||||
["keyup", "b", 66],
|
||||
]);
|
||||
expect(sent).toEqual(["ông", "A", "b"]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["Enter", 13, "\r"],
|
||||
["Tab", 9, "\t"],
|
||||
["Backspace", 8, "\x7f"],
|
||||
["Escape", 27, "\x1b"],
|
||||
["ArrowUp", 38, "\x1b[A"],
|
||||
["ArrowDown", 40, "\x1b[B"],
|
||||
["ArrowLeft", 37, "\x1b[D"],
|
||||
["ArrowRight", 39, "\x1b[C"],
|
||||
["Dead", 0, ""],
|
||||
["Process", 229, ""],
|
||||
["Unidentified", 0, ""],
|
||||
] as const)("leaves %s keydown handling intact after a retyped word", async (key, code, expected) => {
|
||||
const sent = await replay([
|
||||
["keydown", "ông", 65],
|
||||
["keypress", "ông"],
|
||||
["input", "insertText", "ông", "ông"],
|
||||
["keyup", "ông", 65],
|
||||
["keydown", key, code],
|
||||
["keyup", key, code],
|
||||
]);
|
||||
expect(sent.join("")).toBe("ông" + expected);
|
||||
});
|
||||
|
||||
function composeThenSwitch(before: string, shown: string[]): Step[] {
|
||||
const steps: Step[] = [["compositionstart", ""]];
|
||||
for (const text of shown) {
|
||||
|
||||
@@ -60,7 +60,7 @@ export function ownTerminalInput({ host, textarea, send }: TerminalInputOptions)
|
||||
let composition = "";
|
||||
/** The pending keydown went to the input method, so its keypress belongs to it too. */
|
||||
let imeKey = false;
|
||||
/** An IME keydown was seen since the last keyup: its commit is ours to deliver. */
|
||||
/** The pending key belongs to an IME: its commit is ours to deliver. */
|
||||
let imeDown = false;
|
||||
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
@@ -70,10 +70,10 @@ export function ownTerminalInput({ host, textarea, send }: TerminalInputOptions)
|
||||
return;
|
||||
}
|
||||
imeKey = isImeKey(e);
|
||||
imeDown = imeKey;
|
||||
if (imeKey) {
|
||||
// The IME owns this key: xterm must not run its deferred textarea diff or send the
|
||||
// keypress, and the text it commits next is ours.
|
||||
imeDown = true;
|
||||
e.stopPropagation();
|
||||
}
|
||||
};
|
||||
@@ -81,6 +81,9 @@ export function ownTerminalInput({ host, textarea, send }: TerminalInputOptions)
|
||||
// xterm emits a pressed character from keypress; for an IME key that would duplicate the
|
||||
// text the input event delivers.
|
||||
const onKeyPress = (e: KeyboardEvent) => {
|
||||
// Telex tools on macOS post a retyped `ông` as one key event. xterm builds its keypress
|
||||
// text from `charCode`, which holds only `ô`, so the input event must deliver this one.
|
||||
if (!composing && e.key.length > 1 && e.key !== "Enter") imeKey = imeDown = true;
|
||||
if (imeKey) e.stopPropagation();
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Заблокувати користувача