mirror of
https://github.com/stablyai/orca.git
synced 2026-10-17 08:03:14 +00:00
fix: address pr-bug-scan validated finding from #7634
Post-compositionend candidate-key window no longer arms when the committing selector was consumed during composition, so a genuine Space/digit within 250ms reaches the PTY.
This commit is contained in:
@@ -11,7 +11,10 @@ type TrackerHarness = {
|
||||
tracker: TerminalImeCompositionTracker
|
||||
element: HTMLElement
|
||||
advance: (ms: number) => void
|
||||
composition: (type: 'compositionstart' | 'compositionupdate' | 'compositionend', data: string) => void
|
||||
composition: (
|
||||
type: 'compositionstart' | 'compositionupdate' | 'compositionend',
|
||||
data: string
|
||||
) => void
|
||||
input: (inputType: string) => void
|
||||
blur: () => void
|
||||
}
|
||||
@@ -141,6 +144,20 @@ describe('installTerminalImeCompositionTracker', () => {
|
||||
expect(harness.tracker.isCandidateKeyGuardActive()).toBe(false)
|
||||
})
|
||||
|
||||
it('does not arm the post-composition window when the committing selector was consumed during composition', () => {
|
||||
// Regression: committing Chinese via a Space/digit candidate key (already
|
||||
// suppressed during the live composition) armed the post-compositionend
|
||||
// window, which then swallowed the user's very next genuine Space/digit
|
||||
// typed within 250ms (e.g. "2024" -> "024", or a separator space lost).
|
||||
const harness = installTracker()
|
||||
harness.composition('compositionstart', '')
|
||||
harness.composition('compositionupdate', '')
|
||||
// The candidate selector was suppressed while the composition was live.
|
||||
harness.tracker.noteCandidateSelectionDuringComposition()
|
||||
harness.composition('compositionend', '你好')
|
||||
expect(harness.tracker.isCandidateKeyGuardActive()).toBe(false)
|
||||
})
|
||||
|
||||
it('drops the post-composition window once ordinary typing resumes', () => {
|
||||
const harness = installTracker()
|
||||
harness.composition('compositionstart', '')
|
||||
|
||||
@@ -6,6 +6,11 @@ export type TerminalImeCompositionTracker = IDisposable & {
|
||||
* IME-owned: during a live composition, and briefly after compositionend to
|
||||
* absorb the committing key's trailing press/release. */
|
||||
isCandidateKeyGuardActive: () => boolean
|
||||
/** Called by the key handler when a candidate-selection key (Space/digit) is
|
||||
* suppressed while a composition is still live. The committing selector has
|
||||
* then already been consumed, so the post-compositionend window must not arm
|
||||
* and swallow the user's next genuine Space/digit typed within 250ms. */
|
||||
noteCandidateSelectionDuringComposition: () => void
|
||||
}
|
||||
|
||||
// Why: suppressed candidate keys are preventDefault-ed and fire no input
|
||||
@@ -26,6 +31,14 @@ export function installTerminalImeCompositionTracker(
|
||||
let lastCompositionEventAt: number | null = null
|
||||
let compositionEndedAt: number | null = null
|
||||
let sawEmptyCompositionUpdate = false
|
||||
// Why: when the committing Space/digit is consumed during the live
|
||||
// composition, no trailing selector arrives after compositionend, so arming
|
||||
// the post-composition window would only swallow the user's next real key.
|
||||
let candidateSelectionDuringComposition = false
|
||||
|
||||
const noteCandidateSelectionDuringComposition = (): void => {
|
||||
candidateSelectionDuringComposition = true
|
||||
}
|
||||
|
||||
const isActiveAt = (at: number): boolean =>
|
||||
active &&
|
||||
@@ -47,6 +60,7 @@ export function installTerminalImeCompositionTracker(
|
||||
return {
|
||||
isActive: () => active,
|
||||
isCandidateKeyGuardActive,
|
||||
noteCandidateSelectionDuringComposition,
|
||||
dispose: () => undefined
|
||||
}
|
||||
}
|
||||
@@ -56,6 +70,7 @@ export function installTerminalImeCompositionTracker(
|
||||
lastCompositionEventAt = now()
|
||||
compositionEndedAt = null
|
||||
sawEmptyCompositionUpdate = false
|
||||
candidateSelectionDuringComposition = false
|
||||
}
|
||||
const updateComposition = (event: Event): void => {
|
||||
lastCompositionEventAt = now()
|
||||
@@ -75,8 +90,13 @@ export function installTerminalImeCompositionTracker(
|
||||
active = false
|
||||
// Why: only Sogou/fcitx-style empty updates prove a trailing plain
|
||||
// Space/digit is likely IME-owned; broad post-end guards drop real typing.
|
||||
compositionEndedAt = sawEmptyCompositionUpdate ? now() : null
|
||||
// If the committing selector was already consumed during the composition,
|
||||
// no trailing selector follows, so arming the window would only swallow
|
||||
// the user's next genuine Space/digit.
|
||||
compositionEndedAt =
|
||||
sawEmptyCompositionUpdate && !candidateSelectionDuringComposition ? now() : null
|
||||
sawEmptyCompositionUpdate = false
|
||||
candidateSelectionDuringComposition = false
|
||||
}
|
||||
const handleInput = (event: Event): void => {
|
||||
if (event instanceof InputEvent && event.inputType === 'insertCompositionText') {
|
||||
@@ -87,12 +107,14 @@ export function installTerminalImeCompositionTracker(
|
||||
// the post-end window would swallow a legitimate Space/digit.
|
||||
compositionEndedAt = null
|
||||
sawEmptyCompositionUpdate = false
|
||||
candidateSelectionDuringComposition = false
|
||||
}
|
||||
const markInactive = (): void => {
|
||||
active = false
|
||||
lastCompositionEventAt = null
|
||||
compositionEndedAt = null
|
||||
sawEmptyCompositionUpdate = false
|
||||
candidateSelectionDuringComposition = false
|
||||
}
|
||||
|
||||
terminalElement.addEventListener('compositionstart', markActive, true)
|
||||
@@ -104,6 +126,7 @@ export function installTerminalImeCompositionTracker(
|
||||
return {
|
||||
isActive: () => isActiveAt(now()),
|
||||
isCandidateKeyGuardActive,
|
||||
noteCandidateSelectionDuringComposition,
|
||||
dispose: () => {
|
||||
terminalElement.removeEventListener('compositionstart', markActive, true)
|
||||
terminalElement.removeEventListener('compositionupdate', updateComposition, true)
|
||||
|
||||
@@ -881,11 +881,12 @@ export function useTerminalPaneLifecycle({
|
||||
imeNativeTextForwarderDisposablesRef.current.set(pane.id, imeNativeTextForwarder)
|
||||
pane.terminal.attachCustomKeyEventHandler((e) => {
|
||||
const now = Date.now()
|
||||
const pendingCandidateReleaseGuardActive = shouldApplyTerminalImePendingCandidateKeyRelease(
|
||||
e,
|
||||
pendingTerminalImeCandidateKeyReleases,
|
||||
now
|
||||
)
|
||||
const pendingCandidateReleaseGuardActive =
|
||||
shouldApplyTerminalImePendingCandidateKeyRelease(
|
||||
e,
|
||||
pendingTerminalImeCandidateKeyReleases,
|
||||
now
|
||||
)
|
||||
const imeKeyboardOptions = {
|
||||
compositionActive: imeCompositionTracker.isActive(),
|
||||
candidateKeyGuardActive:
|
||||
@@ -908,6 +909,13 @@ export function useTerminalPaneLifecycle({
|
||||
e,
|
||||
now
|
||||
)
|
||||
if (imeKeyboardOptions.compositionActive) {
|
||||
// Why: the committing Space/digit was consumed during the live
|
||||
// composition, so the post-compositionend window must not also
|
||||
// arm and swallow the user's next genuine Space/digit typed
|
||||
// within 250ms.
|
||||
imeCompositionTracker.noteCandidateSelectionDuringComposition()
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user