Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { PtyTransport } from './pty-transport'
export const XTERM_COMPOSITION_SESSION_START_EVENT = 'xterm-composition-session-start'
export const XTERM_COMPOSITION_SESSION_END_EVENT = 'xterm-composition-session-end'

type CompositionSessionDetail = {
export type TerminalImeCompositionSessionDetail = {
id: number
data?: string
dataPendingReconciliation?: boolean
Expand Down Expand Up @@ -75,16 +75,22 @@ export function hasPendingTerminalImeComposition(
return false
}

function getCompositionDetail(event: Event): CompositionSessionDetail | null {
export function readTerminalImeCompositionSessionDetail(
event: Event
): TerminalImeCompositionSessionDetail | null {
if (!(event instanceof CustomEvent)) {
return null
}
const detail = event.detail as Partial<CompositionSessionDetail> | null
if (!detail || !Number.isSafeInteger(detail.id) || detail.id! <= 0) {
const detail = event.detail as Partial<TerminalImeCompositionSessionDetail> | null
if (!detail) {
return null
}
const id = detail.id
if (typeof id !== 'number' || !Number.isSafeInteger(id) || id <= 0) {
return null
}
return {
id: detail.id!,
id,
data: typeof detail.data === 'string' ? detail.data : undefined,
dataPendingReconciliation: detail.dataPendingReconciliation === true
}
Expand All @@ -109,7 +115,7 @@ export function installTerminalImeCompositionRoute(args: {
}

const onSessionStart = (event: Event): void => {
const detail = getCompositionDetail(event)
const detail = readTerminalImeCompositionSessionDetail(event)
if (!detail || disposed) {
return
}
Expand All @@ -122,7 +128,7 @@ export function installTerminalImeCompositionRoute(args: {
}

const onSessionEnd = (event: Event): void => {
const detail = getCompositionDetail(event)
const detail = readTerminalImeCompositionSessionDetail(event)
if (!detail) {
return
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ import { Terminal } from '@xterm/xterm'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import fcitx5Trace from './__fixtures__/fcitx5-hangul-mixed-ascii-terminal-trace.json'
import ibusTrace from './__fixtures__/ibus-hangul-mixed-ascii-terminal-trace.json'
import {
installTypingLatencyInputEvents,
type TypingInputSignal
} from '@/lib/typing-latency/input-events'

type RecordedEvent = {
type: string
Expand Down Expand Up @@ -102,27 +106,35 @@ type PreeditSample = { data: string; shown: boolean }

async function replayTrace(
trace: RecordedTrace
): Promise<{ stream: string; preedits: PreeditSample[] }> {
): Promise<{ stream: string; preedits: PreeditSample[]; inputSignals: TypingInputSignal[] }> {
const { emitted, terminal, textarea } = openTerminal()
const preedits: PreeditSample[] = []
for (const recorded of trace.dom) {
// Keys were driven 20 ms apart, so every physical key event began its own task; the composition
// and input events the IME derived from one key stay in that key's task, which is what lets
// xterm's deferred commit see them.
if (recorded.type === 'keydown' || recorded.type === 'keyup') {
await nextEventLoop()
}
replayEvent(textarea, recorded)
if (recorded.type === 'compositionupdate' && recorded.data) {
const view = terminal.element?.querySelector('.composition-view')
preedits.push({ data: recorded.data, shown: view?.classList.contains('active') === true })
const inputSignals: TypingInputSignal[] = []
const detachInputEvents = installTypingLatencyInputEvents(window, (signal) => {
inputSignals.push(signal)
})
try {
for (const recorded of trace.dom) {
// Keys were driven 20 ms apart, so every physical key event began its own task; the composition
// and input events the IME derived from one key stay in that key's task, which is what lets
// xterm's deferred commit see them.
if (recorded.type === 'keydown' || recorded.type === 'keyup') {
await nextEventLoop()
}
replayEvent(textarea, recorded)
if (recorded.type === 'compositionupdate' && recorded.data) {
const view = terminal.element?.querySelector('.composition-view')
preedits.push({ data: recorded.data, shown: view?.classList.contains('active') === true })
}
}
// 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 }
} finally {
detachInputEvents()
terminal.dispose()
}
// Two turns: the commit's deferred send, then the late-native-commit window it opens.
await nextEventLoop()
await nextEventLoop()
terminal.dispose()
return { stream: emitted.join(''), preedits }
}

describe.each([
Expand Down Expand Up @@ -152,6 +164,23 @@ describe.each([
expect(stream.match(/abc/g)).toHaveLength(trace.repetitions)
})

it('reports each reconciled commit to the typing diagnostic', async () => {
const { inputSignals } = await replayTrace(trace)
const expectedCommits = trace.expectedLines.flatMap((line) => [line.at(0), line.at(-1)])
const commitSignals = inputSignals
.filter((signal) => signal.source === 'ime')
.map((signal) => signal.text)

if (trace.inputFramework === 'ibus') {
const rawEndData = trace.dom
.filter((recorded) => recorded.type === 'compositionend')
.map((recorded) => recorded.data ?? '')
expect(rawEndData.length).toBeGreaterThan(0)
expect(rawEndData.every((data) => data === '')).toBe(true)
}
expect(commitSignals).toEqual(expectedCommits)
})

it('matches the bytes the PTY received on the recorded run', async () => {
const { stream } = await replayTrace(trace)
// The tty turned each CR into LF, so compare against the recorded lines plus that conversion.
Expand Down
203 changes: 0 additions & 203 deletions src/renderer/src/lib/typing-latency-diagnostic.ts

This file was deleted.

Loading