Make use_key_equivalents opt-in (#21662)

Conrad Irwin and Peter Tripp created

When revamping international keyboard shortcuts I wanted to make the
default to use key equivalents; in hindsight, this is not what people
expect.

Release Notes:

- (Breaking) In keymap.json `"use_layout_keys": true` is now the
default. If you want to opt-out of this behaviour, set
`"use_key_equivalents": true` to have keys mapped for your keyboard. See
[documentation](https://zed.dev/docs/key-bindings#non-qwerty-keyboards)

---------

Co-authored-by: Peter Tripp <peter@zed.dev>

Change summary

assets/keymaps/default-macos.json  | 51 ++++++++++++++++++++++++++++++++
assets/keymaps/vim.json            | 28 -----------------
crates/settings/src/keymap_file.rs | 10 +++---
docs/src/key-bindings.md           |  9 +----
4 files changed, 58 insertions(+), 40 deletions(-)

Detailed changes

assets/keymaps/default-macos.json πŸ”—

@@ -1,6 +1,7 @@
 [
   // Standard macOS bindings
   {
+    "use_key_equivalents": true,
     "bindings": {
       "up": "menu::SelectPrev",
       "shift-tab": "menu::SelectPrev",
@@ -40,6 +41,7 @@
   },
   {
     "context": "Editor",
+    "use_key_equivalents": true,
     "bindings": {
       "escape": "editor::Cancel",
       "backspace": "editor::Backspace",
@@ -131,6 +133,7 @@
   },
   {
     "context": "Editor && mode == full",
+    "use_key_equivalents": true,
     "bindings": {
       "enter": "editor::Newline",
       "shift-enter": "editor::Newline",
@@ -148,6 +151,7 @@
   },
   {
     "context": "Editor && mode == full && inline_completion",
+    "use_key_equivalents": true,
     "bindings": {
       "alt-]": "editor::NextInlineCompletion",
       "alt-[": "editor::PreviousInlineCompletion",
@@ -156,12 +160,14 @@
   },
   {
     "context": "Editor && !inline_completion",
+    "use_key_equivalents": true,
     "bindings": {
       "alt-\\": "editor::ShowInlineCompletion"
     }
   },
   {
     "context": "Editor && mode == auto_height",
+    "use_key_equivalents": true,
     "bindings": {
       "ctrl-enter": "editor::Newline",
       "shift-enter": "editor::Newline",
@@ -170,12 +176,14 @@
   },
   {
     "context": "Markdown",
+    "use_key_equivalents": true,
     "bindings": {
       "cmd-c": "markdown::Copy"
     }
   },
   {
     "context": "Editor && jupyter && !ContextEditor",
+    "use_key_equivalents": true,
     "bindings": {
       "ctrl-shift-enter": "repl::Run",
       "ctrl-alt-enter": "repl::RunInPlace"
@@ -183,6 +191,7 @@
   },
   {
     "context": "AssistantPanel",
+    "use_key_equivalents": true,
     "bindings": {
       "cmd-k c": "assistant::CopyCode",
       "cmd-g": "search::SelectNextMatch",
@@ -195,6 +204,7 @@
   },
   {
     "context": "ContextEditor > Editor",
+    "use_key_equivalents": true,
     "bindings": {
       "cmd-enter": "assistant::Assist",
       "cmd-shift-enter": "assistant::Edit",
@@ -209,6 +219,7 @@
   },
   {
     "context": "AssistantPanel2",
+    "use_key_equivalents": true,
     "bindings": {
       "cmd-n": "assistant2::NewThread",
       "cmd-shift-h": "assistant2::OpenHistory"
@@ -216,12 +227,14 @@
   },
   {
     "context": "MessageEditor > Editor",
+    "use_key_equivalents": true,
     "bindings": {
       "cmd-enter": "assistant2::Chat"
     }
   },
   {
     "context": "PromptLibrary",
+    "use_key_equivalents": true,
     "bindings": {
       "cmd-n": "prompt_library::NewPrompt",
       "cmd-shift-s": "prompt_library::ToggleDefaultPrompt",
@@ -230,6 +243,7 @@
   },
   {
     "context": "BufferSearchBar",
+    "use_key_equivalents": true,
     "bindings": {
       "escape": "buffer_search::Dismiss",
       "tab": "buffer_search::FocusEditor",
@@ -243,6 +257,7 @@
   },
   {
     "context": "BufferSearchBar && in_replace > Editor",
+    "use_key_equivalents": true,
     "bindings": {
       "enter": "search::ReplaceNext",
       "cmd-enter": "search::ReplaceAll"
@@ -250,6 +265,7 @@
   },
   {
     "context": "BufferSearchBar && !in_replace > Editor",
+    "use_key_equivalents": true,
     "bindings": {
       "up": "search::PreviousHistoryQuery",
       "down": "search::NextHistoryQuery"
@@ -257,6 +273,7 @@
   },
   {
     "context": "ProjectSearchBar",
+    "use_key_equivalents": true,
     "bindings": {
       "escape": "project_search::ToggleFocus",
       "cmd-shift-j": "project_search::ToggleFilters",
@@ -268,6 +285,7 @@
   },
   {
     "context": "ProjectSearchBar > Editor",
+    "use_key_equivalents": true,
     "bindings": {
       "up": "search::PreviousHistoryQuery",
       "down": "search::NextHistoryQuery"
@@ -275,6 +293,7 @@
   },
   {
     "context": "ProjectSearchBar && in_replace > Editor",
+    "use_key_equivalents": true,
     "bindings": {
       "enter": "search::ReplaceNext",
       "cmd-enter": "search::ReplaceAll"
@@ -282,6 +301,7 @@
   },
   {
     "context": "ProjectSearchView",
+    "use_key_equivalents": true,
     "bindings": {
       "escape": "project_search::ToggleFocus",
       "cmd-shift-j": "project_search::ToggleFilters",
@@ -292,6 +312,7 @@
   },
   {
     "context": "Pane",
+    "use_key_equivalents": true,
     "bindings": {
       "cmd-{": "pane::ActivatePrevItem",
       "cmd-}": "pane::ActivateNextItem",
@@ -320,6 +341,7 @@
   // Bindings from VS Code
   {
     "context": "Editor",
+    "use_key_equivalents": true,
     "bindings": {
       "cmd-[": "editor::Outdent",
       "cmd-]": "editor::Indent",
@@ -383,6 +405,7 @@
   },
   {
     "context": "Editor && mode == full",
+    "use_key_equivalents": true,
     "bindings": {
       "cmd-shift-o": "outline::Toggle",
       "ctrl-g": "go_to_line::Toggle"
@@ -390,6 +413,7 @@
   },
   {
     "context": "Pane",
+    "use_key_equivalents": true,
     "bindings": {
       "ctrl-1": ["pane::ActivateItem", 0],
       "ctrl-2": ["pane::ActivateItem", 1],
@@ -409,6 +433,7 @@
   },
   {
     "context": "Workspace",
+    "use_key_equivalents": true,
     "bindings": {
       // Change the default action on `menu::Confirm` by setting the parameter
       // "alt-cmd-o": ["projects::OpenRecent", {"create_new_window": true }],
@@ -464,6 +489,7 @@
   },
   {
     "context": "Workspace && !Terminal",
+    "use_key_equivalents": true,
     "bindings": {
       "cmd-shift-r": "task::Spawn",
       "cmd-alt-r": "task::Rerun",
@@ -474,6 +500,7 @@
   // Bindings from Sublime Text
   {
     "context": "Editor",
+    "use_key_equivalents": true,
     "bindings": {
       "ctrl-j": "editor::JoinLines",
       "ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart",
@@ -493,6 +520,7 @@
   // Bindings from Atom
   {
     "context": "Pane",
+    "use_key_equivalents": true,
     "bindings": {
       "cmd-k up": "pane::SplitUp",
       "cmd-k down": "pane::SplitDown",
@@ -503,12 +531,14 @@
   // Bindings that should be unified with bindings for more general actions
   {
     "context": "Editor && renaming",
+    "use_key_equivalents": true,
     "bindings": {
       "enter": "editor::ConfirmRename"
     }
   },
   {
     "context": "Editor && showing_completions",
+    "use_key_equivalents": true,
     "bindings": {
       "enter": "editor::ConfirmCompletion",
       "tab": "editor::ComposeCompletion"
@@ -516,18 +546,21 @@
   },
   {
     "context": "Editor && inline_completion && !showing_completions",
+    "use_key_equivalents": true,
     "bindings": {
       "tab": "editor::AcceptInlineCompletion"
     }
   },
   {
     "context": "Editor && showing_code_actions",
+    "use_key_equivalents": true,
     "bindings": {
       "enter": "editor::ConfirmCodeAction"
     }
   },
   {
     "context": "Editor && (showing_code_actions || showing_completions)",
+    "use_key_equivalents": true,
     "bindings": {
       "up": "editor::ContextMenuPrev",
       "ctrl-p": "editor::ContextMenuPrev",
@@ -539,6 +572,7 @@
   },
   // Custom bindings
   {
+    "use_key_equivalents": true,
     "bindings": {
       "ctrl-alt-cmd-f": "workspace::FollowNextCollaborator",
       // TODO: Move this to a dock open action
@@ -549,6 +583,7 @@
   },
   {
     "context": "Editor && mode == full",
+    "use_key_equivalents": true,
     "bindings": {
       "alt-enter": "editor::OpenExcerpts",
       "shift-enter": "editor::ExpandExcerpts",
@@ -560,6 +595,7 @@
   },
   {
     "context": "ProposedChangesEditor",
+    "use_key_equivalents": true,
     "bindings": {
       "cmd-shift-y": "editor::ApplyDiffHunk",
       "cmd-shift-a": "editor::ApplyAllDiffHunks"
@@ -567,6 +603,7 @@
   },
   {
     "context": "PromptEditor",
+    "use_key_equivalents": true,
     "bindings": {
       "ctrl-[": "assistant::CyclePreviousInlineAssist",
       "ctrl-]": "assistant::CycleNextInlineAssist"
@@ -574,12 +611,14 @@
   },
   {
     "context": "ProjectSearchBar && !in_replace",
+    "use_key_equivalents": true,
     "bindings": {
       "cmd-enter": "project_search::SearchInNew"
     }
   },
   {
     "context": "OutlinePanel && not_editing",
+    "use_key_equivalents": true,
     "bindings": {
       "escape": "menu::Cancel",
       "left": "outline_panel::CollapseSelectedEntry",
@@ -596,6 +635,7 @@
   },
   {
     "context": "ProjectPanel",
+    "use_key_equivalents": true,
     "bindings": {
       "left": "project_panel::CollapseSelectedEntry",
       "right": "project_panel::ExpandSelectedEntry",
@@ -625,12 +665,14 @@
   },
   {
     "context": "ProjectPanel && not_editing",
+    "use_key_equivalents": true,
     "bindings": {
       "space": "project_panel::Open"
     }
   },
   {
     "context": "CollabPanel && not_editing",
+    "use_key_equivalents": true,
     "bindings": {
       "ctrl-backspace": "collab_panel::Remove",
       "space": "menu::Confirm"
@@ -638,18 +680,21 @@
   },
   {
     "context": "(CollabPanel && editing) > Editor",
+    "use_key_equivalents": true,
     "bindings": {
       "space": "collab_panel::InsertSpace"
     }
   },
   {
     "context": "ChannelModal",
+    "use_key_equivalents": true,
     "bindings": {
       "tab": "channel_modal::ToggleMode"
     }
   },
   {
     "context": "Picker > Editor",
+    "use_key_equivalents": true,
     "bindings": {
       "tab": "picker::ConfirmCompletion",
       "alt-enter": ["picker::ConfirmInput", { "secondary": false }],
@@ -658,18 +703,21 @@
   },
   {
     "context": "ChannelModal > Picker > Editor",
+    "use_key_equivalents": true,
     "bindings": {
       "tab": "channel_modal::ToggleMode"
     }
   },
   {
     "context": "FileFinder",
+    "use_key_equivalents": true,
     "bindings": {
       "cmd": "file_finder::ToggleMenu"
     }
   },
   {
     "context": "FileFinder && !menu_open",
+    "use_key_equivalents": true,
     "bindings": {
       "cmd-shift-p": "file_finder::SelectPrev",
       "cmd-j": "pane::SplitDown",
@@ -680,6 +728,7 @@
   },
   {
     "context": "FileFinder && menu_open",
+    "use_key_equivalents": true,
     "bindings": {
       "j": "pane::SplitDown",
       "k": "pane::SplitUp",
@@ -689,6 +738,7 @@
   },
   {
     "context": "TabSwitcher",
+    "use_key_equivalents": true,
     "bindings": {
       "ctrl-up": "menu::SelectPrev",
       "ctrl-down": "menu::SelectNext",
@@ -698,6 +748,7 @@
   },
   {
     "context": "Terminal",
+    "use_key_equivalents": true,
     "bindings": {
       "ctrl-cmd-space": "terminal::ShowCharacterPalette",
       "cmd-c": "terminal::Copy",

assets/keymaps/vim.json πŸ”—

@@ -1,7 +1,6 @@
 [
   {
     "context": "VimControl && !menu",
-    "use_layout_keys": true,
     "bindings": {
       "i": ["vim::PushOperator", { "Object": { "around": false } }],
       "a": ["vim::PushOperator", { "Object": { "around": true } }],
@@ -188,7 +187,6 @@
   },
   {
     "context": "vim_mode == normal",
-    "use_layout_keys": true,
     "bindings": {
       "escape": "editor::Cancel",
       "ctrl-[": "editor::Cancel",
@@ -243,7 +241,6 @@
   },
   {
     "context": "VimControl && VimCount",
-    "use_layout_keys": true,
     "bindings": {
       "0": ["vim::Number", 0],
       ":": "vim::CountCommand"
@@ -251,7 +248,6 @@
   },
   {
     "context": "vim_mode == visual",
-    "use_layout_keys": true,
     "bindings": {
       ":": "vim::VisualCommand",
       "u": "vim::ConvertToLowerCase",
@@ -301,7 +297,6 @@
   },
   {
     "context": "vim_mode == insert",
-    "use_layout_keys": true,
     "bindings": {
       "escape": "vim::NormalBefore",
       "ctrl-c": "vim::NormalBefore",
@@ -344,7 +339,6 @@
 
   {
     "context": "vim_mode == insert && !(showing_code_actions || showing_completions)",
-    "use_layout_keys": true,
     "bindings": {
       "ctrl-p": "editor::ShowCompletions",
       "ctrl-n": "editor::ShowCompletions"
@@ -352,7 +346,6 @@
   },
   {
     "context": "vim_mode == replace",
-    "use_layout_keys": true,
     "bindings": {
       "escape": "vim::NormalBefore",
       "ctrl-c": "vim::NormalBefore",
@@ -370,7 +363,6 @@
   },
   {
     "context": "vim_mode == waiting",
-    "use_layout_keys": true,
     "bindings": {
       "tab": "vim::Tab",
       "enter": "vim::Enter",
@@ -384,7 +376,6 @@
   },
   {
     "context": "vim_mode == operator",
-    "use_layout_keys": true,
     "bindings": {
       "escape": "vim::ClearOperators",
       "ctrl-c": "vim::ClearOperators",
@@ -394,7 +385,6 @@
   },
   {
     "context": "vim_operator == a || vim_operator == i || vim_operator == cs",
-    "use_layout_keys": true,
     "bindings": {
       "w": "vim::Word",
       "shift-w": ["vim::Word", { "ignorePunctuation": true }],
@@ -425,7 +415,6 @@
   },
   {
     "context": "vim_operator == c",
-    "use_layout_keys": true,
     "bindings": {
       "c": "vim::CurrentLine",
       "d": "editor::Rename", // zed specific
@@ -434,7 +423,6 @@
   },
   {
     "context": "vim_operator == d",
-    "use_layout_keys": true,
     "bindings": {
       "d": "vim::CurrentLine",
       "s": ["vim::PushOperator", "DeleteSurrounds"],
@@ -444,7 +432,6 @@
   },
   {
     "context": "vim_operator == gu",
-    "use_layout_keys": true,
     "bindings": {
       "g u": "vim::CurrentLine",
       "u": "vim::CurrentLine"
@@ -452,7 +439,6 @@
   },
   {
     "context": "vim_operator == gU",
-    "use_layout_keys": true,
     "bindings": {
       "g shift-u": "vim::CurrentLine",
       "shift-u": "vim::CurrentLine"
@@ -460,7 +446,6 @@
   },
   {
     "context": "vim_operator == g~",
-    "use_layout_keys": true,
     "bindings": {
       "g ~": "vim::CurrentLine",
       "~": "vim::CurrentLine"
@@ -468,7 +453,6 @@
   },
   {
     "context": "vim_operator == gq",
-    "use_layout_keys": true,
     "bindings": {
       "g q": "vim::CurrentLine",
       "q": "vim::CurrentLine",
@@ -478,7 +462,6 @@
   },
   {
     "context": "vim_operator == y",
-    "use_layout_keys": true,
     "bindings": {
       "y": "vim::CurrentLine",
       "s": ["vim::PushOperator", { "AddSurrounds": {} }]
@@ -486,42 +469,36 @@
   },
   {
     "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 == eq",
-    "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"]],
@@ -565,7 +542,6 @@
   },
   {
     "context": "BufferSearchBar && !in_replace",
-    "use_layout_keys": true,
     "bindings": {
       "enter": "vim::SearchSubmit",
       "escape": "buffer_search::Dismiss"
@@ -573,7 +549,6 @@
   },
   {
     "context": "ProjectPanel || CollabPanel || OutlinePanel || ChatPanel || VimControl || EmptyPane || SharedScreen || MarkdownPreview || KeyContextView",
-    "use_layout_keys": true,
     "bindings": {
       // window related commands (ctrl-w X)
       "ctrl-w": null,
@@ -630,7 +605,6 @@
   },
   {
     "context": "EmptyPane || SharedScreen || MarkdownPreview || KeyContextView || Welcome",
-    "use_layout_keys": true,
     "bindings": {
       ":": "command_palette::Toggle",
       "g /": "pane::DeploySearch"
@@ -639,7 +613,6 @@
   {
     // netrw compatibility
     "context": "ProjectPanel && not_editing",
-    "use_layout_keys": true,
     "bindings": {
       ":": "command_palette::Toggle",
       "%": "project_panel::NewFile",
@@ -673,7 +646,6 @@
   },
   {
     "context": "OutlinePanel && not_editing",
-    "use_layout_keys": true,
     "bindings": {
       "j": "menu::SelectNext",
       "k": "menu::SelectPrev",

crates/settings/src/keymap_file.rs πŸ”—

@@ -20,7 +20,7 @@ pub struct KeymapBlock {
     #[serde(default)]
     context: Option<String>,
     #[serde(default)]
-    use_layout_keys: Option<bool>,
+    use_key_equivalents: Option<bool>,
     bindings: BTreeMap<String, KeymapAction>,
 }
 
@@ -80,7 +80,7 @@ impl KeymapFile {
 
         for KeymapBlock {
             context,
-            use_layout_keys,
+            use_key_equivalents,
             bindings,
         } in self.0
         {
@@ -124,10 +124,10 @@ impl KeymapFile {
                             &keystroke,
                             action,
                             context.as_deref(),
-                            if use_layout_keys.unwrap_or_default() {
-                                None
-                            } else {
+                            if use_key_equivalents.unwrap_or_default() {
                                 key_equivalents.as_ref()
+                            } else {
+                                None
                             },
                         )
                     })

docs/src/key-bindings.md πŸ”—

@@ -146,20 +146,15 @@ Finally keyboards that support extended Latin alphabets (usually ISO keyboards)
 
 For example on a German QWERTZ keyboard, the `cmd->` shortcut is moved to `cmd-:` because `cmd->` is the system window switcher and this is where that shortcut is typed on a QWERTY keyboard. `cmd-+` stays the same because + is still typable without option, and as a result, `cmd-[` and `cmd-]` become `cmd-ΓΆ` and `cmd-Γ€`, moving out of the way of the `+` key.
 
-If you are defining shortcuts in your personal keymap, you can opt-out of the key equivalent mapping by setting `use_layout_keys` to `true` in your keymap:
+If you are defining shortcuts in your personal keymap, you can opt into the key equivalent mapping by setting `use_key_equivalents` to `true` in your keymap:
 
 ```json
 [
   {
+    "use_key_equivalents": true,
     "bindings": {
       "ctrl->": "editor::Indent" // parsed as ctrl-: when a German QWERTZ keyboard is active
     }
-  },
-  {
-    "use_layout_keys": true,
-    "bindings": {
-      "ctrl->": "editor::Indent" // remains ctrl-> when a German QWERTZ keyboard is active
-    }
   }
 ]
 ```