macOS: Improve deadkeys (#20515)

Conrad Irwin created

Closes #19738

This change refactors how we handle input on macOS to avoid simulating
our own IME. This fixes a number of small edge-cases, and also lets us
remove a bunch of code that had been added to work around bugs in the
previous version.

Release Notes:

- On macOS: Keyboard shortcuts are now handled before activating the IME
system, this enables using vim's default mode on keyboards that use IME
menus (like Japanese).
- On macOS: Improvements to handling of dead-keys. For example when
typing `""` on a Brazillian keyboard, you now get a committed " and a
new marked ", as happens in other apps. Also, you can now type cmd-^ on
an AZERTY keyboard for indent; and ^ on a QWERTZ keyboard now goes to
the beginning of line in vim normal mode, or `d i "` no requires no
space to delete within quotes on Brazilian keyboards (though `d f "
space` is still required as `f` relies on the input handler, not a
binding).
- On macOS: In the terminal pane, holding down a key will now repeat
that key (as happens in iTerm2) instead of opening the character
selector.

Change summary

Cargo.lock                                   |   2 
crates/gpui/examples/input.rs                |   2 
crates/gpui/src/platform.rs                  |  14 +
crates/gpui/src/platform/keystroke.rs        |   3 
crates/gpui/src/platform/mac/events.rs       | 153 ++++++++-----
crates/gpui/src/platform/mac/platform.rs     |  20 +
crates/gpui/src/platform/mac/window.rs       | 238 ++++++++-------------
crates/terminal_view/src/terminal_element.rs |   4 
8 files changed, 223 insertions(+), 213 deletions(-)

Detailed changes

Cargo.lock πŸ”—

@@ -5657,7 +5657,7 @@ dependencies = [
  "httpdate",
  "itoa",
  "pin-project-lite",
- "socket2 0.4.10",
+ "socket2 0.5.7",
  "tokio",
  "tower-service",
  "tracing",

crates/gpui/examples/input.rs πŸ”—

@@ -580,7 +580,7 @@ impl Render for InputExample {
             .children(self.recent_keystrokes.iter().rev().map(|ks| {
                 format!(
                     "{:} {}",
-                    ks,
+                    ks.unparse(),
                     if let Some(ime_key) = ks.ime_key.as_ref() {
                         format!("-> {}", ime_key)
                     } else {

crates/gpui/src/platform.rs πŸ”—

@@ -683,6 +683,11 @@ impl PlatformInputHandler {
             .flatten()
     }
 
+    #[allow(dead_code)]
+    fn apple_press_and_hold_enabled(&mut self) -> bool {
+        self.handler.apple_press_and_hold_enabled()
+    }
+
     pub(crate) fn dispatch_input(&mut self, input: &str, cx: &mut WindowContext) {
         self.handler.replace_text_in_range(None, input, cx);
     }
@@ -780,6 +785,15 @@ pub trait InputHandler: 'static {
         range_utf16: Range<usize>,
         cx: &mut WindowContext,
     ) -> Option<Bounds<Pixels>>;
+
+    /// Allows a given input context to opt into getting raw key repeats instead of
+    /// sending these to the platform.
+    /// TODO: Ideally we should be able to set ApplePressAndHoldEnabled in NSUserDefaults
+    /// (which is how iTerm does it) but it doesn't seem to work for me.
+    #[allow(dead_code)]
+    fn apple_press_and_hold_enabled(&mut self) -> bool {
+        true
+    }
 }
 
 /// The variables that can be configured when creating a new window

crates/gpui/src/platform/keystroke.rs πŸ”—

@@ -124,6 +124,9 @@ impl Keystroke {
     /// Produces a representation of this key that Parse can understand.
     pub fn unparse(&self) -> String {
         let mut str = String::new();
+        if self.modifiers.function {
+            str.push_str("fn-");
+        }
         if self.modifiers.control {
             str.push_str("ctrl-");
         }

crates/gpui/src/platform/mac/events.rs πŸ”—

@@ -1,20 +1,20 @@
 use crate::{
-    platform::mac::NSStringExt, point, px, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers,
-    ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseExitEvent, MouseMoveEvent,
-    MouseUpEvent, NavigationDirection, Pixels, PlatformInput, ScrollDelta, ScrollWheelEvent,
-    TouchPhase,
+    platform::mac::{
+        kTISPropertyUnicodeKeyLayoutData, LMGetKbdType, NSStringExt,
+        TISCopyCurrentKeyboardLayoutInputSource, TISGetInputSourceProperty, UCKeyTranslate,
+    },
+    point, px, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton,
+    MouseDownEvent, MouseExitEvent, MouseMoveEvent, MouseUpEvent, NavigationDirection, Pixels,
+    PlatformInput, ScrollDelta, ScrollWheelEvent, TouchPhase,
 };
 use cocoa::{
     appkit::{NSEvent, NSEventModifierFlags, NSEventPhase, NSEventType},
     base::{id, YES},
 };
-use core_graphics::{
-    event::{CGEvent, CGEventFlags, CGKeyCode},
-    event_source::{CGEventSource, CGEventSourceStateID},
-};
-use metal::foreign_types::ForeignType as _;
-use objc::{class, msg_send, sel, sel_impl};
-use std::{borrow::Cow, mem, ptr, sync::Once};
+use core_foundation::data::{CFDataGetBytePtr, CFDataRef};
+use core_graphics::event::CGKeyCode;
+use objc::{msg_send, sel, sel_impl};
+use std::{borrow::Cow, ffi::c_void};
 
 const BACKSPACE_KEY: u16 = 0x7f;
 const SPACE_KEY: u16 = b' ' as u16;
@@ -24,24 +24,6 @@ const ESCAPE_KEY: u16 = 0x1b;
 const TAB_KEY: u16 = 0x09;
 const SHIFT_TAB_KEY: u16 = 0x19;
 
-fn synthesize_keyboard_event(code: CGKeyCode) -> CGEvent {
-    static mut EVENT_SOURCE: core_graphics::sys::CGEventSourceRef = ptr::null_mut();
-    static INIT_EVENT_SOURCE: Once = Once::new();
-
-    INIT_EVENT_SOURCE.call_once(|| {
-        let source = CGEventSource::new(CGEventSourceStateID::Private).unwrap();
-        unsafe {
-            EVENT_SOURCE = source.as_ptr();
-        };
-        mem::forget(source);
-    });
-
-    let source = unsafe { core_graphics::event_source::CGEventSource::from_ptr(EVENT_SOURCE) };
-    let event = CGEvent::new_keyboard_event(source.clone(), code, true).unwrap();
-    mem::forget(source);
-    event
-}
-
 pub fn key_to_native(key: &str) -> Cow<str> {
     use cocoa::appkit::*;
     let code = match key {
@@ -259,8 +241,9 @@ impl PlatformInput {
 unsafe fn parse_keystroke(native_event: id) -> Keystroke {
     use cocoa::appkit::*;
 
-    let mut chars_ignoring_modifiers = chars_for_modified_key(native_event.keyCode(), false, false);
-    let first_char = chars_ignoring_modifiers.chars().next().map(|ch| ch as u16);
+    let mut characters = native_event.characters().to_str().to_string();
+    let mut ime_key = None;
+    let first_char = characters.chars().next().map(|ch| ch as u16);
     let modifiers = native_event.modifierFlags();
 
     let control = modifiers.contains(NSEventModifierFlags::NSControlKeyMask);
@@ -321,6 +304,9 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
             // * Norwegian        7 | 7    | cmd-7 | cmd-/        (macOS reports cmd-shift-7 instead of cmd-/)
             // * Russian          7 | 7    | cmd-7 | cmd-&        (shift-7 is . but when cmd is down, should use cmd layout)
             // * German QWERTZ    ; | ΓΆ    | cmd-ΓΆ | cmd-Γ–        (Zed's shift special case only applies to a-z)
+            //
+            let mut chars_ignoring_modifiers =
+                chars_for_modified_key(native_event.keyCode(), false, false);
             let mut chars_with_shift = chars_for_modified_key(native_event.keyCode(), false, true);
 
             // Handle Dvorak+QWERTY / Russian / Armeniam
@@ -341,14 +327,24 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
                 chars_ignoring_modifiers = chars_with_cmd;
             }
 
-            if shift && chars_ignoring_modifiers == chars_with_shift.to_ascii_lowercase() {
+            let mut key = if shift
+                && chars_ignoring_modifiers
+                    .chars()
+                    .all(|c| c.is_ascii_lowercase())
+            {
                 chars_ignoring_modifiers
             } else if shift {
                 shift = false;
                 chars_with_shift
             } else {
                 chars_ignoring_modifiers
-            }
+            };
+
+            if characters.len() > 0 && characters != key {
+                ime_key = Some(characters.clone());
+            };
+
+            key
         }
     };
 
@@ -361,50 +357,81 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
             function,
         },
         key,
-        ime_key: None,
+        ime_key,
     }
 }
 
 fn always_use_command_layout() -> bool {
-    // look at the key to the right of "tab" ('a' in QWERTY)
-    // if it produces a non-ASCII character, but with command held produces ASCII,
-    // we default to the command layout for our keyboard system.
-    let event = synthesize_keyboard_event(0);
-    let without_cmd = unsafe {
-        let event: id = msg_send![class!(NSEvent), eventWithCGEvent: &*event];
-        event.characters().to_str().to_string()
-    };
-    if without_cmd.is_ascii() {
+    if chars_for_modified_key(0, false, false).is_ascii() {
         return false;
     }
 
-    event.set_flags(CGEventFlags::CGEventFlagCommand);
-    let with_cmd = unsafe {
-        let event: id = msg_send![class!(NSEvent), eventWithCGEvent: &*event];
-        event.characters().to_str().to_string()
-    };
-
-    with_cmd.is_ascii()
+    chars_for_modified_key(0, true, false).is_ascii()
 }
 
 fn chars_for_modified_key(code: CGKeyCode, cmd: bool, shift: bool) -> String {
-    // Ideally, we would use `[NSEvent charactersByApplyingModifiers]` but that
-    // always returns an empty string with certain keyboards, e.g. Japanese. Synthesizing
-    // an event with the given flags instead lets us access `characters`, which always
-    // returns a valid string.
-    let event = synthesize_keyboard_event(code);
+    // Values from: https://github.com/phracker/MacOSX-SDKs/blob/master/MacOSX10.6.sdk/System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/Headers/Events.h#L126
+    // shifted >> 8 for UCKeyTranslate
+    const CMD_MOD: u32 = 1;
+    const SHIFT_MOD: u32 = 2;
+    const CG_SPACE_KEY: u16 = 49;
+    // https://github.com/phracker/MacOSX-SDKs/blob/master/MacOSX10.6.sdk/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/CarbonCore.framework/Versions/A/Headers/UnicodeUtilities.h#L278
+    #[allow(non_upper_case_globals)]
+    const kUCKeyActionDown: u16 = 0;
+    #[allow(non_upper_case_globals)]
+    const kUCKeyTranslateNoDeadKeysMask: u32 = 0;
 
-    let mut flags = CGEventFlags::empty();
-    if cmd {
-        flags |= CGEventFlags::CGEventFlagCommand;
+    let keyboard_type = unsafe { LMGetKbdType() as u32 };
+    const BUFFER_SIZE: usize = 4;
+    let mut dead_key_state = 0;
+    let mut buffer: [u16; BUFFER_SIZE] = [0; BUFFER_SIZE];
+    let mut buffer_size: usize = 0;
+
+    let keyboard = unsafe { TISCopyCurrentKeyboardLayoutInputSource() };
+    if keyboard.is_null() {
+        return "".to_string();
     }
-    if shift {
-        flags |= CGEventFlags::CGEventFlagShift;
+    let layout_data = unsafe {
+        TISGetInputSourceProperty(keyboard, kTISPropertyUnicodeKeyLayoutData as *const c_void)
+            as CFDataRef
+    };
+    if layout_data.is_null() {
+        unsafe {
+            let _: () = msg_send![keyboard, release];
+        }
+        return "".to_string();
     }
-    event.set_flags(flags);
+    let keyboard_layout = unsafe { CFDataGetBytePtr(layout_data) };
+    let modifiers = if cmd { CMD_MOD } else { 0 } | if shift { SHIFT_MOD } else { 0 };
 
     unsafe {
-        let event: id = msg_send![class!(NSEvent), eventWithCGEvent: &*event];
-        event.characters().to_str().to_string()
+        UCKeyTranslate(
+            keyboard_layout as *const c_void,
+            code,
+            kUCKeyActionDown,
+            modifiers,
+            keyboard_type,
+            kUCKeyTranslateNoDeadKeysMask,
+            &mut dead_key_state,
+            BUFFER_SIZE,
+            &mut buffer_size as *mut usize,
+            &mut buffer as *mut u16,
+        );
+        if dead_key_state != 0 {
+            UCKeyTranslate(
+                keyboard_layout as *const c_void,
+                CG_SPACE_KEY,
+                kUCKeyActionDown,
+                modifiers,
+                keyboard_type,
+                kUCKeyTranslateNoDeadKeysMask,
+                &mut dead_key_state,
+                BUFFER_SIZE,
+                &mut buffer_size as *mut usize,
+                &mut buffer as *mut u16,
+            );
+        }
+        let _: () = msg_send![keyboard, release];
     }
+    String::from_utf16(&buffer[..buffer_size]).unwrap_or_default()
 }

crates/gpui/src/platform/mac/platform.rs πŸ”—

@@ -1448,13 +1448,27 @@ unsafe fn ns_url_to_path(url: id) -> Result<PathBuf> {
 
 #[link(name = "Carbon", kind = "framework")]
 extern "C" {
-    fn TISCopyCurrentKeyboardLayoutInputSource() -> *mut Object;
-    fn TISGetInputSourceProperty(
+    pub(super) fn TISCopyCurrentKeyboardLayoutInputSource() -> *mut Object;
+    pub(super) fn TISGetInputSourceProperty(
         inputSource: *mut Object,
         propertyKey: *const c_void,
     ) -> *mut Object;
 
-    pub static kTISPropertyInputSourceID: CFStringRef;
+    pub(super) fn UCKeyTranslate(
+        keyLayoutPtr: *const ::std::os::raw::c_void,
+        virtualKeyCode: u16,
+        keyAction: u16,
+        modifierKeyState: u32,
+        keyboardType: u32,
+        keyTranslateOptions: u32,
+        deadKeyState: *mut u32,
+        maxStringLength: usize,
+        actualStringLength: *mut usize,
+        unicodeString: *mut u16,
+    ) -> u32;
+    pub(super) fn LMGetKbdType() -> u16;
+    pub(super) static kTISPropertyUnicodeKeyLayoutData: CFStringRef;
+    pub(super) static kTISPropertyInputSourceID: CFStringRef;
 }
 
 mod security {

crates/gpui/src/platform/mac/window.rs πŸ”—

@@ -38,7 +38,6 @@ use std::{
     cell::Cell,
     ffi::{c_void, CStr},
     mem,
-    ops::Range,
     path::PathBuf,
     ptr::{self, NonNull},
     rc::Rc,
@@ -310,14 +309,6 @@ unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const C
     decl.register()
 }
 
-#[allow(clippy::enum_variant_names)]
-#[derive(Clone, Debug)]
-enum ImeInput {
-    InsertText(String, Option<Range<usize>>),
-    SetMarkedText(String, Option<Range<usize>>, Option<Range<usize>>),
-    UnmarkText,
-}
-
 struct MacWindowState {
     handle: AnyWindowHandle,
     executor: ForegroundExecutor,
@@ -338,14 +329,11 @@ struct MacWindowState {
     synthetic_drag_counter: usize,
     traffic_light_position: Option<Point<Pixels>>,
     previous_modifiers_changed_event: Option<PlatformInput>,
-    // State tracking what the IME did after the last request
-    last_ime_inputs: Option<SmallVec<[(String, Option<Range<usize>>); 1]>>,
-    previous_keydown_inserted_text: Option<String>,
+    keystroke_for_do_command: Option<Keystroke>,
     external_files_dragged: bool,
     // Whether the next left-mouse click is also the focusing click.
     first_mouse: bool,
     fullscreen_restore_bounds: Bounds<Pixels>,
-    ime_composing: bool,
 }
 
 impl MacWindowState {
@@ -619,12 +607,10 @@ impl MacWindow {
                     .as_ref()
                     .and_then(|titlebar| titlebar.traffic_light_position),
                 previous_modifiers_changed_event: None,
-                last_ime_inputs: None,
-                previous_keydown_inserted_text: None,
+                keystroke_for_do_command: None,
                 external_files_dragged: false,
                 first_mouse: false,
                 fullscreen_restore_bounds: Bounds::default(),
-                ime_composing: false,
             })));
 
             (*native_window).set_ivar(
@@ -1226,9 +1212,9 @@ extern "C" fn handle_key_down(this: &Object, _: Sel, native_event: id) {
 //  Brazilian layout:
 //   - `" space` should create an unmarked quote
 //   - `" backspace` should delete the marked quote
+//   - `" "`should create an unmarked quote and a second marked quote
 //   - `" up` should insert a quote, unmark it, and move up one line
 //   - `" cmd-down` should insert a quote, unmark it, and move to the end of the file
-//      - NOTE: The current implementation does not move the selection to the end of the file
 //   - `cmd-ctrl-space` and clicking on an emoji should type it
 //  Czech (QWERTY) layout:
 //   - in vim mode `option-4`  should go to end of line (same as $)
@@ -1241,95 +1227,80 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
     let window_height = lock.content_size().height;
     let event = unsafe { PlatformInput::from_native(native_event, Some(window_height)) };
 
-    if let Some(PlatformInput::KeyDown(mut event)) = event {
-        // For certain keystrokes, macOS will first dispatch a "key equivalent" event.
-        // If that event isn't handled, it will then dispatch a "key down" event. GPUI
-        // makes no distinction between these two types of events, so we need to ignore
-        // the "key down" event if we've already just processed its "key equivalent" version.
-        if key_equivalent {
-            lock.last_key_equivalent = Some(event.clone());
-        } else if lock.last_key_equivalent.take().as_ref() == Some(&event) {
-            return NO;
-        }
-
-        let keydown = event.keystroke.clone();
-        let fn_modifier = keydown.modifiers.function;
-        lock.last_ime_inputs = Some(Default::default());
-        drop(lock);
-
-        // Send the event to the input context for IME handling, unless the `fn` modifier is
-        // being pressed.
-        // this will call back into `insert_text`, etc.
-        if !fn_modifier {
-            unsafe {
-                let input_context: id = msg_send![this, inputContext];
-                let _: BOOL = msg_send![input_context, handleEvent: native_event];
-            }
-        }
-
-        let mut handled = false;
-        let mut lock = window_state.lock();
-        let previous_keydown_inserted_text = lock.previous_keydown_inserted_text.take();
-        let mut last_inserts = lock.last_ime_inputs.take().unwrap();
-        let ime_composing = std::mem::take(&mut lock.ime_composing);
+    let Some(PlatformInput::KeyDown(mut event)) = event else {
+        return NO;
+    };
+    // For certain keystrokes, macOS will first dispatch a "key equivalent" event.
+    // If that event isn't handled, it will then dispatch a "key down" event. GPUI
+    // makes no distinction between these two types of events, so we need to ignore
+    // the "key down" event if we've already just processed its "key equivalent" version.
+    if key_equivalent {
+        lock.last_key_equivalent = Some(event.clone());
+    } else if lock.last_key_equivalent.take().as_ref() == Some(&event) {
+        return NO;
+    }
 
-        let mut callback = lock.event_callback.take();
-        drop(lock);
+    drop(lock);
 
-        let last_insert = last_inserts.pop();
-        // on a brazilian keyboard typing `"` and then hitting `up` will cause two IME
-        // events, one to unmark the quote, and one to send the up arrow.
-        for (text, range) in last_inserts {
-            send_to_input_handler(this, ImeInput::InsertText(text, range));
+    let is_composing = with_input_handler(this, |input_handler| input_handler.marked_text_range())
+        .flatten()
+        .is_some();
+
+    // If we're composing, send the key to the input handler first;
+    // otherwise we only send to the input handler if we don't have a matching binding.
+    // The input handler may call `do_command_by_selector` if it doesn't know how to handle
+    // a key. If it does so, it will return YES so we won't send the key twice.
+    if is_composing || event.keystroke.key.is_empty() {
+        window_state.as_ref().lock().keystroke_for_do_command = Some(event.keystroke.clone());
+        let handled: BOOL = unsafe {
+            let input_context: id = msg_send![this, inputContext];
+            msg_send![input_context, handleEvent: native_event]
+        };
+        window_state.as_ref().lock().keystroke_for_do_command.take();
+        if handled {
+            return YES;
         }
 
-        let is_composing =
-            with_input_handler(this, |input_handler| input_handler.marked_text_range())
-                .flatten()
-                .is_some()
-                || ime_composing;
-
-        if let Some((text, range)) = last_insert {
-            if !is_composing {
-                window_state.lock().previous_keydown_inserted_text = Some(text.clone());
-                if let Some(callback) = callback.as_mut() {
-                    event.keystroke.ime_key = Some(text.clone());
-                    handled = !callback(PlatformInput::KeyDown(event)).propagate;
-                }
-            }
-
-            if !handled {
-                handled = true;
-                send_to_input_handler(this, ImeInput::InsertText(text, range));
-            }
-        } else if !is_composing {
-            let is_held = event.is_held;
+        let mut callback = window_state.as_ref().lock().event_callback.take();
+        let handled = if let Some(callback) = callback.as_mut() {
+            !callback(PlatformInput::KeyDown(event)).propagate
+        } else {
+            false
+        };
+        window_state.as_ref().lock().event_callback = callback;
+        return handled;
+    }
 
-            if let Some(callback) = callback.as_mut() {
-                handled = !callback(PlatformInput::KeyDown(event)).propagate;
-            }
+    let mut callback = window_state.as_ref().lock().event_callback.take();
+    let handled = if let Some(callback) = callback.as_mut() {
+        !callback(PlatformInput::KeyDown(event.clone())).propagate
+    } else {
+        false
+    };
+    window_state.as_ref().lock().event_callback = callback;
+    if handled {
+        return handled;
+    }
 
-            if !handled && is_held {
-                if let Some(text) = previous_keydown_inserted_text {
-                    // macOS IME is a bit funky, and even when you've told it there's nothing to
-                    // enter it will still swallow certain keys (e.g. 'f', 'j') and not others
-                    // (e.g. 'n'). This is a problem for certain kinds of views, like the terminal.
-                    with_input_handler(this, |input_handler| {
-                        if input_handler.selected_text_range(false).is_none() {
-                            handled = true;
-                            input_handler.replace_text_in_range(None, &text)
-                        }
-                    });
-                    window_state.lock().previous_keydown_inserted_text = Some(text);
-                }
+    if event.is_held {
+        let handled = with_input_handler(&this, |input_handler| {
+            if !input_handler.apple_press_and_hold_enabled() {
+                input_handler.replace_text_in_range(
+                    None,
+                    &event.keystroke.ime_key.unwrap_or(event.keystroke.key),
+                );
+                return true;
             }
+            false
+        });
+        if handled == Some(true) {
+            return YES;
         }
+    }
 
-        window_state.lock().event_callback = callback;
-
-        handled as BOOL
-    } else {
-        NO
+    unsafe {
+        let input_context: id = msg_send![this, inputContext];
+        msg_send![input_context, handleEvent: native_event]
     }
 }
 
@@ -1741,10 +1712,9 @@ extern "C" fn insert_text(this: &Object, _: Sel, text: id, replacement_range: NS
 
         let text = text.to_str();
         let replacement_range = replacement_range.to_range();
-        send_to_input_handler(
-            this,
-            ImeInput::InsertText(text.to_string(), replacement_range),
-        );
+        with_input_handler(this, |input_handler| {
+            input_handler.replace_text_in_range(replacement_range, &text)
+        });
     }
 }
 
@@ -1766,15 +1736,13 @@ extern "C" fn set_marked_text(
         let selected_range = selected_range.to_range();
         let replacement_range = replacement_range.to_range();
         let text = text.to_str();
-
-        send_to_input_handler(
-            this,
-            ImeInput::SetMarkedText(text.to_string(), replacement_range, selected_range),
-        );
+        with_input_handler(this, |input_handler| {
+            input_handler.replace_and_mark_text_in_range(replacement_range, &text, selected_range)
+        });
     }
 }
 extern "C" fn unmark_text(this: &Object, _: Sel) {
-    send_to_input_handler(this, ImeInput::UnmarkText);
+    with_input_handler(this, |input_handler| input_handler.unmark_text());
 }
 
 extern "C" fn attributed_substring_for_proposed_range(
@@ -1800,7 +1768,24 @@ extern "C" fn attributed_substring_for_proposed_range(
     .unwrap_or(nil)
 }
 
-extern "C" fn do_command_by_selector(_: &Object, _: Sel, _: Sel) {}
+// We ignore which selector it asks us to do because the user may have
+// bound the shortcut to something else.
+extern "C" fn do_command_by_selector(this: &Object, _: Sel, _: Sel) {
+    let state = unsafe { get_window_state(this) };
+    let mut lock = state.as_ref().lock();
+    let keystroke = lock.keystroke_for_do_command.take();
+    let mut event_callback = lock.event_callback.take();
+    drop(lock);
+
+    if let Some((keystroke, mut callback)) = keystroke.zip(event_callback.as_mut()) {
+        (callback)(PlatformInput::KeyDown(KeyDownEvent {
+            keystroke,
+            is_held: false,
+        }));
+    }
+
+    state.as_ref().lock().event_callback = event_callback;
+}
 
 extern "C" fn view_did_change_effective_appearance(this: &Object, _: Sel) {
     unsafe {
@@ -1950,43 +1935,6 @@ where
     }
 }
 
-fn send_to_input_handler(window: &Object, ime: ImeInput) {
-    unsafe {
-        let window_state = get_window_state(window);
-        let mut lock = window_state.lock();
-
-        if let Some(mut input_handler) = lock.input_handler.take() {
-            match ime {
-                ImeInput::InsertText(text, range) => {
-                    if let Some(ime_input) = lock.last_ime_inputs.as_mut() {
-                        ime_input.push((text, range));
-                        lock.input_handler = Some(input_handler);
-                        return;
-                    }
-                    drop(lock);
-                    input_handler.replace_text_in_range(range, &text)
-                }
-                ImeInput::SetMarkedText(text, range, marked_range) => {
-                    lock.ime_composing = true;
-                    drop(lock);
-                    input_handler.replace_and_mark_text_in_range(range, &text, marked_range)
-                }
-                ImeInput::UnmarkText => {
-                    drop(lock);
-                    input_handler.unmark_text()
-                }
-            }
-            window_state.lock().input_handler = Some(input_handler);
-        } else {
-            if let ImeInput::InsertText(text, range) = ime {
-                if let Some(ime_input) = lock.last_ime_inputs.as_mut() {
-                    ime_input.push((text, range));
-                }
-            }
-        }
-    }
-}
-
 unsafe fn display_id_for_screen(screen: id) -> CGDirectDisplayID {
     let device_description = NSScreen::deviceDescription(screen);
     let screen_number_key: id = NSString::alloc(nil).init_str("NSScreenNumber");

crates/terminal_view/src/terminal_element.rs πŸ”—

@@ -1044,6 +1044,10 @@ impl InputHandler for TerminalInputHandler {
     ) -> Option<Bounds<Pixels>> {
         self.cursor_bounds
     }
+
+    fn apple_press_and_hold_enabled(&mut self) -> bool {
+        false
+    }
 }
 
 pub fn is_blank(cell: &IndexedCell) -> bool {