Файли
openidea/src/features/terminal/imeCommit.test.ts
T
HePengZhou 0a27d02f4d fix(terminal): stop fcitx5 commits from being sent twice (#336)
With WebKitGTK and fcitx5, committed IME text reached the terminal twice: the IME commits without firing compositionstart, so xterm's composition bookkeeping never advanced and every commit was emitted both as the committed text and as a stale textarea diff. An IME commit gate now lets only the first emission of a commit through (with a 250 ms safety timeout) and empties the hidden textarea once the commit ends. It engages only for input that looks like a composition (keyCode 229 / composition input events) and stands down for IMEs that report compositionstart (macOS, IBus, Windows), leaving xterm's own handling unchanged there.

Co-authored-by: Hepengzhou <[email protected]>

未在 Linux(fcitx5)实机验证。
2026-10-30 13:22:12 +08:00

100 рядки
2.9 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { IME_COMMIT_TIMEOUT_MS, ImeCommitGate, type ImeScheduler } from "./imeCommit";
/** Runs queued callbacks in order, the way setTimeout(0) drains after the current event. */
function manualScheduler() {
let next = 1;
const tasks = new Map<number, () => void>();
const scheduler: ImeScheduler = {
after: (callback) => {
const id = next++;
tasks.set(id, callback);
return id;
},
cancel: (id) => {
tasks.delete(id);
},
};
const run = () => {
for (const [id, task] of [...tasks]) {
tasks.delete(id);
task();
}
};
return { scheduler, run, pending: () => tasks.size };
}
function gateWithRebase() {
const { scheduler, run, pending } = manualScheduler();
let rebased = 0;
const gate = new ImeCommitGate(() => rebased++, scheduler);
return { gate, run, pending, rebases: () => rebased };
}
describe("ImeCommitGate", () => {
it("takes the first emission of a commit and drops the repeated one", () => {
const { gate } = gateWithRebase();
gate.begin();
expect(gate.accept()).toBe(true);
expect(gate.accept()).toBe(false);
});
it("keeps the real text of the pi case and drops the stale suffix", () => {
// xterm sends the committed diff first, then a stale `value.substring(start)`.
const { gate, run } = gateWithRebase();
const sent: string[] = [];
const emit = (data: string) => {
if (gate.accept()) sent.push(data);
};
gate.begin();
emit("在吗");
gate.finish();
emit("好在吗");
run();
expect(sent).toEqual(["在吗"]);
});
it("accepts every emission once the commit is over", () => {
const { gate, run } = gateWithRebase();
gate.begin();
expect(gate.accept()).toBe(true);
expect(gate.accept()).toBe(false);
gate.finish();
run();
expect(gate.accept()).toBe(true);
expect(gate.accept()).toBe(true);
});
it("rebases the textarea when the commit closes, not before", () => {
const { gate, run, rebases } = gateWithRebase();
gate.begin();
gate.finish();
expect(rebases()).toBe(0);
run();
expect(rebases()).toBe(1);
});
it("stands down once a real compositionstart arrives", () => {
const { gate, rebases } = gateWithRebase();
gate.begin();
gate.compositionStart();
expect(gate.accept()).toBe(true);
expect(gate.accept()).toBe(true);
expect(rebases()).toBe(0);
// And a later commit is not gated either.
gate.begin();
expect(gate.accept()).toBe(true);
expect(gate.accept()).toBe(true);
});
it("closes a commit whose compositionend never arrives", () => {
const { gate, run, pending, rebases } = gateWithRebase();
gate.begin();
expect(pending()).toBe(1);
run();
expect(IME_COMMIT_TIMEOUT_MS).toBeGreaterThan(0);
expect(rebases()).toBe(1);
expect(gate.accept()).toBe(true);
});
});