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 { unsafe fn parse_keystroke(native_event: id) -> Keystroke {
use cocoa::appkit::*; use cocoa::appkit::*;
let mut chars_ignoring_modifiers = native_event let mut chars_ignoring_modifiers = chars_for_modified_key(native_event.keyCode(), false, false);
.charactersIgnoringModifiers()
.to_str()
.to_string();
let first_char = chars_ignoring_modifiers.chars().next().map(|ch| ch as u16); let first_char = chars_ignoring_modifiers.chars().next().map(|ch| ch as u16);
let modifiers = native_event.modifierFlags(); let modifiers = native_event.modifierFlags();
@ -314,28 +311,41 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
Some(NSF18FunctionKey) => "f18".to_string(), Some(NSF18FunctionKey) => "f18".to_string(),
Some(NSF19FunctionKey) => "f19".to_string(), Some(NSF19FunctionKey) => "f19".to_string(),
_ => { _ => {
let mut chars_ignoring_modifiers_and_shift = // Cases to test when modifying this:
chars_for_modified_key(native_event.keyCode(), false, false); //
// 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. // 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_cmd = chars_for_modified_key(native_event.keyCode(), true, false);
if command && chars_ignoring_modifiers_and_shift != chars_with_cmd { let chars_with_both = chars_for_modified_key(native_event.keyCode(), true, true);
chars_ignoring_modifiers =
chars_for_modified_key(native_event.keyCode(), true, shift); // We don't do this in the case that the shifted command key generates
chars_ignoring_modifiers_and_shift = chars_with_cmd; // 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 shift && chars_ignoring_modifiers == chars_with_shift.to_ascii_lowercase() {
if chars_ignoring_modifiers_and_shift chars_ignoring_modifiers
== chars_ignoring_modifiers.to_ascii_lowercase() } else if shift {
{
chars_ignoring_modifiers_and_shift
} else if chars_ignoring_modifiers_and_shift != chars_ignoring_modifiers {
shift = false; shift = false;
chars_ignoring_modifiers chars_with_shift
} else {
chars_ignoring_modifiers
}
} else { } else {
chars_ignoring_modifiers 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 { fn chars_for_modified_key(code: CGKeyCode, cmd: bool, shift: bool) -> String {
// Ideally, we would use `[NSEvent charactersByApplyingModifiers]` but that // Ideally, we would use `[NSEvent charactersByApplyingModifiers]` but that
// always returns an empty string with certain keyboards, e.g. Japanese. Synthesizing // always returns an empty string with certain keyboards, e.g. Japanese. Synthesizing