From af6385a29355dd2859e9751ebd76e418dd6a249a Mon Sep 17 00:00:00 2001 From: John Tur Date: Sun, 11 Jan 2026 00:10:51 -0500 Subject: [PATCH] Fix Windows IME composition string being displayed incorrectly (#46545) Previously, a new composition string was processed before a new composition result. Thus, in the case that a keystroke creates a new composition result and composition string, the processing of the new result would overwrite the new string, even though they should both be displayed. Processing them in the reverse order fixes this issue. Fixes #42201 Additionally: while fixing this issue, I discovered that the Japanese IME sometimes moves the cursor far away from the inserted text. WPF works around this issue by only respecting the IME's requested cursor position if it's adjacent to uncommitted text. So, we copy this workaround. Release Notes: - N/A --- crates/gpui/src/platform/windows/events.rs | 59 ++++++++++++++++++---- 1 file changed, 50 insertions(+), 9 deletions(-) diff --git a/crates/gpui/src/platform/windows/events.rs b/crates/gpui/src/platform/windows/events.rs index 97c3a12bb028ccf518c09da862a02846eca049f4..f708c8ddb9cc87abbfe43bedbf96b74b15c2ad11 100644 --- a/crates/gpui/src/platform/windows/events.rs +++ b/crates/gpui/src/platform/windows/events.rs @@ -632,22 +632,34 @@ impl WindowsWindowInner { })?; Some(0) } else { + if lparam & GCS_RESULTSTR.0 > 0 { + let comp_result = parse_ime_composition_string(ctx, GCS_RESULTSTR)?; + self.with_input_handler(|input_handler| { + input_handler + .replace_text_in_range(None, &String::from_utf16_lossy(&comp_result)); + })?; + } if lparam & GCS_COMPSTR.0 > 0 { let comp_string = parse_ime_composition_string(ctx, GCS_COMPSTR)?; let caret_pos = (!comp_string.is_empty() && lparam & GCS_CURSORPOS.0 > 0).then(|| { - let pos = retrieve_composition_cursor_position(ctx); + let cursor_pos = retrieve_composition_cursor_position(ctx); + let pos = if should_use_ime_cursor_position(ctx, cursor_pos) { + cursor_pos + } else { + comp_string.len() + }; pos..pos }); self.with_input_handler(|input_handler| { - input_handler.replace_and_mark_text_in_range(None, &comp_string, caret_pos); + input_handler.replace_and_mark_text_in_range( + None, + &String::from_utf16_lossy(&comp_string), + caret_pos, + ); })?; } - if lparam & GCS_RESULTSTR.0 > 0 { - let comp_result = parse_ime_composition_string(ctx, GCS_RESULTSTR)?; - self.with_input_handler(|input_handler| { - input_handler.replace_text_in_range(None, &comp_result); - })?; + if lparam & (GCS_RESULTSTR.0 | GCS_COMPSTR.0) > 0 { return Some(0); } @@ -1450,7 +1462,7 @@ fn process_key(vkey: VIRTUAL_KEY, scan_code: u16) -> (Option, bool) { ) } -fn parse_ime_composition_string(ctx: HIMC, comp_type: IME_COMPOSITION_STRING) -> Option { +fn parse_ime_composition_string(ctx: HIMC, comp_type: IME_COMPOSITION_STRING) -> Option> { unsafe { let string_len = ImmGetCompositionStringW(ctx, comp_type, None, 0); if string_len >= 0 { @@ -1465,7 +1477,7 @@ fn parse_ime_composition_string(ctx: HIMC, comp_type: IME_COMPOSITION_STRING) -> buffer.as_mut_ptr().cast::(), string_len as usize / 2, ); - Some(String::from_utf16_lossy(wstring)) + Some(wstring.to_vec()) } else { None } @@ -1477,6 +1489,35 @@ fn retrieve_composition_cursor_position(ctx: HIMC) -> usize { unsafe { ImmGetCompositionStringW(ctx, GCS_CURSORPOS, None, 0) as usize } } +fn should_use_ime_cursor_position(ctx: HIMC, cursor_pos: usize) -> bool { + let attrs_size = unsafe { ImmGetCompositionStringW(ctx, GCS_COMPATTR, None, 0) } as usize; + if attrs_size == 0 { + return false; + } + + let mut attrs = vec![0u8; attrs_size]; + let result = unsafe { + ImmGetCompositionStringW( + ctx, + GCS_COMPATTR, + Some(attrs.as_mut_ptr() as *mut _), + attrs_size as u32, + ) + }; + if result <= 0 { + return false; + } + + // Keep the cursor adjacent to the inserted text by only using the suggested position + // if it's adjacent to unconverted text. + let at_cursor_is_input = cursor_pos < attrs.len() && attrs[cursor_pos] == (ATTR_INPUT as u8); + let before_cursor_is_input = cursor_pos > 0 + && (cursor_pos - 1) < attrs.len() + && attrs[cursor_pos - 1] == (ATTR_INPUT as u8); + + at_cursor_is_input || before_cursor_is_input +} + #[inline] fn is_virtual_key_pressed(vkey: VIRTUAL_KEY) -> bool { unsafe { GetKeyState(vkey.0 as i32) < 0 }