fix(terminal): keep the text of an IME key pressed before a commit finalizes (#28441)

* fix(terminal): keep the text of an IME key pressed before a commit finalizes

A Process (229) keydown that lands after compositionend but before xterm's deferred
finalizer runs took the composing early return and never recorded that it owed its
own commit. When the finalizer ran first, the key's insertText found no pending
transaction and no owed commit, so xterm dropped it: `你` then `,` reached the PTY
as `你`.

Record the debt for such a keydown when the ended session's commit is already in the
textarea, and clear it when the pending transaction settles the text itself. Sessions
still waiting for their post-composition echo keep their old path, so the echo cannot
be sent twice.

Fixes the intermittent terminal-linux-direct-ime-commit e2e failure.

* fix(terminal): keep a commit when Escape or a keypress follows compositionend

A plain Escape after compositionend cancelled the accepted commit, dropping both the
text and the Escape; it now settles the commit and then sends Escape. The Escape that
itself ends a session (still composing, reported after compositionend) still cancels.

A keypress for the Process key that owes a commit now clears that debt, so a later
input cannot send the key's text a second time.
This commit is contained in:
Neil
2026-10-13 22:13:25 -07:00
committed by GitHub
parent 9b1e83a7d8
commit 58d0de6bb6
5 changed files with 202 additions and 88 deletions
File diff suppressed because one or more lines are too long
@@ -88,7 +88,7 @@ index 497afcf535f3eaca00889525a77e15eb633ccd96..96d499b34605f860608382114c3fbdc0
export interface IBrowser {
diff --git a/src/browser/input/CompositionHelper.ts b/src/browser/input/CompositionHelper.ts
index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..4d62369d0005f465318ec93e221fd3ee3acfddfa 100644
index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..8ce5bad71b26515ecbbb82b76d218d0b16e280a8 100644
--- a/src/browser/input/CompositionHelper.ts
+++ b/src/browser/input/CompositionHelper.ts
@@ -1,10 +1,15 @@
@@ -413,7 +413,7 @@ index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..4d62369d0005f465318ec93e221fd3ee
}
/**
@@ -113,7 +313,19 @@ export class CompositionHelper {
@@ -113,10 +313,33 @@ export class CompositionHelper {
* @returns Whether the Terminal should continue processing the keydown event.
*/
public keydown(ev: KeyboardEvent): boolean {
@@ -422,7 +422,13 @@ index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..4d62369d0005f465318ec93e221fd3ee
+ this._canceledKey = undefined;
+ return false;
+ }
+ if (ev.key === 'Escape' && (this._isComposing || this.hasPendingCompositionFinalization)) {
+ // An ended session's commit is accepted; only the Escape that ended it (macOS reports that
+ // keydown after compositionend, still composing) cancels it.
+ if (
+ ev.key === 'Escape' &&
+ (this._isComposing ||
+ (this.hasPendingCompositionFinalization && (ev.isComposing || ev.keyCode === 229)))
+ ) {
+ this._canceledKey = { code: ev.code, timeStamp: ev.timeStamp };
+ this._cancelComposition();
+ return false;
@@ -434,7 +440,15 @@ index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..4d62369d0005f465318ec93e221fd3ee
if (ev.keyCode === 20 || ev.keyCode === 229) {
// 20 is CapsLock, 229 is Enter
// Continue composing if the keyCode is the "composition character"
@@ -128,6 +340,10 @@ export class CompositionHelper {
+ if (ev.keyCode === 229 && !this._isComposing) {
+ // The ended session's commit is already in the textarea unless it still awaits its echo,
+ // so this key owes its own commit if the finalizer runs before that text arrives.
+ this._imeKeydownAwaitingCommit = !this._pendingComposition?.expectsPostCompositionInput;
+ }
return false;
}
if (ev.keyCode === 16 || ev.keyCode === 17 || ev.keyCode === 18) {
@@ -128,6 +351,10 @@ export class CompositionHelper {
this._finalizeComposition(false);
}
@@ -445,7 +459,7 @@ index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..4d62369d0005f465318ec93e221fd3ee
if (ev.keyCode === 229) {
// If the "composition character" is used but gets to this point it means a non-composition
// character (eg. numbers and punctuation) was pressed when the IME was active.
@@ -138,6 +354,74 @@ export class CompositionHelper {
@@ -138,6 +365,78 @@ export class CompositionHelper {
return true;
}
@@ -454,6 +468,8 @@ index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..4d62369d0005f465318ec93e221fd3ee
+ * after reconciliation with the final textarea candidate.
+ */
+ public keypress(text: string): boolean {
+ // Every path below sends this key's text, so its keydown no longer owes a commit.
+ this._imeKeydownAwaitingCommit = false;
+ const pending = this._pendingComposition;
+ if (!pending) {
+ return false;
@@ -480,6 +496,8 @@ index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..4d62369d0005f465318ec93e221fd3ee
+ if (!pending) {
+ return this._claimImeKeydownCommit(text);
+ }
+ // The pending transaction settles this text below, so no keydown still owes it.
+ this._imeKeydownAwaitingCommit = false;
+ if (pending.expectsPostCompositionInput) {
+ pending.inputData += text;
+ pending.expectsPostCompositionInput = false;
@@ -520,7 +538,7 @@ index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..4d62369d0005f465318ec93e221fd3ee
/**
* Finalizes the composition, resuming regular input actions. This is called when a composition
* is ending.
@@ -146,23 +430,52 @@ export class CompositionHelper {
@@ -146,23 +445,52 @@ export class CompositionHelper {
* compositionend event is triggered, such as enter, so that the composition is sent before
* the command is executed.
*/
@@ -584,7 +602,7 @@ index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..4d62369d0005f465318ec93e221fd3ee
// Since composition* events happen before the changes take place in the textarea on most
// browsers, use a setTimeout with 0ms time to allow the native compositionend event to
@@ -172,37 +485,315 @@ export class CompositionHelper {
@@ -172,35 +500,313 @@ export class CompositionHelper {
// - The last compositionupdate event's data property does not always accurately describe
// the character, a counter example being Korean where an ending consonsant can move to
// the following character if the following input is a vowel.
@@ -621,7 +639,8 @@ index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..4d62369d0005f465318ec93e221fd3ee
+ }
+ if (this._pendingComposition === pending) {
+ this._sendPendingComposition(pending, true);
+ }
}
- }, 0);
+ });
+ }
+ }
@@ -654,7 +673,7 @@ index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..4d62369d0005f465318ec93e221fd3ee
+ private _cancelPendingFinalizer(pending: IPendingComposition): void {
+ if (pending.finalizerTimer === undefined) {
+ return;
+ }
}
+ clearTimeout(pending.finalizerTimer);
+ this._compositionTimers.delete(pending.finalizerTimer);
+ pending.finalizerTimer = undefined;
@@ -813,8 +832,7 @@ index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..4d62369d0005f465318ec93e221fd3ee
+ id: pending.transactionId,
+ data: input,
+ dataPendingReconciliation: true
}
- }, 0);
+ }
+ }
+ ));
+ }
@@ -822,9 +840,9 @@ index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..4d62369d0005f465318ec93e221fd3ee
+ private _dispatchCompositionSessionEvent(event: CustomEvent): void {
+ if (typeof this._textarea.dispatchEvent === 'function') {
+ this._textarea.dispatchEvent(event);
}
}
+ }
+ }
+
+ private _dispatchCompositionTransactionSettled(): void {
+ this._dispatchCompositionSessionEvent(new CustomEvent(
+ 'xterm-composition-transaction-settled',
@@ -922,12 +940,10 @@ index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..4d62369d0005f465318ec93e221fd3ee
+ }
+ clearTimeout(timer);
+ this._compositionTimers.delete(timer);
+ }
+
}
/**
* Apply any changes made to the textarea after the current event chain is allowed to complete.
* This should be called when not currently composing but a keydown event with the "composition
@@ -222,6 +813,9 @@ export class CompositionHelper {
@@ -222,6 +828,9 @@ export class CompositionHelper {
const diff = newValue.replace(oldValue, '');
@@ -937,7 +953,7 @@ index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..4d62369d0005f465318ec93e221fd3ee
this._dataAlreadySent = diff;
if (newValue.length > oldValue.length) {
@@ -236,6 +830,105 @@ export class CompositionHelper {
@@ -236,6 +845,105 @@ export class CompositionHelper {
}, 0);
}
@@ -1043,7 +1059,7 @@ index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..4d62369d0005f465318ec93e221fd3ee
/**
* Positions the composition view on top of the cursor and the textarea just below it (so the
* IME helper dialog is positioned correctly).
@@ -243,10 +936,23 @@ export class CompositionHelper {
@@ -243,10 +951,23 @@ export class CompositionHelper {
* necessary as the IME events across browsers are not consistently triggered.
*/
public updateCompositionElements(dontRecurse?: boolean): void {
@@ -1068,7 +1084,7 @@ index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..4d62369d0005f465318ec93e221fd3ee
if (this._bufferService.buffer.isCursorInViewport) {
const cursorX = Math.min(this._bufferService.buffer.x, this._bufferService.cols - 1);
@@ -254,31 +960,156 @@ export class CompositionHelper {
@@ -254,31 +975,156 @@ export class CompositionHelper {
const cursorTop = this._bufferService.buffer.y * this._renderService.dimensions.css.cell.height;
const cursorLeft = cursorX * this._renderService.dimensions.css.cell.width;
+27 -27
View File
@@ -172,7 +172,7 @@ patchedDependencies:
'@xterm/addon-search@0.17.0-beta.301': eee5338dd2621ece46e79c61ec06766cd7fadaf79ffdb24e2a8ab68e97ef31f0
'@xterm/addon-serialize@0.15.0-beta.301': b35533fe252e7e45433150170348889f4e08a6c17f7017ac34ea694d831fec7f
'@xterm/addon-webgl@0.20.0-beta.300': 5349718f2c19d9319a81552580bf9618ded9b00f3707a5c6cdbbae1910849cd9
'@xterm/xterm@6.1.0-beta.304': 273b687a9c4b571f2f6f4eb4a2da1f651300065500bc30b4df2e911827e9c7eb
'@xterm/xterm@6.1.0-beta.304': 9e1c200b2332745ef9e01e64696e9cda6f629937c37a0ee652323f25ccc8df5c
i18next-cli@1.74.2: 7955b89d3aa229f477408608d331f78c85148a85a85913729f89fac65ad8b207
lint-staged@16.4.0: 7333b3837f80a7fbd045964db6d76ba4fc118e49134bdbabb00585b6b7b60673
monaco-editor@0.55.1: 53b71f7d8a7c00b76ffdb73ce83d18e31692f193f5fafa6a390e48f5b3fdd636
@@ -204,7 +204,7 @@ importers:
version: 2.5.6
'@xterm/addon-serialize':
specifier: 0.15.0-beta.301
version: 0.15.0-beta.301(patch_hash=b35533fe252e7e45433150170348889f4e08a6c17f7017ac34ea694d831fec7f)(@xterm/xterm@6.1.0-beta.304(patch_hash=273b687a9c4b571f2f6f4eb4a2da1f651300065500bc30b4df2e911827e9c7eb))
version: 0.15.0-beta.301(patch_hash=b35533fe252e7e45433150170348889f4e08a6c17f7017ac34ea694d831fec7f)(@xterm/xterm@6.1.0-beta.304(patch_hash=9e1c200b2332745ef9e01e64696e9cda6f629937c37a0ee652323f25ccc8df5c))
'@xterm/headless':
specifier: 6.1.0-beta.303
version: 6.1.0-beta.303
@@ -409,28 +409,28 @@ importers:
version: 1.18.0
'@xterm/addon-fit':
specifier: 0.12.0-beta.301
version: 0.12.0-beta.301(@xterm/xterm@6.1.0-beta.304(patch_hash=273b687a9c4b571f2f6f4eb4a2da1f651300065500bc30b4df2e911827e9c7eb))
version: 0.12.0-beta.301(@xterm/xterm@6.1.0-beta.304(patch_hash=9e1c200b2332745ef9e01e64696e9cda6f629937c37a0ee652323f25ccc8df5c))
'@xterm/addon-image':
specifier: 0.10.0-beta.301
version: 0.10.0-beta.301(patch_hash=cb569e3005b47547189ba473280c3ae7cf4040161da538cd998ae6e69ad403f5)(@xterm/xterm@6.1.0-beta.304(patch_hash=273b687a9c4b571f2f6f4eb4a2da1f651300065500bc30b4df2e911827e9c7eb))
version: 0.10.0-beta.301(patch_hash=cb569e3005b47547189ba473280c3ae7cf4040161da538cd998ae6e69ad403f5)(@xterm/xterm@6.1.0-beta.304(patch_hash=9e1c200b2332745ef9e01e64696e9cda6f629937c37a0ee652323f25ccc8df5c))
'@xterm/addon-ligatures':
specifier: 0.11.0-beta.301
version: 0.11.0-beta.301(patch_hash=47405b9994b5acf1b4e90b49250358c1ca03649854d59560e7732b72fe336920)(@xterm/xterm@6.1.0-beta.304(patch_hash=273b687a9c4b571f2f6f4eb4a2da1f651300065500bc30b4df2e911827e9c7eb))
version: 0.11.0-beta.301(patch_hash=47405b9994b5acf1b4e90b49250358c1ca03649854d59560e7732b72fe336920)(@xterm/xterm@6.1.0-beta.304(patch_hash=9e1c200b2332745ef9e01e64696e9cda6f629937c37a0ee652323f25ccc8df5c))
'@xterm/addon-search':
specifier: 0.17.0-beta.301
version: 0.17.0-beta.301(patch_hash=eee5338dd2621ece46e79c61ec06766cd7fadaf79ffdb24e2a8ab68e97ef31f0)(@xterm/xterm@6.1.0-beta.304(patch_hash=273b687a9c4b571f2f6f4eb4a2da1f651300065500bc30b4df2e911827e9c7eb))
version: 0.17.0-beta.301(patch_hash=eee5338dd2621ece46e79c61ec06766cd7fadaf79ffdb24e2a8ab68e97ef31f0)(@xterm/xterm@6.1.0-beta.304(patch_hash=9e1c200b2332745ef9e01e64696e9cda6f629937c37a0ee652323f25ccc8df5c))
'@xterm/addon-unicode11':
specifier: 0.10.0-beta.301
version: 0.10.0-beta.301(@xterm/xterm@6.1.0-beta.304(patch_hash=273b687a9c4b571f2f6f4eb4a2da1f651300065500bc30b4df2e911827e9c7eb))
version: 0.10.0-beta.301(@xterm/xterm@6.1.0-beta.304(patch_hash=9e1c200b2332745ef9e01e64696e9cda6f629937c37a0ee652323f25ccc8df5c))
'@xterm/addon-web-links':
specifier: 0.13.0-beta.301
version: 0.13.0-beta.301(@xterm/xterm@6.1.0-beta.304(patch_hash=273b687a9c4b571f2f6f4eb4a2da1f651300065500bc30b4df2e911827e9c7eb))
version: 0.13.0-beta.301(@xterm/xterm@6.1.0-beta.304(patch_hash=9e1c200b2332745ef9e01e64696e9cda6f629937c37a0ee652323f25ccc8df5c))
'@xterm/addon-webgl':
specifier: 0.20.0-beta.300
version: 0.20.0-beta.300(patch_hash=5349718f2c19d9319a81552580bf9618ded9b00f3707a5c6cdbbae1910849cd9)(@xterm/xterm@6.1.0-beta.304(patch_hash=273b687a9c4b571f2f6f4eb4a2da1f651300065500bc30b4df2e911827e9c7eb))
version: 0.20.0-beta.300(patch_hash=5349718f2c19d9319a81552580bf9618ded9b00f3707a5c6cdbbae1910849cd9)(@xterm/xterm@6.1.0-beta.304(patch_hash=9e1c200b2332745ef9e01e64696e9cda6f629937c37a0ee652323f25ccc8df5c))
'@xterm/xterm':
specifier: 6.1.0-beta.304
version: 6.1.0-beta.304(patch_hash=273b687a9c4b571f2f6f4eb4a2da1f651300065500bc30b4df2e911827e9c7eb)
version: 6.1.0-beta.304(patch_hash=9e1c200b2332745ef9e01e64696e9cda6f629937c37a0ee652323f25ccc8df5c)
class-variance-authority:
specifier: ^0.7.1
version: 0.7.1
@@ -11234,43 +11234,43 @@ snapshots:
'@xmldom/xmldom@0.8.15': {}
'@xterm/addon-fit@0.12.0-beta.301(@xterm/xterm@6.1.0-beta.304(patch_hash=273b687a9c4b571f2f6f4eb4a2da1f651300065500bc30b4df2e911827e9c7eb))':
'@xterm/addon-fit@0.12.0-beta.301(@xterm/xterm@6.1.0-beta.304(patch_hash=9e1c200b2332745ef9e01e64696e9cda6f629937c37a0ee652323f25ccc8df5c))':
dependencies:
'@xterm/xterm': 6.1.0-beta.304(patch_hash=273b687a9c4b571f2f6f4eb4a2da1f651300065500bc30b4df2e911827e9c7eb)
'@xterm/xterm': 6.1.0-beta.304(patch_hash=9e1c200b2332745ef9e01e64696e9cda6f629937c37a0ee652323f25ccc8df5c)
'@xterm/addon-image@0.10.0-beta.301(patch_hash=cb569e3005b47547189ba473280c3ae7cf4040161da538cd998ae6e69ad403f5)(@xterm/xterm@6.1.0-beta.304(patch_hash=273b687a9c4b571f2f6f4eb4a2da1f651300065500bc30b4df2e911827e9c7eb))':
'@xterm/addon-image@0.10.0-beta.301(patch_hash=cb569e3005b47547189ba473280c3ae7cf4040161da538cd998ae6e69ad403f5)(@xterm/xterm@6.1.0-beta.304(patch_hash=9e1c200b2332745ef9e01e64696e9cda6f629937c37a0ee652323f25ccc8df5c))':
dependencies:
'@xterm/xterm': 6.1.0-beta.304(patch_hash=273b687a9c4b571f2f6f4eb4a2da1f651300065500bc30b4df2e911827e9c7eb)
'@xterm/xterm': 6.1.0-beta.304(patch_hash=9e1c200b2332745ef9e01e64696e9cda6f629937c37a0ee652323f25ccc8df5c)
'@xterm/addon-ligatures@0.11.0-beta.301(patch_hash=47405b9994b5acf1b4e90b49250358c1ca03649854d59560e7732b72fe336920)(@xterm/xterm@6.1.0-beta.304(patch_hash=273b687a9c4b571f2f6f4eb4a2da1f651300065500bc30b4df2e911827e9c7eb))':
'@xterm/addon-ligatures@0.11.0-beta.301(patch_hash=47405b9994b5acf1b4e90b49250358c1ca03649854d59560e7732b72fe336920)(@xterm/xterm@6.1.0-beta.304(patch_hash=9e1c200b2332745ef9e01e64696e9cda6f629937c37a0ee652323f25ccc8df5c))':
dependencies:
'@xterm/xterm': 6.1.0-beta.304(patch_hash=273b687a9c4b571f2f6f4eb4a2da1f651300065500bc30b4df2e911827e9c7eb)
'@xterm/xterm': 6.1.0-beta.304(patch_hash=9e1c200b2332745ef9e01e64696e9cda6f629937c37a0ee652323f25ccc8df5c)
lru-cache: 11.5.1
opentype.js: 2.0.0
'@xterm/addon-search@0.17.0-beta.301(patch_hash=eee5338dd2621ece46e79c61ec06766cd7fadaf79ffdb24e2a8ab68e97ef31f0)(@xterm/xterm@6.1.0-beta.304(patch_hash=273b687a9c4b571f2f6f4eb4a2da1f651300065500bc30b4df2e911827e9c7eb))':
'@xterm/addon-search@0.17.0-beta.301(patch_hash=eee5338dd2621ece46e79c61ec06766cd7fadaf79ffdb24e2a8ab68e97ef31f0)(@xterm/xterm@6.1.0-beta.304(patch_hash=9e1c200b2332745ef9e01e64696e9cda6f629937c37a0ee652323f25ccc8df5c))':
dependencies:
'@xterm/xterm': 6.1.0-beta.304(patch_hash=273b687a9c4b571f2f6f4eb4a2da1f651300065500bc30b4df2e911827e9c7eb)
'@xterm/xterm': 6.1.0-beta.304(patch_hash=9e1c200b2332745ef9e01e64696e9cda6f629937c37a0ee652323f25ccc8df5c)
'@xterm/addon-serialize@0.15.0-beta.301(patch_hash=b35533fe252e7e45433150170348889f4e08a6c17f7017ac34ea694d831fec7f)(@xterm/xterm@6.1.0-beta.304(patch_hash=273b687a9c4b571f2f6f4eb4a2da1f651300065500bc30b4df2e911827e9c7eb))':
'@xterm/addon-serialize@0.15.0-beta.301(patch_hash=b35533fe252e7e45433150170348889f4e08a6c17f7017ac34ea694d831fec7f)(@xterm/xterm@6.1.0-beta.304(patch_hash=9e1c200b2332745ef9e01e64696e9cda6f629937c37a0ee652323f25ccc8df5c))':
dependencies:
'@xterm/xterm': 6.1.0-beta.304(patch_hash=273b687a9c4b571f2f6f4eb4a2da1f651300065500bc30b4df2e911827e9c7eb)
'@xterm/xterm': 6.1.0-beta.304(patch_hash=9e1c200b2332745ef9e01e64696e9cda6f629937c37a0ee652323f25ccc8df5c)
'@xterm/addon-unicode11@0.10.0-beta.301(@xterm/xterm@6.1.0-beta.304(patch_hash=273b687a9c4b571f2f6f4eb4a2da1f651300065500bc30b4df2e911827e9c7eb))':
'@xterm/addon-unicode11@0.10.0-beta.301(@xterm/xterm@6.1.0-beta.304(patch_hash=9e1c200b2332745ef9e01e64696e9cda6f629937c37a0ee652323f25ccc8df5c))':
dependencies:
'@xterm/xterm': 6.1.0-beta.304(patch_hash=273b687a9c4b571f2f6f4eb4a2da1f651300065500bc30b4df2e911827e9c7eb)
'@xterm/xterm': 6.1.0-beta.304(patch_hash=9e1c200b2332745ef9e01e64696e9cda6f629937c37a0ee652323f25ccc8df5c)
'@xterm/addon-web-links@0.13.0-beta.301(@xterm/xterm@6.1.0-beta.304(patch_hash=273b687a9c4b571f2f6f4eb4a2da1f651300065500bc30b4df2e911827e9c7eb))':
'@xterm/addon-web-links@0.13.0-beta.301(@xterm/xterm@6.1.0-beta.304(patch_hash=9e1c200b2332745ef9e01e64696e9cda6f629937c37a0ee652323f25ccc8df5c))':
dependencies:
'@xterm/xterm': 6.1.0-beta.304(patch_hash=273b687a9c4b571f2f6f4eb4a2da1f651300065500bc30b4df2e911827e9c7eb)
'@xterm/xterm': 6.1.0-beta.304(patch_hash=9e1c200b2332745ef9e01e64696e9cda6f629937c37a0ee652323f25ccc8df5c)
'@xterm/addon-webgl@0.20.0-beta.300(patch_hash=5349718f2c19d9319a81552580bf9618ded9b00f3707a5c6cdbbae1910849cd9)(@xterm/xterm@6.1.0-beta.304(patch_hash=273b687a9c4b571f2f6f4eb4a2da1f651300065500bc30b4df2e911827e9c7eb))':
'@xterm/addon-webgl@0.20.0-beta.300(patch_hash=5349718f2c19d9319a81552580bf9618ded9b00f3707a5c6cdbbae1910849cd9)(@xterm/xterm@6.1.0-beta.304(patch_hash=9e1c200b2332745ef9e01e64696e9cda6f629937c37a0ee652323f25ccc8df5c))':
dependencies:
'@xterm/xterm': 6.1.0-beta.304(patch_hash=273b687a9c4b571f2f6f4eb4a2da1f651300065500bc30b4df2e911827e9c7eb)
'@xterm/xterm': 6.1.0-beta.304(patch_hash=9e1c200b2332745ef9e01e64696e9cda6f629937c37a0ee652323f25ccc8df5c)
'@xterm/headless@6.1.0-beta.303': {}
'@xterm/xterm@6.1.0-beta.304(patch_hash=273b687a9c4b571f2f6f4eb4a2da1f651300065500bc30b4df2e911827e9c7eb)': {}
'@xterm/xterm@6.1.0-beta.304(patch_hash=9e1c200b2332745ef9e01e64696e9cda6f629937c37a0ee652323f25ccc8df5c)': {}
abbrev@4.0.0: {}
@@ -730,6 +730,22 @@ describe('xterm IME composition de-duplication', () => {
terminal.dispose()
})
// vim: commit a word, then leave insert mode at once; the accepted commit must precede the ESC.
it('sends an accepted commit before a plain Escape that lands before finalization', async () => {
const { emitted, terminal, textarea } = openTerminal()
startComposition(textarea, '日本語')
await nextEventLoop()
textarea.dispatchEvent(
new CompositionEvent('compositionend', { data: '日本語', bubbles: true })
)
dispatchKeydown(textarea, 'Escape', 'Escape', 27)
await nextEventLoop()
expect(emitted).toEqual(['日本語', '\x1b'])
terminal.dispose()
})
it('cancels a pending completion when Escape follows compositionend', async () => {
const { emitted, terminal, textarea } = openTerminal()
startComposition(textarea, '한')
@@ -145,6 +145,72 @@ describe('a candidate commit that arrives with no composition session', () => {
}
}
// Chromium writes the commit into the textarea before compositionend, so a Process keydown that
// lands before xterm's deferred finalizer owes its own text, not an echo of the commit.
for (const finalizerFirst of [true, false]) {
it(`sends punctuation keyed before the commit finalizes, finalizerFirst=${finalizerFirst}`, async () => {
const rig = openTerminal()
try {
dispatchComposition(rig, 'compositionstart', '')
dispatchComposition(rig, 'compositionupdate', 'ni')
dispatchComposition(rig, 'compositionend', '你')
dispatchKey(rig, 'keydown', { key: 'Process', code: 'Comma', keyCode: IME_KEYCODE })
if (finalizerFirst) {
await nextEventLoop()
}
rig.textarea.value = '你,'
const input = new InputEvent('input', { bubbles: true })
Object.defineProperty(input, 'inputType', { value: 'insertText' })
Object.defineProperty(input, 'data', { value: ',' })
Object.defineProperty(input, 'composed', { value: true })
rig.textarea.dispatchEvent(input)
dispatchKey(rig, 'keyup', { key: 'Process', code: 'Comma', keyCode: IME_KEYCODE })
await nextEventLoop()
await nextEventLoop()
expect(rig.emitted.join('')).toBe('你,')
} finally {
rig.terminal.dispose()
}
})
}
// The keypress sends the key's text on every path, so the input that follows must not claim it.
for (const finalizerRuns of ['beforeKeypress', 'beforeInput', 'afterInput'] as const) {
it(`sends a keypress-carried key once after a commit, finalizer ${finalizerRuns}`, async () => {
const rig = openTerminal()
try {
dispatchComposition(rig, 'compositionstart', '')
dispatchComposition(rig, 'compositionupdate', 'ni')
dispatchComposition(rig, 'compositionend', '你')
dispatchKey(rig, 'keydown', { key: 'Process', code: 'Comma', keyCode: IME_KEYCODE })
if (finalizerRuns === 'beforeKeypress') {
await nextEventLoop()
}
dispatchKey(rig, 'keypress', {
key: ',',
code: 'Comma',
keyCode: 0xff0c,
charCode: 0xff0c
})
if (finalizerRuns === 'beforeInput') {
await nextEventLoop()
}
rig.textarea.value = '你,'
const input = new InputEvent('input', { bubbles: true })
Object.defineProperty(input, 'inputType', { value: 'insertText' })
Object.defineProperty(input, 'data', { value: ',' })
Object.defineProperty(input, 'composed', { value: true })
rig.textarea.dispatchEvent(input)
dispatchKey(rig, 'keyup', { key: 'Process', code: 'Comma', keyCode: IME_KEYCODE })
await nextEventLoop()
await nextEventLoop()
expect(rig.emitted.join('')).toBe('你,')
} finally {
rig.terminal.dispose()
}
})
}
it('sends direct punctuation delivered after the consumed key is released', async () => {
const rig = openTerminal()
try {