From 4031db17df9bc09d263e1b7d116c2cc2f27a45e3 Mon Sep 17 00:00:00 2001 From: John Tur Date: Sat, 7 Mar 2026 23:54:05 -0500 Subject: [PATCH] Disable the IME on Windows when text input is unexpected (#51041) Fixes #42444 - Changed `accepts_text_input` on the editor to be more precise. Previously, it returned `true` only in insert mode. Now it also returns `true` when an operator is pending. - On Windows, we disable the IME whenever there is no input handler which `accepts_text_input`. - How this improves Vim mode: in insert mode, the IME is enabled; in normal mode, it is disabled (command keys are not intercepted); when an operator is pending, the IME is re-enabled. Release Notes: - On Windows, the IME is disabled in Vim normal and visual modes. --- crates/editor/src/editor.rs | 8 ++- crates/gpui/src/platform.rs | 7 ++ crates/gpui_windows/src/events.rs | 103 ++++++++++++++++++++++++------ crates/gpui_windows/src/window.rs | 2 + crates/vim/src/vim.rs | 13 ++++ 5 files changed, 111 insertions(+), 22 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 94c7bb06eb98f56e05ff96bd3b64d96d2397730b..ead4f97ee351246f4d00f4275c4a736c7ffa4926 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1233,6 +1233,7 @@ pub struct Editor { autoindent_mode: Option, workspace: Option<(WeakEntity, Option)>, input_enabled: bool, + expects_character_input: bool, use_modal_editing: bool, read_only: bool, leader_id: Option, @@ -2469,6 +2470,7 @@ impl Editor { collapse_matches: false, workspace: None, input_enabled: !is_minimap, + expects_character_input: !is_minimap, use_modal_editing: full_mode, read_only: is_minimap, use_autoclose: true, @@ -3365,6 +3367,10 @@ impl Editor { self.input_enabled = input_enabled; } + pub fn set_expects_character_input(&mut self, expects_character_input: bool) { + self.expects_character_input = expects_character_input; + } + pub fn set_edit_predictions_hidden_for_vim_mode( &mut self, hidden: bool, @@ -28409,7 +28415,7 @@ impl EntityInputHandler for Editor { } fn accepts_text_input(&self, _window: &mut Window, _cx: &mut Context) -> bool { - self.input_enabled + self.expects_character_input } } diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index a6714ff250f2f854c51d30bfea5e2e5911ce60ee..061a055e7ef23bc4a76b44eaadb90bc1660fdb42 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -1062,6 +1062,13 @@ impl PlatformInputHandler { pub fn accepts_text_input(&mut self, window: &mut Window, cx: &mut App) -> bool { self.handler.accepts_text_input(window, cx) } + + #[allow(dead_code)] + pub fn query_accepts_text_input(&mut self) -> bool { + self.cx + .update(|window, cx| self.handler.accepts_text_input(window, cx)) + .unwrap_or(true) + } } /// A struct representing a selection in a text buffer, in UTF16 characters. diff --git a/crates/gpui_windows/src/events.rs b/crates/gpui_windows/src/events.rs index 6bc7b73cc756b44b08ddf7abc5f668681c03dcb9..3506ae2a2cc22d57c4cefba1a4c5a1850c411453 100644 --- a/crates/gpui_windows/src/events.rs +++ b/crates/gpui_windows/src/events.rs @@ -593,33 +593,63 @@ impl WindowsWindowInner { } pub(crate) fn update_ime_position(&self, handle: HWND, caret_position: POINT) { + let Some(ctx) = ImeContext::get(handle) else { + return; + }; unsafe { - let ctx = ImmGetContext(handle); - if ctx.is_invalid() { - return; - } + ImmSetCompositionWindow( + *ctx, + &COMPOSITIONFORM { + dwStyle: CFS_POINT, + ptCurrentPos: caret_position, + ..Default::default() + }, + ) + .ok() + .log_err(); - let config = COMPOSITIONFORM { - dwStyle: CFS_POINT, - ptCurrentPos: caret_position, - ..Default::default() - }; - ImmSetCompositionWindow(ctx, &config).ok().log_err(); - let config = CANDIDATEFORM { - dwStyle: CFS_CANDIDATEPOS, - ptCurrentPos: caret_position, - ..Default::default() - }; - ImmSetCandidateWindow(ctx, &config).ok().log_err(); - ImmReleaseContext(handle, ctx).ok().log_err(); + ImmSetCandidateWindow( + *ctx, + &CANDIDATEFORM { + dwStyle: CFS_CANDIDATEPOS, + ptCurrentPos: caret_position, + ..Default::default() + }, + ) + .ok() + .log_err(); + } + } + + fn update_ime_enabled(&self, handle: HWND) { + let ime_enabled = self + .with_input_handler(|input_handler| input_handler.query_accepts_text_input()) + .unwrap_or(false); + if ime_enabled == self.state.ime_enabled.get() { + return; + } + self.state.ime_enabled.set(ime_enabled); + unsafe { + if ime_enabled { + ImmAssociateContextEx(handle, HIMC::default(), IACE_DEFAULT) + .ok() + .log_err(); + } else { + if let Some(ctx) = ImeContext::get(handle) { + ImmNotifyIME(*ctx, NI_COMPOSITIONSTR, CPS_COMPLETE, 0) + .ok() + .log_err(); + } + ImmAssociateContextEx(handle, HIMC::default(), 0) + .ok() + .log_err(); + } } } fn handle_ime_composition(&self, handle: HWND, lparam: LPARAM) -> Option { - let ctx = unsafe { ImmGetContext(handle) }; - let result = self.handle_ime_composition_inner(ctx, lparam); - unsafe { ImmReleaseContext(handle, ctx).ok().log_err() }; - result + let ctx = ImeContext::get(handle)?; + self.handle_ime_composition_inner(*ctx, lparam) } fn handle_ime_composition_inner(&self, ctx: HIMC, lparam: LPARAM) -> Option { @@ -1123,6 +1153,7 @@ impl WindowsWindowInner { }); self.state.callbacks.request_frame.set(Some(request_frame)); + self.update_ime_enabled(handle); unsafe { ValidateRect(Some(handle), None).ok().log_err() }; Some(0) @@ -1205,6 +1236,36 @@ impl WindowsWindowInner { } } +struct ImeContext { + hwnd: HWND, + himc: HIMC, +} + +impl ImeContext { + fn get(hwnd: HWND) -> Option { + let himc = unsafe { ImmGetContext(hwnd) }; + if himc.is_invalid() { + return None; + } + Some(Self { hwnd, himc }) + } +} + +impl std::ops::Deref for ImeContext { + type Target = HIMC; + fn deref(&self) -> &HIMC { + &self.himc + } +} + +impl Drop for ImeContext { + fn drop(&mut self) { + unsafe { + ImmReleaseContext(self.hwnd, self.himc).ok().log_err(); + } + } +} + fn handle_key_event( wparam: WPARAM, lparam: LPARAM, diff --git a/crates/gpui_windows/src/window.rs b/crates/gpui_windows/src/window.rs index 02653d7e53a4356979b81897b39ab0393bbf54a9..62e88c47dfc10fedf6d636e2c6d6cbdcdc2e37c5 100644 --- a/crates/gpui_windows/src/window.rs +++ b/crates/gpui_windows/src/window.rs @@ -52,6 +52,7 @@ pub struct WindowsWindowState { pub callbacks: Callbacks, pub input_handler: Cell>, + pub ime_enabled: Cell, pub pending_surrogate: Cell>, pub last_reported_modifiers: Cell>, pub last_reported_capslock: Cell>, @@ -142,6 +143,7 @@ impl WindowsWindowState { min_size, callbacks, input_handler: Cell::new(input_handler), + ime_enabled: Cell::new(true), pending_surrogate: Cell::new(pending_surrogate), last_reported_modifiers: Cell::new(last_reported_modifiers), last_reported_capslock: Cell::new(last_reported_capslock), diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index edbbca1c30fb1bda0bedc35d0de6666228b9ef5d..8c551bcd2768043ae416157c80d4d2f9faa19092 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -978,6 +978,7 @@ impl Vim { editor.set_clip_at_line_ends(false, cx); editor.set_collapse_matches(false); editor.set_input_enabled(true); + editor.set_expects_character_input(true); editor.set_autoindent(true); editor.selections.set_line_mode(false); editor.unregister_addon::(); @@ -1346,6 +1347,15 @@ impl Vim { } } + fn expects_character_input(&self) -> bool { + if let Some(operator) = self.operator_stack.last() { + if operator.is_waiting(self.mode) { + return true; + } + } + self.editor_input_enabled() + } + pub fn editor_input_enabled(&self) -> bool { match self.mode { Mode::Insert => { @@ -2058,6 +2068,7 @@ impl Vim { clip_at_line_ends: self.clip_at_line_ends(), collapse_matches: !HelixModeSetting::get_global(cx).0, input_enabled: self.editor_input_enabled(), + expects_character_input: self.expects_character_input(), autoindent: self.should_autoindent(), cursor_offset_on_selection: self.mode.is_visual(), line_mode: matches!(self.mode, Mode::VisualLine), @@ -2075,6 +2086,7 @@ impl Vim { editor.set_clip_at_line_ends(state.clip_at_line_ends, cx); editor.set_collapse_matches(state.collapse_matches); editor.set_input_enabled(state.input_enabled); + editor.set_expects_character_input(state.expects_character_input); editor.set_autoindent(state.autoindent); editor.set_cursor_offset_on_selection(state.cursor_offset_on_selection); editor.selections.set_line_mode(state.line_mode); @@ -2087,6 +2099,7 @@ struct VimEditorSettingsState { clip_at_line_ends: bool, collapse_matches: bool, input_enabled: bool, + expects_character_input: bool, autoindent: bool, cursor_offset_on_selection: bool, line_mode: bool,