From d60a16c1465415cedf7154d1fd4ad44ca66c3ebe Mon Sep 17 00:00:00 2001 From: Akinori Musha Date: Fri, 1 May 2026 23:18:42 +0900 Subject: [PATCH] macos: avoid replaying keys that commit preedit Refs #10460 Related: #12518 When an input method commits all or part of marked text during keyDown, AppKit returns the committed text through insertText. Treat that as text committed by the input method instead of replaying the original key event to the terminal. Previously this path only handled arrow-key commits specially. A control-key shortcut that commits preedit text could still be encoded as the original control input after composition, such as ctrl+j becoming LF. Send committed preedit text as a text-only event for any key that causes the commit. Only replay arrow navigation keys that the existing Korean IME handling expects, and keep plain left-arrow suppressed because AppKit already leaves the caret in place. AI usage: OpenAI Codex helped investigate, implement, test, and refine this change. I reviewed and tested the resulting code. --- .../Surface View/SurfaceView_AppKit.swift | 37 +++++++++++-------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift index 649ed4a0c..b1920f170 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift @@ -1138,22 +1138,25 @@ extension Ghostty { // actually delete the prior input characters (prior to the composing). let composing = markedText.length > 0 || markedTextBefore - // Korean IMEs on macOS may commit preedit text via insertText - // while handling an arrow key. Send that committed text separately - // before replaying arrow movement, except for plain left-arrow - // where AppKit already leaves the caret in place. + // The input method may commit all or part of the preedit text via + // insertText while handling a key that should not itself be + // encoded. Send that committed text separately, then only replay + // keys that should still affect the terminal after committing. if markedTextBefore, - markedText.length == 0, let list = keyTextAccumulator, - list.count > 0, - let preeditCommitArrow = preeditCommitArrowKey(translationEvent) { + list.count > 0 { for text in list { + if Ghostty.SurfaceView.shouldSuppressComposingControlInput( + text, + composing: composing + ) { + continue + } + _ = committedPreeditTextAction(action, text: text) } - let isPlainLeftArrow = preeditCommitArrow == .arrowLeft && - event.modifierFlags.isDisjoint(with: [.shift, .control, .option, .command]) - if !isPlainLeftArrow { + if shouldReplayCommittedPreeditKey(translationEvent) { _ = keyAction( action, event: event, @@ -1436,13 +1439,17 @@ extension Ghostty { } } - private func preeditCommitArrowKey(_ event: NSEvent) -> Ghostty.Input.Key? { - guard let key = Ghostty.Input.Key(keyCode: event.keyCode) else { return nil } + private func shouldReplayCommittedPreeditKey(_ event: NSEvent) -> Bool { + guard let key = Ghostty.Input.Key(keyCode: event.keyCode) else { return false } switch key { - case .arrowDown, .arrowLeft, .arrowRight, .arrowUp: - return key + case .arrowDown, .arrowRight, .arrowUp: + return true + case .arrowLeft: + // Don't replay plain left-arrow because AppKit already leaves + // the caret in place after Korean IMEs commit preedit text. + return !event.modifierFlags.isDisjoint(with: [.shift, .control, .option, .command]) default: - return nil + return false } }