-
Notifications
You must be signed in to change notification settings - Fork 1.9k
fix: handle insertReplacementText for Korean IME on WKWebView/Safari #5704
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 2 commits
d70c52d
a3e9c84
c02ba2c
16c7a83
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -120,6 +120,64 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal { | |
| */ | ||
| private _unprocessedDeadKey: boolean = false; | ||
|
|
||
| /** | ||
| * WKWebView IME workaround: holds the latest composed text from | ||
| * insertReplacementText events, flushed on next insertText or non-IME keydown. | ||
| */ | ||
| private _wkImeComposing: boolean = false; | ||
| private _wkImePending: string = ''; | ||
|
Comment on lines
+124
to
+129
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I looked at how the events work Safari and it's unfortunate. How about instead of hardcoding this stuff into CoreBrowserTerminal, have a |
||
|
|
||
| private static _isHangul(text: string): boolean { | ||
| const cp = text.codePointAt(0)!; | ||
| return (cp >= 0x1100 && cp <= 0x11FF) || | ||
| (cp >= 0x3130 && cp <= 0x318F) || | ||
| (cp >= 0xAC00 && cp <= 0xD7AF) || | ||
| (cp >= 0xA960 && cp <= 0xA97F) || | ||
| (cp >= 0xD7B0 && cp <= 0xD7FF); | ||
| } | ||
|
|
||
| private _wkShowComposition(text: string): void { | ||
| if (!this._compositionView || !this._renderService) return; | ||
| this._compositionView.textContent = text; | ||
| this._compositionView.classList.add('active'); | ||
|
|
||
| const cursorX = Math.min(this.buffer.x, this.cols - 1); | ||
| const cellHeight = this._renderService.dimensions.css.cell.height; | ||
| const cursorTop = this.buffer.y * cellHeight; | ||
| const cursorLeft = cursorX * this._renderService.dimensions.css.cell.width; | ||
|
|
||
| this._compositionView.style.left = cursorLeft + 'px'; | ||
| this._compositionView.style.top = cursorTop + 'px'; | ||
| this._compositionView.style.height = cellHeight + 'px'; | ||
| this._compositionView.style.lineHeight = cellHeight + 'px'; | ||
| this._compositionView.style.fontFamily = this.optionsService.rawOptions.fontFamily ?? ''; | ||
| this._compositionView.style.fontSize = this.optionsService.rawOptions.fontSize + 'px'; | ||
| } | ||
|
|
||
| private _wkHideComposition(): void { | ||
| if (!this._compositionView) return; | ||
| this._compositionView.textContent = ''; | ||
| this._compositionView.classList.remove('active'); | ||
| } | ||
|
|
||
| private _wkSetComposing(value: boolean): void { | ||
| this._wkImeComposing = value; | ||
| if (this._compositionHelper) { | ||
| this._compositionHelper.wkImeComposing = value; | ||
| } | ||
| } | ||
|
|
||
| private _wkFlush(): void { | ||
| if (!this._wkImeComposing) return; | ||
| const text = this._wkImePending; | ||
| this._wkSetComposing(false); | ||
| this._wkImePending = ''; | ||
| this._wkHideComposition(); | ||
| if (text) { | ||
| this.coreService.triggerDataEvent(text, true); | ||
| } | ||
| } | ||
|
|
||
| private _compositionHelper: ICompositionHelper | undefined; | ||
| private _accessibilityManager: MutableDisposable<AccessibilityManager> = this._register(new MutableDisposable()); | ||
|
|
||
|
|
@@ -1096,6 +1154,11 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal { | |
| this._keyDownHandled = false; | ||
| this._keyDownSeen = true; | ||
|
|
||
| // Flush pending WKWebView IME composition on non-IME keydown (keyCode 229 = IME processing) | ||
| if (this._wkImeComposing && event.keyCode !== 229) { | ||
| this._wkFlush(); | ||
| } | ||
|
|
||
| if (this._customKeyEventHandler && this._customKeyEventHandler(event) === false) { | ||
| return false; | ||
| } | ||
|
|
@@ -1276,6 +1339,33 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal { | |
| * @param ev The input event to be handled. | ||
| */ | ||
| protected _inputEvent(ev: InputEvent): boolean { | ||
| // WKWebView/Safari fires insertReplacementText instead of composition events | ||
| // for Korean (and other CJK) IME input. Buffer the latest value and show preview. | ||
| if (ev.data && ev.inputType === 'insertReplacementText' && !this.optionsService.rawOptions.screenReaderMode) { | ||
| this._wkSetComposing(true); | ||
| this._wkImePending = ev.data; | ||
| this._wkShowComposition(ev.data); | ||
| this.cancel(ev); | ||
| return true; | ||
| } | ||
|
|
||
| // WKWebView IME: Hangul insertText starts a new composition. | ||
| // This check must be before the composed/keyDownSeen guard because WKWebView | ||
| // may fire insertText for composed Hangul syllables with composed=true. | ||
| if (ev.data && ev.inputType === 'insertText' && CoreBrowserTerminal._isHangul(ev.data) && !this.optionsService.rawOptions.screenReaderMode) { | ||
| const hadPending = this._wkImeComposing; | ||
| this._wkFlush(); | ||
| this._wkSetComposing(true); | ||
| this._wkImePending = ev.data; | ||
| // Show preview immediately only if there was no prior flush | ||
| // (avoids stale cursor position after flush + PTY echo delay) | ||
| if (!hadPending) { | ||
| this._wkShowComposition(ev.data); | ||
| } | ||
| this.cancel(ev); | ||
| return true; | ||
| } | ||
|
|
||
| // Only support emoji IMEs when screen reader mode is disabled as the event must bubble up to | ||
| // support reading out character input which can doubling up input characters | ||
| // Based on these event traces: https://github.com/xtermjs/xterm.js/issues/3679 | ||
|
|
@@ -1284,6 +1374,9 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal { | |
| return false; | ||
| } | ||
|
|
||
| // Non-Hangul: flush any pending WKWebView composition first | ||
| this._wkFlush(); | ||
|
|
||
| // The key was handled so clear the dead key state, otherwise certain keystrokes like arrow | ||
| // keys could be ignored | ||
| this._unprocessedDeadKey = false; | ||
|
|
||

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I notice when finishing a character and moving onto the next, the next doesn't show until the second part is typed: