Skip to content

Commit 296760d

Browse files
authored
Fix IME issues (#2938)
1 parent 8306c6b commit 296760d

1 file changed

Lines changed: 19 additions & 26 deletions

File tree

frontend/app/view/term/termwrap.ts

Lines changed: 19 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ const TermFileName = "term";
4242
const TermCacheFileName = "cache:term:full";
4343
const MinDataProcessedForCache = 100 * 1024;
4444
export const SupportsImageInput = true;
45+
const IMEDedupWindowMs = 20;
4546

4647
// detect webgl support
4748
function detectWebGLSupport(): boolean {
@@ -93,8 +94,6 @@ export class TermWrap {
9394
onLinkHover?: (uri: string | null, mouseX: number, mouseY: number) => void;
9495

9596
// IME composition state tracking
96-
// Prevents duplicate input when switching input methods during composition (e.g., using Capslock)
97-
// xterm.js sends data during compositionupdate AND after compositionend, causing duplicates
9897
isComposing: boolean = false;
9998
composingData: string = "";
10099
lastCompositionEnd: number = 0;
@@ -205,7 +204,15 @@ export class TermWrap {
205204
return true;
206205
})
207206
);
208-
this.terminal.attachCustomKeyEventHandler(waveOptions.keydownHandler);
207+
this.terminal.attachCustomKeyEventHandler((e: KeyboardEvent) => {
208+
if (e.isComposing && !e.ctrlKey && !e.altKey && !e.metaKey) {
209+
return true;
210+
}
211+
if (!waveOptions.keydownHandler) {
212+
return true;
213+
}
214+
return waveOptions.keydownHandler(e);
215+
});
209216
this.connectElem = connectElem;
210217
this.mainFileSubject = null;
211218
this.heldData = [];
@@ -236,6 +243,9 @@ export class TermWrap {
236243
resetCompositionState() {
237244
this.isComposing = false;
238245
this.composingData = "";
246+
this.lastComposedText = "";
247+
this.lastCompositionEnd = 0;
248+
this.firstDataAfterCompositionSent = false;
239249
}
240250

241251
private handleCompositionStart = (e: CompositionEvent) => {
@@ -353,30 +363,13 @@ export class TermWrap {
353363
return;
354364
}
355365

356-
// IME Composition Handling
357-
// Block all data during composition - only send the final text after compositionend
358-
// This prevents xterm.js from sending intermediate composition data (e.g., during compositionupdate)
366+
// IME fix: suppress isComposing=true events unless they immediately follow
367+
// a compositionend (within 20ms). This handles CapsLock input method switching
368+
// where the composition buffer gets flushed as a spurious isComposing=true event
359369
if (this.isComposing) {
360-
dlog("Blocked data during composition:", data);
361-
return;
362-
}
363-
364-
// IME Deduplication (for Capslock input method switching)
365-
// When switching input methods with Capslock during composition, some systems send the
366-
// composed text twice. We allow the first send and block subsequent duplicates.
367-
const IMEDedupWindowMs = 50;
368-
const now = Date.now();
369-
const timeSinceCompositionEnd = now - this.lastCompositionEnd;
370-
if (timeSinceCompositionEnd < IMEDedupWindowMs && data === this.lastComposedText && this.lastComposedText) {
371-
if (!this.firstDataAfterCompositionSent) {
372-
// First send after composition - allow it but mark as sent
373-
this.firstDataAfterCompositionSent = true;
374-
dlog("First data after composition, allowing:", data);
375-
} else {
376-
// Second send of the same data - this is a duplicate from Capslock switching, block it
377-
dlog("Blocked duplicate IME data:", data);
378-
this.lastComposedText = ""; // Clear to allow same text to be typed again later
379-
this.firstDataAfterCompositionSent = false;
370+
const timeSinceCompositionEnd = Date.now() - this.lastCompositionEnd;
371+
if (timeSinceCompositionEnd > IMEDedupWindowMs) {
372+
dlog("Suppressed IME data (composing, not near compositionend):", data);
380373
return;
381374
}
382375
}

0 commit comments

Comments
 (0)