Skip to content

Commit

Permalink
macOS: Add key equivalents for non-Latin layouts (#20401)
Browse files Browse the repository at this point in the history
Closes  #16343
Closes #10972

Release Notes:

- (breaking change) On macOS when using a keyboard that supports an
extended Latin character set (e.g. French, German, ...) keyboard
shortcuts are automatically updated so that they can be typed without
`option`. This fixes several long-standing problems where some keyboards
could not type some shortcuts.
- This mapping works the same way as
[macOS](https://developer.apple.com/documentation/swiftui/view/keyboardshortcut(_:modifiers:localization:)).
For example on a German keyboard shortcuts like `cmd->` become `cmd-:`,
`cmd-[` and `cmd-]` become `cmd-ö` and `cmd-ä`. This mapping happens at
the time keyboard layout files are read so the keybindings are visible
in the command palette. To opt out of this behavior for your custom
keyboard shortcuts, set `"use_layout_keys": true` in your binding
section. For the mappings used for each layout [see
here](https://github.com/zed-industries/zed/blob/a890df1863ca939ee7f0ada6e629b6f83eb18bc5/crates/settings/src/key_equivalents.rs#L7).

---------

Co-authored-by: Will <[email protected]>
  • Loading branch information
ConradIrwin and Will authored Nov 8, 2024
1 parent 0782108 commit ff4f679
Show file tree
Hide file tree
Showing 16 changed files with 435 additions and 10 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,7 @@ chrono = { version = "0.4", features = ["serde"] }
clap = { version = "4.4", features = ["derive"] }
clickhouse = "0.11.6"
cocoa = "0.26"
cocoa-foundation = "0.2.0"
convert_case = "0.6.0"
core-foundation = "0.9.3"
core-foundation-sys = "0.8.6"
Expand Down
27 changes: 27 additions & 0 deletions assets/keymaps/vim.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[
{
"context": "VimControl && !menu",
"use_layout_keys": true,
"bindings": {
"i": ["vim::PushOperator", { "Object": { "around": false } }],
"a": ["vim::PushOperator", { "Object": { "around": true } }],
Expand Down Expand Up @@ -171,6 +172,7 @@
},
{
"context": "vim_mode == normal",
"use_layout_keys": true,
"bindings": {
"escape": "editor::Cancel",
"ctrl-[": "editor::Cancel",
Expand Down Expand Up @@ -224,13 +226,15 @@
},
{
"context": "VimControl && VimCount",
"use_layout_keys": true,
"bindings": {
"0": ["vim::Number", 0],
":": "vim::CountCommand"
}
},
{
"context": "vim_mode == visual",
"use_layout_keys": true,
"bindings": {
":": "vim::VisualCommand",
"u": "vim::ConvertToLowerCase",
Expand Down Expand Up @@ -279,6 +283,7 @@
},
{
"context": "vim_mode == insert",
"use_layout_keys": true,
"bindings": {
"escape": "vim::NormalBefore",
"ctrl-c": "vim::NormalBefore",
Expand All @@ -304,13 +309,15 @@
},
{
"context": "vim_mode == insert && !(showing_code_actions || showing_completions)",
"use_layout_keys": true,
"bindings": {
"ctrl-p": "editor::ShowCompletions",
"ctrl-n": "editor::ShowCompletions"
}
},
{
"context": "vim_mode == replace",
"use_layout_keys": true,
"bindings": {
"escape": "vim::NormalBefore",
"ctrl-c": "vim::NormalBefore",
Expand All @@ -328,6 +335,7 @@
},
{
"context": "vim_mode == waiting",
"use_layout_keys": true,
"bindings": {
"tab": "vim::Tab",
"enter": "vim::Enter",
Expand All @@ -341,6 +349,7 @@
},
{
"context": "vim_mode == operator",
"use_layout_keys": true,
"bindings": {
"escape": "vim::ClearOperators",
"ctrl-c": "vim::ClearOperators",
Expand All @@ -349,6 +358,7 @@
},
{
"context": "vim_operator == a || vim_operator == i || vim_operator == cs",
"use_layout_keys": true,
"bindings": {
"w": "vim::Word",
"shift-w": ["vim::Word", { "ignorePunctuation": true }],
Expand Down Expand Up @@ -376,6 +386,7 @@
},
{
"context": "vim_operator == c",
"use_layout_keys": true,
"bindings": {
"c": "vim::CurrentLine",
"d": "editor::Rename", // zed specific
Expand All @@ -384,6 +395,7 @@
},
{
"context": "vim_operator == d",
"use_layout_keys": true,
"bindings": {
"d": "vim::CurrentLine",
"s": ["vim::PushOperator", "DeleteSurrounds"],
Expand All @@ -393,27 +405,31 @@
},
{
"context": "vim_operator == gu",
"use_layout_keys": true,
"bindings": {
"g u": "vim::CurrentLine",
"u": "vim::CurrentLine"
}
},
{
"context": "vim_operator == gU",
"use_layout_keys": true,
"bindings": {
"g shift-u": "vim::CurrentLine",
"shift-u": "vim::CurrentLine"
}
},
{
"context": "vim_operator == g~",
"use_layout_keys": true,
"bindings": {
"g ~": "vim::CurrentLine",
"~": "vim::CurrentLine"
}
},
{
"context": "vim_operator == gq",
"use_layout_keys": true,
"bindings": {
"g q": "vim::CurrentLine",
"q": "vim::CurrentLine",
Expand All @@ -423,37 +439,43 @@
},
{
"context": "vim_operator == y",
"use_layout_keys": true,
"bindings": {
"y": "vim::CurrentLine",
"s": ["vim::PushOperator", { "AddSurrounds": {} }]
}
},
{
"context": "vim_operator == ys",
"use_layout_keys": true,
"bindings": {
"s": "vim::CurrentLine"
}
},
{
"context": "vim_operator == >",
"use_layout_keys": true,
"bindings": {
">": "vim::CurrentLine"
}
},
{
"context": "vim_operator == <",
"use_layout_keys": true,
"bindings": {
"<": "vim::CurrentLine"
}
},
{
"context": "vim_operator == gc",
"use_layout_keys": true,
"bindings": {
"c": "vim::CurrentLine"
}
},
{
"context": "vim_mode == literal",
"use_layout_keys": true,
"bindings": {
"ctrl-@": ["vim::Literal", ["ctrl-@", "\u0000"]],
"ctrl-a": ["vim::Literal", ["ctrl-a", "\u0001"]],
Expand Down Expand Up @@ -497,13 +519,15 @@
},
{
"context": "BufferSearchBar && !in_replace",
"use_layout_keys": true,
"bindings": {
"enter": "vim::SearchSubmit",
"escape": "buffer_search::Dismiss"
}
},
{
"context": "ProjectPanel || CollabPanel || OutlinePanel || ChatPanel || VimControl || EmptyPane || SharedScreen || MarkdownPreview || KeyContextView",
"use_layout_keys": true,
"bindings": {
// window related commands (ctrl-w X)
"ctrl-w": null,
Expand Down Expand Up @@ -554,6 +578,7 @@
},
{
"context": "EmptyPane || SharedScreen || MarkdownPreview || KeyContextView",
"use_layout_keys": true,
"bindings": {
":": "command_palette::Toggle",
"g /": "pane::DeploySearch"
Expand All @@ -562,6 +587,7 @@
{
// netrw compatibility
"context": "ProjectPanel && not_editing",
"use_layout_keys": true,
"bindings": {
":": "command_palette::Toggle",
"%": "project_panel::NewFile",
Expand Down Expand Up @@ -589,6 +615,7 @@
},
{
"context": "OutlinePanel && not_editing",
"use_layout_keys": true,
"bindings": {
"j": "menu::SelectNext",
"k": "menu::SelectPrev",
Expand Down
11 changes: 9 additions & 2 deletions crates/gpui/examples/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -546,7 +546,6 @@ impl InputExample {

impl Render for InputExample {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let num_keystrokes = self.recent_keystrokes.len();
div()
.bg(rgb(0xaaaaaa))
.track_focus(&self.focus_handle(cx))
Expand All @@ -561,7 +560,7 @@ impl Render for InputExample {
.flex()
.flex_row()
.justify_between()
.child(format!("Keystrokes: {}", num_keystrokes))
.child(format!("Keyboard {}", cx.keyboard_layout()))
.child(
div()
.border_1()
Expand Down Expand Up @@ -607,6 +606,7 @@ fn main() {
KeyBinding::new("end", End, None),
KeyBinding::new("ctrl-cmd-space", ShowCharacterPalette, None),
]);

let window = cx
.open_window(
WindowOptions {
Expand Down Expand Up @@ -642,6 +642,13 @@ fn main() {
.unwrap();
})
.detach();
cx.on_keyboard_layout_change({
move |cx| {
window.update(cx, |_, cx| cx.notify()).ok();
}
})
.detach();

window
.update(cx, |view, cx| {
cx.focus_view(&view.text_input);
Expand Down
39 changes: 39 additions & 0 deletions crates/gpui/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ pub struct AppContext {
pub(crate) windows: SlotMap<WindowId, Option<Window>>,
pub(crate) window_handles: FxHashMap<WindowId, AnyWindowHandle>,
pub(crate) keymap: Rc<RefCell<Keymap>>,
pub(crate) keyboard_layout: SharedString,
pub(crate) global_action_listeners:
FxHashMap<TypeId, Vec<Rc<dyn Fn(&dyn Any, DispatchPhase, &mut Self)>>>,
pending_effects: VecDeque<Effect>,
Expand All @@ -252,6 +253,7 @@ pub struct AppContext {
// TypeId is the type of the event that the listener callback expects
pub(crate) event_listeners: SubscriberSet<EntityId, (TypeId, Listener)>,
pub(crate) keystroke_observers: SubscriberSet<(), KeystrokeObserver>,
pub(crate) keyboard_layout_observers: SubscriberSet<(), Handler>,
pub(crate) release_listeners: SubscriberSet<EntityId, ReleaseListener>,
pub(crate) global_observers: SubscriberSet<TypeId, Handler>,
pub(crate) quit_observers: SubscriberSet<(), QuitHandler>,
Expand Down Expand Up @@ -279,6 +281,7 @@ impl AppContext {

let text_system = Arc::new(TextSystem::new(platform.text_system()));
let entities = EntityMap::new();
let keyboard_layout = SharedString::from(platform.keyboard_layout());

let app = Rc::new_cyclic(|this| AppCell {
app: RefCell::new(AppContext {
Expand All @@ -302,6 +305,7 @@ impl AppContext {
window_handles: FxHashMap::default(),
windows: SlotMap::with_key(),
keymap: Rc::new(RefCell::new(Keymap::default())),
keyboard_layout,
global_action_listeners: FxHashMap::default(),
pending_effects: VecDeque::new(),
pending_notifications: FxHashSet::default(),
Expand All @@ -310,6 +314,7 @@ impl AppContext {
event_listeners: SubscriberSet::new(),
release_listeners: SubscriberSet::new(),
keystroke_observers: SubscriberSet::new(),
keyboard_layout_observers: SubscriberSet::new(),
global_observers: SubscriberSet::new(),
quit_observers: SubscriberSet::new(),
layout_id_buffer: Default::default(),
Expand All @@ -323,6 +328,19 @@ impl AppContext {

init_app_menus(platform.as_ref(), &mut app.borrow_mut());

platform.on_keyboard_layout_change(Box::new({
let app = Rc::downgrade(&app);
move || {
if let Some(app) = app.upgrade() {
let cx = &mut app.borrow_mut();
cx.keyboard_layout = SharedString::from(cx.platform.keyboard_layout());
cx.keyboard_layout_observers
.clone()
.retain(&(), move |callback| (callback)(cx));
}
}
}));

platform.on_quit(Box::new({
let cx = app.clone();
move || {
Expand Down Expand Up @@ -356,6 +374,27 @@ impl AppContext {
}
}

/// Get the id of the current keyboard layout
pub fn keyboard_layout(&self) -> &SharedString {
&self.keyboard_layout
}

/// Invokes a handler when the current keyboard layout changes
pub fn on_keyboard_layout_change<F>(&self, mut callback: F) -> Subscription
where
F: 'static + FnMut(&mut AppContext),
{
let (subscription, activate) = self.keyboard_layout_observers.insert(
(),
Box::new(move |cx| {
callback(cx);
true
}),
);
activate();
subscription
}

/// Gracefully quit the application via the platform's standard routine.
pub fn quit(&self) {
self.platform.quit();
Expand Down
23 changes: 20 additions & 3 deletions crates/gpui/src/keymap/binding.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use collections::HashMap;

use crate::{Action, KeyBindingContextPredicate, Keystroke};
use anyhow::Result;
use smallvec::SmallVec;
Expand All @@ -22,22 +24,37 @@ impl Clone for KeyBinding {
impl KeyBinding {
/// Construct a new keybinding from the given data.
pub fn new<A: Action>(keystrokes: &str, action: A, context_predicate: Option<&str>) -> Self {
Self::load(keystrokes, Box::new(action), context_predicate).unwrap()
Self::load(keystrokes, Box::new(action), context_predicate, None).unwrap()
}

/// Load a keybinding from the given raw data.
pub fn load(keystrokes: &str, action: Box<dyn Action>, context: Option<&str>) -> Result<Self> {
pub fn load(
keystrokes: &str,
action: Box<dyn Action>,
context: Option<&str>,
key_equivalents: Option<&HashMap<char, char>>,
) -> Result<Self> {
let context = if let Some(context) = context {
Some(KeyBindingContextPredicate::parse(context)?)
} else {
None
};

let keystrokes = keystrokes
let mut keystrokes: SmallVec<[Keystroke; 2]> = keystrokes
.split_whitespace()
.map(Keystroke::parse)
.collect::<Result<_>>()?;

if let Some(equivalents) = key_equivalents {
for keystroke in keystrokes.iter_mut() {
if keystroke.key.chars().count() == 1 {
if let Some(key) = equivalents.get(&keystroke.key.chars().next().unwrap()) {
keystroke.key = key.to_string();
}
}
}
}

Ok(Self {
keystrokes,
action,
Expand Down
Loading

0 comments on commit ff4f679

Please sign in to comment.