@@ -187,4 +187,9 @@ impl<V: EntityInputHandler> InputHandler for ElementInputHandler<V> {
self.view
.update(cx, |view, cx| view.accepts_text_input(window, cx))
}
+
+ fn prefers_ime_for_printable_keys(&mut self, window: &mut Window, cx: &mut App) -> bool {
+ self.view
+ .update(cx, |view, cx| view.accepts_text_input(window, cx))
+ }
}
@@ -1242,6 +1242,13 @@ impl PlatformInputHandler {
.update(|window, cx| self.handler.accepts_text_input(window, cx))
.unwrap_or(true)
}
+
+ #[allow(dead_code)]
+ pub fn query_prefers_ime_for_printable_keys(&mut self) -> bool {
+ self.cx
+ .update(|window, cx| self.handler.prefers_ime_for_printable_keys(window, cx))
+ .unwrap_or(false)
+ }
}
/// A struct representing a selection in a text buffer, in UTF16 characters.
@@ -1355,6 +1362,18 @@ pub trait InputHandler: 'static {
fn accepts_text_input(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
true
}
+
+ /// Returns whether printable keys should be routed to the IME before keybinding
+ /// matching when a non-ASCII input source (e.g. Japanese, Korean, Chinese IME)
+ /// is active. This prevents multi-stroke keybindings like `jj` from intercepting
+ /// keys that the IME should compose.
+ ///
+ /// Defaults to `false`. The editor overrides this based on whether it expects
+ /// character input (e.g. Vim insert mode returns `true`, normal mode returns `false`).
+ /// The terminal keeps the default `false` so that raw keys reach the terminal process.
+ fn prefers_ime_for_printable_keys(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
+ false
+ }
}
/// The variables that can be configured when creating a new window
@@ -1389,6 +1389,7 @@ unsafe fn ns_url_to_path(url: id) -> Result<PathBuf> {
#[link(name = "Carbon", kind = "framework")]
unsafe extern "C" {
pub(super) fn TISCopyCurrentKeyboardLayoutInputSource() -> *mut Object;
+ pub(super) fn TISCopyCurrentKeyboardInputSource() -> *mut Object;
pub(super) fn TISGetInputSourceProperty(
inputSource: *mut Object,
propertyKey: *const c_void,
@@ -1410,6 +1411,9 @@ unsafe extern "C" {
pub(super) static kTISPropertyUnicodeKeyLayoutData: CFStringRef;
pub(super) static kTISPropertyInputSourceID: CFStringRef;
pub(super) static kTISPropertyLocalizedName: CFStringRef;
+ pub(super) static kTISPropertyInputSourceIsASCIICapable: CFStringRef;
+ pub(super) static kTISPropertyInputSourceType: CFStringRef;
+ pub(super) static kTISTypeKeyboardInputMode: CFStringRef;
}
mod security {
@@ -1,5 +1,7 @@
use crate::{
- BoolExt, DisplayLink, MacDisplay, NSRange, NSStringExt, events::platform_input_from_native,
+ BoolExt, DisplayLink, MacDisplay, NSRange, NSStringExt, TISCopyCurrentKeyboardInputSource,
+ TISGetInputSourceProperty, events::platform_input_from_native,
+ kTISPropertyInputSourceIsASCIICapable, kTISPropertyInputSourceType, kTISTypeKeyboardInputMode,
ns_string, renderer,
};
#[cfg(any(test, feature = "test-support"))]
@@ -34,6 +36,9 @@ use gpui::{
#[cfg(any(test, feature = "test-support"))]
use image::RgbaImage;
+use core_foundation::base::{CFRelease, CFTypeRef};
+use core_foundation_sys::base::CFEqual;
+use core_foundation_sys::number::{CFBooleanGetValue, CFBooleanRef};
use core_graphics::display::{CGDirectDisplayID, CGPoint, CGRect};
use ctor::ctor;
use futures::channel::oneshot;
@@ -1782,6 +1787,45 @@ extern "C" fn handle_key_up(this: &Object, _: Sel, native_event: id) {
// - in vim mode `option-4` should go to end of line (same as $)
// Japanese (Romaji) layout:
// - type `a i left down up enter enter` should create an unmarked text "愛"
+// - In vim mode with `jj` bound to `vim::NormalBefore` in insert mode, typing 'j i' with
+// Japanese IME should produce "じ" (ji), not "jい"
+
+/// Returns true if the current keyboard input source is a composition-based IME
+/// (e.g. Japanese Hiragana, Korean, Chinese Pinyin) that produces non-ASCII output.
+///
+/// This checks two properties:
+/// 1. The source type is `kTISTypeKeyboardInputMode` (an IME input mode, not a plain
+/// keyboard layout). This excludes non-ASCII layouts like Armenian and Ukrainian
+/// that map keys directly without composition.
+/// 2. The source is not ASCII-capable, which excludes modes like Japanese Romaji that
+/// produce ASCII characters and should allow multi-stroke keybindings like `jj`.
+unsafe fn is_ime_input_source_active() -> bool {
+ unsafe {
+ let source = TISCopyCurrentKeyboardInputSource();
+ if source.is_null() {
+ return false;
+ }
+
+ let source_type =
+ TISGetInputSourceProperty(source, kTISPropertyInputSourceType as *const c_void);
+ let is_input_mode = !source_type.is_null()
+ && CFEqual(
+ source_type as CFTypeRef,
+ kTISTypeKeyboardInputMode as CFTypeRef,
+ ) != 0;
+
+ let is_ascii = TISGetInputSourceProperty(
+ source,
+ kTISPropertyInputSourceIsASCIICapable as *const c_void,
+ );
+ let is_ascii_capable = !is_ascii.is_null() && CFBooleanGetValue(is_ascii as CFBooleanRef);
+
+ CFRelease(source as CFTypeRef);
+
+ is_input_mode && !is_ascii_capable
+ }
+}
+
extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent: bool) -> BOOL {
let window_state = unsafe { get_window_state(this) };
let mut lock = window_state.as_ref().lock();
@@ -1833,7 +1877,28 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
// and keys with function, as the input handler swallows them.
// and keys with platform (Cmd), so that Cmd+key events (e.g. Cmd+`) are not
// consumed by the IME on non-QWERTY / dead-key layouts.
+ // We also send printable keys to the IME first when an IME input source (e.g. Japanese,
+ // Korean, Chinese) is active and the input handler accepts text input. This prevents
+ // multi-stroke keybindings like `jj` from intercepting keys that the IME should compose
+ // (e.g. typing 'ji' should produce 'じ', not 'jい'). If the IME doesn't handle the key,
+ // it calls `doCommandBySelector:` which routes it back to keybinding matching.
+ let is_ime_printable_key = !is_composing
+ && key_down_event
+ .keystroke
+ .key_char
+ .as_ref()
+ .is_some_and(|key_char| key_char.chars().all(|c| !c.is_control()))
+ && !key_down_event.keystroke.modifiers.control
+ && !key_down_event.keystroke.modifiers.function
+ && !key_down_event.keystroke.modifiers.platform
+ && unsafe { is_ime_input_source_active() }
+ && with_input_handler(this, |input_handler| {
+ input_handler.query_prefers_ime_for_printable_keys()
+ })
+ .unwrap_or(false);
+
if is_composing
+ || is_ime_printable_key
|| (key_down_event.keystroke.key_char.is_none()
&& !key_down_event.keystroke.modifiers.control
&& !key_down_event.keystroke.modifiers.function