fix(terminal): flush Hangul syllables before the next composition (#12278)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> Co-authored-by: Hyunggyun Lyou <hg.lyou@miraeasset.com>
This commit is contained in:
parent
6f7a30ac2e
commit
f82d4100b7
File diff suppressed because one or more lines are too long
|
|
@ -19,7 +19,7 @@ patchedDependencies:
|
|||
hash: 6da7d7770b6427246f2a0d057d97da418040e498068b41d0c2d3c6b20bf49258
|
||||
path: config/patches/@xterm__addon-webgl@0.20.0-beta.286.patch
|
||||
'@xterm/xterm@6.1.0-beta.287':
|
||||
hash: 595d23676ffcec9a99a09e9fa2b8f12b9ae2934cf1ff376bcc68f4cf1ec8766e
|
||||
hash: 037272642db6a9bf5242c02acacf8fc41f4c6e91268d8a47ea083c98fb2535b8
|
||||
path: config/patches/@xterm__xterm@6.1.0-beta.287.patch
|
||||
node-pty@1.1.0:
|
||||
hash: 8fc49f17011b6611a5b8c00e83a6f12e14e75aada2b0ef26dc5393f8376d20e8
|
||||
|
|
@ -46,7 +46,7 @@ importers:
|
|||
version: 2.5.6
|
||||
'@xterm/addon-serialize':
|
||||
specifier: 0.15.0-beta.287
|
||||
version: 0.15.0-beta.287(patch_hash=81575700d58b62f9262d302aa4ae43b445a6273d579cd75dd6729c8883011ab9)(@xterm/xterm@6.1.0-beta.287(patch_hash=595d23676ffcec9a99a09e9fa2b8f12b9ae2934cf1ff376bcc68f4cf1ec8766e))
|
||||
version: 0.15.0-beta.287(patch_hash=81575700d58b62f9262d302aa4ae43b445a6273d579cd75dd6729c8883011ab9)(@xterm/xterm@6.1.0-beta.287(patch_hash=037272642db6a9bf5242c02acacf8fc41f4c6e91268d8a47ea083c98fb2535b8))
|
||||
'@xterm/headless':
|
||||
specifier: 6.1.0-beta.287
|
||||
version: 6.1.0-beta.287
|
||||
|
|
@ -206,25 +206,25 @@ importers:
|
|||
version: 5.2.0(rolldown-vite@7.3.1(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(@types/node@25.9.5)(jiti@2.7.0)(yaml@2.8.4))
|
||||
'@xterm/addon-fit':
|
||||
specifier: 0.12.0-beta.287
|
||||
version: 0.12.0-beta.287(@xterm/xterm@6.1.0-beta.287(patch_hash=595d23676ffcec9a99a09e9fa2b8f12b9ae2934cf1ff376bcc68f4cf1ec8766e))
|
||||
version: 0.12.0-beta.287(@xterm/xterm@6.1.0-beta.287(patch_hash=037272642db6a9bf5242c02acacf8fc41f4c6e91268d8a47ea083c98fb2535b8))
|
||||
'@xterm/addon-ligatures':
|
||||
specifier: 0.11.0-beta.287
|
||||
version: 0.11.0-beta.287(patch_hash=47405b9994b5acf1b4e90b49250358c1ca03649854d59560e7732b72fe336920)(@xterm/xterm@6.1.0-beta.287(patch_hash=595d23676ffcec9a99a09e9fa2b8f12b9ae2934cf1ff376bcc68f4cf1ec8766e))
|
||||
version: 0.11.0-beta.287(patch_hash=47405b9994b5acf1b4e90b49250358c1ca03649854d59560e7732b72fe336920)(@xterm/xterm@6.1.0-beta.287(patch_hash=037272642db6a9bf5242c02acacf8fc41f4c6e91268d8a47ea083c98fb2535b8))
|
||||
'@xterm/addon-search':
|
||||
specifier: 0.17.0-beta.287
|
||||
version: 0.17.0-beta.287(@xterm/xterm@6.1.0-beta.287(patch_hash=595d23676ffcec9a99a09e9fa2b8f12b9ae2934cf1ff376bcc68f4cf1ec8766e))
|
||||
version: 0.17.0-beta.287(@xterm/xterm@6.1.0-beta.287(patch_hash=037272642db6a9bf5242c02acacf8fc41f4c6e91268d8a47ea083c98fb2535b8))
|
||||
'@xterm/addon-unicode11':
|
||||
specifier: 0.10.0-beta.287
|
||||
version: 0.10.0-beta.287(@xterm/xterm@6.1.0-beta.287(patch_hash=595d23676ffcec9a99a09e9fa2b8f12b9ae2934cf1ff376bcc68f4cf1ec8766e))
|
||||
version: 0.10.0-beta.287(@xterm/xterm@6.1.0-beta.287(patch_hash=037272642db6a9bf5242c02acacf8fc41f4c6e91268d8a47ea083c98fb2535b8))
|
||||
'@xterm/addon-web-links':
|
||||
specifier: 0.13.0-beta.287
|
||||
version: 0.13.0-beta.287(@xterm/xterm@6.1.0-beta.287(patch_hash=595d23676ffcec9a99a09e9fa2b8f12b9ae2934cf1ff376bcc68f4cf1ec8766e))
|
||||
version: 0.13.0-beta.287(@xterm/xterm@6.1.0-beta.287(patch_hash=037272642db6a9bf5242c02acacf8fc41f4c6e91268d8a47ea083c98fb2535b8))
|
||||
'@xterm/addon-webgl':
|
||||
specifier: 0.20.0-beta.286
|
||||
version: 0.20.0-beta.286(patch_hash=6da7d7770b6427246f2a0d057d97da418040e498068b41d0c2d3c6b20bf49258)(@xterm/xterm@6.1.0-beta.287(patch_hash=595d23676ffcec9a99a09e9fa2b8f12b9ae2934cf1ff376bcc68f4cf1ec8766e))
|
||||
version: 0.20.0-beta.286(patch_hash=6da7d7770b6427246f2a0d057d97da418040e498068b41d0c2d3c6b20bf49258)(@xterm/xterm@6.1.0-beta.287(patch_hash=037272642db6a9bf5242c02acacf8fc41f4c6e91268d8a47ea083c98fb2535b8))
|
||||
'@xterm/xterm':
|
||||
specifier: 6.1.0-beta.287
|
||||
version: 6.1.0-beta.287(patch_hash=595d23676ffcec9a99a09e9fa2b8f12b9ae2934cf1ff376bcc68f4cf1ec8766e)
|
||||
version: 6.1.0-beta.287(patch_hash=037272642db6a9bf5242c02acacf8fc41f4c6e91268d8a47ea083c98fb2535b8)
|
||||
class-variance-authority:
|
||||
specifier: ^0.7.1
|
||||
version: 0.7.1
|
||||
|
|
@ -9582,39 +9582,39 @@ snapshots:
|
|||
|
||||
'@xmldom/xmldom@0.8.13': {}
|
||||
|
||||
'@xterm/addon-fit@0.12.0-beta.287(@xterm/xterm@6.1.0-beta.287(patch_hash=595d23676ffcec9a99a09e9fa2b8f12b9ae2934cf1ff376bcc68f4cf1ec8766e))':
|
||||
'@xterm/addon-fit@0.12.0-beta.287(@xterm/xterm@6.1.0-beta.287(patch_hash=037272642db6a9bf5242c02acacf8fc41f4c6e91268d8a47ea083c98fb2535b8))':
|
||||
dependencies:
|
||||
'@xterm/xterm': 6.1.0-beta.287(patch_hash=595d23676ffcec9a99a09e9fa2b8f12b9ae2934cf1ff376bcc68f4cf1ec8766e)
|
||||
'@xterm/xterm': 6.1.0-beta.287(patch_hash=037272642db6a9bf5242c02acacf8fc41f4c6e91268d8a47ea083c98fb2535b8)
|
||||
|
||||
'@xterm/addon-ligatures@0.11.0-beta.287(patch_hash=47405b9994b5acf1b4e90b49250358c1ca03649854d59560e7732b72fe336920)(@xterm/xterm@6.1.0-beta.287(patch_hash=595d23676ffcec9a99a09e9fa2b8f12b9ae2934cf1ff376bcc68f4cf1ec8766e))':
|
||||
'@xterm/addon-ligatures@0.11.0-beta.287(patch_hash=47405b9994b5acf1b4e90b49250358c1ca03649854d59560e7732b72fe336920)(@xterm/xterm@6.1.0-beta.287(patch_hash=037272642db6a9bf5242c02acacf8fc41f4c6e91268d8a47ea083c98fb2535b8))':
|
||||
dependencies:
|
||||
'@xterm/xterm': 6.1.0-beta.287(patch_hash=595d23676ffcec9a99a09e9fa2b8f12b9ae2934cf1ff376bcc68f4cf1ec8766e)
|
||||
'@xterm/xterm': 6.1.0-beta.287(patch_hash=037272642db6a9bf5242c02acacf8fc41f4c6e91268d8a47ea083c98fb2535b8)
|
||||
lru-cache: 11.5.1
|
||||
opentype.js: 2.0.0
|
||||
|
||||
'@xterm/addon-search@0.17.0-beta.287(@xterm/xterm@6.1.0-beta.287(patch_hash=595d23676ffcec9a99a09e9fa2b8f12b9ae2934cf1ff376bcc68f4cf1ec8766e))':
|
||||
'@xterm/addon-search@0.17.0-beta.287(@xterm/xterm@6.1.0-beta.287(patch_hash=037272642db6a9bf5242c02acacf8fc41f4c6e91268d8a47ea083c98fb2535b8))':
|
||||
dependencies:
|
||||
'@xterm/xterm': 6.1.0-beta.287(patch_hash=595d23676ffcec9a99a09e9fa2b8f12b9ae2934cf1ff376bcc68f4cf1ec8766e)
|
||||
'@xterm/xterm': 6.1.0-beta.287(patch_hash=037272642db6a9bf5242c02acacf8fc41f4c6e91268d8a47ea083c98fb2535b8)
|
||||
|
||||
'@xterm/addon-serialize@0.15.0-beta.287(patch_hash=81575700d58b62f9262d302aa4ae43b445a6273d579cd75dd6729c8883011ab9)(@xterm/xterm@6.1.0-beta.287(patch_hash=595d23676ffcec9a99a09e9fa2b8f12b9ae2934cf1ff376bcc68f4cf1ec8766e))':
|
||||
'@xterm/addon-serialize@0.15.0-beta.287(patch_hash=81575700d58b62f9262d302aa4ae43b445a6273d579cd75dd6729c8883011ab9)(@xterm/xterm@6.1.0-beta.287(patch_hash=037272642db6a9bf5242c02acacf8fc41f4c6e91268d8a47ea083c98fb2535b8))':
|
||||
dependencies:
|
||||
'@xterm/xterm': 6.1.0-beta.287(patch_hash=595d23676ffcec9a99a09e9fa2b8f12b9ae2934cf1ff376bcc68f4cf1ec8766e)
|
||||
'@xterm/xterm': 6.1.0-beta.287(patch_hash=037272642db6a9bf5242c02acacf8fc41f4c6e91268d8a47ea083c98fb2535b8)
|
||||
|
||||
'@xterm/addon-unicode11@0.10.0-beta.287(@xterm/xterm@6.1.0-beta.287(patch_hash=595d23676ffcec9a99a09e9fa2b8f12b9ae2934cf1ff376bcc68f4cf1ec8766e))':
|
||||
'@xterm/addon-unicode11@0.10.0-beta.287(@xterm/xterm@6.1.0-beta.287(patch_hash=037272642db6a9bf5242c02acacf8fc41f4c6e91268d8a47ea083c98fb2535b8))':
|
||||
dependencies:
|
||||
'@xterm/xterm': 6.1.0-beta.287(patch_hash=595d23676ffcec9a99a09e9fa2b8f12b9ae2934cf1ff376bcc68f4cf1ec8766e)
|
||||
'@xterm/xterm': 6.1.0-beta.287(patch_hash=037272642db6a9bf5242c02acacf8fc41f4c6e91268d8a47ea083c98fb2535b8)
|
||||
|
||||
'@xterm/addon-web-links@0.13.0-beta.287(@xterm/xterm@6.1.0-beta.287(patch_hash=595d23676ffcec9a99a09e9fa2b8f12b9ae2934cf1ff376bcc68f4cf1ec8766e))':
|
||||
'@xterm/addon-web-links@0.13.0-beta.287(@xterm/xterm@6.1.0-beta.287(patch_hash=037272642db6a9bf5242c02acacf8fc41f4c6e91268d8a47ea083c98fb2535b8))':
|
||||
dependencies:
|
||||
'@xterm/xterm': 6.1.0-beta.287(patch_hash=595d23676ffcec9a99a09e9fa2b8f12b9ae2934cf1ff376bcc68f4cf1ec8766e)
|
||||
'@xterm/xterm': 6.1.0-beta.287(patch_hash=037272642db6a9bf5242c02acacf8fc41f4c6e91268d8a47ea083c98fb2535b8)
|
||||
|
||||
'@xterm/addon-webgl@0.20.0-beta.286(patch_hash=6da7d7770b6427246f2a0d057d97da418040e498068b41d0c2d3c6b20bf49258)(@xterm/xterm@6.1.0-beta.287(patch_hash=595d23676ffcec9a99a09e9fa2b8f12b9ae2934cf1ff376bcc68f4cf1ec8766e))':
|
||||
'@xterm/addon-webgl@0.20.0-beta.286(patch_hash=6da7d7770b6427246f2a0d057d97da418040e498068b41d0c2d3c6b20bf49258)(@xterm/xterm@6.1.0-beta.287(patch_hash=037272642db6a9bf5242c02acacf8fc41f4c6e91268d8a47ea083c98fb2535b8))':
|
||||
dependencies:
|
||||
'@xterm/xterm': 6.1.0-beta.287(patch_hash=595d23676ffcec9a99a09e9fa2b8f12b9ae2934cf1ff376bcc68f4cf1ec8766e)
|
||||
'@xterm/xterm': 6.1.0-beta.287(patch_hash=037272642db6a9bf5242c02acacf8fc41f4c6e91268d8a47ea083c98fb2535b8)
|
||||
|
||||
'@xterm/headless@6.1.0-beta.287': {}
|
||||
|
||||
'@xterm/xterm@6.1.0-beta.287(patch_hash=595d23676ffcec9a99a09e9fa2b8f12b9ae2934cf1ff376bcc68f4cf1ec8766e)': {}
|
||||
'@xterm/xterm@6.1.0-beta.287(patch_hash=037272642db6a9bf5242c02acacf8fc41f4c6e91268d8a47ea083c98fb2535b8)': {}
|
||||
|
||||
abbrev@4.0.0: {}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,178 @@
|
|||
// @vitest-environment happy-dom
|
||||
import { createRequire } from 'node:module'
|
||||
import { Terminal as EsmTerminal } from '@xterm/xterm'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const requireFromHere = createRequire(import.meta.url)
|
||||
const { Terminal: CjsTerminal } = requireFromHere('@xterm/xterm') as {
|
||||
Terminal: typeof EsmTerminal
|
||||
}
|
||||
const openTerminals: EsmTerminal[] = []
|
||||
|
||||
function nextEventLoop(): Promise<void> {
|
||||
return new Promise((resolve) => window.setTimeout(resolve, 0))
|
||||
}
|
||||
|
||||
function openTerminal(TerminalType: typeof EsmTerminal): {
|
||||
emitted: string[]
|
||||
textarea: HTMLTextAreaElement
|
||||
} {
|
||||
const container = document.createElement('div')
|
||||
document.body.appendChild(container)
|
||||
const terminal = new TerminalType()
|
||||
openTerminals.push(terminal)
|
||||
terminal.open(container)
|
||||
if (!terminal.textarea) {
|
||||
throw new Error('xterm textarea was not created')
|
||||
}
|
||||
const emitted: string[] = []
|
||||
terminal.onData((data) => emitted.push(data))
|
||||
return { emitted, textarea: terminal.textarea }
|
||||
}
|
||||
|
||||
function composition(
|
||||
textarea: HTMLTextAreaElement,
|
||||
type: 'compositionstart' | 'compositionupdate' | 'compositionend',
|
||||
data?: string
|
||||
): void {
|
||||
const event = new CompositionEvent(type, { bubbles: true })
|
||||
if (data !== undefined) {
|
||||
Object.defineProperty(event, 'data', { value: data })
|
||||
}
|
||||
textarea.dispatchEvent(event)
|
||||
}
|
||||
|
||||
function input(textarea: HTMLTextAreaElement, inputType: string, data?: string): void {
|
||||
const event = new InputEvent('input', { bubbles: true, data: data ?? null, inputType })
|
||||
Object.defineProperty(event, 'composed', { value: true })
|
||||
textarea.dispatchEvent(event)
|
||||
}
|
||||
|
||||
function keydown(textarea: HTMLTextAreaElement, key: string, code: string, keyCode: number): void {
|
||||
const event = new KeyboardEvent('keydown', { bubbles: true, code, isComposing: true, key })
|
||||
Object.defineProperty(event, 'keyCode', { value: keyCode })
|
||||
textarea.dispatchEvent(event)
|
||||
}
|
||||
|
||||
function composeSyllable(textarea: HTMLTextAreaElement, prefix: string, steps: string[]): void {
|
||||
textarea.setSelectionRange(prefix.length, prefix.length)
|
||||
composition(textarea, 'compositionstart')
|
||||
for (const step of steps) {
|
||||
composition(textarea, 'compositionupdate', step)
|
||||
textarea.value = `${prefix}${step}`
|
||||
textarea.setSelectionRange(textarea.value.length, textarea.value.length)
|
||||
}
|
||||
}
|
||||
|
||||
describe.each([
|
||||
['ESM', EsmTerminal],
|
||||
['CJS', CjsTerminal]
|
||||
])('installed xterm Hangul syllable flush (%s)', (_format, TerminalType) => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue({
|
||||
measureText: () => ({ width: 10 })
|
||||
} as unknown as CanvasRenderingContext2D)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
while (openTerminals.length > 0) {
|
||||
openTerminals.pop()?.dispose()
|
||||
}
|
||||
vi.useRealTimers()
|
||||
vi.restoreAllMocks()
|
||||
document.body.replaceChildren()
|
||||
})
|
||||
|
||||
it('flushes syllable N before syllable N+1 finishes composing', async () => {
|
||||
const { emitted, textarea } = openTerminal(TerminalType)
|
||||
composeSyllable(textarea, '', ['ㅎ', '하', '한'])
|
||||
composition(textarea, 'compositionend', '한')
|
||||
textarea.setSelectionRange(1, 1)
|
||||
composition(textarea, 'compositionstart')
|
||||
await nextEventLoop()
|
||||
|
||||
expect(emitted.join('')).toBe('한')
|
||||
})
|
||||
|
||||
it('preserves Korean final-consonant transfer', async () => {
|
||||
const { emitted, textarea } = openTerminal(TerminalType)
|
||||
composeSyllable(textarea, '', ['ㅇ', '아', '앙'])
|
||||
composition(textarea, 'compositionend', '앙')
|
||||
textarea.value = '아아'
|
||||
textarea.setSelectionRange(1, 1)
|
||||
composition(textarea, 'compositionstart')
|
||||
await nextEventLoop()
|
||||
|
||||
expect(emitted.join('')).toBe('아')
|
||||
composition(textarea, 'compositionupdate', '아')
|
||||
textarea.setSelectionRange(2, 2)
|
||||
composition(textarea, 'compositionend', '아')
|
||||
await nextEventLoop()
|
||||
|
||||
expect(emitted.join('')).toBe('아아')
|
||||
expect(emitted.join('')).not.toContain('앙')
|
||||
})
|
||||
|
||||
it('emits 한글 exactly once', async () => {
|
||||
const { emitted, textarea } = openTerminal(TerminalType)
|
||||
composeSyllable(textarea, '', ['ㅎ', '하', '한'])
|
||||
composition(textarea, 'compositionend', '한')
|
||||
composeSyllable(textarea, '한', ['ㄱ', '그', '글'])
|
||||
composition(textarea, 'compositionend', '글')
|
||||
await nextEventLoop()
|
||||
|
||||
expect(emitted.join('')).toBe('한글')
|
||||
})
|
||||
|
||||
it('does not double-send after an IBus insertText commit', async () => {
|
||||
const { emitted, textarea } = openTerminal(TerminalType)
|
||||
textarea.setSelectionRange(0, 0)
|
||||
composition(textarea, 'compositionstart')
|
||||
keydown(textarea, 'Process', 'KeyG', 229)
|
||||
composition(textarea, 'compositionupdate', '한')
|
||||
textarea.value = '한'
|
||||
input(textarea, 'insertCompositionText', '한')
|
||||
composition(textarea, 'compositionupdate')
|
||||
textarea.value = ''
|
||||
input(textarea, 'deleteContentBackward')
|
||||
composition(textarea, 'compositionend')
|
||||
textarea.value = '한'
|
||||
input(textarea, 'insertText', '한')
|
||||
await nextEventLoop()
|
||||
|
||||
expect(emitted.join('')).toBe('한')
|
||||
textarea.setSelectionRange(1, 1)
|
||||
composition(textarea, 'compositionstart')
|
||||
await nextEventLoop()
|
||||
|
||||
expect(emitted.join('')).toBe('한')
|
||||
})
|
||||
|
||||
it('does not commit a composition deleted to empty', async () => {
|
||||
const { emitted, textarea } = openTerminal(TerminalType)
|
||||
composeSyllable(textarea, '', ['ㅇ'])
|
||||
composition(textarea, 'compositionupdate', '')
|
||||
textarea.value = ''
|
||||
textarea.setSelectionRange(0, 0)
|
||||
composition(textarea, 'compositionend', '')
|
||||
await nextEventLoop()
|
||||
|
||||
expect(emitted).toEqual([])
|
||||
})
|
||||
|
||||
it('starts cleanly after deleting a composition to empty', async () => {
|
||||
const { emitted, textarea } = openTerminal(TerminalType)
|
||||
composeSyllable(textarea, '', ['ㅇ'])
|
||||
composition(textarea, 'compositionupdate', '')
|
||||
textarea.value = ''
|
||||
textarea.setSelectionRange(0, 0)
|
||||
composition(textarea, 'compositionend', '')
|
||||
await nextEventLoop()
|
||||
|
||||
composeSyllable(textarea, '', ['ㅎ', '하', '한'])
|
||||
composition(textarea, 'compositionend', '한')
|
||||
await nextEventLoop()
|
||||
|
||||
expect(emitted.join('')).toBe('한')
|
||||
})
|
||||
})
|
||||
Loading…
Reference in New Issue