mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 08:37:49 +00:00
fix(terminal): highlight the active IME conversion range (#27903)
Japanese IMEs convert one clause at a time, but the terminal's preedit overlay painted the whole composition uniformly, so the clause the candidate window targets was invisible. Read the IME's selection from xterm's textarea on the insertCompositionText input event (Chromium only updates it there) and paint that range of the preedit in the theme's selection colors. Cell-grid advance corrections are unchanged and nothing reaches the PTY; a collapsed caret or a range outside the preedit highlights nothing. Fixes #27873 Co-authored-by: Neil <neil@stably.ai>
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
||||
diff --git a/src/browser/CoreBrowserTerminal.ts b/src/browser/CoreBrowserTerminal.ts
|
||||
index 67893b7966eb095db4459b8837de9766921f36c5..0abf7ecac1aa108f7caf711d0a5c610a688d34fd 100644
|
||||
index 67893b7966eb095db4459b8837de9766921f36c5..3c2ac1afaeda22da4482099702d3ca673c57f4e1 100644
|
||||
--- a/src/browser/CoreBrowserTerminal.ts
|
||||
+++ b/src/browser/CoreBrowserTerminal.ts
|
||||
@@ -325,6 +325,9 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
||||
@@ -55,10 +55,18 @@ index 67893b7966eb095db4459b8837de9766921f36c5..0abf7ecac1aa108f7caf711d0a5c610a
|
||||
|
||||
this._keyPressHandled = true;
|
||||
|
||||
@@ -1027,6 +1048,15 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
||||
@@ -1027,6 +1048,23 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
||||
* @param ev The input event to be handled.
|
||||
*/
|
||||
protected _inputEvent(ev: InputEvent): boolean {
|
||||
+ // Why: Chromium updates the composing selection on input, after compositionupdate.
|
||||
+ if (
|
||||
+ ev.inputType === 'insertCompositionText' &&
|
||||
+ this._compositionHelper instanceof CompositionHelper
|
||||
+ ) {
|
||||
+ // Read-only and never consumes the event, so unlike insertText it stays on in screen-reader mode.
|
||||
+ this._compositionHelper.observeCompositionSelection();
|
||||
+ }
|
||||
+ if (
|
||||
+ ev.data &&
|
||||
+ ev.inputType === 'insertText' &&
|
||||
@@ -88,7 +96,7 @@ index 497afcf535f3eaca00889525a77e15eb633ccd96..96d499b34605f860608382114c3fbdc0
|
||||
|
||||
export interface IBrowser {
|
||||
diff --git a/src/browser/input/CompositionHelper.ts b/src/browser/input/CompositionHelper.ts
|
||||
index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..8ce5bad71b26515ecbbb82b76d218d0b16e280a8 100644
|
||||
index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..cfc6385982b16b8c46b1eff1bbd6a1770c942abe 100644
|
||||
--- a/src/browser/input/CompositionHelper.ts
|
||||
+++ b/src/browser/input/CompositionHelper.ts
|
||||
@@ -1,10 +1,15 @@
|
||||
@@ -153,7 +161,7 @@ index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..8ce5bad71b26515ecbbb82b76d218d0b
|
||||
|
||||
/**
|
||||
* The position within the input textarea's value of the current composition.
|
||||
@@ -36,52 +71,144 @@ export class CompositionHelper {
|
||||
@@ -36,52 +71,146 @@ export class CompositionHelper {
|
||||
*/
|
||||
private _compositionSuffix: string;
|
||||
|
||||
@@ -216,6 +224,8 @@ index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..8ce5bad71b26515ecbbb82b76d218d0b
|
||||
+ /** The preedit's own span, used to anchor the native candidate window. */
|
||||
+ private _compositionPreedit?: HTMLElement;
|
||||
+
|
||||
+ private _compositionSelection?: IPosition;
|
||||
+
|
||||
+ /** The rendered row tail, set only while the cursor sits mid-line. */
|
||||
+ private _compositionRemainder?: HTMLElement;
|
||||
+
|
||||
@@ -308,7 +318,7 @@ index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..8ce5bad71b26515ecbbb82b76d218d0b
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -89,22 +216,95 @@ export class CompositionHelper {
|
||||
@@ -89,22 +218,95 @@ export class CompositionHelper {
|
||||
* @param ev The event.
|
||||
*/
|
||||
public compositionupdate(ev: Pick<CompositionEvent, 'data'>): void {
|
||||
@@ -413,7 +423,7 @@ index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..8ce5bad71b26515ecbbb82b76d218d0b
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -113,10 +313,33 @@ export class CompositionHelper {
|
||||
@@ -113,10 +315,33 @@ export class CompositionHelper {
|
||||
* @returns Whether the Terminal should continue processing the keydown event.
|
||||
*/
|
||||
public keydown(ev: KeyboardEvent): boolean {
|
||||
@@ -448,7 +458,7 @@ index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..8ce5bad71b26515ecbbb82b76d218d0b
|
||||
return false;
|
||||
}
|
||||
if (ev.keyCode === 16 || ev.keyCode === 17 || ev.keyCode === 18) {
|
||||
@@ -128,6 +351,10 @@ export class CompositionHelper {
|
||||
@@ -128,6 +353,10 @@ export class CompositionHelper {
|
||||
this._finalizeComposition(false);
|
||||
}
|
||||
|
||||
@@ -459,7 +469,7 @@ index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..8ce5bad71b26515ecbbb82b76d218d0b
|
||||
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 +365,78 @@ export class CompositionHelper {
|
||||
@@ -138,6 +367,95 @@ export class CompositionHelper {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -515,6 +525,23 @@ index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..8ce5bad71b26515ecbbb82b76d218d0b
|
||||
+ return true;
|
||||
+ }
|
||||
+
|
||||
+ /** The IME's active clause is the textarea selection inside the preedit; a caret marks none. */
|
||||
+ public observeCompositionSelection(): void {
|
||||
+ const preedit = this._compositionPreedit;
|
||||
+ const text = preedit?.textContent?.slice(1, -1) ?? '';
|
||||
+ const start = this._compositionPosition.start;
|
||||
+ const selectionStart = this._textarea.selectionStart - start;
|
||||
+ const selectionEnd = this._textarea.selectionEnd - start;
|
||||
+ this._compositionSelection =
|
||||
+ this._textarea.value.substring(start, start + text.length) === text &&
|
||||
+ 0 <= selectionStart && selectionStart < selectionEnd && selectionEnd <= text.length
|
||||
+ ? { start: selectionStart, end: selectionEnd }
|
||||
+ : undefined;
|
||||
+ if (preedit) {
|
||||
+ this._preeditRenderer?.render(preedit, this._compositionSelection);
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ /**
|
||||
+ * Settles the commit a keydown the IME consumed still owes. That commit normally arrives as the
|
||||
+ * deferred textarea diff, but an asynchronous IME can deliver it after the diff has already run
|
||||
@@ -538,7 +565,7 @@ index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..8ce5bad71b26515ecbbb82b76d218d0b
|
||||
/**
|
||||
* Finalizes the composition, resuming regular input actions. This is called when a composition
|
||||
* is ending.
|
||||
@@ -146,23 +445,52 @@ export class CompositionHelper {
|
||||
@@ -146,23 +464,52 @@ export class CompositionHelper {
|
||||
* compositionend event is triggered, such as enter, so that the composition is sent before
|
||||
* the command is executed.
|
||||
*/
|
||||
@@ -602,7 +629,7 @@ index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..8ce5bad71b26515ecbbb82b76d218d0b
|
||||
|
||||
// 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,35 +500,313 @@ export class CompositionHelper {
|
||||
@@ -172,35 +519,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.
|
||||
@@ -639,8 +666,7 @@ index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..8ce5bad71b26515ecbbb82b76d218d0b
|
||||
+ }
|
||||
+ if (this._pendingComposition === pending) {
|
||||
+ this._sendPendingComposition(pending, true);
|
||||
}
|
||||
- }, 0);
|
||||
+ }
|
||||
+ });
|
||||
+ }
|
||||
+ }
|
||||
@@ -673,7 +699,7 @@ index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..8ce5bad71b26515ecbbb82b76d218d0b
|
||||
+ private _cancelPendingFinalizer(pending: IPendingComposition): void {
|
||||
+ if (pending.finalizerTimer === undefined) {
|
||||
+ return;
|
||||
}
|
||||
+ }
|
||||
+ clearTimeout(pending.finalizerTimer);
|
||||
+ this._compositionTimers.delete(pending.finalizerTimer);
|
||||
+ pending.finalizerTimer = undefined;
|
||||
@@ -865,7 +891,8 @@ index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..8ce5bad71b26515ecbbb82b76d218d0b
|
||||
+ if (!this._compositionEndBelongsToCurrentTransaction(endData)) {
|
||||
+ if (endData.length === 0 && !this._hasCompositionProgress()) {
|
||||
+ this._cancelComposition();
|
||||
+ }
|
||||
}
|
||||
- }, 0);
|
||||
+ return;
|
||||
+ }
|
||||
+ this._finalizeComposition(true, endData);
|
||||
@@ -937,13 +964,13 @@ index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..8ce5bad71b26515ecbbb82b76d218d0b
|
||||
+ private _cancelDeferredTimer(timer?: ReturnType<typeof setTimeout>): void {
|
||||
+ if (timer === undefined) {
|
||||
+ return;
|
||||
+ }
|
||||
}
|
||||
+ clearTimeout(timer);
|
||||
+ this._compositionTimers.delete(timer);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -222,6 +828,9 @@ export class CompositionHelper {
|
||||
@@ -222,6 +847,9 @@ export class CompositionHelper {
|
||||
|
||||
const diff = newValue.replace(oldValue, '');
|
||||
|
||||
@@ -953,7 +980,7 @@ index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..8ce5bad71b26515ecbbb82b76d218d0b
|
||||
this._dataAlreadySent = diff;
|
||||
|
||||
if (newValue.length > oldValue.length) {
|
||||
@@ -236,6 +845,105 @@ export class CompositionHelper {
|
||||
@@ -236,6 +864,106 @@ export class CompositionHelper {
|
||||
}, 0);
|
||||
}
|
||||
|
||||
@@ -968,6 +995,7 @@ index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..8ce5bad71b26515ecbbb82b76d218d0b
|
||||
+ this._resetCompositionView();
|
||||
+ return;
|
||||
+ }
|
||||
+ this._compositionSelection = undefined;
|
||||
+ const doc = this._compositionView.ownerDocument;
|
||||
+ const preedit = doc.createElement('span');
|
||||
+ preedit.className = 'xterm-composition-preedit';
|
||||
@@ -1059,7 +1087,7 @@ index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..8ce5bad71b26515ecbbb82b76d218d0b
|
||||
/**
|
||||
* 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 +951,23 @@ export class CompositionHelper {
|
||||
@@ -243,10 +971,33 @@ export class CompositionHelper {
|
||||
* necessary as the IME events across browsers are not consistently triggered.
|
||||
*/
|
||||
public updateCompositionElements(dontRecurse?: boolean): void {
|
||||
@@ -1069,6 +1097,16 @@ index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..8ce5bad71b26515ecbbb82b76d218d0b
|
||||
return;
|
||||
}
|
||||
|
||||
+ const colors = this._themeService?.colors;
|
||||
+ this._compositionView.style.setProperty(
|
||||
+ '--xterm-composition-selection-background',
|
||||
+ colors?.selectionBackgroundOpaque.css ?? '#FFF'
|
||||
+ );
|
||||
+ this._compositionView.style.setProperty(
|
||||
+ '--xterm-composition-selection-foreground',
|
||||
+ colors?.selectionForeground?.css ?? colors?.foreground.css ?? '#000'
|
||||
+ );
|
||||
+
|
||||
+ // A TUI can repaint the row under an open composition (spinners, streamed output), and this
|
||||
+ // already runs on every render — so keep the rendered tail current with the buffer. A string
|
||||
+ // compare adds no layout read.
|
||||
@@ -1084,7 +1122,7 @@ index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..8ce5bad71b26515ecbbb82b76d218d0b
|
||||
if (this._bufferService.buffer.isCursorInViewport) {
|
||||
const cursorX = Math.min(this._bufferService.buffer.x, this._bufferService.cols - 1);
|
||||
|
||||
@@ -254,31 +975,156 @@ export class CompositionHelper {
|
||||
@@ -254,31 +1005,176 @@ export class CompositionHelper {
|
||||
const cursorTop = this._bufferService.buffer.y * this._renderService.dimensions.css.cell.height;
|
||||
const cursorLeft = cursorX * this._renderService.dimensions.css.cell.width;
|
||||
|
||||
@@ -1099,7 +1137,7 @@ index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..8ce5bad71b26515ecbbb82b76d218d0b
|
||||
+ this._preeditRenderer ??= new CompositionPreeditRenderer(
|
||||
+ this._unicodeService, this._compositionView.ownerDocument, () => this.updateCompositionElements(true)
|
||||
+ );
|
||||
+ this._preeditRenderer.render(this._compositionPreedit);
|
||||
+ this._preeditRenderer.render(this._compositionPreedit, this._compositionSelection);
|
||||
+ }
|
||||
// Limit the composition view width to the space between the cursor and
|
||||
// the terminal's right edge, preventing it from overflowing the terminal.
|
||||
@@ -1160,6 +1198,8 @@ index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..8ce5bad71b26515ecbbb82b76d218d0b
|
||||
+ private _measure?: WidthCacheFontVariantCanvas;
|
||||
+ private _preedit?: HTMLElement;
|
||||
+ private _font = '';
|
||||
+ private _selectionStart?: number;
|
||||
+ private _selectionEnd?: number;
|
||||
+
|
||||
+ constructor(private readonly _unicodeService: IUnicodeService, doc: Document, refresh: () => void) {
|
||||
+ super();
|
||||
@@ -1184,52 +1224,67 @@ index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..8ce5bad71b26515ecbbb82b76d218d0b
|
||||
+ super.dispose();
|
||||
+ }
|
||||
+
|
||||
+ public render(preedit: HTMLElement): void {
|
||||
+ public render(preedit: HTMLElement, selection?: IPosition): void {
|
||||
+ const doc = preedit.ownerDocument;
|
||||
+ const style = doc.defaultView?.getComputedStyle(preedit);
|
||||
+ if (!style) {
|
||||
+ return;
|
||||
+ }
|
||||
+ const font = [style.fontFamily, style.fontSize, style.fontWeight, style.fontStyle, doc.defaultView?.devicePixelRatio].join('|');
|
||||
+ if (this._preedit === preedit && this._font === font) {
|
||||
+ if (
|
||||
+ this._preedit === preedit &&
|
||||
+ this._font === font &&
|
||||
+ this._selectionStart === selection?.start &&
|
||||
+ this._selectionEnd === selection?.end
|
||||
+ ) {
|
||||
+ return;
|
||||
+ }
|
||||
+ this._preedit = preedit;
|
||||
+ this._font = font;
|
||||
+ this._selectionStart = selection?.start;
|
||||
+ this._selectionEnd = selection?.end;
|
||||
+ const data = (preedit.textContent ?? '').slice(1, -1);
|
||||
+ const widths = new Map<string, number>();
|
||||
+ const nodes: Node[] = [doc.createTextNode('')];
|
||||
+ let start = 0;
|
||||
+ let spacing: string | undefined;
|
||||
+ let selected = false;
|
||||
+ let work = 0;
|
||||
+ const emit = (end: number): void => {
|
||||
+ const emit = (end: number, countLayoutWork: boolean = true): void => {
|
||||
+ if (end === start) {
|
||||
+ return;
|
||||
+ }
|
||||
+ const text = data.slice(start, end);
|
||||
+ if (spacing === undefined) {
|
||||
+ if (spacing === undefined && !selected) {
|
||||
+ nodes.push(doc.createTextNode(text));
|
||||
+ } else {
|
||||
+ const span = doc.createElement('span');
|
||||
+ span.style.letterSpacing = spacing;
|
||||
+ span.style.fontKerning = 'none';
|
||||
+ if (spacing !== undefined) {
|
||||
+ span.style.letterSpacing = spacing;
|
||||
+ span.style.fontKerning = 'none';
|
||||
+ if (countLayoutWork) {
|
||||
+ work++;
|
||||
+ }
|
||||
+ }
|
||||
+ span.style.textDecoration = 'inherit';
|
||||
+ if (selected) {
|
||||
+ span.classList.add('xterm-composition-preedit-selected');
|
||||
+ span.style.backgroundColor = 'var(--xterm-composition-selection-background)';
|
||||
+ span.style.color = 'var(--xterm-composition-selection-foreground)';
|
||||
+ }
|
||||
+ span.textContent = text;
|
||||
+ nodes.push(span);
|
||||
+ work++;
|
||||
+ }
|
||||
+ start = end;
|
||||
+ };
|
||||
+ for (const { segment, index } of this._segmenter.segment(data)) {
|
||||
+ // Bound cold measurements and styled runs without changing an already-corrected prefix.
|
||||
+ if (work >= MAX_PREEDIT_LAYOUT_WORK) {
|
||||
+ emit(index);
|
||||
+ spacing = undefined;
|
||||
+ break;
|
||||
+ }
|
||||
+ // Why: a clause boundary inside a grapheme highlights the whole grapheme.
|
||||
+ const nextSelected =
|
||||
+ selection !== undefined && index < selection.end && index + segment.length > selection.start;
|
||||
+ let nextSpacing: string | undefined;
|
||||
+ const columns = this._unicodeService.wcwidth(segment.codePointAt(0)!);
|
||||
+ if (CJK.test(segment) && !NATIVE.test(segment) && (columns === 2 || /^[\uff61-\uff9f]$/u.test(segment))) {
|
||||
+ // Bound cold measurements and styled runs without changing an already-corrected prefix.
|
||||
+ if (work < MAX_PREEDIT_LAYOUT_WORK && CJK.test(segment) && !NATIVE.test(segment) && (columns === 2 || /^[\uff61-\uff9f]$/u.test(segment))) {
|
||||
+ let width = widths.get(segment);
|
||||
+ if (width === undefined) {
|
||||
+ this._measure ??= new WidthCacheFontVariantCanvas();
|
||||
@@ -1240,9 +1295,12 @@ index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..8ce5bad71b26515ecbbb82b76d218d0b
|
||||
+ }
|
||||
+ nextSpacing = `calc(var(--xterm-composition-cell-width) * ${columns} - ${width}px)`;
|
||||
+ }
|
||||
+ if (nextSpacing !== spacing) {
|
||||
+ emit(index);
|
||||
+ const spacingChanged = nextSpacing !== spacing;
|
||||
+ if (spacingChanged || nextSelected !== selected) {
|
||||
+ // Why: selection-only splits do not add layout or measurement work.
|
||||
+ emit(index, spacingChanged);
|
||||
+ spacing = nextSpacing;
|
||||
+ selected = nextSelected;
|
||||
+ }
|
||||
}
|
||||
+ emit(data.length);
|
||||
|
||||
Generated
+27
-27
@@ -180,7 +180,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': 4542f9cd2e9ac646c0f58575d9dc7308009d49bba04dee45e12fd8437c266b7e
|
||||
'@xterm/xterm@6.1.0-beta.304': 6a96a556cb32825d4084043f7e18fe16203686d8352affd2f01613c0b464a6ed
|
||||
i18next-cli@1.74.2: 2ee32eef742615cd133e1a6940f027deb3723a8430bb7953bba3a6769b0aac47
|
||||
lint-staged@16.4.0: 7333b3837f80a7fbd045964db6d76ba4fc118e49134bdbabb00585b6b7b60673
|
||||
monaco-editor@0.55.1: 53b71f7d8a7c00b76ffdb73ce83d18e31692f193f5fafa6a390e48f5b3fdd636
|
||||
@@ -211,7 +211,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=4542f9cd2e9ac646c0f58575d9dc7308009d49bba04dee45e12fd8437c266b7e))
|
||||
version: 0.15.0-beta.301(patch_hash=b35533fe252e7e45433150170348889f4e08a6c17f7017ac34ea694d831fec7f)(@xterm/xterm@6.1.0-beta.304(patch_hash=6a96a556cb32825d4084043f7e18fe16203686d8352affd2f01613c0b464a6ed))
|
||||
'@xterm/headless':
|
||||
specifier: 6.1.0-beta.303
|
||||
version: 6.1.0-beta.303
|
||||
@@ -407,28 +407,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=4542f9cd2e9ac646c0f58575d9dc7308009d49bba04dee45e12fd8437c266b7e))
|
||||
version: 0.12.0-beta.301(@xterm/xterm@6.1.0-beta.304(patch_hash=6a96a556cb32825d4084043f7e18fe16203686d8352affd2f01613c0b464a6ed))
|
||||
'@xterm/addon-image':
|
||||
specifier: 0.10.0-beta.301
|
||||
version: 0.10.0-beta.301(patch_hash=f088c9207dac42844a542b9da8078be52351cee60585725ef189b9fd30f33f34)(@xterm/xterm@6.1.0-beta.304(patch_hash=4542f9cd2e9ac646c0f58575d9dc7308009d49bba04dee45e12fd8437c266b7e))
|
||||
version: 0.10.0-beta.301(patch_hash=f088c9207dac42844a542b9da8078be52351cee60585725ef189b9fd30f33f34)(@xterm/xterm@6.1.0-beta.304(patch_hash=6a96a556cb32825d4084043f7e18fe16203686d8352affd2f01613c0b464a6ed))
|
||||
'@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=4542f9cd2e9ac646c0f58575d9dc7308009d49bba04dee45e12fd8437c266b7e))
|
||||
version: 0.11.0-beta.301(patch_hash=47405b9994b5acf1b4e90b49250358c1ca03649854d59560e7732b72fe336920)(@xterm/xterm@6.1.0-beta.304(patch_hash=6a96a556cb32825d4084043f7e18fe16203686d8352affd2f01613c0b464a6ed))
|
||||
'@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=4542f9cd2e9ac646c0f58575d9dc7308009d49bba04dee45e12fd8437c266b7e))
|
||||
version: 0.17.0-beta.301(patch_hash=eee5338dd2621ece46e79c61ec06766cd7fadaf79ffdb24e2a8ab68e97ef31f0)(@xterm/xterm@6.1.0-beta.304(patch_hash=6a96a556cb32825d4084043f7e18fe16203686d8352affd2f01613c0b464a6ed))
|
||||
'@xterm/addon-unicode11':
|
||||
specifier: 0.10.0-beta.301
|
||||
version: 0.10.0-beta.301(@xterm/xterm@6.1.0-beta.304(patch_hash=4542f9cd2e9ac646c0f58575d9dc7308009d49bba04dee45e12fd8437c266b7e))
|
||||
version: 0.10.0-beta.301(@xterm/xterm@6.1.0-beta.304(patch_hash=6a96a556cb32825d4084043f7e18fe16203686d8352affd2f01613c0b464a6ed))
|
||||
'@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=4542f9cd2e9ac646c0f58575d9dc7308009d49bba04dee45e12fd8437c266b7e))
|
||||
version: 0.13.0-beta.301(@xterm/xterm@6.1.0-beta.304(patch_hash=6a96a556cb32825d4084043f7e18fe16203686d8352affd2f01613c0b464a6ed))
|
||||
'@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=4542f9cd2e9ac646c0f58575d9dc7308009d49bba04dee45e12fd8437c266b7e))
|
||||
version: 0.20.0-beta.300(patch_hash=5349718f2c19d9319a81552580bf9618ded9b00f3707a5c6cdbbae1910849cd9)(@xterm/xterm@6.1.0-beta.304(patch_hash=6a96a556cb32825d4084043f7e18fe16203686d8352affd2f01613c0b464a6ed))
|
||||
'@xterm/xterm':
|
||||
specifier: 6.1.0-beta.304
|
||||
version: 6.1.0-beta.304(patch_hash=4542f9cd2e9ac646c0f58575d9dc7308009d49bba04dee45e12fd8437c266b7e)
|
||||
version: 6.1.0-beta.304(patch_hash=6a96a556cb32825d4084043f7e18fe16203686d8352affd2f01613c0b464a6ed)
|
||||
bun:
|
||||
specifier: 1.4.4
|
||||
version: 1.4.4
|
||||
@@ -11043,43 +11043,43 @@ snapshots:
|
||||
|
||||
'@xmldom/xmldom@0.8.15': {}
|
||||
|
||||
'@xterm/addon-fit@0.12.0-beta.301(@xterm/xterm@6.1.0-beta.304(patch_hash=4542f9cd2e9ac646c0f58575d9dc7308009d49bba04dee45e12fd8437c266b7e))':
|
||||
'@xterm/addon-fit@0.12.0-beta.301(@xterm/xterm@6.1.0-beta.304(patch_hash=6a96a556cb32825d4084043f7e18fe16203686d8352affd2f01613c0b464a6ed))':
|
||||
dependencies:
|
||||
'@xterm/xterm': 6.1.0-beta.304(patch_hash=4542f9cd2e9ac646c0f58575d9dc7308009d49bba04dee45e12fd8437c266b7e)
|
||||
'@xterm/xterm': 6.1.0-beta.304(patch_hash=6a96a556cb32825d4084043f7e18fe16203686d8352affd2f01613c0b464a6ed)
|
||||
|
||||
'@xterm/addon-image@0.10.0-beta.301(patch_hash=f088c9207dac42844a542b9da8078be52351cee60585725ef189b9fd30f33f34)(@xterm/xterm@6.1.0-beta.304(patch_hash=4542f9cd2e9ac646c0f58575d9dc7308009d49bba04dee45e12fd8437c266b7e))':
|
||||
'@xterm/addon-image@0.10.0-beta.301(patch_hash=f088c9207dac42844a542b9da8078be52351cee60585725ef189b9fd30f33f34)(@xterm/xterm@6.1.0-beta.304(patch_hash=6a96a556cb32825d4084043f7e18fe16203686d8352affd2f01613c0b464a6ed))':
|
||||
dependencies:
|
||||
'@xterm/xterm': 6.1.0-beta.304(patch_hash=4542f9cd2e9ac646c0f58575d9dc7308009d49bba04dee45e12fd8437c266b7e)
|
||||
'@xterm/xterm': 6.1.0-beta.304(patch_hash=6a96a556cb32825d4084043f7e18fe16203686d8352affd2f01613c0b464a6ed)
|
||||
|
||||
'@xterm/addon-ligatures@0.11.0-beta.301(patch_hash=47405b9994b5acf1b4e90b49250358c1ca03649854d59560e7732b72fe336920)(@xterm/xterm@6.1.0-beta.304(patch_hash=4542f9cd2e9ac646c0f58575d9dc7308009d49bba04dee45e12fd8437c266b7e))':
|
||||
'@xterm/addon-ligatures@0.11.0-beta.301(patch_hash=47405b9994b5acf1b4e90b49250358c1ca03649854d59560e7732b72fe336920)(@xterm/xterm@6.1.0-beta.304(patch_hash=6a96a556cb32825d4084043f7e18fe16203686d8352affd2f01613c0b464a6ed))':
|
||||
dependencies:
|
||||
'@xterm/xterm': 6.1.0-beta.304(patch_hash=4542f9cd2e9ac646c0f58575d9dc7308009d49bba04dee45e12fd8437c266b7e)
|
||||
'@xterm/xterm': 6.1.0-beta.304(patch_hash=6a96a556cb32825d4084043f7e18fe16203686d8352affd2f01613c0b464a6ed)
|
||||
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=4542f9cd2e9ac646c0f58575d9dc7308009d49bba04dee45e12fd8437c266b7e))':
|
||||
'@xterm/addon-search@0.17.0-beta.301(patch_hash=eee5338dd2621ece46e79c61ec06766cd7fadaf79ffdb24e2a8ab68e97ef31f0)(@xterm/xterm@6.1.0-beta.304(patch_hash=6a96a556cb32825d4084043f7e18fe16203686d8352affd2f01613c0b464a6ed))':
|
||||
dependencies:
|
||||
'@xterm/xterm': 6.1.0-beta.304(patch_hash=4542f9cd2e9ac646c0f58575d9dc7308009d49bba04dee45e12fd8437c266b7e)
|
||||
'@xterm/xterm': 6.1.0-beta.304(patch_hash=6a96a556cb32825d4084043f7e18fe16203686d8352affd2f01613c0b464a6ed)
|
||||
|
||||
'@xterm/addon-serialize@0.15.0-beta.301(patch_hash=b35533fe252e7e45433150170348889f4e08a6c17f7017ac34ea694d831fec7f)(@xterm/xterm@6.1.0-beta.304(patch_hash=4542f9cd2e9ac646c0f58575d9dc7308009d49bba04dee45e12fd8437c266b7e))':
|
||||
'@xterm/addon-serialize@0.15.0-beta.301(patch_hash=b35533fe252e7e45433150170348889f4e08a6c17f7017ac34ea694d831fec7f)(@xterm/xterm@6.1.0-beta.304(patch_hash=6a96a556cb32825d4084043f7e18fe16203686d8352affd2f01613c0b464a6ed))':
|
||||
dependencies:
|
||||
'@xterm/xterm': 6.1.0-beta.304(patch_hash=4542f9cd2e9ac646c0f58575d9dc7308009d49bba04dee45e12fd8437c266b7e)
|
||||
'@xterm/xterm': 6.1.0-beta.304(patch_hash=6a96a556cb32825d4084043f7e18fe16203686d8352affd2f01613c0b464a6ed)
|
||||
|
||||
'@xterm/addon-unicode11@0.10.0-beta.301(@xterm/xterm@6.1.0-beta.304(patch_hash=4542f9cd2e9ac646c0f58575d9dc7308009d49bba04dee45e12fd8437c266b7e))':
|
||||
'@xterm/addon-unicode11@0.10.0-beta.301(@xterm/xterm@6.1.0-beta.304(patch_hash=6a96a556cb32825d4084043f7e18fe16203686d8352affd2f01613c0b464a6ed))':
|
||||
dependencies:
|
||||
'@xterm/xterm': 6.1.0-beta.304(patch_hash=4542f9cd2e9ac646c0f58575d9dc7308009d49bba04dee45e12fd8437c266b7e)
|
||||
'@xterm/xterm': 6.1.0-beta.304(patch_hash=6a96a556cb32825d4084043f7e18fe16203686d8352affd2f01613c0b464a6ed)
|
||||
|
||||
'@xterm/addon-web-links@0.13.0-beta.301(@xterm/xterm@6.1.0-beta.304(patch_hash=4542f9cd2e9ac646c0f58575d9dc7308009d49bba04dee45e12fd8437c266b7e))':
|
||||
'@xterm/addon-web-links@0.13.0-beta.301(@xterm/xterm@6.1.0-beta.304(patch_hash=6a96a556cb32825d4084043f7e18fe16203686d8352affd2f01613c0b464a6ed))':
|
||||
dependencies:
|
||||
'@xterm/xterm': 6.1.0-beta.304(patch_hash=4542f9cd2e9ac646c0f58575d9dc7308009d49bba04dee45e12fd8437c266b7e)
|
||||
'@xterm/xterm': 6.1.0-beta.304(patch_hash=6a96a556cb32825d4084043f7e18fe16203686d8352affd2f01613c0b464a6ed)
|
||||
|
||||
'@xterm/addon-webgl@0.20.0-beta.300(patch_hash=5349718f2c19d9319a81552580bf9618ded9b00f3707a5c6cdbbae1910849cd9)(@xterm/xterm@6.1.0-beta.304(patch_hash=4542f9cd2e9ac646c0f58575d9dc7308009d49bba04dee45e12fd8437c266b7e))':
|
||||
'@xterm/addon-webgl@0.20.0-beta.300(patch_hash=5349718f2c19d9319a81552580bf9618ded9b00f3707a5c6cdbbae1910849cd9)(@xterm/xterm@6.1.0-beta.304(patch_hash=6a96a556cb32825d4084043f7e18fe16203686d8352affd2f01613c0b464a6ed))':
|
||||
dependencies:
|
||||
'@xterm/xterm': 6.1.0-beta.304(patch_hash=4542f9cd2e9ac646c0f58575d9dc7308009d49bba04dee45e12fd8437c266b7e)
|
||||
'@xterm/xterm': 6.1.0-beta.304(patch_hash=6a96a556cb32825d4084043f7e18fe16203686d8352affd2f01613c0b464a6ed)
|
||||
|
||||
'@xterm/headless@6.1.0-beta.303': {}
|
||||
|
||||
'@xterm/xterm@6.1.0-beta.304(patch_hash=4542f9cd2e9ac646c0f58575d9dc7308009d49bba04dee45e12fd8437c266b7e)': {}
|
||||
'@xterm/xterm@6.1.0-beta.304(patch_hash=6a96a556cb32825d4084043f7e18fe16203686d8352affd2f01613c0b464a6ed)': {}
|
||||
|
||||
abbrev@4.0.0: {}
|
||||
|
||||
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { Terminal, type ITerminalOptions } from '@xterm/xterm'
|
||||
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
|
||||
|
||||
// Text an earlier commit left in xterm's textarea; Chromium reports clause offsets past it.
|
||||
const committed = 'ls\n'
|
||||
|
||||
let terminal: Terminal
|
||||
let sent: string[]
|
||||
|
||||
beforeEach(() => {
|
||||
vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockImplementation(() => {
|
||||
const context: CanvasRenderingContext2D = Object.create(null)
|
||||
context.measureText = () => Object.assign(Object.create(null), { width: 10 })
|
||||
return context
|
||||
})
|
||||
sent = []
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
terminal.dispose()
|
||||
vi.restoreAllMocks()
|
||||
document.body.replaceChildren()
|
||||
})
|
||||
|
||||
// happy-dom drops these fields from event init dictionaries; Chromium supplies them.
|
||||
function dispatch(event: Event, fields: Record<string, unknown>): void {
|
||||
for (const [key, value] of Object.entries(fields)) {
|
||||
Object.defineProperty(event, key, { value })
|
||||
}
|
||||
terminal.textarea!.dispatchEvent(event)
|
||||
}
|
||||
|
||||
function openComposition(options: ITerminalOptions = {}): void {
|
||||
const container = document.createElement('div')
|
||||
document.body.appendChild(container)
|
||||
terminal = new Terminal(options)
|
||||
terminal.open(container)
|
||||
terminal.onData((data) => sent.push(data))
|
||||
terminal.textarea!.value = committed
|
||||
terminal.textarea!.setSelectionRange(committed.length, committed.length)
|
||||
dispatch(new CompositionEvent('compositionstart'), { data: '' })
|
||||
}
|
||||
|
||||
function updatePreedit(text: string, textarea = text): void {
|
||||
terminal.textarea!.value = committed + textarea
|
||||
dispatch(new CompositionEvent('compositionupdate'), { data: text })
|
||||
}
|
||||
|
||||
/** Chromium's insertCompositionText input, with the IME's clause as the textarea selection. */
|
||||
function selectClause(start: number, end: number): void {
|
||||
terminal.textarea!.setSelectionRange(committed.length + start, committed.length + end)
|
||||
dispatch(new InputEvent('input'), { inputType: 'insertCompositionText', composed: true })
|
||||
}
|
||||
|
||||
function highlighted(): string {
|
||||
return Array.from(
|
||||
terminal.element!.querySelectorAll('.xterm-composition-preedit-selected'),
|
||||
(span) => span.textContent
|
||||
).join('')
|
||||
}
|
||||
|
||||
/** Each preedit character's letter-spacing, i.e. its advance correction onto the cell grid. */
|
||||
function advanceCorrections(): string[] {
|
||||
const preedit = terminal.element!.querySelector('.xterm-composition-preedit')!
|
||||
return Array.from(preedit.childNodes).flatMap((node) =>
|
||||
Array.from(node.textContent ?? '', () =>
|
||||
node instanceof HTMLElement ? node.style.letterSpacing : ''
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
it.each([false, true])(
|
||||
'highlights each clause the IME selects without sending it (screenReaderMode: %s)',
|
||||
async (screenReaderMode) => {
|
||||
openComposition({ screenReaderMode })
|
||||
for (const [preedit, start, end] of [
|
||||
['今日はいい天気です', 0, 3],
|
||||
['今日はいい天気です', 3, 9],
|
||||
['きょうはいいてんきです', 0, 5]
|
||||
] as const) {
|
||||
updatePreedit(preedit)
|
||||
selectClause(start, end)
|
||||
// xterm re-lays out the preedit one task after every update.
|
||||
await new Promise((resolve) => setTimeout(resolve))
|
||||
expect(highlighted()).toBe(preedit.slice(start, end))
|
||||
}
|
||||
expect(sent).toEqual([])
|
||||
}
|
||||
)
|
||||
|
||||
it('drops the highlight when the IME replaces the preedit', () => {
|
||||
openComposition()
|
||||
updatePreedit('きょう')
|
||||
selectClause(0, 3)
|
||||
updatePreedit('きょうは')
|
||||
expect(highlighted()).toBe('')
|
||||
})
|
||||
|
||||
it('highlights a whole grapheme when a clause boundary falls inside it, but not a caret', () => {
|
||||
openComposition()
|
||||
updatePreedit('か\u3099X')
|
||||
for (const [start, end, expected] of [
|
||||
[0, 1, 'か\u3099'],
|
||||
[1, 2, 'か\u3099'],
|
||||
[1, 1, '']
|
||||
] as const) {
|
||||
selectClause(start, end)
|
||||
expect(highlighted()).toBe(expected)
|
||||
}
|
||||
})
|
||||
|
||||
it.each([
|
||||
['that starts in committed text', '今日は', -1, 2],
|
||||
['that runs past the preedit', '今日はx', 2, 4],
|
||||
['in a textarea that no longer holds the preedit', 'きょう', 0, 2]
|
||||
])('ignores a selection %s', (_selection, textarea, start, end) => {
|
||||
openComposition()
|
||||
updatePreedit('今日は', textarea)
|
||||
selectClause(start, end)
|
||||
expect(highlighted()).toBe('')
|
||||
})
|
||||
|
||||
it.each([
|
||||
['selectionForeground', { selectionForeground: '#abcdef' }, '#abcdef'],
|
||||
['foreground when selectionForeground is unset', { foreground: '#fedcba' }, '#fedcba']
|
||||
])(
|
||||
'colors the highlight with selectionBackground and the theme %s',
|
||||
(_source, theme, foreground) => {
|
||||
openComposition({ theme: { selectionBackground: '#123456', ...theme } })
|
||||
updatePreedit('今日は')
|
||||
const view = terminal.element!.querySelector<HTMLElement>('.composition-view')!
|
||||
expect(view.style.getPropertyValue('--xterm-composition-selection-background')).toBe('#123456')
|
||||
expect(view.style.getPropertyValue('--xterm-composition-selection-foreground')).toBe(foreground)
|
||||
}
|
||||
)
|
||||
|
||||
it.each([
|
||||
{ where: 'inside the corrected prefix', start: 10, end: 20 },
|
||||
{ where: 'past the layout budget', start: 252, end: 255 }
|
||||
])(
|
||||
'highlights a clause $where without moving any character off the cell grid',
|
||||
({ start, end }) => {
|
||||
const text = Array.from({ length: 256 }, (_, index) =>
|
||||
String.fromCodePoint(0x4e00 + index)
|
||||
).join('')
|
||||
openComposition()
|
||||
updatePreedit(text)
|
||||
const corrections = advanceCorrections()
|
||||
selectClause(start, end)
|
||||
expect(highlighted()).toBe(text.slice(start, end))
|
||||
expect(advanceCorrections()).toEqual(corrections)
|
||||
}
|
||||
)
|
||||
+17
-4
@@ -92,11 +92,15 @@ function replayEvent(textarea: HTMLTextAreaElement, recorded: RecordedEvent): vo
|
||||
|
||||
type PreeditSample = { data: string; shown: boolean }
|
||||
|
||||
async function replayTrace(
|
||||
trace: RecordedTrace
|
||||
): Promise<{ stream: string; preedits: PreeditSample[]; inputSignals: TypingInputSignal[] }> {
|
||||
async function replayTrace(trace: RecordedTrace): Promise<{
|
||||
stream: string
|
||||
preedits: PreeditSample[]
|
||||
highlights: string[]
|
||||
inputSignals: TypingInputSignal[]
|
||||
}> {
|
||||
const { emitted, terminal, textarea } = openTerminal()
|
||||
const preedits: PreeditSample[] = []
|
||||
const highlights: string[] = []
|
||||
const inputSignals: TypingInputSignal[] = []
|
||||
const detachInputEvents = installTypingLatencyInputEvents(window, (signal) => {
|
||||
inputSignals.push(signal)
|
||||
@@ -108,6 +112,9 @@ async function replayTrace(
|
||||
await nextEventLoop()
|
||||
}
|
||||
replayEvent(textarea, recorded)
|
||||
if (terminal.element?.querySelector('.xterm-composition-preedit-selected')) {
|
||||
highlights.push(recorded.type)
|
||||
}
|
||||
if (recorded.type === 'compositionupdate' && recorded.data) {
|
||||
const view = terminal.element?.querySelector('.composition-view')
|
||||
preedits.push({ data: recorded.data, shown: view?.classList.contains('active') === true })
|
||||
@@ -116,7 +123,7 @@ async function replayTrace(
|
||||
// Two turns: the commit's deferred send, then the late-native-commit window it opens.
|
||||
await nextEventLoop()
|
||||
await nextEventLoop()
|
||||
return { stream: emitted.join(''), preedits, inputSignals }
|
||||
return { stream: emitted.join(''), preedits, highlights, inputSignals }
|
||||
} finally {
|
||||
detachInputEvents()
|
||||
terminal.dispose()
|
||||
@@ -180,4 +187,10 @@ describe.each([
|
||||
expect(preedits.length).toBeGreaterThan(0)
|
||||
expect(preedits.filter((sample) => !sample.shown)).toEqual([])
|
||||
})
|
||||
|
||||
// Why: Hangul IMEs keep the syllable they are replacing selected until the input event collapses
|
||||
// it, so reading the selection any earlier flashes every syllable as a converted clause.
|
||||
it('does not highlight the syllable the IME is replacing', async () => {
|
||||
expect((await replayTrace(trace)).highlights).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -109,12 +109,16 @@ export async function dispatchPlainEnter(session: CDPSession): Promise<void> {
|
||||
}
|
||||
|
||||
// imeSetComposition opens a real Chromium composition session for native-IME-free preedit geometry checks.
|
||||
export async function setImeComposition(session: CDPSession, text: string): Promise<void> {
|
||||
export async function setImeComposition(
|
||||
session: CDPSession,
|
||||
text: string,
|
||||
selection?: { start: number; end: number }
|
||||
): Promise<void> {
|
||||
const length = Array.from(text).length
|
||||
await session.send('Input.imeSetComposition', {
|
||||
text,
|
||||
selectionStart: length,
|
||||
selectionEnd: length
|
||||
selectionStart: selection?.start ?? length,
|
||||
selectionEnd: selection?.end ?? length
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { expect, test } from './helpers/orca-app'
|
||||
import { closeTerminalImePaneArena, openTerminalImePaneArena } from './terminal-ime-pane-arena'
|
||||
import { setImeComposition } from './terminal-ime-cdp-composition'
|
||||
import { writeToActiveTerminal } from './terminal-ime-midline-occlusion-probe'
|
||||
|
||||
/** Every preedit code unit's left edge, then the preedit width, relative to the preedit. */
|
||||
function measurePreeditEdges(): number[] {
|
||||
const preedit = document.querySelector<HTMLElement>(
|
||||
'.composition-view.active .xterm-composition-preedit'
|
||||
)!
|
||||
const bounds = preedit.getBoundingClientRect()
|
||||
const edges: number[] = []
|
||||
const walker = document.createTreeWalker(preedit, NodeFilter.SHOW_TEXT)
|
||||
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
|
||||
for (let offset = 0; offset < node.textContent!.length; offset++) {
|
||||
const range = document.createRange()
|
||||
range.setStart(node, offset)
|
||||
range.setEnd(node, offset + 1)
|
||||
edges.push(range.getBoundingClientRect().left - bounds.left)
|
||||
}
|
||||
}
|
||||
return [...edges, bounds.width]
|
||||
}
|
||||
|
||||
test('paints the selected clause in the theme selection colors without moving any cell', async ({
|
||||
orcaPage
|
||||
}, testInfo) => {
|
||||
const arena = await openTerminalImePaneArena(orcaPage)
|
||||
let completed = false
|
||||
try {
|
||||
await orcaPage.evaluate(() => {
|
||||
const state = window.__store!.getState()
|
||||
const terminal = window.__paneManagers!.get(state.activeTabId!)!.getActivePane()!.terminal
|
||||
terminal.options.theme = {
|
||||
...terminal.options.theme,
|
||||
selectionBackground: '#7357d9',
|
||||
selectionForeground: '#ffffff'
|
||||
}
|
||||
})
|
||||
await writeToActiveTerminal(orcaPage, '\x1b[2J\x1b[H')
|
||||
const text = '今日はいい天気です'
|
||||
await setImeComposition(arena.session, text)
|
||||
await expect(
|
||||
orcaPage.locator('.composition-view.active .xterm-composition-preedit')
|
||||
).toHaveText(`${text}`)
|
||||
const unselected = await orcaPage.evaluate(measurePreeditEdges)
|
||||
|
||||
await setImeComposition(arena.session, text, { start: 3, end: 9 })
|
||||
const clause = orcaPage.locator('.xterm-composition-preedit-selected')
|
||||
await expect(clause).toHaveText('いい天気です')
|
||||
await expect(clause).toHaveCSS('background-color', 'rgb(115, 87, 217)')
|
||||
await expect(clause).toHaveCSS('color', 'rgb(255, 255, 255)')
|
||||
const selected = await orcaPage.evaluate(measurePreeditEdges)
|
||||
// Chromium snaps each inline box the selection adds to 1/64px layout units.
|
||||
expect(selected).toHaveLength(unselected.length)
|
||||
selected.forEach((edge, index) => expect(edge).toBeCloseTo(unselected[index], 1))
|
||||
|
||||
await setImeComposition(arena.session, '')
|
||||
completed = true
|
||||
} finally {
|
||||
await closeTerminalImePaneArena(arena, testInfo, 'ime-clause-selection', !completed)
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user