Make keymaps reusable across platforms (#10811)

ElKowar created

This PR includes two relevant changes:
- Platform binds (super, windows, cmd) will now parse on all platforms,
regardless of which one is being used. While very counter-intuitive
(this means that `cmd-d` will actually be triggered by `win-d` on
windows) this makes it possible to reuse keymap files across platforms
easily
- There is now a KeyContext `os == linux`, `os == macos` or `os ==
windows` available in keymaps. This allows users to specify certain
blocks of keybinds only for one OS, allowing you to minimize the amount
of keymappings that you have to re-configure for each platform.

Release Notes:

- Added `os` KeyContext, set to either `linux`, `macos` or `windows`
- Fixed keymap parsing errors when `cmd` was used on linux, `super` was
used on mac, etc.

Change summary

crates/editor/src/editor.rs               |  2 +-
crates/extensions_ui/src/extensions_ui.rs |  2 +-
crates/gpui/src/keymap/context.rs         | 14 ++++++++++++++
crates/gpui/src/platform/keystroke.rs     |  7 +------
crates/project_panel/src/project_panel.rs |  2 +-
crates/search/src/buffer_search.rs        |  2 +-
crates/terminal_view/src/terminal_view.rs |  2 +-
crates/vim/src/state.rs                   |  2 +-
crates/workspace/src/dock.rs              |  2 +-
crates/workspace/src/pane.rs              |  2 +-
crates/workspace/src/workspace.rs         |  2 +-
11 files changed, 24 insertions(+), 15 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -1550,7 +1550,7 @@ impl Editor {
     }
 
     fn key_context(&self, cx: &AppContext) -> KeyContext {
-        let mut key_context = KeyContext::default();
+        let mut key_context = KeyContext::new_with_defaults();
         key_context.add("Editor");
         let mode = match self.mode {
             EditorMode::SingleLine => "single_line",

crates/extensions_ui/src/extensions_ui.rs 🔗

@@ -700,7 +700,7 @@ impl ExtensionsPage {
     }
 
     fn render_search(&self, cx: &mut ViewContext<Self>) -> Div {
-        let mut key_context = KeyContext::default();
+        let mut key_context = KeyContext::new_with_defaults();
         key_context.add("BufferSearchBar");
 
         let editor_border = if self.query_contains_error {

crates/gpui/src/keymap/context.rs 🔗

@@ -25,6 +25,20 @@ impl<'a> TryFrom<&'a str> for KeyContext {
 }
 
 impl KeyContext {
+    /// Initialize a new [`KeyContext`] that contains an `os` key set to either `macos`, `linux`, `windows` or `unknown`.
+    pub fn new_with_defaults() -> Self {
+        let mut context = Self::default();
+        #[cfg(target_os = "macos")]
+        context.set("os", "macos");
+        #[cfg(target_os = "linux")]
+        context.set("os", "linux");
+        #[cfg(target_os = "windows")]
+        context.set("os", "windows");
+        #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
+        context.set("os", "unknown");
+        context
+    }
+
     /// Parse a key context from a string.
     /// The key context format is very simple:
     /// - either a single identifier, such as `StatusBar`

crates/gpui/src/platform/keystroke.rs 🔗

@@ -74,12 +74,7 @@ impl Keystroke {
                 "alt" => alt = true,
                 "shift" => shift = true,
                 "fn" => function = true,
-                #[cfg(target_os = "macos")]
-                "cmd" => platform = true,
-                #[cfg(target_os = "linux")]
-                "super" => platform = true,
-                #[cfg(target_os = "windows")]
-                "win" => platform = true,
+                "cmd" | "super" | "win" => platform = true,
                 _ => {
                     if let Some(next) = components.peek() {
                         if next.is_empty() && source.ends_with('-') {

crates/project_panel/src/project_panel.rs 🔗

@@ -1698,7 +1698,7 @@ impl ProjectPanel {
     }
 
     fn dispatch_context(&self, cx: &ViewContext<Self>) -> KeyContext {
-        let mut dispatch_context = KeyContext::default();
+        let mut dispatch_context = KeyContext::new_with_defaults();
         dispatch_context.add("ProjectPanel");
         dispatch_context.add("menu");
 

crates/search/src/buffer_search.rs 🔗

@@ -188,7 +188,7 @@ impl Render for BufferSearchBar {
         let should_show_replace_input = self.replace_enabled && supported_options.replacement;
         let in_replace = self.replacement_editor.focus_handle(cx).is_focused(cx);
 
-        let mut key_context = KeyContext::default();
+        let mut key_context = KeyContext::new_with_defaults();
         key_context.add("BufferSearchBar");
         if in_replace {
             key_context.add("in_replace");

crates/terminal_view/src/terminal_view.rs 🔗

@@ -348,7 +348,7 @@ impl TerminalView {
     }
 
     fn dispatch_context(&self, cx: &AppContext) -> KeyContext {
-        let mut dispatch_context = KeyContext::default();
+        let mut dispatch_context = KeyContext::new_with_defaults();
         dispatch_context.add("Terminal");
 
         let mode = self.terminal.read(cx).last_content.mode;

crates/vim/src/state.rs 🔗

@@ -194,7 +194,7 @@ impl EditorState {
     }
 
     pub fn keymap_context_layer(&self) -> KeyContext {
-        let mut context = KeyContext::default();
+        let mut context = KeyContext::new_with_defaults();
         context.set(
             "vim_mode",
             match self.mode {

crates/workspace/src/dock.rs 🔗

@@ -552,7 +552,7 @@ impl Dock {
     }
 
     fn dispatch_context() -> KeyContext {
-        let mut dispatch_context = KeyContext::default();
+        let mut dispatch_context = KeyContext::new_with_defaults();
         dispatch_context.add("Dock");
 
         dispatch_context

crates/workspace/src/pane.rs 🔗

@@ -1944,7 +1944,7 @@ impl FocusableView for Pane {
 
 impl Render for Pane {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
-        let mut key_context = KeyContext::default();
+        let mut key_context = KeyContext::new_with_defaults();
         key_context.add("Pane");
         if self.active_item().is_none() {
             key_context.add("EmptyPane");

crates/workspace/src/workspace.rs 🔗

@@ -3945,7 +3945,7 @@ struct DraggedDock(DockPosition);
 
 impl Render for Workspace {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
-        let mut context = KeyContext::default();
+        let mut context = KeyContext::new_with_defaults();
         context.add("Workspace");
         let centered_layout = self.centered_layout
             && self.center.panes().len() == 1