-
Notifications
You must be signed in to change notification settings - Fork 37
Added support for IME input for languages such as Chinese and Japanese. #90
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -33,6 +33,9 @@ interface MockClipboardEvent { | |
| interface MockHTMLElement { | ||
| addEventListener: (event: string, handler: (e: any) => void) => void; | ||
| removeEventListener: (event: string, handler: (e: any) => void) => void; | ||
| childNodes: Node[]; | ||
| removeChild: (node: Node) => Node; | ||
| appendChild: (node: Node) => Node; | ||
| } | ||
|
|
||
| // Helper to create mock keyboard event | ||
|
|
@@ -76,7 +79,25 @@ function createClipboardEvent(text: string | null): MockClipboardEvent { | |
| stopPropagation: mock(() => {}), | ||
| }; | ||
| } | ||
| interface MockCompositionEvent { | ||
| type: string; | ||
| data: string | null; | ||
| preventDefault: () => void; | ||
| stopPropagation: () => void; | ||
| } | ||
|
|
||
| // Helper to create mock composition event | ||
| function createCompositionEvent( | ||
| type: 'compositionstart' | 'compositionupdate' | 'compositionend', | ||
| data: string | null | ||
| ): MockCompositionEvent { | ||
| return { | ||
| type, | ||
| data, | ||
| preventDefault: mock(() => {}), | ||
| stopPropagation: mock(() => {}), | ||
| }; | ||
| } | ||
| // Helper to create mock container | ||
| function createMockContainer(): MockHTMLElement & { | ||
| _listeners: Map<string, ((e: any) => void)[]>; | ||
|
|
@@ -107,6 +128,19 @@ function createMockContainer(): MockHTMLElement & { | |
| handler(event); | ||
| } | ||
| }, | ||
| // Mock childNodes and removeChild for text node cleanup test | ||
| childNodes: [] as Node[], | ||
| removeChild(node: Node) { | ||
| const index = this.childNodes.indexOf(node); | ||
| if (index >= 0) { | ||
| this.childNodes.splice(index, 1); | ||
| } | ||
| return node; | ||
| }, | ||
| appendChild(node: Node) { | ||
| this.childNodes.push(node); | ||
| return node; | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
|
|
@@ -269,6 +303,104 @@ describe('InputHandler', () => { | |
| }); | ||
| }); | ||
|
|
||
| describe('IME Composition', () => { | ||
| test('handles composition sequence', () => { | ||
| const handler = new InputHandler( | ||
| ghostty, | ||
| container as any, | ||
| (data) => dataReceived.push(data), | ||
| () => { | ||
| bellCalled = true; | ||
| } | ||
| ); | ||
|
|
||
| // Start composition | ||
| const startEvent = createCompositionEvent('compositionstart', ''); | ||
| container.dispatchEvent(startEvent); | ||
|
|
||
| // Update composition (typing) | ||
| const updateEvent1 = createCompositionEvent('compositionupdate', 'n'); | ||
| container.dispatchEvent(updateEvent1); | ||
|
|
||
| // Keydown events during composition should be ignored | ||
| const keyEvent1 = createKeyEvent('KeyN', 'n'); | ||
| Object.defineProperty(keyEvent1, 'isComposing', { value: true }); | ||
| simulateKey(container, keyEvent1); | ||
|
|
||
| // Update composition (more typing) | ||
| const updateEvent2 = createCompositionEvent('compositionupdate', 'ni'); | ||
| container.dispatchEvent(updateEvent2); | ||
|
|
||
| // End composition (commit) | ||
| const endEvent = createCompositionEvent('compositionend', '你好'); | ||
| container.dispatchEvent(endEvent); | ||
|
|
||
| // Should only receive the final committed text | ||
| expect(dataReceived).toEqual(['你好']); | ||
| }); | ||
|
|
||
| test('ignores keydown during composition', () => { | ||
| const handler = new InputHandler( | ||
|
||
| ghostty, | ||
| container as any, | ||
| (data) => dataReceived.push(data), | ||
| () => { | ||
| bellCalled = true; | ||
| } | ||
| ); | ||
|
|
||
| // Start composition | ||
| container.dispatchEvent(createCompositionEvent('compositionstart', '')); | ||
|
|
||
| // Simulate keydown with isComposing=true | ||
| const keyEvent = createKeyEvent('KeyA', 'a'); | ||
| Object.defineProperty(keyEvent, 'isComposing', { value: true }); | ||
| simulateKey(container, keyEvent); | ||
|
|
||
| // Simulate keydown with keyCode 229 | ||
| const keyEvent229 = createKeyEvent('KeyB', 'b'); | ||
| Object.defineProperty(keyEvent229, 'keyCode', { value: 229 }); | ||
| simulateKey(container, keyEvent229); | ||
|
|
||
| // Should not receive any data | ||
| expect(dataReceived.length).toBe(0); | ||
|
|
||
| // End composition | ||
| container.dispatchEvent(createCompositionEvent('compositionend', 'a')); | ||
| expect(dataReceived).toEqual(['a']); | ||
| }); | ||
|
|
||
| test('cleans up text nodes in container after composition', () => { | ||
| const handler = new InputHandler( | ||
kylecarbs marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ghostty, | ||
| container as any, | ||
| (data) => dataReceived.push(data), | ||
| () => { | ||
| bellCalled = true; | ||
| } | ||
| ); | ||
|
|
||
| // Simulate browser inserting text node during composition | ||
| const textNode = { nodeType: 3, textContent: '你好' } as Node; | ||
| container.appendChild(textNode); | ||
|
|
||
| // Also add a non-text node (e.g. canvas) to ensure it's not removed | ||
| const elementNode = { nodeType: 1, nodeName: 'CANVAS' } as Node; | ||
| container.appendChild(elementNode); | ||
|
|
||
| expect(container.childNodes.length).toBe(2); | ||
|
|
||
| // End composition | ||
| const endEvent = createCompositionEvent('compositionend', '你好'); | ||
| container.dispatchEvent(endEvent); | ||
|
|
||
| // Should have removed the text node but kept the element node | ||
| expect(container.childNodes.length).toBe(1); | ||
| expect(container.childNodes[0]).toBe(elementNode); | ||
| expect(dataReceived).toEqual(['你好']); | ||
| }); | ||
| }); | ||
|
Comment on lines
+306
to
+402
|
||
|
|
||
| describe('Control Characters', () => { | ||
| test('encodes Ctrl+A', () => { | ||
| const handler = new InputHandler( | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -169,6 +169,10 @@ export class InputHandler { | |||||||||||||||
| private keydownListener: ((e: KeyboardEvent) => void) | null = null; | ||||||||||||||||
| private keypressListener: ((e: KeyboardEvent) => void) | null = null; | ||||||||||||||||
| private pasteListener: ((e: ClipboardEvent) => void) | null = null; | ||||||||||||||||
| private compositionStartListener: ((e: CompositionEvent) => void) | null = null; | ||||||||||||||||
| private compositionUpdateListener: ((e: CompositionEvent) => void) | null = null; | ||||||||||||||||
| private compositionEndListener: ((e: CompositionEvent) => void) | null = null; | ||||||||||||||||
| private isComposing = false; | ||||||||||||||||
| private isDisposed = false; | ||||||||||||||||
|
|
||||||||||||||||
| /** | ||||||||||||||||
|
|
@@ -233,6 +237,15 @@ export class InputHandler { | |||||||||||||||
|
|
||||||||||||||||
| this.pasteListener = this.handlePaste.bind(this); | ||||||||||||||||
| this.container.addEventListener('paste', this.pasteListener); | ||||||||||||||||
|
|
||||||||||||||||
| this.compositionStartListener = this.handleCompositionStart.bind(this); | ||||||||||||||||
| this.container.addEventListener('compositionstart', this.compositionStartListener); | ||||||||||||||||
|
|
||||||||||||||||
| this.compositionUpdateListener = this.handleCompositionUpdate.bind(this); | ||||||||||||||||
| this.container.addEventListener('compositionupdate', this.compositionUpdateListener); | ||||||||||||||||
|
|
||||||||||||||||
| this.compositionEndListener = this.handleCompositionEnd.bind(this); | ||||||||||||||||
| this.container.addEventListener('compositionend', this.compositionEndListener); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| /** | ||||||||||||||||
|
|
@@ -287,6 +300,12 @@ export class InputHandler { | |||||||||||||||
| private handleKeyDown(event: KeyboardEvent): void { | ||||||||||||||||
| if (this.isDisposed) return; | ||||||||||||||||
|
|
||||||||||||||||
| // Ignore keydown events during composition | ||||||||||||||||
| // Note: Some browsers send keyCode 229 for all keys during composition | ||||||||||||||||
|
Comment on lines
+303
to
+304
|
||||||||||||||||
| // Ignore keydown events during composition | |
| // Note: Some browsers send keyCode 229 for all keys during composition | |
| // Ignore keydown events during composition. | |
| // We check three ways for cross-browser compatibility: | |
| // - this.isComposing: internal state set by composition events | |
| // - event.isComposing: standard property supported by modern browsers | |
| // - event.keyCode === 229: legacy fallback for older browsers (e.g., IE/Edge), as keyCode is deprecated |
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.
Unused variable handler.