fix(macos/ime): skip key-equivalent priority path while IME is composing (#9709)#9711
Conversation
Navigation keys with the function-key modifier flag (e.g. arrow keys, modifierFlags=0xa00100) qualify as key equivalents on macOS, so AppKit fires performKeyEquivalent: before keyDown:. The previous implementation called keyDownImpl from performKeyEquivalent:, which invokes interpretKeyEvents and forwards to Rust. During IME composition, Rust suppresses the keystroke and keyDownImpl returns NO, so we fall through to [super performKeyEquivalent:]. AppKit then dispatches keyDown: to the host view, and interpretKeyEvents runs a second time. Most macOS IMEs render their candidate UI through IMKCandidates (a separate NSPanel), so they only observe the first delivery. 超注音 / Yahoo Bopomofo, however, draws its candidate panel itself and listens on the input context directly, so it receives both deliveries and advances candidate selection by 2 per arrow press. Skip the priority path entirely while hasMarkedText is YES so AppKit routes the event through keyDown: exactly once. Confirmed via NSLog instrumentation: arrow press during composition now produces a single keyDownImpl / interpretKeyEvents pair instead of two. Fixes warpdotdev#9709 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Thank you for your pull request and welcome to our community. We require contributors to sign our Contributor License Agreement, and we don't seem to have the users @maxmilian on file. In order for us to review and merge your code, each contributor must visit https://cla.warp.dev to read and agree to our CLA. Once you have done so, please comment |
|
I'm starting a first review of this pull request. You can view the conversation on Warp. I reviewed this pull request and requested human review from: @vorporeal. Comment Powered by Oz |
There was a problem hiding this comment.
Overview
This PR skips Warp's key-equivalent priority path while macOS marked text is active, allowing AppKit to deliver IME composition keys through the normal keyDown: path exactly once.
Concerns
- No blocking correctness or security concerns found in the changed lines.
Verdict
Found: 0 critical, 0 important, 0 suggestions
Approve
Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).
Powered by Oz
|
@cla-bot check |
|
The cla-bot has been summoned, and re-checked this pull request! |
|
reassigning to @abhishekp106, who has done the most IME work |
|
@maxmilian Thanks for opening this and diagnosing the issue! I agree with your assessment and the approach here looks sound. The only tweak I want to make is to make the comment a little more concise. |
|
@oz-agent Can you change the comment above the new if statement added in this change to be the following?
|
|
Update: Our oz-agent github bot does not yet run in forks yet. @maxmilian do you mind updating the comment to be what I wrote above? Then let's get this merged in! |
abhishekp106
left a comment
There was a problem hiding this comment.
LGTM! Just one small nit about the comment
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@abhishekp106 Updated the comment in 8b2311c — ready for merge. |
…ing (warpdotdev#9709) (warpdotdev#9711) ## Description Fixes warpdotdev#9709 — on macOS, third-party IMEs that listen on the input context (e.g. 超注音 / Yahoo Bopomofo) observed every arrow keystroke twice during candidate selection, advancing the highlight by 2 instead of 1. ### Root cause Arrow keys carry the function-key flag (`modifierFlags=0xa00100`), so they qualify as key equivalents and AppKit dispatches them through `performKeyEquivalent:` before `keyDown:`. In `crates/warpui/src/platform/mac/objc/window.m::performKeyEquivalent:`, the previous implementation: 1. Called `[self.contentView keyDownImpl:event]` — which invokes `interpretKeyEvents:` and forwards to Rust 2. Rust suppresses keystrokes during composition (`is_composing=true`), so `keyDownImpl` returned `NO` 3. Fell through to `[super performKeyEquivalent:event]` 4. AppKit then dispatched `keyDown:` on the host view → `keyDownImpl` again → **`interpretKeyEvents` ran a second time** Apple's built-in IME uses `IMKCandidates` (a separate `NSPanel`) for candidate selection, so the second delivery doesn't reach its candidate index. 超注音 draws its candidate panel itself and listens on the input context directly — it observes both deliveries and advances by 2. ### Fix Skip the priority path in `performKeyEquivalent:` while `hasMarkedText` is `YES`. AppKit then routes the event through the standard `keyDown:` chain exactly once. ## Testing ### NSLog instrumentation (before/after) I added per-call instrumentation around `keyDownImpl`, `interpretKeyEvents`, `setMarkedText:`, `insertText:`, `unmarkText`, `doCommandBySelector:`, and `performKeyEquivalent:` and reproduced with 超注音 (Yahoo Bopomofo) on macOS 26.4.1. **Before the fix** — single right-arrow press during candidate selection: ``` performKeyEquivalent: keyCode=124 modifiers=0xa00100 performKeyEquivalent: keystrokeIsAssigned=1 keyDownImpl#8 ENTER keyCode=124 wasComposing=1 interpretKeyEvents BEGIN ← IME +1 setMarkedText: '...' (interpretingKeyEvents=1) setMarkedText: '...' (interpretingKeyEvents=1) interpretKeyEvents END (hasMarkedText=1) warp_handle_view_event(composing=1) handled=0 performKeyEquivalent: keyDownImpl returned NO, falling through to super keyDown: keyCode=124 ← AppKit fires again keyDownImpl#9 ENTER keyCode=124 wasComposing=1 interpretKeyEvents BEGIN ← IME +1 again → total +2 ... ``` **After the fix**: ``` performKeyEquivalent: keyCode=124 modifiers=0xa00100 performKeyEquivalent: IME composing, skipping priority path keyDown: keyCode=124 keyDownImpl#22 ENTER keyCode=124 wasComposing=1 interpretKeyEvents BEGIN ← single delivery interpretKeyEvents END (hasMarkedText=1) ``` ### Manual verification (macOS) - 超注音 (Yahoo Bopomofo): right/left arrow now advances candidate by exactly 1 per press (was 2). ✅ - Apple built-in 注音 (Bopomofo): no regression — candidate window behaves identically. ✅ - Apple Pinyin: no regression. ✅ - Cmd-shortcut keys without IME composition (e.g. Cmd-T new tab): no regression — the `hasMarkedText` guard only triggers during active composition, so the priority path still executes for ordinary key equivalents. ✅ - Plain typing without an IME: no regression. ✅ No automated tests were added: the bug requires running an IME on macOS and observing AppKit's two-stage event delivery (`performKeyEquivalent:` → `keyDown:`), which is impractical to simulate in the existing unit/integration test harnesses. The change is small, well-scoped (`performKeyEquivalent:` only), and the guard short-circuits to `[super performKeyEquivalent:]` on the same path AppKit would fall back to anyway. ## Server API dependencies - [ ] Is this change necessary to make the client compatible with a desired server API breaking change? - [ ] Does this change rely on a new server API? - [ ] Is this change enabling the use of a server API on client channels that rely on the production server? None — this is a pure native-input-handling change. ## Agent Mode - [ ] Warp Agent Mode - This PR was created via Warp's AI Agent Mode ## Changelog Entries for Stable CHANGELOG-BUG-FIX: macOS: fix third-party IMEs (e.g. 超注音 / Yahoo Bopomofo) advancing candidate selection by 2 per arrow press during composition. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> (cherry picked from commit fc1157e)
|
Merged in! Thanks @maxmilian for your contribution :) |
…ing (warpdotdev#9709) (warpdotdev#9711) ## Description Fixes warpdotdev#9709 — on macOS, third-party IMEs that listen on the input context (e.g. 超注音 / Yahoo Bopomofo) observed every arrow keystroke twice during candidate selection, advancing the highlight by 2 instead of 1. ### Root cause Arrow keys carry the function-key flag (`modifierFlags=0xa00100`), so they qualify as key equivalents and AppKit dispatches them through `performKeyEquivalent:` before `keyDown:`. In `crates/warpui/src/platform/mac/objc/window.m::performKeyEquivalent:`, the previous implementation: 1. Called `[self.contentView keyDownImpl:event]` — which invokes `interpretKeyEvents:` and forwards to Rust 2. Rust suppresses keystrokes during composition (`is_composing=true`), so `keyDownImpl` returned `NO` 3. Fell through to `[super performKeyEquivalent:event]` 4. AppKit then dispatched `keyDown:` on the host view → `keyDownImpl` again → **`interpretKeyEvents` ran a second time** Apple's built-in IME uses `IMKCandidates` (a separate `NSPanel`) for candidate selection, so the second delivery doesn't reach its candidate index. 超注音 draws its candidate panel itself and listens on the input context directly — it observes both deliveries and advances by 2. ### Fix Skip the priority path in `performKeyEquivalent:` while `hasMarkedText` is `YES`. AppKit then routes the event through the standard `keyDown:` chain exactly once. ## Testing ### NSLog instrumentation (before/after) I added per-call instrumentation around `keyDownImpl`, `interpretKeyEvents`, `setMarkedText:`, `insertText:`, `unmarkText`, `doCommandBySelector:`, and `performKeyEquivalent:` and reproduced with 超注音 (Yahoo Bopomofo) on macOS 26.4.1. **Before the fix** — single right-arrow press during candidate selection: ``` performKeyEquivalent: keyCode=124 modifiers=0xa00100 performKeyEquivalent: keystrokeIsAssigned=1 keyDownImpl#8 ENTER keyCode=124 wasComposing=1 interpretKeyEvents BEGIN ← IME +1 setMarkedText: '...' (interpretingKeyEvents=1) setMarkedText: '...' (interpretingKeyEvents=1) interpretKeyEvents END (hasMarkedText=1) warp_handle_view_event(composing=1) handled=0 performKeyEquivalent: keyDownImpl returned NO, falling through to super keyDown: keyCode=124 ← AppKit fires again keyDownImpl#9 ENTER keyCode=124 wasComposing=1 interpretKeyEvents BEGIN ← IME +1 again → total +2 ... ``` **After the fix**: ``` performKeyEquivalent: keyCode=124 modifiers=0xa00100 performKeyEquivalent: IME composing, skipping priority path keyDown: keyCode=124 keyDownImpl#22 ENTER keyCode=124 wasComposing=1 interpretKeyEvents BEGIN ← single delivery interpretKeyEvents END (hasMarkedText=1) ``` ### Manual verification (macOS) - 超注音 (Yahoo Bopomofo): right/left arrow now advances candidate by exactly 1 per press (was 2). ✅ - Apple built-in 注音 (Bopomofo): no regression — candidate window behaves identically. ✅ - Apple Pinyin: no regression. ✅ - Cmd-shortcut keys without IME composition (e.g. Cmd-T new tab): no regression — the `hasMarkedText` guard only triggers during active composition, so the priority path still executes for ordinary key equivalents. ✅ - Plain typing without an IME: no regression. ✅ No automated tests were added: the bug requires running an IME on macOS and observing AppKit's two-stage event delivery (`performKeyEquivalent:` → `keyDown:`), which is impractical to simulate in the existing unit/integration test harnesses. The change is small, well-scoped (`performKeyEquivalent:` only), and the guard short-circuits to `[super performKeyEquivalent:]` on the same path AppKit would fall back to anyway. ## Server API dependencies - [ ] Is this change necessary to make the client compatible with a desired server API breaking change? - [ ] Does this change rely on a new server API? - [ ] Is this change enabling the use of a server API on client channels that rely on the production server? None — this is a pure native-input-handling change. ## Agent Mode - [ ] Warp Agent Mode - This PR was created via Warp's AI Agent Mode ## Changelog Entries for Stable CHANGELOG-BUG-FIX: macOS: fix third-party IMEs (e.g. 超注音 / Yahoo Bopomofo) advancing candidate selection by 2 per arrow press during composition. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ing (warpdotdev#9709) (warpdotdev#9711) ## Description Fixes warpdotdev#9709 — on macOS, third-party IMEs that listen on the input context (e.g. 超注音 / Yahoo Bopomofo) observed every arrow keystroke twice during candidate selection, advancing the highlight by 2 instead of 1. ### Root cause Arrow keys carry the function-key flag (`modifierFlags=0xa00100`), so they qualify as key equivalents and AppKit dispatches them through `performKeyEquivalent:` before `keyDown:`. In `crates/warpui/src/platform/mac/objc/window.m::performKeyEquivalent:`, the previous implementation: 1. Called `[self.contentView keyDownImpl:event]` — which invokes `interpretKeyEvents:` and forwards to Rust 2. Rust suppresses keystrokes during composition (`is_composing=true`), so `keyDownImpl` returned `NO` 3. Fell through to `[super performKeyEquivalent:event]` 4. AppKit then dispatched `keyDown:` on the host view → `keyDownImpl` again → **`interpretKeyEvents` ran a second time** Apple's built-in IME uses `IMKCandidates` (a separate `NSPanel`) for candidate selection, so the second delivery doesn't reach its candidate index. 超注音 draws its candidate panel itself and listens on the input context directly — it observes both deliveries and advances by 2. ### Fix Skip the priority path in `performKeyEquivalent:` while `hasMarkedText` is `YES`. AppKit then routes the event through the standard `keyDown:` chain exactly once. ## Testing ### NSLog instrumentation (before/after) I added per-call instrumentation around `keyDownImpl`, `interpretKeyEvents`, `setMarkedText:`, `insertText:`, `unmarkText`, `doCommandBySelector:`, and `performKeyEquivalent:` and reproduced with 超注音 (Yahoo Bopomofo) on macOS 26.4.1. **Before the fix** — single right-arrow press during candidate selection: ``` performKeyEquivalent: keyCode=124 modifiers=0xa00100 performKeyEquivalent: keystrokeIsAssigned=1 keyDownImpl#8 ENTER keyCode=124 wasComposing=1 interpretKeyEvents BEGIN ← IME +1 setMarkedText: '...' (interpretingKeyEvents=1) setMarkedText: '...' (interpretingKeyEvents=1) interpretKeyEvents END (hasMarkedText=1) warp_handle_view_event(composing=1) handled=0 performKeyEquivalent: keyDownImpl returned NO, falling through to super keyDown: keyCode=124 ← AppKit fires again keyDownImpl#9 ENTER keyCode=124 wasComposing=1 interpretKeyEvents BEGIN ← IME +1 again → total +2 ... ``` **After the fix**: ``` performKeyEquivalent: keyCode=124 modifiers=0xa00100 performKeyEquivalent: IME composing, skipping priority path keyDown: keyCode=124 keyDownImpl#22 ENTER keyCode=124 wasComposing=1 interpretKeyEvents BEGIN ← single delivery interpretKeyEvents END (hasMarkedText=1) ``` ### Manual verification (macOS) - 超注音 (Yahoo Bopomofo): right/left arrow now advances candidate by exactly 1 per press (was 2). ✅ - Apple built-in 注音 (Bopomofo): no regression — candidate window behaves identically. ✅ - Apple Pinyin: no regression. ✅ - Cmd-shortcut keys without IME composition (e.g. Cmd-T new tab): no regression — the `hasMarkedText` guard only triggers during active composition, so the priority path still executes for ordinary key equivalents. ✅ - Plain typing without an IME: no regression. ✅ No automated tests were added: the bug requires running an IME on macOS and observing AppKit's two-stage event delivery (`performKeyEquivalent:` → `keyDown:`), which is impractical to simulate in the existing unit/integration test harnesses. The change is small, well-scoped (`performKeyEquivalent:` only), and the guard short-circuits to `[super performKeyEquivalent:]` on the same path AppKit would fall back to anyway. ## Server API dependencies - [ ] Is this change necessary to make the client compatible with a desired server API breaking change? - [ ] Does this change rely on a new server API? - [ ] Is this change enabling the use of a server API on client channels that rely on the production server? None — this is a pure native-input-handling change. ## Agent Mode - [ ] Warp Agent Mode - This PR was created via Warp's AI Agent Mode ## Changelog Entries for Stable CHANGELOG-BUG-FIX: macOS: fix third-party IMEs (e.g. 超注音 / Yahoo Bopomofo) advancing candidate selection by 2 per arrow press during composition. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> (cherry picked from commit fc1157e)
Description
Fixes #9709 — on macOS, third-party IMEs that listen on the input context (e.g. 超注音 / Yahoo Bopomofo) observed every arrow keystroke twice during candidate selection, advancing the highlight by 2 instead of 1.
Root cause
Arrow keys carry the function-key flag (
modifierFlags=0xa00100), so they qualify as key equivalents and AppKit dispatches them throughperformKeyEquivalent:beforekeyDown:. Incrates/warpui/src/platform/mac/objc/window.m::performKeyEquivalent:, the previous implementation:[self.contentView keyDownImpl:event]— which invokesinterpretKeyEvents:and forwards to Rustis_composing=true), sokeyDownImplreturnedNO[super performKeyEquivalent:event]keyDown:on the host view →keyDownImplagain →interpretKeyEventsran a second timeApple's built-in IME uses
IMKCandidates(a separateNSPanel) for candidate selection, so the second delivery doesn't reach its candidate index. 超注音 draws its candidate panel itself and listens on the input context directly — it observes both deliveries and advances by 2.Fix
Skip the priority path in
performKeyEquivalent:whilehasMarkedTextisYES. AppKit then routes the event through the standardkeyDown:chain exactly once.Testing
NSLog instrumentation (before/after)
I added per-call instrumentation around
keyDownImpl,interpretKeyEvents,setMarkedText:,insertText:,unmarkText,doCommandBySelector:, andperformKeyEquivalent:and reproduced with 超注音 (Yahoo Bopomofo) on macOS 26.4.1.Before the fix — single right-arrow press during candidate selection:
After the fix:
Manual verification (macOS)
hasMarkedTextguard only triggers during active composition, so the priority path still executes for ordinary key equivalents. ✅No automated tests were added: the bug requires running an IME on macOS and observing AppKit's two-stage event delivery (
performKeyEquivalent:→keyDown:), which is impractical to simulate in the existing unit/integration test harnesses. The change is small, well-scoped (performKeyEquivalent:only), and the guard short-circuits to[super performKeyEquivalent:]on the same path AppKit would fall back to anyway.Server API dependencies
None — this is a pure native-input-handling change.
Agent Mode
Changelog Entries for Stable
CHANGELOG-BUG-FIX: macOS: fix third-party IMEs (e.g. 超注音 / Yahoo Bopomofo) advancing candidate selection by 2 per arrow press during composition.