Disable the IME on Windows when text input is unexpected (#51041)

John Tur created

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.

Change summary

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(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -1233,6 +1233,7 @@ pub struct Editor {
     autoindent_mode: Option<AutoindentMode>,
     workspace: Option<(WeakEntity<Workspace>, Option<WorkspaceId>)>,
     input_enabled: bool,
+    expects_character_input: bool,
     use_modal_editing: bool,
     read_only: bool,
     leader_id: Option<CollaboratorId>,
@@ -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<Self>) -> bool {
-        self.input_enabled
+        self.expects_character_input
     }
 }
 

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.

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<isize> {
-        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<isize> {
@@ -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<Self> {
+        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<F>(
     wparam: WPARAM,
     lparam: LPARAM,

crates/gpui_windows/src/window.rs 🔗

@@ -52,6 +52,7 @@ pub struct WindowsWindowState {
 
     pub callbacks: Callbacks,
     pub input_handler: Cell<Option<PlatformInputHandler>>,
+    pub ime_enabled: Cell<bool>,
     pub pending_surrogate: Cell<Option<u16>>,
     pub last_reported_modifiers: Cell<Option<Modifiers>>,
     pub last_reported_capslock: Cell<Option<Capslock>>,
@@ -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),

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::<VimAddon>();
@@ -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,