Skip to content
Open
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
55 changes: 53 additions & 2 deletions crates/warpui/src/platform/mac/objc/host_view.m
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,17 @@ @implementation WarpHostView {

// Whether we're in the middle of a call to interpretKeyEvents.
BOOL interpretingKeyEvents;

// The selectedRange of the most recent setMarkedText: call, kept so we
// can re-dispatch the marked text after a commit (see keyDownImpl:).
NSRange lastMarkedSelectedRange;

// True if setMarkedText: was actually called during the current
// interpretKeyEvents:. Used to distinguish a freshly-started composition
// from stale markedText that survived a commit-only flow — insertText:
// deliberately skips unmarkText while interpretingKeyEvents is true, so
// hasMarkedText alone cannot tell the two apart.
BOOL setMarkedTextDuringInterpret;
}

- (BOOL)acceptsFirstResponder {
Expand Down Expand Up @@ -153,6 +164,7 @@ - (void)keyDown:(NSEvent *)event {
- (BOOL)keyDownImpl:(NSEvent *)event {
BOOL wasComposing = [self hasMarkedText];
[textToInsert setString:@""];
setMarkedTextDuringInterpret = NO;

// Interpret the key events here so we could check whether user is composing
// text within the IME and pass the state down to the KeyDown events.
Expand Down Expand Up @@ -180,9 +192,43 @@ - (BOOL)keyDownImpl:(NSEvent *)event {
}

// Dispatch TypedCharacter event after KeyDown has been dispatched.
if ([textToInsert length] > 0 && !handled) {
// If the key event committed previously-composed marked text (wasComposing),
// dispatch the committed text even when `handled` is true — it represents
// the user's already-typed input (e.g. the last Korean syllable being
// committed by an Arrow/Enter key), not text produced by the keybinding.
if ([textToInsert length] > 0 && (!handled || wasComposing)) {
// Korean (and other CJK) IMEs can both commit the previous syllable
// AND start a new composition in the same keyDown — e.g. typing 'ㅏ'
// after '간' produces commit '가' + new marked '나'. By the time we
// get here, setMarkedText: has already placed the new marked text as
// a selection in the input field's buffer. Inserting the committed
// text would then overwrite that selection (losing the next
// character). Workaround: temporarily clear the marked text,
// dispatch the commit, then re-apply the marked text.
//
// We can only treat hasMarkedText as a "freshly-started composition"
// when setMarkedText: was actually called during this keyDown.
// insertText: leaves stale markedText untouched while
// interpretingKeyEvents, so commit-only flows (e.g. dead-key + char)
// would otherwise see stale marked text and incorrectly re-emit it.
BOOL hasNewMarked = setMarkedTextDuringInterpret && [self hasMarkedText];
if (hasNewMarked) {
warp_marked_text_cleared(self);
warp_update_ime_state(self, NO);
}
warp_handle_insert_text(self, (NSString *)textToInsert);
[self unmarkText];
if (hasNewMarked) {
warp_marked_text_updated(self, markedText.string, lastMarkedSelectedRange);
warp_update_ime_state(self, YES);
} else {
// Commit-only flow (no new composition started). End IME state
// explicitly — insertText: deliberately skipped unmarkText while
// interpretingKeyEvents was true, so the stale markedText object
// and ime_active flag would otherwise survive into the next
// keyDown and break downstream input (e.g. Backspace would be
// treated as composing-state input and not delete the character).
[self unmarkText];
}
}

return handled;
Expand Down Expand Up @@ -471,6 +517,11 @@ - (void)setMarkedText:(id)string
else
markedText = [[NSMutableAttributedString alloc] initWithString:string];

lastMarkedSelectedRange = selectedRange;
if (interpretingKeyEvents) {
setMarkedTextDuringInterpret = YES;
}

if (self.readyForWarp) {
warp_marked_text_updated(self, markedText.string, selectedRange);
if ([markedText length] > 0) {
Expand Down