macOS: Allow non-cmd keyboard shortcuts to work on non-Latin layouts (#20336)

Updates #10972

Release Notes:

- Fixed builtin keybindings that don't require cmd on macOS, non-Latin,
ANSI layouts. For example you can now use ctrl-ա (equivalent to ctrl-a)
on an Armenian keyboard to get to the beginning of the line.

---------

Co-authored-by: Will <will@zed.dev>
This commit is contained in:
Conrad Irwin 2024-11-08 11:49:13 -07:00 committed by GitHub
parent 09c599385a
commit 07821083df
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -259,10 +259,7 @@ impl PlatformInput {
unsafe fn parse_keystroke(native_event: id) -> Keystroke {
use cocoa::appkit::*;
let mut chars_ignoring_modifiers = native_event
.charactersIgnoringModifiers()
.to_str()
.to_string();
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 modifiers = native_event.modifierFlags();
@ -314,28 +311,41 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
Some(NSF18FunctionKey) => "f18".to_string(),
Some(NSF19FunctionKey) => "f19".to_string(),
_ => {
let mut chars_ignoring_modifiers_and_shift =
chars_for_modified_key(native_event.keyCode(), false, false);
// Cases to test when modifying this:
//
// qwerty key | none | cmd | cmd-shift
// * Armenian s | ս | cmd-s | cmd-shift-s (layout is non-ASCII, so we use cmd layout)
// * Dvorak+QWERTY s | o | cmd-s | cmd-shift-s (layout switches on cmd)
// * Ukrainian+QWERTY s | с | cmd-s | cmd-shift-s (macOS reports cmd-s instead of cmd-S)
// * Czech 7 | ý | cmd-ý | cmd-7 (layout has shifted numbers)
// * 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_with_shift = chars_for_modified_key(native_event.keyCode(), false, true);
// Honor ⌘ when Dvorak-QWERTY is used.
let chars_with_cmd = chars_for_modified_key(native_event.keyCode(), true, false);
if command && chars_ignoring_modifiers_and_shift != chars_with_cmd {
chars_ignoring_modifiers =
chars_for_modified_key(native_event.keyCode(), true, shift);
chars_ignoring_modifiers_and_shift = chars_with_cmd;
// Handle Dvorak+QWERTY / Russian / Armeniam
if command || always_use_command_layout() {
let chars_with_cmd = chars_for_modified_key(native_event.keyCode(), true, false);
let chars_with_both = chars_for_modified_key(native_event.keyCode(), true, true);
// We don't do this in the case that the shifted command key generates
// the same character as the unshifted command key (Norwegian, e.g.)
if chars_with_both != chars_with_cmd {
chars_with_shift = chars_with_both;
// Handle edge-case where cmd-shift-s reports cmd-s instead of
// cmd-shift-s (Ukrainian, etc.)
} else if chars_with_cmd.to_ascii_uppercase() != chars_with_cmd {
chars_with_shift = chars_with_cmd.to_ascii_uppercase();
}
chars_ignoring_modifiers = chars_with_cmd;
}
if shift {
if chars_ignoring_modifiers_and_shift
== chars_ignoring_modifiers.to_ascii_lowercase()
{
chars_ignoring_modifiers_and_shift
} else if chars_ignoring_modifiers_and_shift != chars_ignoring_modifiers {
shift = false;
chars_ignoring_modifiers
} else {
chars_ignoring_modifiers
}
if shift && chars_ignoring_modifiers == chars_with_shift.to_ascii_lowercase() {
chars_ignoring_modifiers
} else if shift {
shift = false;
chars_with_shift
} else {
chars_ignoring_modifiers
}
@ -355,6 +365,28 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
}
}
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() {
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()
}
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