windows: Fix keystroke & keymap (#36572)

张小白 and Kate created

Closes #36300

This PR follows Windows conventions by introducing
`KeybindingKeystroke`, so shortcuts now show up as `ctrl-shift-4`
instead of `ctrl-$`.

It also fixes issues with keyboard layouts: when `use_key_equivalents`
is set to true, keys are remapped based on their virtual key codes. For
example, `ctrl-\` on a standard English layout will be mapped to
`ctrl-ё` on a Russian layout.


Release Notes:

- N/A

---------

Co-authored-by: Kate <kate@zed.dev>

Change summary

assets/keymaps/default-windows.json                     | 1260 +++++++++
crates/docs_preprocessor/src/main.rs                    |    5 
crates/editor/src/editor.rs                             |   26 
crates/editor/src/element.rs                            |   10 
crates/gpui/src/app.rs                                  |   17 
crates/gpui/src/keymap.rs                               |    7 
crates/gpui/src/keymap/binding.rs                       |   49 
crates/gpui/src/platform.rs                             |    6 
crates/gpui/src/platform/keyboard.rs                    |   34 
crates/gpui/src/platform/keystroke.rs                   |  252 +
crates/gpui/src/platform/linux/platform.rs              |    8 
crates/gpui/src/platform/mac/keyboard.rs                | 1453 ++++++++++
crates/gpui/src/platform/mac/platform.rs                |   28 
crates/gpui/src/platform/test/platform.rs               |   11 
crates/gpui/src/platform/windows/keyboard.rs            |  244 +
crates/gpui/src/platform/windows/platform.rs            |    4 
crates/language_tools/src/key_context_view.rs           |    4 
crates/settings/src/key_equivalents.rs                  | 1424 ----------
crates/settings/src/keymap_file.rs                      |   66 
crates/settings/src/settings.rs                         |    7 
crates/settings_ui/src/keybindings.rs                   |  101 
crates/settings_ui/src/ui_components/keystroke_input.rs |   74 
crates/ui/src/components/keybinding.rs                  |  131 
crates/zed/src/zed.rs                                   |   10 
crates/zed/src/zed/quick_action_bar/preview.rs          |    5 
25 files changed, 3,515 insertions(+), 1,721 deletions(-)

Detailed changes

assets/keymaps/default-windows.json 🔗

@@ -0,0 +1,1260 @@
+[
+  // Standard Windows bindings
+  {
+    "use_key_equivalents": true,
+    "bindings": {
+      "home": "menu::SelectFirst",
+      "shift-pageup": "menu::SelectFirst",
+      "pageup": "menu::SelectFirst",
+      "end": "menu::SelectLast",
+      "shift-pagedown": "menu::SelectLast",
+      "pagedown": "menu::SelectLast",
+      "ctrl-n": "menu::SelectNext",
+      "tab": "menu::SelectNext",
+      "down": "menu::SelectNext",
+      "ctrl-p": "menu::SelectPrevious",
+      "shift-tab": "menu::SelectPrevious",
+      "up": "menu::SelectPrevious",
+      "enter": "menu::Confirm",
+      "ctrl-enter": "menu::SecondaryConfirm",
+      "ctrl-escape": "menu::Cancel",
+      "ctrl-c": "menu::Cancel",
+      "escape": "menu::Cancel",
+      "shift-alt-enter": "menu::Restart",
+      "alt-enter": ["picker::ConfirmInput", { "secondary": false }],
+      "ctrl-alt-enter": ["picker::ConfirmInput", { "secondary": true }],
+      "ctrl-shift-w": "workspace::CloseWindow",
+      "shift-escape": "workspace::ToggleZoom",
+      "open": "workspace::Open",
+      "ctrl-o": "workspace::Open",
+      "ctrl-=": ["zed::IncreaseBufferFontSize", { "persist": false }],
+      "ctrl-shift-=": ["zed::IncreaseBufferFontSize", { "persist": false }],
+      "ctrl--": ["zed::DecreaseBufferFontSize", { "persist": false }],
+      "ctrl-0": ["zed::ResetBufferFontSize", { "persist": false }],
+      "ctrl-,": "zed::OpenSettings",
+      "ctrl-q": "zed::Quit",
+      "f4": "debugger::Start",
+      "shift-f5": "debugger::Stop",
+      "ctrl-shift-f5": "debugger::RerunSession",
+      "f6": "debugger::Pause",
+      "f7": "debugger::StepOver",
+      "ctrl-f11": "debugger::StepInto",
+      "shift-f11": "debugger::StepOut",
+      "f11": "zed::ToggleFullScreen",
+      "ctrl-shift-i": "edit_prediction::ToggleMenu",
+      "shift-alt-l": "lsp_tool::ToggleMenu"
+    }
+  },
+  {
+    "context": "Picker || menu",
+    "use_key_equivalents": true,
+    "bindings": {
+      "up": "menu::SelectPrevious",
+      "down": "menu::SelectNext"
+    }
+  },
+  {
+    "context": "Editor",
+    "use_key_equivalents": true,
+    "bindings": {
+      "escape": "editor::Cancel",
+      "shift-backspace": "editor::Backspace",
+      "backspace": "editor::Backspace",
+      "delete": "editor::Delete",
+      "tab": "editor::Tab",
+      "shift-tab": "editor::Backtab",
+      "ctrl-k": "editor::CutToEndOfLine",
+      "ctrl-k ctrl-q": "editor::Rewrap",
+      "ctrl-k q": "editor::Rewrap",
+      "ctrl-backspace": "editor::DeleteToPreviousWordStart",
+      "ctrl-delete": "editor::DeleteToNextWordEnd",
+      "cut": "editor::Cut",
+      "shift-delete": "editor::Cut",
+      "ctrl-x": "editor::Cut",
+      "copy": "editor::Copy",
+      "ctrl-insert": "editor::Copy",
+      "ctrl-c": "editor::Copy",
+      "paste": "editor::Paste",
+      "shift-insert": "editor::Paste",
+      "ctrl-v": "editor::Paste",
+      "undo": "editor::Undo",
+      "ctrl-z": "editor::Undo",
+      "redo": "editor::Redo",
+      "ctrl-y": "editor::Redo",
+      "ctrl-shift-z": "editor::Redo",
+      "up": "editor::MoveUp",
+      "ctrl-up": "editor::LineUp",
+      "ctrl-down": "editor::LineDown",
+      "pageup": "editor::MovePageUp",
+      "alt-pageup": "editor::PageUp",
+      "shift-pageup": "editor::SelectPageUp",
+      "home": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }],
+      "down": "editor::MoveDown",
+      "pagedown": "editor::MovePageDown",
+      "alt-pagedown": "editor::PageDown",
+      "shift-pagedown": "editor::SelectPageDown",
+      "end": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": true }],
+      "left": "editor::MoveLeft",
+      "right": "editor::MoveRight",
+      "ctrl-left": "editor::MoveToPreviousWordStart",
+      "ctrl-right": "editor::MoveToNextWordEnd",
+      "ctrl-home": "editor::MoveToBeginning",
+      "ctrl-end": "editor::MoveToEnd",
+      "shift-up": "editor::SelectUp",
+      "shift-down": "editor::SelectDown",
+      "shift-left": "editor::SelectLeft",
+      "shift-right": "editor::SelectRight",
+      "ctrl-shift-left": "editor::SelectToPreviousWordStart",
+      "ctrl-shift-right": "editor::SelectToNextWordEnd",
+      "ctrl-shift-home": "editor::SelectToBeginning",
+      "ctrl-shift-end": "editor::SelectToEnd",
+      "ctrl-a": "editor::SelectAll",
+      "ctrl-l": "editor::SelectLine",
+      "shift-alt-f": "editor::Format",
+      "shift-alt-o": "editor::OrganizeImports",
+      "shift-home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }],
+      "shift-end": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
+      "ctrl-alt-space": "editor::ShowCharacterPalette",
+      "ctrl-;": "editor::ToggleLineNumbers",
+      "ctrl-'": "editor::ToggleSelectedDiffHunks",
+      "ctrl-\"": "editor::ExpandAllDiffHunks",
+      "ctrl-i": "editor::ShowSignatureHelp",
+      "alt-g b": "git::Blame",
+      "alt-g m": "git::OpenModifiedFiles",
+      "menu": "editor::OpenContextMenu",
+      "shift-f10": "editor::OpenContextMenu",
+      "ctrl-shift-e": "editor::ToggleEditPrediction",
+      "f9": "editor::ToggleBreakpoint",
+      "shift-f9": "editor::EditLogBreakpoint"
+    }
+  },
+  {
+    "context": "Editor && mode == full",
+    "use_key_equivalents": true,
+    "bindings": {
+      "shift-enter": "editor::Newline",
+      "enter": "editor::Newline",
+      "ctrl-enter": "editor::NewlineAbove",
+      "ctrl-shift-enter": "editor::NewlineBelow",
+      "ctrl-k ctrl-z": "editor::ToggleSoftWrap",
+      "ctrl-k z": "editor::ToggleSoftWrap",
+      "find": "buffer_search::Deploy",
+      "ctrl-f": "buffer_search::Deploy",
+      "ctrl-h": "buffer_search::DeployReplace",
+      "ctrl-shift-.": "assistant::QuoteSelection",
+      "ctrl-shift-,": "assistant::InsertIntoEditor",
+      "shift-alt-e": "editor::SelectEnclosingSymbol",
+      "ctrl-shift-backspace": "editor::GoToPreviousChange",
+      "ctrl-shift-alt-backspace": "editor::GoToNextChange",
+      "alt-enter": "editor::OpenSelectionsInMultibuffer"
+    }
+  },
+  {
+    "context": "Editor && mode == full && edit_prediction",
+    "use_key_equivalents": true,
+    "bindings": {
+      "alt-]": "editor::NextEditPrediction",
+      "alt-[": "editor::PreviousEditPrediction"
+    }
+  },
+  {
+    "context": "Editor && !edit_prediction",
+    "use_key_equivalents": true,
+    "bindings": {
+      "alt-\\": "editor::ShowEditPrediction"
+    }
+  },
+  {
+    "context": "Editor && mode == auto_height",
+    "use_key_equivalents": true,
+    "bindings": {
+      "ctrl-enter": "editor::Newline",
+      "shift-enter": "editor::Newline",
+      "ctrl-shift-enter": "editor::NewlineBelow"
+    }
+  },
+  {
+    "context": "Markdown",
+    "use_key_equivalents": true,
+    "bindings": {
+      "copy": "markdown::Copy",
+      "ctrl-c": "markdown::Copy"
+    }
+  },
+  {
+    "context": "Editor && jupyter && !ContextEditor",
+    "use_key_equivalents": true,
+    "bindings": {
+      "ctrl-shift-enter": "repl::Run",
+      "ctrl-alt-enter": "repl::RunInPlace"
+    }
+  },
+  {
+    "context": "Editor && !agent_diff",
+    "use_key_equivalents": true,
+    "bindings": {
+      "ctrl-k ctrl-r": "git::Restore",
+      "alt-y": "git::StageAndNext",
+      "shift-alt-y": "git::UnstageAndNext"
+    }
+  },
+  {
+    "context": "Editor && editor_agent_diff",
+    "use_key_equivalents": true,
+    "bindings": {
+      "ctrl-y": "agent::Keep",
+      "ctrl-n": "agent::Reject",
+      "ctrl-shift-y": "agent::KeepAll",
+      "ctrl-shift-n": "agent::RejectAll",
+      "ctrl-shift-r": "agent::OpenAgentDiff"
+    }
+  },
+  {
+    "context": "AgentDiff",
+    "use_key_equivalents": true,
+    "bindings": {
+      "ctrl-y": "agent::Keep",
+      "ctrl-n": "agent::Reject",
+      "ctrl-shift-y": "agent::KeepAll",
+      "ctrl-shift-n": "agent::RejectAll"
+    }
+  },
+  {
+    "context": "ContextEditor > Editor",
+    "use_key_equivalents": true,
+    "bindings": {
+      "ctrl-enter": "assistant::Assist",
+      "ctrl-s": "workspace::Save",
+      "save": "workspace::Save",
+      "ctrl-shift-,": "assistant::InsertIntoEditor",
+      "shift-enter": "assistant::Split",
+      "ctrl-r": "assistant::CycleMessageRole",
+      "enter": "assistant::ConfirmCommand",
+      "alt-enter": "editor::Newline",
+      "ctrl-k c": "assistant::CopyCode",
+      "ctrl-g": "search::SelectNextMatch",
+      "ctrl-shift-g": "search::SelectPreviousMatch",
+      "ctrl-k l": "agent::OpenRulesLibrary"
+    }
+  },
+  {
+    "context": "AgentPanel",
+    "use_key_equivalents": true,
+    "bindings": {
+      "ctrl-n": "agent::NewThread",
+      "shift-alt-n": "agent::NewTextThread",
+      "ctrl-shift-h": "agent::OpenHistory",
+      "shift-alt-c": "agent::OpenSettings",
+      "shift-alt-p": "agent::OpenRulesLibrary",
+      "ctrl-i": "agent::ToggleProfileSelector",
+      "shift-alt-/": "agent::ToggleModelSelector",
+      "ctrl-shift-a": "agent::ToggleContextPicker",
+      "ctrl-shift-j": "agent::ToggleNavigationMenu",
+      "ctrl-shift-i": "agent::ToggleOptionsMenu",
+      // "ctrl-shift-alt-n": "agent::ToggleNewThreadMenu",
+      "shift-alt-escape": "agent::ExpandMessageEditor",
+      "ctrl-shift-.": "assistant::QuoteSelection",
+      "shift-alt-e": "agent::RemoveAllContext",
+      "ctrl-shift-e": "project_panel::ToggleFocus",
+      "ctrl-shift-enter": "agent::ContinueThread",
+      "super-ctrl-b": "agent::ToggleBurnMode",
+      "alt-enter": "agent::ContinueWithBurnMode"
+    }
+  },
+  {
+    "context": "AgentPanel > NavigationMenu",
+    "use_key_equivalents": true,
+    "bindings": {
+      "shift-backspace": "agent::DeleteRecentlyOpenThread"
+    }
+  },
+  {
+    "context": "AgentPanel > Markdown",
+    "use_key_equivalents": true,
+    "bindings": {
+      "copy": "markdown::CopyAsMarkdown",
+      "ctrl-c": "markdown::CopyAsMarkdown"
+    }
+  },
+  {
+    "context": "AgentPanel && prompt_editor",
+    "use_key_equivalents": true,
+    "bindings": {
+      "ctrl-n": "agent::NewTextThread",
+      "ctrl-alt-t": "agent::NewThread"
+    }
+  },
+  {
+    "context": "AgentPanel && external_agent_thread",
+    "use_key_equivalents": true,
+    "bindings": {
+      "ctrl-n": "agent::NewExternalAgentThread",
+      "ctrl-alt-t": "agent::NewThread"
+    }
+  },
+  {
+    "context": "MessageEditor && !Picker > Editor && !use_modifier_to_send",
+    "use_key_equivalents": true,
+    "bindings": {
+      "enter": "agent::Chat",
+      "ctrl-enter": "agent::ChatWithFollow",
+      "ctrl-i": "agent::ToggleProfileSelector",
+      "ctrl-shift-r": "agent::OpenAgentDiff",
+      "ctrl-shift-y": "agent::KeepAll",
+      "ctrl-shift-n": "agent::RejectAll"
+    }
+  },
+  {
+    "context": "MessageEditor && !Picker > Editor && use_modifier_to_send",
+    "use_key_equivalents": true,
+    "bindings": {
+      "ctrl-enter": "agent::Chat",
+      "enter": "editor::Newline",
+      "ctrl-i": "agent::ToggleProfileSelector",
+      "ctrl-shift-r": "agent::OpenAgentDiff",
+      "ctrl-shift-y": "agent::KeepAll",
+      "ctrl-shift-n": "agent::RejectAll"
+    }
+  },
+  {
+    "context": "EditMessageEditor > Editor",
+    "use_key_equivalents": true,
+    "bindings": {
+      "escape": "menu::Cancel",
+      "enter": "menu::Confirm",
+      "alt-enter": "editor::Newline"
+    }
+  },
+  {
+    "context": "AgentFeedbackMessageEditor > Editor",
+    "use_key_equivalents": true,
+    "bindings": {
+      "escape": "menu::Cancel",
+      "enter": "menu::Confirm",
+      "alt-enter": "editor::Newline"
+    }
+  },
+  {
+    "context": "ContextStrip",
+    "use_key_equivalents": true,
+    "bindings": {
+      "up": "agent::FocusUp",
+      "right": "agent::FocusRight",
+      "left": "agent::FocusLeft",
+      "down": "agent::FocusDown",
+      "backspace": "agent::RemoveFocusedContext",
+      "enter": "agent::AcceptSuggestedContext"
+    }
+  },
+  {
+    "context": "AcpThread > Editor",
+    "use_key_equivalents": true,
+    "bindings": {
+      "enter": "agent::Chat",
+      "ctrl-shift-r": "agent::OpenAgentDiff",
+      "ctrl-shift-y": "agent::KeepAll",
+      "ctrl-shift-n": "agent::RejectAll"
+    }
+  },
+  {
+    "context": "ThreadHistory",
+    "use_key_equivalents": true,
+    "bindings": {
+      "backspace": "agent::RemoveSelectedThread"
+    }
+  },
+  {
+    "context": "PromptLibrary",
+    "use_key_equivalents": true,
+    "bindings": {
+      "new": "rules_library::NewRule",
+      "ctrl-n": "rules_library::NewRule",
+      "ctrl-shift-s": "rules_library::ToggleDefaultRule"
+    }
+  },
+  {
+    "context": "BufferSearchBar",
+    "use_key_equivalents": true,
+    "bindings": {
+      "escape": "buffer_search::Dismiss",
+      "tab": "buffer_search::FocusEditor",
+      "enter": "search::SelectNextMatch",
+      "shift-enter": "search::SelectPreviousMatch",
+      "alt-enter": "search::SelectAllMatches",
+      "find": "search::FocusSearch",
+      "ctrl-f": "search::FocusSearch",
+      "ctrl-h": "search::ToggleReplace",
+      "ctrl-l": "search::ToggleSelection"
+    }
+  },
+  {
+    "context": "BufferSearchBar && in_replace > Editor",
+    "use_key_equivalents": true,
+    "bindings": {
+      "enter": "search::ReplaceNext",
+      "ctrl-enter": "search::ReplaceAll"
+    }
+  },
+  {
+    "context": "BufferSearchBar && !in_replace > Editor",
+    "use_key_equivalents": true,
+    "bindings": {
+      "up": "search::PreviousHistoryQuery",
+      "down": "search::NextHistoryQuery"
+    }
+  },
+  {
+    "context": "ProjectSearchBar",
+    "use_key_equivalents": true,
+    "bindings": {
+      "escape": "project_search::ToggleFocus",
+      "shift-find": "search::FocusSearch",
+      "ctrl-shift-f": "search::FocusSearch",
+      "ctrl-shift-h": "search::ToggleReplace",
+      "alt-r": "search::ToggleRegex" // vscode
+    }
+  },
+  {
+    "context": "ProjectSearchBar > Editor",
+    "use_key_equivalents": true,
+    "bindings": {
+      "up": "search::PreviousHistoryQuery",
+      "down": "search::NextHistoryQuery"
+    }
+  },
+  {
+    "context": "ProjectSearchBar && in_replace > Editor",
+    "use_key_equivalents": true,
+    "bindings": {
+      "enter": "search::ReplaceNext",
+      "ctrl-alt-enter": "search::ReplaceAll"
+    }
+  },
+  {
+    "context": "ProjectSearchView",
+    "use_key_equivalents": true,
+    "bindings": {
+      "escape": "project_search::ToggleFocus",
+      "ctrl-shift-h": "search::ToggleReplace",
+      "alt-r": "search::ToggleRegex" // vscode
+    }
+  },
+  {
+    "context": "Pane",
+    "use_key_equivalents": true,
+    "bindings": {
+      "alt-1": ["pane::ActivateItem", 0],
+      "alt-2": ["pane::ActivateItem", 1],
+      "alt-3": ["pane::ActivateItem", 2],
+      "alt-4": ["pane::ActivateItem", 3],
+      "alt-5": ["pane::ActivateItem", 4],
+      "alt-6": ["pane::ActivateItem", 5],
+      "alt-7": ["pane::ActivateItem", 6],
+      "alt-8": ["pane::ActivateItem", 7],
+      "alt-9": ["pane::ActivateItem", 8],
+      "alt-0": "pane::ActivateLastItem",
+      "ctrl-pageup": "pane::ActivatePreviousItem",
+      "ctrl-pagedown": "pane::ActivateNextItem",
+      "ctrl-shift-pageup": "pane::SwapItemLeft",
+      "ctrl-shift-pagedown": "pane::SwapItemRight",
+      "ctrl-f4": ["pane::CloseActiveItem", { "close_pinned": false }],
+      "ctrl-w": ["pane::CloseActiveItem", { "close_pinned": false }],
+      "ctrl-shift-alt-t": ["pane::CloseOtherItems", { "close_pinned": false }],
+      "ctrl-shift-alt-w": "workspace::CloseInactiveTabsAndPanes",
+      "ctrl-k e": ["pane::CloseItemsToTheLeft", { "close_pinned": false }],
+      "ctrl-k t": ["pane::CloseItemsToTheRight", { "close_pinned": false }],
+      "ctrl-k u": ["pane::CloseCleanItems", { "close_pinned": false }],
+      "ctrl-k w": ["pane::CloseAllItems", { "close_pinned": false }],
+      "ctrl-k ctrl-w": "workspace::CloseAllItemsAndPanes",
+      "back": "pane::GoBack",
+      "alt--": "pane::GoBack",
+      "alt-=": "pane::GoForward",
+      "forward": "pane::GoForward",
+      "f3": "search::SelectNextMatch",
+      "shift-f3": "search::SelectPreviousMatch",
+      "shift-find": "project_search::ToggleFocus",
+      "ctrl-shift-f": "project_search::ToggleFocus",
+      "shift-alt-h": "search::ToggleReplace",
+      "alt-l": "search::ToggleSelection",
+      "alt-enter": "search::SelectAllMatches",
+      "alt-c": "search::ToggleCaseSensitive",
+      "alt-w": "search::ToggleWholeWord",
+      "alt-find": "project_search::ToggleFilters",
+      "alt-f": "project_search::ToggleFilters",
+      "alt-r": "search::ToggleRegex",
+      // "ctrl-shift-alt-x": "search::ToggleRegex",
+      "ctrl-k shift-enter": "pane::TogglePinTab"
+    }
+  },
+  // Bindings from VS Code
+  {
+    "context": "Editor",
+    "use_key_equivalents": true,
+    "bindings": {
+      "ctrl-[": "editor::Outdent",
+      "ctrl-]": "editor::Indent",
+      "ctrl-shift-alt-up": "editor::AddSelectionAbove", // Insert Cursor Above
+      "ctrl-shift-alt-down": "editor::AddSelectionBelow", // Insert Cursor Below
+      "ctrl-shift-k": "editor::DeleteLine",
+      "alt-up": "editor::MoveLineUp",
+      "alt-down": "editor::MoveLineDown",
+      "shift-alt-up": "editor::DuplicateLineUp",
+      "shift-alt-down": "editor::DuplicateLineDown",
+      "shift-alt-right": "editor::SelectLargerSyntaxNode", // Expand Selection
+      "shift-alt-left": "editor::SelectSmallerSyntaxNode", // Shrink Selection
+      "ctrl-shift-l": "editor::SelectAllMatches", // Select all occurrences of current selection
+      "ctrl-f2": "editor::SelectAllMatches", // Select all occurrences of current word
+      "ctrl-d": ["editor::SelectNext", { "replace_newest": false }], // editor.action.addSelectionToNextFindMatch  / find_under_expand
+      "ctrl-shift-down": ["editor::SelectNext", { "replace_newest": false }], // editor.action.addSelectionToNextFindMatch
+      "ctrl-shift-up": ["editor::SelectPrevious", { "replace_newest": false }], // editor.action.addSelectionToPreviousFindMatch
+      "ctrl-k ctrl-d": ["editor::SelectNext", { "replace_newest": true }], // editor.action.moveSelectionToNextFindMatch  / find_under_expand_skip
+      "ctrl-k ctrl-shift-d": ["editor::SelectPrevious", { "replace_newest": true }], // editor.action.moveSelectionToPreviousFindMatch
+      "ctrl-k ctrl-i": "editor::Hover",
+      "ctrl-k ctrl-b": "editor::BlameHover",
+      "ctrl-/": ["editor::ToggleComments", { "advance_downwards": false }],
+      "f8": ["editor::GoToDiagnostic", { "severity": { "min": "hint", "max": "error" } }],
+      "shift-f8": ["editor::GoToPreviousDiagnostic", { "severity": { "min": "hint", "max": "error" } }],
+      "f2": "editor::Rename",
+      "f12": "editor::GoToDefinition",
+      "alt-f12": "editor::GoToDefinitionSplit",
+      "ctrl-shift-f10": "editor::GoToDefinitionSplit",
+      "ctrl-f12": "editor::GoToImplementation",
+      "shift-f12": "editor::GoToTypeDefinition",
+      "ctrl-alt-f12": "editor::GoToTypeDefinitionSplit",
+      "shift-alt-f12": "editor::FindAllReferences",
+      "ctrl-m": "editor::MoveToEnclosingBracket", // from jetbrains
+      "ctrl-shift-\\": "editor::MoveToEnclosingBracket",
+      "ctrl-shift-[": "editor::Fold",
+      "ctrl-shift-]": "editor::UnfoldLines",
+      "ctrl-k ctrl-l": "editor::ToggleFold",
+      "ctrl-k ctrl-[": "editor::FoldRecursive",
+      "ctrl-k ctrl-]": "editor::UnfoldRecursive",
+      "ctrl-k ctrl-1": ["editor::FoldAtLevel", 1],
+      "ctrl-k ctrl-2": ["editor::FoldAtLevel", 2],
+      "ctrl-k ctrl-3": ["editor::FoldAtLevel", 3],
+      "ctrl-k ctrl-4": ["editor::FoldAtLevel", 4],
+      "ctrl-k ctrl-5": ["editor::FoldAtLevel", 5],
+      "ctrl-k ctrl-6": ["editor::FoldAtLevel", 6],
+      "ctrl-k ctrl-7": ["editor::FoldAtLevel", 7],
+      "ctrl-k ctrl-8": ["editor::FoldAtLevel", 8],
+      "ctrl-k ctrl-9": ["editor::FoldAtLevel", 9],
+      "ctrl-k ctrl-0": "editor::FoldAll",
+      "ctrl-k ctrl-j": "editor::UnfoldAll",
+      "ctrl-space": "editor::ShowCompletions",
+      "ctrl-shift-space": "editor::ShowWordCompletions",
+      "ctrl-.": "editor::ToggleCodeActions",
+      "ctrl-k r": "editor::RevealInFileManager",
+      "ctrl-k p": "editor::CopyPath",
+      "ctrl-\\": "pane::SplitRight",
+      "ctrl-shift-alt-c": "editor::DisplayCursorNames",
+      "alt-.": "editor::GoToHunk",
+      "alt-,": "editor::GoToPreviousHunk"
+    }
+  },
+  {
+    "context": "Editor && extension == md",
+    "use_key_equivalents": true,
+    "bindings": {
+      "ctrl-k v": "markdown::OpenPreviewToTheSide",
+      "ctrl-shift-v": "markdown::OpenPreview"
+    }
+  },
+  {
+    "context": "Editor && extension == svg",
+    "use_key_equivalents": true,
+    "bindings": {
+      "ctrl-k v": "svg::OpenPreviewToTheSide",
+      "ctrl-shift-v": "svg::OpenPreview"
+    }
+  },
+  {
+    "context": "Editor && mode == full",
+    "use_key_equivalents": true,
+    "bindings": {
+      "ctrl-shift-o": "outline::Toggle",
+      "ctrl-g": "go_to_line::Toggle"
+    }
+  },
+  {
+    "context": "Workspace",
+    "use_key_equivalents": true,
+    "bindings": {
+      "alt-open": ["projects::OpenRecent", { "create_new_window": false }],
+      // Change the default action on `menu::Confirm` by setting the parameter
+      // "ctrl-alt-o": ["projects::OpenRecent", { "create_new_window": true }],
+      "ctrl-r": ["projects::OpenRecent", { "create_new_window": false }],
+      "shift-alt-open": ["projects::OpenRemote", { "from_existing_connection": false, "create_new_window": false }],
+      // Change to open path modal for existing remote connection by setting the parameter
+      // "ctrl-shift-alt-o": "["projects::OpenRemote", { "from_existing_connection": true }]",
+      "ctrl-shift-alt-o": ["projects::OpenRemote", { "from_existing_connection": false, "create_new_window": false }],
+      "shift-alt-b": "branches::OpenRecent",
+      "shift-alt-enter": "toast::RunAction",
+      "ctrl-shift-`": "workspace::NewTerminal",
+      "save": "workspace::Save",
+      "ctrl-s": "workspace::Save",
+      "ctrl-k ctrl-shift-s": "workspace::SaveWithoutFormat",
+      "shift-save": "workspace::SaveAs",
+      "ctrl-shift-s": "workspace::SaveAs",
+      "new": "workspace::NewFile",
+      "ctrl-n": "workspace::NewFile",
+      "shift-new": "workspace::NewWindow",
+      "ctrl-shift-n": "workspace::NewWindow",
+      "ctrl-`": "terminal_panel::ToggleFocus",
+      "f10": ["app_menu::OpenApplicationMenu", "Zed"],
+      "alt-1": ["workspace::ActivatePane", 0],
+      "alt-2": ["workspace::ActivatePane", 1],
+      "alt-3": ["workspace::ActivatePane", 2],
+      "alt-4": ["workspace::ActivatePane", 3],
+      "alt-5": ["workspace::ActivatePane", 4],
+      "alt-6": ["workspace::ActivatePane", 5],
+      "alt-7": ["workspace::ActivatePane", 6],
+      "alt-8": ["workspace::ActivatePane", 7],
+      "alt-9": ["workspace::ActivatePane", 8],
+      "ctrl-alt-b": "workspace::ToggleRightDock",
+      "ctrl-b": "workspace::ToggleLeftDock",
+      "ctrl-j": "workspace::ToggleBottomDock",
+      "ctrl-shift-y": "workspace::CloseAllDocks",
+      "alt-r": "workspace::ResetActiveDockSize",
+      // For 0px parameter, uses UI font size value.
+      "shift-alt--": ["workspace::DecreaseActiveDockSize", { "px": 0 }],
+      "shift-alt-=": ["workspace::IncreaseActiveDockSize", { "px": 0 }],
+      "shift-alt-0": "workspace::ResetOpenDocksSize",
+      "ctrl-shift-alt--": ["workspace::DecreaseOpenDocksSize", { "px": 0 }],
+      "ctrl-shift-alt-=": ["workspace::IncreaseOpenDocksSize", { "px": 0 }],
+      "shift-find": "pane::DeploySearch",
+      "ctrl-shift-f": "pane::DeploySearch",
+      "ctrl-shift-h": ["pane::DeploySearch", { "replace_enabled": true }],
+      "ctrl-shift-t": "pane::ReopenClosedItem",
+      "ctrl-k ctrl-s": "zed::OpenKeymapEditor",
+      "ctrl-k ctrl-t": "theme_selector::Toggle",
+      "ctrl-alt-super-p": "settings_profile_selector::Toggle",
+      "ctrl-t": "project_symbols::Toggle",
+      "ctrl-p": "file_finder::Toggle",
+      "ctrl-tab": "tab_switcher::Toggle",
+      "ctrl-shift-tab": ["tab_switcher::Toggle", { "select_last": true }],
+      "ctrl-e": "file_finder::Toggle",
+      "f1": "command_palette::Toggle",
+      "ctrl-shift-p": "command_palette::Toggle",
+      "ctrl-shift-m": "diagnostics::Deploy",
+      "ctrl-shift-e": "project_panel::ToggleFocus",
+      "ctrl-shift-b": "outline_panel::ToggleFocus",
+      "ctrl-shift-g": "git_panel::ToggleFocus",
+      "ctrl-shift-d": "debug_panel::ToggleFocus",
+      "ctrl-shift-/": "agent::ToggleFocus",
+      "alt-save": "workspace::SaveAll",
+      "ctrl-k s": "workspace::SaveAll",
+      "ctrl-k m": "language_selector::Toggle",
+      "escape": "workspace::Unfollow",
+      "ctrl-k ctrl-left": "workspace::ActivatePaneLeft",
+      "ctrl-k ctrl-right": "workspace::ActivatePaneRight",
+      "ctrl-k ctrl-up": "workspace::ActivatePaneUp",
+      "ctrl-k ctrl-down": "workspace::ActivatePaneDown",
+      "ctrl-k shift-left": "workspace::SwapPaneLeft",
+      "ctrl-k shift-right": "workspace::SwapPaneRight",
+      "ctrl-k shift-up": "workspace::SwapPaneUp",
+      "ctrl-k shift-down": "workspace::SwapPaneDown",
+      "ctrl-shift-x": "zed::Extensions",
+      "ctrl-shift-r": "task::Rerun",
+      "alt-t": "task::Rerun",
+      "shift-alt-t": "task::Spawn",
+      "shift-alt-r": ["task::Spawn", { "reveal_target": "center" }],
+      // also possible to spawn tasks by name:
+      // "foo-bar": ["task::Spawn", { "task_name": "MyTask", "reveal_target": "dock" }]
+      // or by tag:
+      // "foo-bar": ["task::Spawn", { "task_tag": "MyTag" }],
+      "f5": "debugger::Rerun",
+      "ctrl-f4": "workspace::CloseActiveDock",
+      "ctrl-w": "workspace::CloseActiveDock"
+    }
+  },
+  {
+    "context": "Workspace && debugger_running",
+    "use_key_equivalents": true,
+    "bindings": {
+      "f5": "zed::NoAction"
+    }
+  },
+  {
+    "context": "Workspace && debugger_stopped",
+    "use_key_equivalents": true,
+    "bindings": {
+      "f5": "debugger::Continue"
+    }
+  },
+  {
+    "context": "ApplicationMenu",
+    "use_key_equivalents": true,
+    "bindings": {
+      "f10": "menu::Cancel",
+      "left": "app_menu::ActivateMenuLeft",
+      "right": "app_menu::ActivateMenuRight"
+    }
+  },
+  // Bindings from Sublime Text
+  {
+    "context": "Editor",
+    "use_key_equivalents": true,
+    "bindings": {
+      "ctrl-u": "editor::UndoSelection",
+      "ctrl-shift-u": "editor::RedoSelection",
+      "ctrl-shift-j": "editor::JoinLines",
+      "ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart",
+      "shift-alt-h": "editor::DeleteToPreviousSubwordStart",
+      "ctrl-alt-delete": "editor::DeleteToNextSubwordEnd",
+      "shift-alt-d": "editor::DeleteToNextSubwordEnd",
+      "ctrl-alt-left": "editor::MoveToPreviousSubwordStart",
+      "ctrl-alt-right": "editor::MoveToNextSubwordEnd",
+      "ctrl-shift-alt-left": "editor::SelectToPreviousSubwordStart",
+      "ctrl-shift-alt-right": "editor::SelectToNextSubwordEnd"
+    }
+  },
+  // Bindings from Atom
+  {
+    "context": "Pane",
+    "use_key_equivalents": true,
+    "bindings": {
+      "ctrl-k up": "pane::SplitUp",
+      "ctrl-k down": "pane::SplitDown",
+      "ctrl-k left": "pane::SplitLeft",
+      "ctrl-k right": "pane::SplitRight"
+    }
+  },
+  // 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",
+      "shift-enter": "editor::ConfirmCompletionReplace",
+      "tab": "editor::ComposeCompletion"
+    }
+  },
+  // Bindings for accepting edit predictions
+  //
+  // alt-l is provided as an alternative to tab/alt-tab. and will be displayed in the UI. This is
+  // because alt-tab may not be available, as it is often used for window switching.
+  {
+    "context": "Editor && edit_prediction",
+    "use_key_equivalents": true,
+    "bindings": {
+      "alt-tab": "editor::AcceptEditPrediction",
+      "alt-l": "editor::AcceptEditPrediction",
+      "tab": "editor::AcceptEditPrediction",
+      "alt-right": "editor::AcceptPartialEditPrediction"
+    }
+  },
+  {
+    "context": "Editor && edit_prediction_conflict",
+    "use_key_equivalents": true,
+    "bindings": {
+      "alt-tab": "editor::AcceptEditPrediction",
+      "alt-l": "editor::AcceptEditPrediction",
+      "alt-right": "editor::AcceptPartialEditPrediction"
+    }
+  },
+  {
+    "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": {
+      "ctrl-p": "editor::ContextMenuPrevious",
+      "up": "editor::ContextMenuPrevious",
+      "ctrl-n": "editor::ContextMenuNext",
+      "down": "editor::ContextMenuNext",
+      "pageup": "editor::ContextMenuFirst",
+      "pagedown": "editor::ContextMenuLast"
+    }
+  },
+  {
+    "context": "Editor && showing_signature_help && !showing_completions",
+    "use_key_equivalents": true,
+    "bindings": {
+      "up": "editor::SignatureHelpPrevious",
+      "down": "editor::SignatureHelpNext"
+    }
+  },
+  // Custom bindings
+  {
+    "use_key_equivalents": true,
+    "bindings": {
+      "ctrl-shift-alt-f": "workspace::FollowNextCollaborator",
+      // Only available in debug builds: opens an element inspector for development.
+      "shift-alt-i": "dev::ToggleInspector"
+    }
+  },
+  {
+    "context": "!Terminal",
+    "use_key_equivalents": true,
+    "bindings": {
+      "ctrl-shift-c": "collab_panel::ToggleFocus"
+    }
+  },
+  {
+    "context": "!ContextEditor > Editor && mode == full",
+    "use_key_equivalents": true,
+    "bindings": {
+      "alt-enter": "editor::OpenExcerpts",
+      "shift-enter": "editor::ExpandExcerpts",
+      "ctrl-alt-enter": "editor::OpenExcerptsSplit",
+      "ctrl-shift-e": "pane::RevealInProjectPanel",
+      "ctrl-f8": "editor::GoToHunk",
+      "ctrl-shift-f8": "editor::GoToPreviousHunk",
+      "ctrl-enter": "assistant::InlineAssist",
+      "ctrl-shift-;": "editor::ToggleInlayHints"
+    }
+  },
+  {
+    "context": "PromptEditor",
+    "use_key_equivalents": true,
+    "bindings": {
+      "ctrl-[": "agent::CyclePreviousInlineAssist",
+      "ctrl-]": "agent::CycleNextInlineAssist",
+      "shift-alt-e": "agent::RemoveAllContext"
+    }
+  },
+  {
+    "context": "Prompt",
+    "use_key_equivalents": true,
+    "bindings": {
+      "left": "menu::SelectPrevious",
+      "right": "menu::SelectNext",
+      "h": "menu::SelectPrevious",
+      "l": "menu::SelectNext"
+    }
+  },
+  {
+    "context": "ProjectSearchBar && !in_replace",
+    "use_key_equivalents": true,
+    "bindings": {
+      "ctrl-enter": "project_search::SearchInNew"
+    }
+  },
+  {
+    "context": "OutlinePanel && not_editing",
+    "use_key_equivalents": true,
+    "bindings": {
+      "left": "outline_panel::CollapseSelectedEntry",
+      "right": "outline_panel::ExpandSelectedEntry",
+      "alt-copy": "outline_panel::CopyPath",
+      "shift-alt-c": "outline_panel::CopyPath",
+      "shift-alt-copy": "workspace::CopyRelativePath",
+      "ctrl-shift-alt-c": "workspace::CopyRelativePath",
+      "ctrl-alt-r": "outline_panel::RevealInFileManager",
+      "space": "outline_panel::OpenSelectedEntry",
+      "shift-down": "menu::SelectNext",
+      "shift-up": "menu::SelectPrevious",
+      "alt-enter": "editor::OpenExcerpts",
+      "ctrl-alt-enter": "editor::OpenExcerptsSplit"
+    }
+  },
+  {
+    "context": "ProjectPanel",
+    "use_key_equivalents": true,
+    "bindings": {
+      "left": "project_panel::CollapseSelectedEntry",
+      "right": "project_panel::ExpandSelectedEntry",
+      "new": "project_panel::NewFile",
+      "ctrl-n": "project_panel::NewFile",
+      "alt-new": "project_panel::NewDirectory",
+      "alt-n": "project_panel::NewDirectory",
+      "cut": "project_panel::Cut",
+      "ctrl-x": "project_panel::Cut",
+      "copy": "project_panel::Copy",
+      "ctrl-insert": "project_panel::Copy",
+      "ctrl-c": "project_panel::Copy",
+      "paste": "project_panel::Paste",
+      "shift-insert": "project_panel::Paste",
+      "ctrl-v": "project_panel::Paste",
+      "alt-copy": "project_panel::CopyPath",
+      "shift-alt-c": "project_panel::CopyPath",
+      "shift-alt-copy": "workspace::CopyRelativePath",
+      "ctrl-k ctrl-shift-c": "workspace::CopyRelativePath",
+      "enter": "project_panel::Rename",
+      "f2": "project_panel::Rename",
+      "backspace": ["project_panel::Trash", { "skip_prompt": false }],
+      "delete": ["project_panel::Trash", { "skip_prompt": false }],
+      "shift-delete": ["project_panel::Delete", { "skip_prompt": false }],
+      "ctrl-backspace": ["project_panel::Delete", { "skip_prompt": false }],
+      "ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }],
+      "ctrl-alt-r": "project_panel::RevealInFileManager",
+      "ctrl-shift-enter": "project_panel::OpenWithSystem",
+      "alt-d": "project_panel::CompareMarkedFiles",
+      "shift-find": "project_panel::NewSearchInDirectory",
+      "ctrl-k ctrl-shift-f": "project_panel::NewSearchInDirectory",
+      "shift-down": "menu::SelectNext",
+      "shift-up": "menu::SelectPrevious",
+      "escape": "menu::Cancel"
+    }
+  },
+  {
+    "context": "ProjectPanel && not_editing",
+    "use_key_equivalents": true,
+    "bindings": {
+      "space": "project_panel::Open"
+    }
+  },
+  {
+    "context": "GitPanel && ChangesList",
+    "use_key_equivalents": true,
+    "bindings": {
+      "up": "menu::SelectPrevious",
+      "down": "menu::SelectNext",
+      "enter": "menu::Confirm",
+      "alt-y": "git::StageFile",
+      "shift-alt-y": "git::UnstageFile",
+      "space": "git::ToggleStaged",
+      "shift-space": "git::StageRange",
+      "tab": "git_panel::FocusEditor",
+      "shift-tab": "git_panel::FocusEditor",
+      "escape": "git_panel::ToggleFocus",
+      "alt-enter": "menu::SecondaryConfirm",
+      "delete": ["git::RestoreFile", { "skip_prompt": false }],
+      "backspace": ["git::RestoreFile", { "skip_prompt": false }],
+      "shift-delete": ["git::RestoreFile", { "skip_prompt": false }],
+      "ctrl-backspace": ["git::RestoreFile", { "skip_prompt": false }],
+      "ctrl-delete": ["git::RestoreFile", { "skip_prompt": false }]
+    }
+  },
+  {
+    "context": "GitPanel && CommitEditor",
+    "use_key_equivalents": true,
+    "bindings": {
+      "escape": "git::Cancel"
+    }
+  },
+  {
+    "context": "GitCommit > Editor",
+    "use_key_equivalents": true,
+    "bindings": {
+      "escape": "menu::Cancel",
+      "enter": "editor::Newline",
+      "ctrl-enter": "git::Commit",
+      "ctrl-shift-enter": "git::Amend",
+      "alt-l": "git::GenerateCommitMessage"
+    }
+  },
+  {
+    "context": "GitPanel",
+    "use_key_equivalents": true,
+    "bindings": {
+      "ctrl-g ctrl-g": "git::Fetch",
+      "ctrl-g up": "git::Push",
+      "ctrl-g down": "git::Pull",
+      "ctrl-g shift-up": "git::ForcePush",
+      "ctrl-g d": "git::Diff",
+      "ctrl-g backspace": "git::RestoreTrackedFiles",
+      "ctrl-g shift-backspace": "git::TrashUntrackedFiles",
+      "ctrl-space": "git::StageAll",
+      "ctrl-shift-space": "git::UnstageAll",
+      "ctrl-enter": "git::Commit",
+      "ctrl-shift-enter": "git::Amend"
+    }
+  },
+  {
+    "context": "GitDiff > Editor",
+    "use_key_equivalents": true,
+    "bindings": {
+      "ctrl-enter": "git::Commit",
+      "ctrl-shift-enter": "git::Amend",
+      "ctrl-space": "git::StageAll",
+      "ctrl-shift-space": "git::UnstageAll"
+    }
+  },
+  {
+    "context": "AskPass > Editor",
+    "use_key_equivalents": true,
+    "bindings": {
+      "enter": "menu::Confirm"
+    }
+  },
+  {
+    "context": "CommitEditor > Editor",
+    "use_key_equivalents": true,
+    "bindings": {
+      "escape": "git_panel::FocusChanges",
+      "tab": "git_panel::FocusChanges",
+      "shift-tab": "git_panel::FocusChanges",
+      "enter": "editor::Newline",
+      "ctrl-enter": "git::Commit",
+      "ctrl-shift-enter": "git::Amend",
+      "alt-up": "git_panel::FocusChanges",
+      "alt-l": "git::GenerateCommitMessage"
+    }
+  },
+  {
+    "context": "DebugPanel",
+    "use_key_equivalents": true,
+    "bindings": {
+      "ctrl-t": "debugger::ToggleThreadPicker",
+      "ctrl-i": "debugger::ToggleSessionPicker",
+      "shift-alt-escape": "debugger::ToggleExpandItem"
+    }
+  },
+  {
+    "context": "VariableList",
+    "use_key_equivalents": true,
+    "bindings": {
+      "left": "variable_list::CollapseSelectedEntry",
+      "right": "variable_list::ExpandSelectedEntry",
+      "enter": "variable_list::EditVariable",
+      "ctrl-c": "variable_list::CopyVariableValue",
+      "ctrl-alt-c": "variable_list::CopyVariableName",
+      "delete": "variable_list::RemoveWatch",
+      "backspace": "variable_list::RemoveWatch",
+      "alt-enter": "variable_list::AddWatch"
+    }
+  },
+  {
+    "context": "BreakpointList",
+    "use_key_equivalents": true,
+    "bindings": {
+      "space": "debugger::ToggleEnableBreakpoint",
+      "backspace": "debugger::UnsetBreakpoint",
+      "left": "debugger::PreviousBreakpointProperty",
+      "right": "debugger::NextBreakpointProperty"
+    }
+  },
+  {
+    "context": "CollabPanel && not_editing",
+    "use_key_equivalents": true,
+    "bindings": {
+      "ctrl-backspace": "collab_panel::Remove",
+      "space": "menu::Confirm"
+    }
+  },
+  {
+    "context": "CollabPanel",
+    "use_key_equivalents": true,
+    "bindings": {
+      "alt-up": "collab_panel::MoveChannelUp",
+      "alt-down": "collab_panel::MoveChannelDown"
+    }
+  },
+  {
+    "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": {
+      "escape": "menu::Cancel",
+      "up": "menu::SelectPrevious",
+      "down": "menu::SelectNext",
+      "tab": "picker::ConfirmCompletion",
+      "alt-enter": ["picker::ConfirmInput", { "secondary": false }]
+    }
+  },
+  {
+    "context": "ChannelModal > Picker > Editor",
+    "use_key_equivalents": true,
+    "bindings": {
+      "tab": "channel_modal::ToggleMode"
+    }
+  },
+  {
+    "context": "FileFinder || (FileFinder > Picker > Editor)",
+    "use_key_equivalents": true,
+    "bindings": {
+      "ctrl-p": "file_finder::Toggle",
+      "ctrl-shift-a": "file_finder::ToggleSplitMenu",
+      "ctrl-shift-i": "file_finder::ToggleFilterMenu"
+    }
+  },
+  {
+    "context": "FileFinder || (FileFinder > Picker > Editor) || (FileFinder > Picker > menu)",
+    "use_key_equivalents": true,
+    "bindings": {
+      "ctrl-shift-p": "file_finder::SelectPrevious",
+      "ctrl-j": "pane::SplitDown",
+      "ctrl-k": "pane::SplitUp",
+      "ctrl-h": "pane::SplitLeft",
+      "ctrl-l": "pane::SplitRight"
+    }
+  },
+  {
+    "context": "TabSwitcher",
+    "use_key_equivalents": true,
+    "bindings": {
+      "ctrl-shift-tab": "menu::SelectPrevious",
+      "ctrl-up": "menu::SelectPrevious",
+      "ctrl-down": "menu::SelectNext",
+      "ctrl-backspace": "tab_switcher::CloseSelectedItem"
+    }
+  },
+  {
+    "context": "Terminal",
+    "use_key_equivalents": true,
+    "bindings": {
+      "ctrl-alt-space": "terminal::ShowCharacterPalette",
+      "copy": "terminal::Copy",
+      "ctrl-insert": "terminal::Copy",
+      "ctrl-shift-c": "terminal::Copy",
+      "paste": "terminal::Paste",
+      "shift-insert": "terminal::Paste",
+      "ctrl-shift-v": "terminal::Paste",
+      "ctrl-enter": "assistant::InlineAssist",
+      "alt-b": ["terminal::SendText", "\u001bb"],
+      "alt-f": ["terminal::SendText", "\u001bf"],
+      "alt-.": ["terminal::SendText", "\u001b."],
+      "ctrl-delete": ["terminal::SendText", "\u001bd"],
+      // Overrides for conflicting keybindings
+      "ctrl-b": ["terminal::SendKeystroke", "ctrl-b"],
+      "ctrl-c": ["terminal::SendKeystroke", "ctrl-c"],
+      "ctrl-e": ["terminal::SendKeystroke", "ctrl-e"],
+      "ctrl-o": ["terminal::SendKeystroke", "ctrl-o"],
+      "ctrl-w": ["terminal::SendKeystroke", "ctrl-w"],
+      "ctrl-backspace": ["terminal::SendKeystroke", "ctrl-w"],
+      "ctrl-shift-a": "editor::SelectAll",
+      "find": "buffer_search::Deploy",
+      "ctrl-shift-f": "buffer_search::Deploy",
+      "ctrl-shift-l": "terminal::Clear",
+      "ctrl-shift-w": "pane::CloseActiveItem",
+      "up": ["terminal::SendKeystroke", "up"],
+      "pageup": ["terminal::SendKeystroke", "pageup"],
+      "down": ["terminal::SendKeystroke", "down"],
+      "pagedown": ["terminal::SendKeystroke", "pagedown"],
+      "escape": ["terminal::SendKeystroke", "escape"],
+      "enter": ["terminal::SendKeystroke", "enter"],
+      "shift-pageup": "terminal::ScrollPageUp",
+      "shift-pagedown": "terminal::ScrollPageDown",
+      "shift-up": "terminal::ScrollLineUp",
+      "shift-down": "terminal::ScrollLineDown",
+      "shift-home": "terminal::ScrollToTop",
+      "shift-end": "terminal::ScrollToBottom",
+      "ctrl-shift-space": "terminal::ToggleViMode",
+      "ctrl-shift-r": "terminal::RerunTask",
+      "ctrl-alt-r": "terminal::RerunTask",
+      "alt-t": "terminal::RerunTask"
+    }
+  },
+  {
+    "context": "ZedPredictModal",
+    "use_key_equivalents": true,
+    "bindings": {
+      "escape": "menu::Cancel"
+    }
+  },
+  {
+    "context": "ConfigureContextServerModal > Editor",
+    "use_key_equivalents": true,
+    "bindings": {
+      "escape": "menu::Cancel",
+      "enter": "editor::Newline",
+      "ctrl-enter": "menu::Confirm"
+    }
+  },
+  {
+    "context": "OnboardingAiConfigurationModal",
+    "use_key_equivalents": true,
+    "bindings": {
+      "escape": "menu::Cancel"
+    }
+  },
+  {
+    "context": "Diagnostics",
+    "use_key_equivalents": true,
+    "bindings": {
+      "ctrl-r": "diagnostics::ToggleDiagnosticsRefresh"
+    }
+  },
+  {
+    "context": "DebugConsole > Editor",
+    "use_key_equivalents": true,
+    "bindings": {
+      "enter": "menu::Confirm",
+      "alt-enter": "console::WatchExpression"
+    }
+  },
+  {
+    "context": "RunModal",
+    "use_key_equivalents": true,
+    "bindings": {
+      "ctrl-tab": "pane::ActivateNextItem",
+      "ctrl-shift-tab": "pane::ActivatePreviousItem"
+    }
+  },
+  {
+    "context": "MarkdownPreview",
+    "use_key_equivalents": true,
+    "bindings": {
+      "pageup": "markdown::MovePageUp",
+      "pagedown": "markdown::MovePageDown"
+    }
+  },
+  {
+    "context": "KeymapEditor",
+    "use_key_equivalents": true,
+    "bindings": {
+      "ctrl-f": "search::FocusSearch",
+      "alt-find": "keymap_editor::ToggleKeystrokeSearch",
+      "alt-f": "keymap_editor::ToggleKeystrokeSearch",
+      "alt-c": "keymap_editor::ToggleConflictFilter",
+      "enter": "keymap_editor::EditBinding",
+      "alt-enter": "keymap_editor::CreateBinding",
+      "ctrl-c": "keymap_editor::CopyAction",
+      "ctrl-shift-c": "keymap_editor::CopyContext",
+      "ctrl-t": "keymap_editor::ShowMatchingKeybinds"
+    }
+  },
+  {
+    "context": "KeystrokeInput",
+    "use_key_equivalents": true,
+    "bindings": {
+      "enter": "keystroke_input::StartRecording",
+      "escape escape escape": "keystroke_input::StopRecording",
+      "delete": "keystroke_input::ClearKeystrokes"
+    }
+  },
+  {
+    "context": "KeybindEditorModal",
+    "use_key_equivalents": true,
+    "bindings": {
+      "ctrl-enter": "menu::Confirm",
+      "escape": "menu::Cancel"
+    }
+  },
+  {
+    "context": "KeybindEditorModal > Editor",
+    "use_key_equivalents": true,
+    "bindings": {
+      "up": "menu::SelectPrevious",
+      "down": "menu::SelectNext"
+    }
+  },
+  {
+    "context": "Onboarding",
+    "use_key_equivalents": true,
+    "bindings": {
+      "ctrl-1": "onboarding::ActivateBasicsPage",
+      "ctrl-2": "onboarding::ActivateEditingPage",
+      "ctrl-3": "onboarding::ActivateAISetupPage",
+      "ctrl-escape": "onboarding::Finish",
+      "alt-tab": "onboarding::SignIn",
+      "shift-alt-a": "onboarding::OpenAccount"
+    }
+  }
+]

crates/docs_preprocessor/src/main.rs 🔗

@@ -19,6 +19,10 @@ static KEYMAP_LINUX: LazyLock<KeymapFile> = LazyLock::new(|| {
     load_keymap("keymaps/default-linux.json").expect("Failed to load Linux keymap")
 });
 
+static KEYMAP_WINDOWS: LazyLock<KeymapFile> = LazyLock::new(|| {
+    load_keymap("keymaps/default-windows.json").expect("Failed to load Windows keymap")
+});
+
 static ALL_ACTIONS: LazyLock<Vec<ActionDef>> = LazyLock::new(dump_all_gpui_actions);
 
 const FRONT_MATTER_COMMENT: &str = "<!-- ZED_META {} -->";
@@ -216,6 +220,7 @@ fn find_binding(os: &str, action: &str) -> Option<String> {
     let keymap = match os {
         "macos" => &KEYMAP_MACOS,
         "linux" | "freebsd" => &KEYMAP_LINUX,
+        "windows" => &KEYMAP_WINDOWS,
         _ => unreachable!("Not a valid OS: {}", os),
     };
 

crates/editor/src/editor.rs 🔗

@@ -2588,7 +2588,7 @@ impl Editor {
                 || binding
                     .keystrokes()
                     .first()
-                    .is_some_and(|keystroke| keystroke.modifiers.modified())
+                    .is_some_and(|keystroke| keystroke.display_modifiers.modified())
         }))
     }
 
@@ -7686,16 +7686,16 @@ impl Editor {
             .keystroke()
         {
             modifiers_held = modifiers_held
-                || (&accept_keystroke.modifiers == modifiers
-                    && accept_keystroke.modifiers.modified());
+                || (&accept_keystroke.display_modifiers == modifiers
+                    && accept_keystroke.display_modifiers.modified());
         };
         if let Some(accept_partial_keystroke) = self
             .accept_edit_prediction_keybind(true, window, cx)
             .keystroke()
         {
             modifiers_held = modifiers_held
-                || (&accept_partial_keystroke.modifiers == modifiers
-                    && accept_partial_keystroke.modifiers.modified());
+                || (&accept_partial_keystroke.display_modifiers == modifiers
+                    && accept_partial_keystroke.display_modifiers.modified());
         }
 
         if modifiers_held {
@@ -9044,7 +9044,7 @@ impl Editor {
 
         let is_platform_style_mac = PlatformStyle::platform() == PlatformStyle::Mac;
 
-        let modifiers_color = if accept_keystroke.modifiers == window.modifiers() {
+        let modifiers_color = if accept_keystroke.display_modifiers == window.modifiers() {
             Color::Accent
         } else {
             Color::Muted
@@ -9056,19 +9056,19 @@ impl Editor {
             .font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
             .text_size(TextSize::XSmall.rems(cx))
             .child(h_flex().children(ui::render_modifiers(
-                &accept_keystroke.modifiers,
+                &accept_keystroke.display_modifiers,
                 PlatformStyle::platform(),
                 Some(modifiers_color),
                 Some(IconSize::XSmall.rems().into()),
                 true,
             )))
             .when(is_platform_style_mac, |parent| {
-                parent.child(accept_keystroke.key.clone())
+                parent.child(accept_keystroke.display_key.clone())
             })
             .when(!is_platform_style_mac, |parent| {
                 parent.child(
                     Key::new(
-                        util::capitalize(&accept_keystroke.key),
+                        util::capitalize(&accept_keystroke.display_key),
                         Some(Color::Default),
                     )
                     .size(Some(IconSize::XSmall.rems().into())),
@@ -9171,7 +9171,7 @@ impl Editor {
         max_width: Pixels,
         cursor_point: Point,
         style: &EditorStyle,
-        accept_keystroke: Option<&gpui::Keystroke>,
+        accept_keystroke: Option<&gpui::KeybindingKeystroke>,
         _window: &Window,
         cx: &mut Context<Editor>,
     ) -> Option<AnyElement> {
@@ -9249,7 +9249,7 @@ impl Editor {
                                         accept_keystroke.as_ref(),
                                         |el, accept_keystroke| {
                                             el.child(h_flex().children(ui::render_modifiers(
-                                                &accept_keystroke.modifiers,
+                                                &accept_keystroke.display_modifiers,
                                                 PlatformStyle::platform(),
                                                 Some(Color::Default),
                                                 Some(IconSize::XSmall.rems().into()),
@@ -9319,7 +9319,7 @@ impl Editor {
                         .child(completion),
                 )
                 .when_some(accept_keystroke, |el, accept_keystroke| {
-                    if !accept_keystroke.modifiers.modified() {
+                    if !accept_keystroke.display_modifiers.modified() {
                         return el;
                     }
 
@@ -9338,7 +9338,7 @@ impl Editor {
                                     .font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
                                     .when(is_platform_style_mac, |parent| parent.gap_1())
                                     .child(h_flex().children(ui::render_modifiers(
-                                        &accept_keystroke.modifiers,
+                                        &accept_keystroke.display_modifiers,
                                         PlatformStyle::platform(),
                                         Some(if !has_completion {
                                             Color::Muted

crates/editor/src/element.rs 🔗

@@ -43,10 +43,10 @@ use gpui::{
     Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle,
     DispatchPhase, Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId,
     GlobalElementId, Hitbox, HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero,
-    Keystroke, Length, ModifiersChangedEvent, MouseButton, MouseClickEvent, MouseDownEvent,
-    MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, ScrollHandle,
-    ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled,
-    TextRun, TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill,
+    KeybindingKeystroke, Length, ModifiersChangedEvent, MouseButton, MouseClickEvent,
+    MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta,
+    ScrollHandle, ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement,
+    Style, Styled, TextRun, TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill,
     linear_color_stop, linear_gradient, outline, point, px, quad, relative, size, solid_background,
     transparent_black,
 };
@@ -7150,7 +7150,7 @@ fn header_jump_data(
 pub struct AcceptEditPredictionBinding(pub(crate) Option<gpui::KeyBinding>);
 
 impl AcceptEditPredictionBinding {
-    pub fn keystroke(&self) -> Option<&Keystroke> {
+    pub fn keystroke(&self) -> Option<&KeybindingKeystroke> {
         if let Some(binding) = self.0.as_ref() {
             match &binding.keystrokes() {
                 [keystroke, ..] => Some(keystroke),

crates/gpui/src/app.rs 🔗

@@ -37,10 +37,10 @@ use crate::{
     AssetSource, BackgroundExecutor, Bounds, ClipboardItem, CursorStyle, DispatchPhase, DisplayId,
     EventEmitter, FocusHandle, FocusMap, ForegroundExecutor, Global, KeyBinding, KeyContext,
     Keymap, Keystroke, LayoutId, Menu, MenuItem, OwnedMenu, PathPromptOptions, Pixels, Platform,
-    PlatformDisplay, PlatformKeyboardLayout, Point, PromptBuilder, PromptButton, PromptHandle,
-    PromptLevel, Render, RenderImage, RenderablePromptHandle, Reservation, ScreenCaptureSource,
-    SubscriberSet, Subscription, SvgRenderer, Task, TextSystem, Window, WindowAppearance,
-    WindowHandle, WindowId, WindowInvalidator,
+    PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, Point, PromptBuilder,
+    PromptButton, PromptHandle, PromptLevel, Render, RenderImage, RenderablePromptHandle,
+    Reservation, ScreenCaptureSource, SubscriberSet, Subscription, SvgRenderer, Task, TextSystem,
+    Window, WindowAppearance, WindowHandle, WindowId, WindowInvalidator,
     colors::{Colors, GlobalColors},
     current_platform, hash, init_app_menus,
 };
@@ -263,6 +263,7 @@ pub struct App {
     pub(crate) focus_handles: Arc<FocusMap>,
     pub(crate) keymap: Rc<RefCell<Keymap>>,
     pub(crate) keyboard_layout: Box<dyn PlatformKeyboardLayout>,
+    pub(crate) keyboard_mapper: Rc<dyn PlatformKeyboardMapper>,
     pub(crate) global_action_listeners:
         FxHashMap<TypeId, Vec<Rc<dyn Fn(&dyn Any, DispatchPhase, &mut Self)>>>,
     pending_effects: VecDeque<Effect>,
@@ -312,6 +313,7 @@ impl App {
         let text_system = Arc::new(TextSystem::new(platform.text_system()));
         let entities = EntityMap::new();
         let keyboard_layout = platform.keyboard_layout();
+        let keyboard_mapper = platform.keyboard_mapper();
 
         let app = Rc::new_cyclic(|this| AppCell {
             app: RefCell::new(App {
@@ -337,6 +339,7 @@ impl App {
                 focus_handles: Arc::new(RwLock::new(SlotMap::with_key())),
                 keymap: Rc::new(RefCell::new(Keymap::default())),
                 keyboard_layout,
+                keyboard_mapper,
                 global_action_listeners: FxHashMap::default(),
                 pending_effects: VecDeque::new(),
                 pending_notifications: FxHashSet::default(),
@@ -376,6 +379,7 @@ impl App {
                 if let Some(app) = app.upgrade() {
                     let cx = &mut app.borrow_mut();
                     cx.keyboard_layout = cx.platform.keyboard_layout();
+                    cx.keyboard_mapper = cx.platform.keyboard_mapper();
                     cx.keyboard_layout_observers
                         .clone()
                         .retain(&(), move |callback| (callback)(cx));
@@ -424,6 +428,11 @@ impl App {
         self.keyboard_layout.as_ref()
     }
 
+    /// Get the current keyboard mapper.
+    pub fn keyboard_mapper(&self) -> &Rc<dyn PlatformKeyboardMapper> {
+        &self.keyboard_mapper
+    }
+
     /// Invokes a handler when the current keyboard layout changes
     pub fn on_keyboard_layout_change<F>(&self, mut callback: F) -> Subscription
     where

crates/gpui/src/keymap.rs 🔗

@@ -4,7 +4,7 @@ mod context;
 pub use binding::*;
 pub use context::*;
 
-use crate::{Action, Keystroke, is_no_action};
+use crate::{Action, AsKeystroke, Keystroke, is_no_action};
 use collections::{HashMap, HashSet};
 use smallvec::SmallVec;
 use std::any::TypeId;
@@ -141,7 +141,7 @@ impl Keymap {
     /// only.
     pub fn bindings_for_input(
         &self,
-        input: &[Keystroke],
+        input: &[impl AsKeystroke],
         context_stack: &[KeyContext],
     ) -> (SmallVec<[KeyBinding; 1]>, bool) {
         let mut matched_bindings = SmallVec::<[(usize, BindingIndex, &KeyBinding); 1]>::new();
@@ -192,7 +192,6 @@ impl Keymap {
 
         (bindings, !pending.is_empty())
     }
-
     /// Check if the given binding is enabled, given a certain key context.
     /// Returns the deepest depth at which the binding matches, or None if it doesn't match.
     fn binding_enabled(&self, binding: &KeyBinding, contexts: &[KeyContext]) -> Option<usize> {
@@ -639,7 +638,7 @@ mod tests {
         fn assert_bindings(keymap: &Keymap, action: &dyn Action, expected: &[&str]) {
             let actual = keymap
                 .bindings_for_action(action)
-                .map(|binding| binding.keystrokes[0].unparse())
+                .map(|binding| binding.keystrokes[0].inner.unparse())
                 .collect::<Vec<_>>();
             assert_eq!(actual, expected, "{:?}", action);
         }

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

@@ -1,14 +1,15 @@
 use std::rc::Rc;
 
-use collections::HashMap;
-
-use crate::{Action, InvalidKeystrokeError, KeyBindingContextPredicate, Keystroke, SharedString};
+use crate::{
+    Action, AsKeystroke, DummyKeyboardMapper, InvalidKeystrokeError, KeyBindingContextPredicate,
+    KeybindingKeystroke, Keystroke, PlatformKeyboardMapper, SharedString,
+};
 use smallvec::SmallVec;
 
 /// A keybinding and its associated metadata, from the keymap.
 pub struct KeyBinding {
     pub(crate) action: Box<dyn Action>,
-    pub(crate) keystrokes: SmallVec<[Keystroke; 2]>,
+    pub(crate) keystrokes: SmallVec<[KeybindingKeystroke; 2]>,
     pub(crate) context_predicate: Option<Rc<KeyBindingContextPredicate>>,
     pub(crate) meta: Option<KeyBindingMetaIndex>,
     /// The json input string used when building the keybinding, if any
@@ -32,7 +33,15 @@ impl KeyBinding {
     pub fn new<A: Action>(keystrokes: &str, action: A, context: Option<&str>) -> Self {
         let context_predicate =
             context.map(|context| KeyBindingContextPredicate::parse(context).unwrap().into());
-        Self::load(keystrokes, Box::new(action), context_predicate, None, None).unwrap()
+        Self::load(
+            keystrokes,
+            Box::new(action),
+            context_predicate,
+            false,
+            None,
+            &DummyKeyboardMapper,
+        )
+        .unwrap()
     }
 
     /// Load a keybinding from the given raw data.
@@ -40,24 +49,22 @@ impl KeyBinding {
         keystrokes: &str,
         action: Box<dyn Action>,
         context_predicate: Option<Rc<KeyBindingContextPredicate>>,
-        key_equivalents: Option<&HashMap<char, char>>,
+        use_key_equivalents: bool,
         action_input: Option<SharedString>,
+        keyboard_mapper: &dyn PlatformKeyboardMapper,
     ) -> std::result::Result<Self, InvalidKeystrokeError> {
-        let mut keystrokes: SmallVec<[Keystroke; 2]> = keystrokes
+        let keystrokes: SmallVec<[KeybindingKeystroke; 2]> = keystrokes
             .split_whitespace()
-            .map(Keystroke::parse)
+            .map(|source| {
+                let keystroke = Keystroke::parse(source)?;
+                Ok(KeybindingKeystroke::new(
+                    keystroke,
+                    use_key_equivalents,
+                    keyboard_mapper,
+                ))
+            })
             .collect::<std::result::Result<_, _>>()?;
 
-        if let Some(equivalents) = key_equivalents {
-            for keystroke in keystrokes.iter_mut() {
-                if keystroke.key.chars().count() == 1
-                    && let Some(key) = equivalents.get(&keystroke.key.chars().next().unwrap())
-                {
-                    keystroke.key = key.to_string();
-                }
-            }
-        }
-
         Ok(Self {
             keystrokes,
             action,
@@ -79,13 +86,13 @@ impl KeyBinding {
     }
 
     /// Check if the given keystrokes match this binding.
-    pub fn match_keystrokes(&self, typed: &[Keystroke]) -> Option<bool> {
+    pub fn match_keystrokes(&self, typed: &[impl AsKeystroke]) -> Option<bool> {
         if self.keystrokes.len() < typed.len() {
             return None;
         }
 
         for (target, typed) in self.keystrokes.iter().zip(typed.iter()) {
-            if !typed.should_match(target) {
+            if !typed.as_keystroke().should_match(target) {
                 return None;
             }
         }
@@ -94,7 +101,7 @@ impl KeyBinding {
     }
 
     /// Get the keystrokes associated with this binding
-    pub fn keystrokes(&self) -> &[Keystroke] {
+    pub fn keystrokes(&self) -> &[KeybindingKeystroke] {
         self.keystrokes.as_slice()
     }
 

crates/gpui/src/platform.rs 🔗

@@ -231,7 +231,6 @@ pub(crate) trait Platform: 'static {
 
     fn on_quit(&self, callback: Box<dyn FnMut()>);
     fn on_reopen(&self, callback: Box<dyn FnMut()>);
-    fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>);
 
     fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap);
     fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
@@ -251,7 +250,6 @@ pub(crate) trait Platform: 'static {
     fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>);
     fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>);
     fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
-    fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
 
     fn compositor_name(&self) -> &'static str {
         ""
@@ -272,6 +270,10 @@ pub(crate) trait Platform: 'static {
     fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>>;
     fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>>;
     fn delete_credentials(&self, url: &str) -> Task<Result<()>>;
+
+    fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
+    fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper>;
+    fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>);
 }
 
 /// A handle to a platform's display, e.g. a monitor or laptop screen.

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

@@ -1,3 +1,7 @@
+use collections::HashMap;
+
+use crate::{KeybindingKeystroke, Keystroke};
+
 /// A trait for platform-specific keyboard layouts
 pub trait PlatformKeyboardLayout {
     /// Get the keyboard layout ID, which should be unique to the layout
@@ -5,3 +9,33 @@ pub trait PlatformKeyboardLayout {
     /// Get the keyboard layout display name
     fn name(&self) -> &str;
 }
+
+/// A trait for platform-specific keyboard mappings
+pub trait PlatformKeyboardMapper {
+    /// Map a key equivalent to its platform-specific representation
+    fn map_key_equivalent(
+        &self,
+        keystroke: Keystroke,
+        use_key_equivalents: bool,
+    ) -> KeybindingKeystroke;
+    /// Get the key equivalents for the current keyboard layout,
+    /// only used on macOS
+    fn get_key_equivalents(&self) -> Option<&HashMap<char, char>>;
+}
+
+/// A dummy implementation of the platform keyboard mapper
+pub struct DummyKeyboardMapper;
+
+impl PlatformKeyboardMapper for DummyKeyboardMapper {
+    fn map_key_equivalent(
+        &self,
+        keystroke: Keystroke,
+        _use_key_equivalents: bool,
+    ) -> KeybindingKeystroke {
+        KeybindingKeystroke::from_keystroke(keystroke)
+    }
+
+    fn get_key_equivalents(&self) -> Option<&HashMap<char, char>> {
+        None
+    }
+}

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

@@ -5,6 +5,14 @@ use std::{
     fmt::{Display, Write},
 };
 
+use crate::PlatformKeyboardMapper;
+
+/// This is a helper trait so that we can simplify the implementation of some functions
+pub trait AsKeystroke {
+    /// Returns the GPUI representation of the keystroke.
+    fn as_keystroke(&self) -> &Keystroke;
+}
+
 /// A keystroke and associated metadata generated by the platform
 #[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)]
 pub struct Keystroke {
@@ -24,6 +32,17 @@ pub struct Keystroke {
     pub key_char: Option<String>,
 }
 
+/// Represents a keystroke that can be used in keybindings and displayed to the user.
+#[derive(Debug, Clone, Eq, PartialEq, Hash)]
+pub struct KeybindingKeystroke {
+    /// The GPUI representation of the keystroke.
+    pub inner: Keystroke,
+    /// The modifiers to display.
+    pub display_modifiers: Modifiers,
+    /// The key to display.
+    pub display_key: String,
+}
+
 /// Error type for `Keystroke::parse`. This is used instead of `anyhow::Error` so that Zed can use
 /// markdown to display it.
 #[derive(Debug)]
@@ -58,7 +77,7 @@ impl Keystroke {
     ///
     /// This method assumes that `self` was typed and `target' is in the keymap, and checks
     /// both possibilities for self against the target.
-    pub fn should_match(&self, target: &Keystroke) -> bool {
+    pub fn should_match(&self, target: &KeybindingKeystroke) -> bool {
         #[cfg(not(target_os = "windows"))]
         if let Some(key_char) = self
             .key_char
@@ -71,7 +90,7 @@ impl Keystroke {
                 ..Default::default()
             };
 
-            if &target.key == key_char && target.modifiers == ime_modifiers {
+            if &target.inner.key == key_char && target.inner.modifiers == ime_modifiers {
                 return true;
             }
         }
@@ -83,12 +102,12 @@ impl Keystroke {
             .filter(|key_char| key_char != &&self.key)
         {
             // On Windows, if key_char is set, then the typed keystroke produced the key_char
-            if &target.key == key_char && target.modifiers == Modifiers::none() {
+            if &target.inner.key == key_char && target.inner.modifiers == Modifiers::none() {
                 return true;
             }
         }
 
-        target.modifiers == self.modifiers && target.key == self.key
+        target.inner.modifiers == self.modifiers && target.inner.key == self.key
     }
 
     /// key syntax is:
@@ -200,31 +219,7 @@ impl Keystroke {
 
     /// Produces a representation of this key that Parse can understand.
     pub fn unparse(&self) -> String {
-        let mut str = String::new();
-        if self.modifiers.function {
-            str.push_str("fn-");
-        }
-        if self.modifiers.control {
-            str.push_str("ctrl-");
-        }
-        if self.modifiers.alt {
-            str.push_str("alt-");
-        }
-        if self.modifiers.platform {
-            #[cfg(target_os = "macos")]
-            str.push_str("cmd-");
-
-            #[cfg(any(target_os = "linux", target_os = "freebsd"))]
-            str.push_str("super-");
-
-            #[cfg(target_os = "windows")]
-            str.push_str("win-");
-        }
-        if self.modifiers.shift {
-            str.push_str("shift-");
-        }
-        str.push_str(&self.key);
-        str
+        unparse(&self.modifiers, &self.key)
     }
 
     /// Returns true if this keystroke left
@@ -266,6 +261,32 @@ impl Keystroke {
     }
 }
 
+impl KeybindingKeystroke {
+    /// Create a new keybinding keystroke from the given keystroke
+    pub fn new(
+        inner: Keystroke,
+        use_key_equivalents: bool,
+        keyboard_mapper: &dyn PlatformKeyboardMapper,
+    ) -> Self {
+        keyboard_mapper.map_key_equivalent(inner, use_key_equivalents)
+    }
+
+    pub(crate) fn from_keystroke(keystroke: Keystroke) -> Self {
+        let key = keystroke.key.clone();
+        let modifiers = keystroke.modifiers;
+        KeybindingKeystroke {
+            inner: keystroke,
+            display_modifiers: modifiers,
+            display_key: key,
+        }
+    }
+
+    /// Produces a representation of this key that Parse can understand.
+    pub fn unparse(&self) -> String {
+        unparse(&self.display_modifiers, &self.display_key)
+    }
+}
+
 fn is_printable_key(key: &str) -> bool {
     !matches!(
         key,
@@ -322,65 +343,15 @@ fn is_printable_key(key: &str) -> bool {
 
 impl std::fmt::Display for Keystroke {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        if self.modifiers.control {
-            #[cfg(target_os = "macos")]
-            f.write_char('^')?;
-
-            #[cfg(not(target_os = "macos"))]
-            write!(f, "ctrl-")?;
-        }
-        if self.modifiers.alt {
-            #[cfg(target_os = "macos")]
-            f.write_char('⌥')?;
-
-            #[cfg(not(target_os = "macos"))]
-            write!(f, "alt-")?;
-        }
-        if self.modifiers.platform {
-            #[cfg(target_os = "macos")]
-            f.write_char('⌘')?;
-
-            #[cfg(any(target_os = "linux", target_os = "freebsd"))]
-            f.write_char('❖')?;
-
-            #[cfg(target_os = "windows")]
-            f.write_char('⊞')?;
-        }
-        if self.modifiers.shift {
-            #[cfg(target_os = "macos")]
-            f.write_char('⇧')?;
+        display_modifiers(&self.modifiers, f)?;
+        display_key(&self.key, f)
+    }
+}
 
-            #[cfg(not(target_os = "macos"))]
-            write!(f, "shift-")?;
-        }
-        let key = match self.key.as_str() {
-            #[cfg(target_os = "macos")]
-            "backspace" => '⌫',
-            #[cfg(target_os = "macos")]
-            "up" => '↑',
-            #[cfg(target_os = "macos")]
-            "down" => '↓',
-            #[cfg(target_os = "macos")]
-            "left" => '←',
-            #[cfg(target_os = "macos")]
-            "right" => '→',
-            #[cfg(target_os = "macos")]
-            "tab" => '⇥',
-            #[cfg(target_os = "macos")]
-            "escape" => '⎋',
-            #[cfg(target_os = "macos")]
-            "shift" => '⇧',
-            #[cfg(target_os = "macos")]
-            "control" => '⌃',
-            #[cfg(target_os = "macos")]
-            "alt" => '⌥',
-            #[cfg(target_os = "macos")]
-            "platform" => '⌘',
-
-            key if key.len() == 1 => key.chars().next().unwrap().to_ascii_uppercase(),
-            key => return f.write_str(key),
-        };
-        f.write_char(key)
+impl std::fmt::Display for KeybindingKeystroke {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        display_modifiers(&self.display_modifiers, f)?;
+        display_key(&self.display_key, f)
     }
 }
 
@@ -600,3 +571,110 @@ pub struct Capslock {
     #[serde(default)]
     pub on: bool,
 }
+
+impl AsKeystroke for Keystroke {
+    fn as_keystroke(&self) -> &Keystroke {
+        self
+    }
+}
+
+impl AsKeystroke for KeybindingKeystroke {
+    fn as_keystroke(&self) -> &Keystroke {
+        &self.inner
+    }
+}
+
+fn display_modifiers(modifiers: &Modifiers, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+    if modifiers.control {
+        #[cfg(target_os = "macos")]
+        f.write_char('^')?;
+
+        #[cfg(not(target_os = "macos"))]
+        write!(f, "ctrl-")?;
+    }
+    if modifiers.alt {
+        #[cfg(target_os = "macos")]
+        f.write_char('⌥')?;
+
+        #[cfg(not(target_os = "macos"))]
+        write!(f, "alt-")?;
+    }
+    if modifiers.platform {
+        #[cfg(target_os = "macos")]
+        f.write_char('⌘')?;
+
+        #[cfg(any(target_os = "linux", target_os = "freebsd"))]
+        f.write_char('❖')?;
+
+        #[cfg(target_os = "windows")]
+        f.write_char('⊞')?;
+    }
+    if modifiers.shift {
+        #[cfg(target_os = "macos")]
+        f.write_char('⇧')?;
+
+        #[cfg(not(target_os = "macos"))]
+        write!(f, "shift-")?;
+    }
+    Ok(())
+}
+
+fn display_key(key: &str, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+    let key = match key {
+        #[cfg(target_os = "macos")]
+        "backspace" => '⌫',
+        #[cfg(target_os = "macos")]
+        "up" => '↑',
+        #[cfg(target_os = "macos")]
+        "down" => '↓',
+        #[cfg(target_os = "macos")]
+        "left" => '←',
+        #[cfg(target_os = "macos")]
+        "right" => '→',
+        #[cfg(target_os = "macos")]
+        "tab" => '⇥',
+        #[cfg(target_os = "macos")]
+        "escape" => '⎋',
+        #[cfg(target_os = "macos")]
+        "shift" => '⇧',
+        #[cfg(target_os = "macos")]
+        "control" => '⌃',
+        #[cfg(target_os = "macos")]
+        "alt" => '⌥',
+        #[cfg(target_os = "macos")]
+        "platform" => '⌘',
+
+        key if key.len() == 1 => key.chars().next().unwrap().to_ascii_uppercase(),
+        key => return f.write_str(key),
+    };
+    f.write_char(key)
+}
+
+#[inline]
+fn unparse(modifiers: &Modifiers, key: &str) -> String {
+    let mut result = String::new();
+    if modifiers.function {
+        result.push_str("fn-");
+    }
+    if modifiers.control {
+        result.push_str("ctrl-");
+    }
+    if modifiers.alt {
+        result.push_str("alt-");
+    }
+    if modifiers.platform {
+        #[cfg(target_os = "macos")]
+        result.push_str("cmd-");
+
+        #[cfg(any(target_os = "linux", target_os = "freebsd"))]
+        result.push_str("super-");
+
+        #[cfg(target_os = "windows")]
+        result.push_str("win-");
+    }
+    if modifiers.shift {
+        result.push_str("shift-");
+    }
+    result.push_str(&key);
+    result
+}

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

@@ -25,8 +25,8 @@ use xkbcommon::xkb::{self, Keycode, Keysym, State};
 use crate::{
     Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId,
     ForegroundExecutor, Keymap, LinuxDispatcher, Menu, MenuItem, OwnedMenu, PathPromptOptions,
-    Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow,
-    Point, Result, Task, WindowAppearance, WindowParams, px,
+    Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper,
+    PlatformTextSystem, PlatformWindow, Point, Result, Task, WindowAppearance, WindowParams, px,
 };
 
 #[cfg(any(feature = "wayland", feature = "x11"))]
@@ -144,6 +144,10 @@ impl<P: LinuxClient + 'static> Platform for P {
         self.keyboard_layout()
     }
 
+    fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
+        Rc::new(crate::DummyKeyboardMapper)
+    }
+
     fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>) {
         self.with_common(|common| common.callbacks.keyboard_layout_change = Some(callback));
     }

crates/gpui/src/platform/mac/keyboard.rs 🔗

@@ -1,8 +1,9 @@
+use collections::HashMap;
 use std::ffi::{CStr, c_void};
 
 use objc::{msg_send, runtime::Object, sel, sel_impl};
 
-use crate::PlatformKeyboardLayout;
+use crate::{KeybindingKeystroke, Keystroke, PlatformKeyboardLayout, PlatformKeyboardMapper};
 
 use super::{
     TISCopyCurrentKeyboardLayoutInputSource, TISGetInputSourceProperty, kTISPropertyInputSourceID,
@@ -14,6 +15,10 @@ pub(crate) struct MacKeyboardLayout {
     name: String,
 }
 
+pub(crate) struct MacKeyboardMapper {
+    key_equivalents: Option<HashMap<char, char>>,
+}
+
 impl PlatformKeyboardLayout for MacKeyboardLayout {
     fn id(&self) -> &str {
         &self.id
@@ -24,6 +29,27 @@ impl PlatformKeyboardLayout for MacKeyboardLayout {
     }
 }
 
+impl PlatformKeyboardMapper for MacKeyboardMapper {
+    fn map_key_equivalent(
+        &self,
+        mut keystroke: Keystroke,
+        use_key_equivalents: bool,
+    ) -> KeybindingKeystroke {
+        if use_key_equivalents && let Some(key_equivalents) = &self.key_equivalents {
+            if keystroke.key.chars().count() == 1
+                && let Some(key) = key_equivalents.get(&keystroke.key.chars().next().unwrap())
+            {
+                keystroke.key = key.to_string();
+            }
+        }
+        KeybindingKeystroke::from_keystroke(keystroke)
+    }
+
+    fn get_key_equivalents(&self) -> Option<&HashMap<char, char>> {
+        self.key_equivalents.as_ref()
+    }
+}
+
 impl MacKeyboardLayout {
     pub(crate) fn new() -> Self {
         unsafe {
@@ -47,3 +73,1428 @@ impl MacKeyboardLayout {
         }
     }
 }
+
+impl MacKeyboardMapper {
+    pub(crate) fn new(layout_id: &str) -> Self {
+        let key_equivalents = get_key_equivalents(layout_id);
+
+        Self { key_equivalents }
+    }
+}
+
+// On some keyboards (e.g. German QWERTZ) it is not possible to type the full ASCII range
+// without using option. This means that some of our built in keyboard shortcuts do not work
+// for those users.
+//
+// The way macOS solves this problem is to move shortcuts around so that they are all reachable,
+// even if the mnemonic changes. https://developer.apple.com/documentation/swiftui/keyboardshortcut/localization-swift.struct
+//
+// For example, cmd-> is the "switch window" shortcut because the > key is right above tab.
+// To ensure this doesn't cause problems for shortcuts defined for a QWERTY layout, apple moves
+// any shortcuts defined as cmd-> to cmd-:. Coincidentally this s also the same keyboard position
+// as cmd-> on a QWERTY layout.
+//
+// Another example is cmd-[ and cmd-], as they cannot be typed without option, those keys are remapped to cmd-ö
+// and cmd-ä. These shortcuts are not in the same position as a QWERTY keyboard, because on a QWERTZ keyboard
+// the + key is in the way; and shortcuts bound to cmd-+ are still typed as cmd-+ on either keyboard (though the
+// specific key moves)
+//
+// As far as I can tell, there's no way to query the mappings Apple uses except by rendering a menu with every
+// possible key combination, and inspecting the UI to see what it rendered. So that's what we did...
+//
+// These mappings were generated by running https://github.com/ConradIrwin/keyboard-inspector, tidying up the
+// output to remove languages with no mappings and other oddities, and converting it to a less verbose representation with:
+//  jq -s 'map(to_entries | map({key: .key, value: [(.value | to_entries | map(.key) | join("")), (.value | to_entries | map(.value) | join(""))]}) | from_entries) | add'
+// From there I used multi-cursor to produce this match statement.
+fn get_key_equivalents(layout_id: &str) -> Option<HashMap<char, char>> {
+    let mappings: &[(char, char)] = match layout_id {
+        "com.apple.keylayout.ABC-AZERTY" => &[
+            ('!', '1'),
+            ('"', '%'),
+            ('#', '3'),
+            ('$', '4'),
+            ('%', '5'),
+            ('&', '7'),
+            ('(', '9'),
+            (')', '0'),
+            ('*', '8'),
+            ('.', ';'),
+            ('/', ':'),
+            ('0', 'à'),
+            ('1', '&'),
+            ('2', 'é'),
+            ('3', '"'),
+            ('4', '\''),
+            ('5', '('),
+            ('6', '§'),
+            ('7', 'è'),
+            ('8', '!'),
+            ('9', 'ç'),
+            (':', '°'),
+            (';', ')'),
+            ('<', '.'),
+            ('>', '/'),
+            ('@', '2'),
+            ('[', '^'),
+            ('\'', 'ù'),
+            ('\\', '`'),
+            (']', '$'),
+            ('^', '6'),
+            ('`', '<'),
+            ('{', '¨'),
+            ('|', '£'),
+            ('}', '*'),
+            ('~', '>'),
+        ],
+        "com.apple.keylayout.ABC-QWERTZ" => &[
+            ('"', '`'),
+            ('#', '§'),
+            ('&', '/'),
+            ('(', ')'),
+            (')', '='),
+            ('*', '('),
+            ('/', 'ß'),
+            (':', 'Ü'),
+            (';', 'ü'),
+            ('<', ';'),
+            ('=', '*'),
+            ('>', ':'),
+            ('@', '"'),
+            ('[', 'ö'),
+            ('\'', '´'),
+            ('\\', '#'),
+            (']', 'ä'),
+            ('^', '&'),
+            ('`', '<'),
+            ('{', 'Ö'),
+            ('|', '\''),
+            ('}', 'Ä'),
+            ('~', '>'),
+        ],
+        "com.apple.keylayout.Albanian" => &[
+            ('"', '\''),
+            (':', 'Ç'),
+            (';', 'ç'),
+            ('<', ';'),
+            ('>', ':'),
+            ('@', '"'),
+            ('\'', '@'),
+            ('\\', 'ë'),
+            ('`', '<'),
+            ('|', 'Ë'),
+            ('~', '>'),
+        ],
+        "com.apple.keylayout.Austrian" => &[
+            ('"', '`'),
+            ('#', '§'),
+            ('&', '/'),
+            ('(', ')'),
+            (')', '='),
+            ('*', '('),
+            ('/', 'ß'),
+            (':', 'Ü'),
+            (';', 'ü'),
+            ('<', ';'),
+            ('=', '*'),
+            ('>', ':'),
+            ('@', '"'),
+            ('[', 'ö'),
+            ('\'', '´'),
+            ('\\', '#'),
+            (']', 'ä'),
+            ('^', '&'),
+            ('`', '<'),
+            ('{', 'Ö'),
+            ('|', '\''),
+            ('}', 'Ä'),
+            ('~', '>'),
+        ],
+        "com.apple.keylayout.Azeri" => &[
+            ('"', 'Ə'),
+            (',', 'ç'),
+            ('.', 'ş'),
+            ('/', '.'),
+            (':', 'I'),
+            (';', 'ı'),
+            ('<', 'Ç'),
+            ('>', 'Ş'),
+            ('?', ','),
+            ('W', 'Ü'),
+            ('[', 'ö'),
+            ('\'', 'ə'),
+            (']', 'ğ'),
+            ('w', 'ü'),
+            ('{', 'Ö'),
+            ('|', '/'),
+            ('}', 'Ğ'),
+        ],
+        "com.apple.keylayout.Belgian" => &[
+            ('!', '1'),
+            ('"', '%'),
+            ('#', '3'),
+            ('$', '4'),
+            ('%', '5'),
+            ('&', '7'),
+            ('(', '9'),
+            (')', '0'),
+            ('*', '8'),
+            ('.', ';'),
+            ('/', ':'),
+            ('0', 'à'),
+            ('1', '&'),
+            ('2', 'é'),
+            ('3', '"'),
+            ('4', '\''),
+            ('5', '('),
+            ('6', '§'),
+            ('7', 'è'),
+            ('8', '!'),
+            ('9', 'ç'),
+            (':', '°'),
+            (';', ')'),
+            ('<', '.'),
+            ('>', '/'),
+            ('@', '2'),
+            ('[', '^'),
+            ('\'', 'ù'),
+            ('\\', '`'),
+            (']', '$'),
+            ('^', '6'),
+            ('`', '<'),
+            ('{', '¨'),
+            ('|', '£'),
+            ('}', '*'),
+            ('~', '>'),
+        ],
+        "com.apple.keylayout.Brazilian-ABNT2" => &[
+            ('"', '`'),
+            ('/', 'ç'),
+            ('?', 'Ç'),
+            ('\'', '´'),
+            ('\\', '~'),
+            ('^', '¨'),
+            ('`', '\''),
+            ('|', '^'),
+            ('~', '"'),
+        ],
+        "com.apple.keylayout.Brazilian-Pro" => &[('^', 'ˆ'), ('~', '˜')],
+        "com.apple.keylayout.British" => &[('#', '£')],
+        "com.apple.keylayout.Canadian-CSA" => &[
+            ('"', 'È'),
+            ('/', 'é'),
+            ('<', '\''),
+            ('>', '"'),
+            ('?', 'É'),
+            ('[', '^'),
+            ('\'', 'è'),
+            ('\\', 'à'),
+            (']', 'ç'),
+            ('`', 'ù'),
+            ('{', '¨'),
+            ('|', 'À'),
+            ('}', 'Ç'),
+            ('~', 'Ù'),
+        ],
+        "com.apple.keylayout.Croatian" => &[
+            ('"', 'Ć'),
+            ('&', '\''),
+            ('(', ')'),
+            (')', '='),
+            ('*', '('),
+            (':', 'Č'),
+            (';', 'č'),
+            ('<', ';'),
+            ('=', '*'),
+            ('>', ':'),
+            ('@', '"'),
+            ('[', 'š'),
+            ('\'', 'ć'),
+            ('\\', 'ž'),
+            (']', 'đ'),
+            ('^', '&'),
+            ('`', '<'),
+            ('{', 'Š'),
+            ('|', 'Ž'),
+            ('}', 'Đ'),
+            ('~', '>'),
+        ],
+        "com.apple.keylayout.Croatian-PC" => &[
+            ('"', 'Ć'),
+            ('&', '/'),
+            ('(', ')'),
+            (')', '='),
+            ('*', '('),
+            ('/', '\''),
+            (':', 'Č'),
+            (';', 'č'),
+            ('<', ';'),
+            ('=', '*'),
+            ('>', ':'),
+            ('@', '"'),
+            ('[', 'š'),
+            ('\'', 'ć'),
+            ('\\', 'ž'),
+            (']', 'đ'),
+            ('^', '&'),
+            ('`', '<'),
+            ('{', 'Š'),
+            ('|', 'Ž'),
+            ('}', 'Đ'),
+            ('~', '>'),
+        ],
+        "com.apple.keylayout.Czech" => &[
+            ('!', '1'),
+            ('"', '!'),
+            ('#', '3'),
+            ('$', '4'),
+            ('%', '5'),
+            ('&', '7'),
+            ('(', '9'),
+            (')', '0'),
+            ('*', '8'),
+            ('+', '%'),
+            ('/', '\''),
+            ('0', 'é'),
+            ('1', '+'),
+            ('2', 'ě'),
+            ('3', 'š'),
+            ('4', 'č'),
+            ('5', 'ř'),
+            ('6', 'ž'),
+            ('7', 'ý'),
+            ('8', 'á'),
+            ('9', 'í'),
+            (':', '"'),
+            (';', 'ů'),
+            ('<', '?'),
+            ('>', ':'),
+            ('?', 'ˇ'),
+            ('@', '2'),
+            ('[', 'ú'),
+            ('\'', '§'),
+            (']', ')'),
+            ('^', '6'),
+            ('`', '¨'),
+            ('{', 'Ú'),
+            ('}', '('),
+            ('~', '`'),
+        ],
+        "com.apple.keylayout.Czech-QWERTY" => &[
+            ('!', '1'),
+            ('"', '!'),
+            ('#', '3'),
+            ('$', '4'),
+            ('%', '5'),
+            ('&', '7'),
+            ('(', '9'),
+            (')', '0'),
+            ('*', '8'),
+            ('+', '%'),
+            ('/', '\''),
+            ('0', 'é'),
+            ('1', '+'),
+            ('2', 'ě'),
+            ('3', 'š'),
+            ('4', 'č'),
+            ('5', 'ř'),
+            ('6', 'ž'),
+            ('7', 'ý'),
+            ('8', 'á'),
+            ('9', 'í'),
+            (':', '"'),
+            (';', 'ů'),
+            ('<', '?'),
+            ('>', ':'),
+            ('?', 'ˇ'),
+            ('@', '2'),
+            ('[', 'ú'),
+            ('\'', '§'),
+            (']', ')'),
+            ('^', '6'),
+            ('`', '¨'),
+            ('{', 'Ú'),
+            ('}', '('),
+            ('~', '`'),
+        ],
+        "com.apple.keylayout.Danish" => &[
+            ('"', '^'),
+            ('$', '€'),
+            ('&', '/'),
+            ('(', ')'),
+            (')', '='),
+            ('*', '('),
+            ('/', '´'),
+            (':', 'Å'),
+            (';', 'å'),
+            ('<', ';'),
+            ('=', '`'),
+            ('>', ':'),
+            ('@', '"'),
+            ('[', 'æ'),
+            ('\'', '¨'),
+            ('\\', '\''),
+            (']', 'ø'),
+            ('^', '&'),
+            ('`', '<'),
+            ('{', 'Æ'),
+            ('|', '*'),
+            ('}', 'Ø'),
+            ('~', '>'),
+        ],
+        "com.apple.keylayout.Faroese" => &[
+            ('"', 'Ø'),
+            ('$', '€'),
+            ('&', '/'),
+            ('(', ')'),
+            (')', '='),
+            ('*', '('),
+            ('/', '´'),
+            (':', 'Æ'),
+            (';', 'æ'),
+            ('<', ';'),
+            ('=', '`'),
+            ('>', ':'),
+            ('@', '"'),
+            ('[', 'å'),
+            ('\'', 'ø'),
+            ('\\', '\''),
+            (']', 'ð'),
+            ('^', '&'),
+            ('`', '<'),
+            ('{', 'Å'),
+            ('|', '*'),
+            ('}', 'Ð'),
+            ('~', '>'),
+        ],
+        "com.apple.keylayout.Finnish" => &[
+            ('"', '^'),
+            ('$', '€'),
+            ('&', '/'),
+            ('(', ')'),
+            (')', '='),
+            ('*', '('),
+            ('/', '´'),
+            (':', 'Å'),
+            (';', 'å'),
+            ('<', ';'),
+            ('=', '`'),
+            ('>', ':'),
+            ('@', '"'),
+            ('[', 'ö'),
+            ('\'', '¨'),
+            ('\\', '\''),
+            (']', 'ä'),
+            ('^', '&'),
+            ('`', '<'),
+            ('{', 'Ö'),
+            ('|', '*'),
+            ('}', 'Ä'),
+            ('~', '>'),
+        ],
+        "com.apple.keylayout.FinnishExtended" => &[
+            ('"', 'ˆ'),
+            ('$', '€'),
+            ('&', '/'),
+            ('(', ')'),
+            (')', '='),
+            ('*', '('),
+            ('/', '´'),
+            (':', 'Å'),
+            (';', 'å'),
+            ('<', ';'),
+            ('=', '`'),
+            ('>', ':'),
+            ('@', '"'),
+            ('[', 'ö'),
+            ('\'', '¨'),
+            ('\\', '\''),
+            (']', 'ä'),
+            ('^', '&'),
+            ('`', '<'),
+            ('{', 'Ö'),
+            ('|', '*'),
+            ('}', 'Ä'),
+            ('~', '>'),
+        ],
+        "com.apple.keylayout.FinnishSami-PC" => &[
+            ('"', 'ˆ'),
+            ('&', '/'),
+            ('(', ')'),
+            (')', '='),
+            ('*', '('),
+            ('/', '´'),
+            (':', 'Å'),
+            (';', 'å'),
+            ('<', ';'),
+            ('=', '`'),
+            ('>', ':'),
+            ('@', '"'),
+            ('[', 'ö'),
+            ('\'', '¨'),
+            ('\\', '@'),
+            (']', 'ä'),
+            ('^', '&'),
+            ('`', '<'),
+            ('{', 'Ö'),
+            ('|', '*'),
+            ('}', 'Ä'),
+            ('~', '>'),
+        ],
+        "com.apple.keylayout.French" => &[
+            ('!', '1'),
+            ('"', '%'),
+            ('#', '3'),
+            ('$', '4'),
+            ('%', '5'),
+            ('&', '7'),
+            ('(', '9'),
+            (')', '0'),
+            ('*', '8'),
+            ('.', ';'),
+            ('/', ':'),
+            ('0', 'à'),
+            ('1', '&'),
+            ('2', 'é'),
+            ('3', '"'),
+            ('4', '\''),
+            ('5', '('),
+            ('6', '§'),
+            ('7', 'è'),
+            ('8', '!'),
+            ('9', 'ç'),
+            (':', '°'),
+            (';', ')'),
+            ('<', '.'),
+            ('>', '/'),
+            ('@', '2'),
+            ('[', '^'),
+            ('\'', 'ù'),
+            ('\\', '`'),
+            (']', '$'),
+            ('^', '6'),
+            ('`', '<'),
+            ('{', '¨'),
+            ('|', '£'),
+            ('}', '*'),
+            ('~', '>'),
+        ],
+        "com.apple.keylayout.French-PC" => &[
+            ('!', '1'),
+            ('"', '%'),
+            ('#', '3'),
+            ('$', '4'),
+            ('%', '5'),
+            ('&', '7'),
+            ('(', '9'),
+            (')', '0'),
+            ('*', '8'),
+            ('-', ')'),
+            ('.', ';'),
+            ('/', ':'),
+            ('0', 'à'),
+            ('1', '&'),
+            ('2', 'é'),
+            ('3', '"'),
+            ('4', '\''),
+            ('5', '('),
+            ('6', '-'),
+            ('7', 'è'),
+            ('8', '_'),
+            ('9', 'ç'),
+            (':', '§'),
+            (';', '!'),
+            ('<', '.'),
+            ('>', '/'),
+            ('@', '2'),
+            ('[', '^'),
+            ('\'', 'ù'),
+            ('\\', '*'),
+            (']', '$'),
+            ('^', '6'),
+            ('_', '°'),
+            ('`', '<'),
+            ('{', '¨'),
+            ('|', 'μ'),
+            ('}', '£'),
+            ('~', '>'),
+        ],
+        "com.apple.keylayout.French-numerical" => &[
+            ('!', '1'),
+            ('"', '%'),
+            ('#', '3'),
+            ('$', '4'),
+            ('%', '5'),
+            ('&', '7'),
+            ('(', '9'),
+            (')', '0'),
+            ('*', '8'),
+            ('.', ';'),
+            ('/', ':'),
+            ('0', 'à'),
+            ('1', '&'),
+            ('2', 'é'),
+            ('3', '"'),
+            ('4', '\''),
+            ('5', '('),
+            ('6', '§'),
+            ('7', 'è'),
+            ('8', '!'),
+            ('9', 'ç'),
+            (':', '°'),
+            (';', ')'),
+            ('<', '.'),
+            ('>', '/'),
+            ('@', '2'),
+            ('[', '^'),
+            ('\'', 'ù'),
+            ('\\', '`'),
+            (']', '$'),
+            ('^', '6'),
+            ('`', '<'),
+            ('{', '¨'),
+            ('|', '£'),
+            ('}', '*'),
+            ('~', '>'),
+        ],
+        "com.apple.keylayout.German" => &[
+            ('"', '`'),
+            ('#', '§'),
+            ('&', '/'),
+            ('(', ')'),
+            (')', '='),
+            ('*', '('),
+            ('/', 'ß'),
+            (':', 'Ü'),
+            (';', 'ü'),
+            ('<', ';'),
+            ('=', '*'),
+            ('>', ':'),
+            ('@', '"'),
+            ('[', 'ö'),
+            ('\'', '´'),
+            ('\\', '#'),
+            (']', 'ä'),
+            ('^', '&'),
+            ('`', '<'),
+            ('{', 'Ö'),
+            ('|', '\''),
+            ('}', 'Ä'),
+            ('~', '>'),
+        ],
+        "com.apple.keylayout.German-DIN-2137" => &[
+            ('"', '`'),
+            ('#', '§'),
+            ('&', '/'),
+            ('(', ')'),
+            (')', '='),
+            ('*', '('),
+            ('/', 'ß'),
+            (':', 'Ü'),
+            (';', 'ü'),
+            ('<', ';'),
+            ('=', '*'),
+            ('>', ':'),
+            ('@', '"'),
+            ('[', 'ö'),
+            ('\'', '´'),
+            ('\\', '#'),
+            (']', 'ä'),
+            ('^', '&'),
+            ('`', '<'),
+            ('{', 'Ö'),
+            ('|', '\''),
+            ('}', 'Ä'),
+            ('~', '>'),
+        ],
+        "com.apple.keylayout.Hawaiian" => &[('\'', 'ʻ')],
+        "com.apple.keylayout.Hungarian" => &[
+            ('!', '\''),
+            ('"', 'Á'),
+            ('#', '+'),
+            ('$', '!'),
+            ('&', '='),
+            ('(', ')'),
+            (')', 'Ö'),
+            ('*', '('),
+            ('+', 'Ó'),
+            ('/', 'ü'),
+            ('0', 'ö'),
+            (':', 'É'),
+            (';', 'é'),
+            ('<', 'Ü'),
+            ('=', 'ó'),
+            ('>', ':'),
+            ('@', '"'),
+            ('[', 'ő'),
+            ('\'', 'á'),
+            ('\\', 'ű'),
+            (']', 'ú'),
+            ('^', '/'),
+            ('`', 'í'),
+            ('{', 'Ő'),
+            ('|', 'Ű'),
+            ('}', 'Ú'),
+            ('~', 'Í'),
+        ],
+        "com.apple.keylayout.Hungarian-QWERTY" => &[
+            ('!', '\''),
+            ('"', 'Á'),
+            ('#', '+'),
+            ('$', '!'),
+            ('&', '='),
+            ('(', ')'),
+            (')', 'Ö'),
+            ('*', '('),
+            ('+', 'Ó'),
+            ('/', 'ü'),
+            ('0', 'ö'),
+            (':', 'É'),
+            (';', 'é'),
+            ('<', 'Ü'),
+            ('=', 'ó'),
+            ('>', ':'),
+            ('@', '"'),
+            ('[', 'ő'),
+            ('\'', 'á'),
+            ('\\', 'ű'),
+            (']', 'ú'),
+            ('^', '/'),
+            ('`', 'í'),
+            ('{', 'Ő'),
+            ('|', 'Ű'),
+            ('}', 'Ú'),
+            ('~', 'Í'),
+        ],
+        "com.apple.keylayout.Icelandic" => &[
+            ('"', 'Ö'),
+            ('&', '/'),
+            ('(', ')'),
+            (')', '='),
+            ('*', '('),
+            ('/', '\''),
+            (':', 'Ð'),
+            (';', 'ð'),
+            ('<', ';'),
+            ('=', '*'),
+            ('>', ':'),
+            ('@', '"'),
+            ('[', 'æ'),
+            ('\'', 'ö'),
+            ('\\', 'þ'),
+            (']', '´'),
+            ('^', '&'),
+            ('`', '<'),
+            ('{', 'Æ'),
+            ('|', 'Þ'),
+            ('}', '´'),
+            ('~', '>'),
+        ],
+        "com.apple.keylayout.Irish" => &[('#', '£')],
+        "com.apple.keylayout.IrishExtended" => &[('#', '£')],
+        "com.apple.keylayout.Italian" => &[
+            ('!', '1'),
+            ('"', '%'),
+            ('#', '3'),
+            ('$', '4'),
+            ('%', '5'),
+            ('&', '7'),
+            ('(', '9'),
+            (')', '0'),
+            ('*', '8'),
+            (',', ';'),
+            ('.', ':'),
+            ('/', ','),
+            ('0', 'é'),
+            ('1', '&'),
+            ('2', '"'),
+            ('3', '\''),
+            ('4', '('),
+            ('5', 'ç'),
+            ('6', 'è'),
+            ('7', ')'),
+            ('8', '£'),
+            ('9', 'à'),
+            (':', '!'),
+            (';', 'ò'),
+            ('<', '.'),
+            ('>', '/'),
+            ('@', '2'),
+            ('[', 'ì'),
+            ('\'', 'ù'),
+            ('\\', '§'),
+            (']', '$'),
+            ('^', '6'),
+            ('`', '<'),
+            ('{', '^'),
+            ('|', '°'),
+            ('}', '*'),
+            ('~', '>'),
+        ],
+        "com.apple.keylayout.Italian-Pro" => &[
+            ('"', '^'),
+            ('#', '£'),
+            ('&', '/'),
+            ('(', ')'),
+            (')', '='),
+            ('*', '('),
+            ('/', '\''),
+            (':', 'é'),
+            (';', 'è'),
+            ('<', ';'),
+            ('=', '*'),
+            ('>', ':'),
+            ('@', '"'),
+            ('[', 'ò'),
+            ('\'', 'ì'),
+            ('\\', 'ù'),
+            (']', 'à'),
+            ('^', '&'),
+            ('`', '<'),
+            ('{', 'ç'),
+            ('|', '§'),
+            ('}', '°'),
+            ('~', '>'),
+        ],
+        "com.apple.keylayout.LatinAmerican" => &[
+            ('"', '¨'),
+            ('&', '/'),
+            ('(', ')'),
+            (')', '='),
+            ('*', '('),
+            ('/', '\''),
+            (':', 'Ñ'),
+            (';', 'ñ'),
+            ('<', ';'),
+            ('=', '*'),
+            ('>', ':'),
+            ('@', '"'),
+            ('[', '{'),
+            ('\'', '´'),
+            ('\\', '¿'),
+            (']', '}'),
+            ('^', '&'),
+            ('`', '<'),
+            ('{', '['),
+            ('|', '¡'),
+            ('}', ']'),
+            ('~', '>'),
+        ],
+        "com.apple.keylayout.Lithuanian" => &[
+            ('!', 'Ą'),
+            ('#', 'Ę'),
+            ('$', 'Ė'),
+            ('%', 'Į'),
+            ('&', 'Ų'),
+            ('*', 'Ū'),
+            ('+', 'Ž'),
+            ('1', 'ą'),
+            ('2', 'č'),
+            ('3', 'ę'),
+            ('4', 'ė'),
+            ('5', 'į'),
+            ('6', 'š'),
+            ('7', 'ų'),
+            ('8', 'ū'),
+            ('=', 'ž'),
+            ('@', 'Č'),
+            ('^', 'Š'),
+        ],
+        "com.apple.keylayout.Maltese" => &[
+            ('#', '£'),
+            ('[', 'ġ'),
+            (']', 'ħ'),
+            ('`', 'ż'),
+            ('{', 'Ġ'),
+            ('}', 'Ħ'),
+            ('~', 'Ż'),
+        ],
+        "com.apple.keylayout.NorthernSami" => &[
+            ('"', 'Ŋ'),
+            ('&', '/'),
+            ('(', ')'),
+            (')', '='),
+            ('*', '('),
+            ('/', '´'),
+            (':', 'Å'),
+            (';', 'å'),
+            ('<', ';'),
+            ('=', '`'),
+            ('>', ':'),
+            ('@', '"'),
+            ('Q', 'Á'),
+            ('W', 'Š'),
+            ('X', 'Č'),
+            ('[', 'ø'),
+            ('\'', 'ŋ'),
+            ('\\', 'đ'),
+            (']', 'æ'),
+            ('^', '&'),
+            ('`', 'ž'),
+            ('q', 'á'),
+            ('w', 'š'),
+            ('x', 'č'),
+            ('{', 'Ø'),
+            ('|', 'Đ'),
+            ('}', 'Æ'),
+            ('~', 'Ž'),
+        ],
+        "com.apple.keylayout.Norwegian" => &[
+            ('"', '^'),
+            ('&', '/'),
+            ('(', ')'),
+            (')', '='),
+            ('*', '('),
+            ('/', '´'),
+            (':', 'Å'),
+            (';', 'å'),
+            ('<', ';'),
+            ('=', '`'),
+            ('>', ':'),
+            ('@', '"'),
+            ('[', 'ø'),
+            ('\'', '¨'),
+            ('\\', '@'),
+            (']', 'æ'),
+            ('^', '&'),
+            ('`', '<'),
+            ('{', 'Ø'),
+            ('|', '*'),
+            ('}', 'Æ'),
+            ('~', '>'),
+        ],
+        "com.apple.keylayout.NorwegianExtended" => &[
+            ('"', 'ˆ'),
+            ('&', '/'),
+            ('(', ')'),
+            (')', '='),
+            ('*', '('),
+            ('/', '´'),
+            (':', 'Å'),
+            (';', 'å'),
+            ('<', ';'),
+            ('=', '`'),
+            ('>', ':'),
+            ('@', '"'),
+            ('[', 'ø'),
+            ('\\', '@'),
+            (']', 'æ'),
+            ('`', '<'),
+            ('}', 'Æ'),
+            ('~', '>'),
+        ],
+        "com.apple.keylayout.NorwegianSami-PC" => &[
+            ('"', 'ˆ'),
+            ('&', '/'),
+            ('(', ')'),
+            (')', '='),
+            ('*', '('),
+            ('/', '´'),
+            (':', 'Å'),
+            (';', 'å'),
+            ('<', ';'),
+            ('=', '`'),
+            ('>', ':'),
+            ('@', '"'),
+            ('[', 'ø'),
+            ('\'', '¨'),
+            ('\\', '@'),
+            (']', 'æ'),
+            ('^', '&'),
+            ('`', '<'),
+            ('{', 'Ø'),
+            ('|', '*'),
+            ('}', 'Æ'),
+            ('~', '>'),
+        ],
+        "com.apple.keylayout.Polish" => &[
+            ('!', '§'),
+            ('"', 'ę'),
+            ('#', '!'),
+            ('$', '?'),
+            ('%', '+'),
+            ('&', ':'),
+            ('(', '/'),
+            (')', '"'),
+            ('*', '_'),
+            ('+', ']'),
+            (',', '.'),
+            ('.', ','),
+            ('/', 'ż'),
+            (':', 'Ł'),
+            (';', 'ł'),
+            ('<', 'ś'),
+            ('=', '['),
+            ('>', 'ń'),
+            ('?', 'Ż'),
+            ('@', '%'),
+            ('[', 'ó'),
+            ('\'', 'ą'),
+            ('\\', ';'),
+            (']', '('),
+            ('^', '='),
+            ('_', 'ć'),
+            ('`', '<'),
+            ('{', 'ź'),
+            ('|', '$'),
+            ('}', ')'),
+            ('~', '>'),
+        ],
+        "com.apple.keylayout.Portuguese" => &[
+            ('"', '`'),
+            ('&', '/'),
+            ('(', ')'),
+            (')', '='),
+            ('*', '('),
+            ('/', '\''),
+            (':', 'ª'),
+            (';', 'º'),
+            ('<', ';'),
+            ('=', '*'),
+            ('>', ':'),
+            ('@', '"'),
+            ('[', 'ç'),
+            ('\'', '´'),
+            (']', '~'),
+            ('^', '&'),
+            ('`', '<'),
+            ('{', 'Ç'),
+            ('}', '^'),
+            ('~', '>'),
+        ],
+        "com.apple.keylayout.Sami-PC" => &[
+            ('"', 'Ŋ'),
+            ('&', '/'),
+            ('(', ')'),
+            (')', '='),
+            ('*', '('),
+            ('/', '´'),
+            (':', 'Å'),
+            (';', 'å'),
+            ('<', ';'),
+            ('=', '`'),
+            ('>', ':'),
+            ('@', '"'),
+            ('Q', 'Á'),
+            ('W', 'Š'),
+            ('X', 'Č'),
+            ('[', 'ø'),
+            ('\'', 'ŋ'),
+            ('\\', 'đ'),
+            (']', 'æ'),
+            ('^', '&'),
+            ('`', 'ž'),
+            ('q', 'á'),
+            ('w', 'š'),
+            ('x', 'č'),
+            ('{', 'Ø'),
+            ('|', 'Đ'),
+            ('}', 'Æ'),
+            ('~', 'Ž'),
+        ],
+        "com.apple.keylayout.Serbian-Latin" => &[
+            ('"', 'Ć'),
+            ('&', '\''),
+            ('(', ')'),
+            (')', '='),
+            ('*', '('),
+            (':', 'Č'),
+            (';', 'č'),
+            ('<', ';'),
+            ('=', '*'),
+            ('>', ':'),
+            ('@', '"'),
+            ('[', 'š'),
+            ('\'', 'ć'),
+            ('\\', 'ž'),
+            (']', 'đ'),
+            ('^', '&'),
+            ('`', '<'),
+            ('{', 'Š'),
+            ('|', 'Ž'),
+            ('}', 'Đ'),
+            ('~', '>'),
+        ],
+        "com.apple.keylayout.Slovak" => &[
+            ('!', '1'),
+            ('"', '!'),
+            ('#', '3'),
+            ('$', '4'),
+            ('%', '5'),
+            ('&', '7'),
+            ('(', '9'),
+            (')', '0'),
+            ('*', '8'),
+            ('+', '%'),
+            ('/', '\''),
+            ('0', 'é'),
+            ('1', '+'),
+            ('2', 'ľ'),
+            ('3', 'š'),
+            ('4', 'č'),
+            ('5', 'ť'),
+            ('6', 'ž'),
+            ('7', 'ý'),
+            ('8', 'á'),
+            ('9', 'í'),
+            (':', '"'),
+            (';', 'ô'),
+            ('<', '?'),
+            ('>', ':'),
+            ('?', 'ˇ'),
+            ('@', '2'),
+            ('[', 'ú'),
+            ('\'', '§'),
+            (']', 'ä'),
+            ('^', '6'),
+            ('`', 'ň'),
+            ('{', 'Ú'),
+            ('}', 'Ä'),
+            ('~', 'Ň'),
+        ],
+        "com.apple.keylayout.Slovak-QWERTY" => &[
+            ('!', '1'),
+            ('"', '!'),
+            ('#', '3'),
+            ('$', '4'),
+            ('%', '5'),
+            ('&', '7'),
+            ('(', '9'),
+            (')', '0'),
+            ('*', '8'),
+            ('+', '%'),
+            ('/', '\''),
+            ('0', 'é'),
+            ('1', '+'),
+            ('2', 'ľ'),
+            ('3', 'š'),
+            ('4', 'č'),
+            ('5', 'ť'),
+            ('6', 'ž'),
+            ('7', 'ý'),
+            ('8', 'á'),
+            ('9', 'í'),
+            (':', '"'),
+            (';', 'ô'),
+            ('<', '?'),
+            ('>', ':'),
+            ('?', 'ˇ'),
+            ('@', '2'),
+            ('[', 'ú'),
+            ('\'', '§'),
+            (']', 'ä'),
+            ('^', '6'),
+            ('`', 'ň'),
+            ('{', 'Ú'),
+            ('}', 'Ä'),
+            ('~', 'Ň'),
+        ],
+        "com.apple.keylayout.Slovenian" => &[
+            ('"', 'Ć'),
+            ('&', '\''),
+            ('(', ')'),
+            (')', '='),
+            ('*', '('),
+            (':', 'Č'),
+            (';', 'č'),
+            ('<', ';'),
+            ('=', '*'),
+            ('>', ':'),
+            ('@', '"'),
+            ('[', 'š'),
+            ('\'', 'ć'),
+            ('\\', 'ž'),
+            (']', 'đ'),
+            ('^', '&'),
+            ('`', '<'),
+            ('{', 'Š'),
+            ('|', 'Ž'),
+            ('}', 'Đ'),
+            ('~', '>'),
+        ],
+        "com.apple.keylayout.Spanish" => &[
+            ('!', '¡'),
+            ('"', '¨'),
+            ('.', 'ç'),
+            ('/', '.'),
+            (':', 'º'),
+            (';', '´'),
+            ('<', '¿'),
+            ('>', 'Ç'),
+            ('@', '!'),
+            ('[', 'ñ'),
+            ('\'', '`'),
+            ('\\', '\''),
+            (']', ';'),
+            ('^', '/'),
+            ('`', '<'),
+            ('{', 'Ñ'),
+            ('|', '"'),
+            ('}', ':'),
+            ('~', '>'),
+        ],
+        "com.apple.keylayout.Spanish-ISO" => &[
+            ('"', '¨'),
+            ('#', '·'),
+            ('&', '/'),
+            ('(', ')'),
+            (')', '='),
+            ('*', '('),
+            ('.', 'ç'),
+            ('/', '.'),
+            (':', 'º'),
+            (';', '´'),
+            ('<', '¿'),
+            ('>', 'Ç'),
+            ('@', '"'),
+            ('[', 'ñ'),
+            ('\'', '`'),
+            ('\\', '\''),
+            (']', ';'),
+            ('^', '&'),
+            ('`', '<'),
+            ('{', 'Ñ'),
+            ('|', '"'),
+            ('}', '`'),
+            ('~', '>'),
+        ],
+        "com.apple.keylayout.Swedish" => &[
+            ('"', '^'),
+            ('$', '€'),
+            ('&', '/'),
+            ('(', ')'),
+            (')', '='),
+            ('*', '('),
+            ('/', '´'),
+            (':', 'Å'),
+            (';', 'å'),
+            ('<', ';'),
+            ('=', '`'),
+            ('>', ':'),
+            ('@', '"'),
+            ('[', 'ö'),
+            ('\'', '¨'),
+            ('\\', '\''),
+            (']', 'ä'),
+            ('^', '&'),
+            ('`', '<'),
+            ('{', 'Ö'),
+            ('|', '*'),
+            ('}', 'Ä'),
+            ('~', '>'),
+        ],
+        "com.apple.keylayout.Swedish-Pro" => &[
+            ('"', '^'),
+            ('$', '€'),
+            ('&', '/'),
+            ('(', ')'),
+            (')', '='),
+            ('*', '('),
+            ('/', '´'),
+            (':', 'Å'),
+            (';', 'å'),
+            ('<', ';'),
+            ('=', '`'),
+            ('>', ':'),
+            ('@', '"'),
+            ('[', 'ö'),
+            ('\'', '¨'),
+            ('\\', '\''),
+            (']', 'ä'),
+            ('^', '&'),
+            ('`', '<'),
+            ('{', 'Ö'),
+            ('|', '*'),
+            ('}', 'Ä'),
+            ('~', '>'),
+        ],
+        "com.apple.keylayout.SwedishSami-PC" => &[
+            ('"', 'ˆ'),
+            ('&', '/'),
+            ('(', ')'),
+            (')', '='),
+            ('*', '('),
+            ('/', '´'),
+            (':', 'Å'),
+            (';', 'å'),
+            ('<', ';'),
+            ('=', '`'),
+            ('>', ':'),
+            ('@', '"'),
+            ('[', 'ö'),
+            ('\'', '¨'),
+            ('\\', '@'),
+            (']', 'ä'),
+            ('^', '&'),
+            ('`', '<'),
+            ('{', 'Ö'),
+            ('|', '*'),
+            ('}', 'Ä'),
+            ('~', '>'),
+        ],
+        "com.apple.keylayout.SwissFrench" => &[
+            ('!', '+'),
+            ('"', '`'),
+            ('#', '*'),
+            ('$', 'ç'),
+            ('&', '/'),
+            ('(', ')'),
+            (')', '='),
+            ('*', '('),
+            ('+', '!'),
+            ('/', '\''),
+            (':', 'ü'),
+            (';', 'è'),
+            ('<', ';'),
+            ('=', '¨'),
+            ('>', ':'),
+            ('@', '"'),
+            ('[', 'é'),
+            ('\'', '^'),
+            ('\\', '$'),
+            (']', 'à'),
+            ('^', '&'),
+            ('`', '<'),
+            ('{', 'ö'),
+            ('|', '£'),
+            ('}', 'ä'),
+            ('~', '>'),
+        ],
+        "com.apple.keylayout.SwissGerman" => &[
+            ('!', '+'),
+            ('"', '`'),
+            ('#', '*'),
+            ('$', 'ç'),
+            ('&', '/'),
+            ('(', ')'),
+            (')', '='),
+            ('*', '('),
+            ('+', '!'),
+            ('/', '\''),
+            (':', 'è'),
+            (';', 'ü'),
+            ('<', ';'),
+            ('=', '¨'),
+            ('>', ':'),
+            ('@', '"'),
+            ('[', 'ö'),
+            ('\'', '^'),
+            ('\\', '$'),
+            (']', 'ä'),
+            ('^', '&'),
+            ('`', '<'),
+            ('{', 'é'),
+            ('|', '£'),
+            ('}', 'à'),
+            ('~', '>'),
+        ],
+        "com.apple.keylayout.Turkish" => &[
+            ('"', '-'),
+            ('#', '"'),
+            ('$', '\''),
+            ('%', '('),
+            ('&', ')'),
+            ('(', '%'),
+            (')', ':'),
+            ('*', '_'),
+            (',', 'ö'),
+            ('-', 'ş'),
+            ('.', 'ç'),
+            ('/', '.'),
+            (':', '$'),
+            ('<', 'Ö'),
+            ('>', 'Ç'),
+            ('@', '*'),
+            ('[', 'ğ'),
+            ('\'', ','),
+            ('\\', 'ü'),
+            (']', 'ı'),
+            ('^', '/'),
+            ('_', 'Ş'),
+            ('`', '<'),
+            ('{', 'Ğ'),
+            ('|', 'Ü'),
+            ('}', 'I'),
+            ('~', '>'),
+        ],
+        "com.apple.keylayout.Turkish-QWERTY-PC" => &[
+            ('"', 'I'),
+            ('#', '^'),
+            ('$', '+'),
+            ('&', '/'),
+            ('(', ')'),
+            (')', '='),
+            ('*', '('),
+            ('+', ':'),
+            (',', 'ö'),
+            ('.', 'ç'),
+            ('/', '*'),
+            (':', 'Ş'),
+            (';', 'ş'),
+            ('<', 'Ö'),
+            ('=', '.'),
+            ('>', 'Ç'),
+            ('@', '\''),
+            ('[', 'ğ'),
+            ('\'', 'ı'),
+            ('\\', ','),
+            (']', 'ü'),
+            ('^', '&'),
+            ('`', '<'),
+            ('{', 'Ğ'),
+            ('|', ';'),
+            ('}', 'Ü'),
+            ('~', '>'),
+        ],
+        "com.apple.keylayout.Turkish-Standard" => &[
+            ('"', 'Ş'),
+            ('#', '^'),
+            ('&', '\''),
+            ('(', ')'),
+            (')', '='),
+            ('*', '('),
+            (',', '.'),
+            ('.', ','),
+            (':', 'Ç'),
+            (';', 'ç'),
+            ('<', ':'),
+            ('=', '*'),
+            ('>', ';'),
+            ('@', '"'),
+            ('[', 'ğ'),
+            ('\'', 'ş'),
+            ('\\', 'ü'),
+            (']', 'ı'),
+            ('^', '&'),
+            ('`', 'ö'),
+            ('{', 'Ğ'),
+            ('|', 'Ü'),
+            ('}', 'I'),
+            ('~', 'Ö'),
+        ],
+        "com.apple.keylayout.Turkmen" => &[
+            ('C', 'Ç'),
+            ('Q', 'Ä'),
+            ('V', 'Ý'),
+            ('X', 'Ü'),
+            ('[', 'ň'),
+            ('\\', 'ş'),
+            (']', 'ö'),
+            ('^', '№'),
+            ('`', 'ž'),
+            ('c', 'ç'),
+            ('q', 'ä'),
+            ('v', 'ý'),
+            ('x', 'ü'),
+            ('{', 'Ň'),
+            ('|', 'Ş'),
+            ('}', 'Ö'),
+            ('~', 'Ž'),
+        ],
+        "com.apple.keylayout.USInternational-PC" => &[('^', 'ˆ'), ('~', '˜')],
+        "com.apple.keylayout.Welsh" => &[('#', '£')],
+
+        _ => return None,
+    };
+
+    Some(HashMap::from_iter(mappings.iter().cloned()))
+}

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

@@ -1,5 +1,5 @@
 use super::{
-    BoolExt, MacKeyboardLayout,
+    BoolExt, MacKeyboardLayout, MacKeyboardMapper,
     attributed_string::{NSAttributedString, NSMutableAttributedString},
     events::key_to_native,
     renderer,
@@ -8,8 +8,9 @@ use crate::{
     Action, AnyWindowHandle, BackgroundExecutor, ClipboardEntry, ClipboardItem, ClipboardString,
     CursorStyle, ForegroundExecutor, Image, ImageFormat, KeyContext, Keymap, MacDispatcher,
     MacDisplay, MacWindow, Menu, MenuItem, OsMenu, OwnedMenu, PathPromptOptions, Platform,
-    PlatformDisplay, PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow, Result,
-    SemanticVersion, SystemMenuType, Task, WindowAppearance, WindowParams, hash,
+    PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem,
+    PlatformWindow, Result, SemanticVersion, SystemMenuType, Task, WindowAppearance, WindowParams,
+    hash,
 };
 use anyhow::{Context as _, anyhow};
 use block::ConcreteBlock;
@@ -171,6 +172,7 @@ pub(crate) struct MacPlatformState {
     finish_launching: Option<Box<dyn FnOnce()>>,
     dock_menu: Option<id>,
     menus: Option<Vec<OwnedMenu>>,
+    keyboard_mapper: Rc<MacKeyboardMapper>,
 }
 
 impl Default for MacPlatform {
@@ -189,6 +191,9 @@ impl MacPlatform {
         #[cfg(not(feature = "font-kit"))]
         let text_system = Arc::new(crate::NoopTextSystem::new());
 
+        let keyboard_layout = MacKeyboardLayout::new();
+        let keyboard_mapper = Rc::new(MacKeyboardMapper::new(keyboard_layout.id()));
+
         Self(Mutex::new(MacPlatformState {
             headless,
             text_system,
@@ -209,6 +214,7 @@ impl MacPlatform {
             dock_menu: None,
             on_keyboard_layout_change: None,
             menus: None,
+            keyboard_mapper,
         }))
     }
 
@@ -348,19 +354,19 @@ impl MacPlatform {
                             let mut mask = NSEventModifierFlags::empty();
                             for (modifier, flag) in &[
                                 (
-                                    keystroke.modifiers.platform,
+                                    keystroke.display_modifiers.platform,
                                     NSEventModifierFlags::NSCommandKeyMask,
                                 ),
                                 (
-                                    keystroke.modifiers.control,
+                                    keystroke.display_modifiers.control,
                                     NSEventModifierFlags::NSControlKeyMask,
                                 ),
                                 (
-                                    keystroke.modifiers.alt,
+                                    keystroke.display_modifiers.alt,
                                     NSEventModifierFlags::NSAlternateKeyMask,
                                 ),
                                 (
-                                    keystroke.modifiers.shift,
+                                    keystroke.display_modifiers.shift,
                                     NSEventModifierFlags::NSShiftKeyMask,
                                 ),
                             ] {
@@ -373,7 +379,7 @@ impl MacPlatform {
                                 .initWithTitle_action_keyEquivalent_(
                                     ns_string(name),
                                     selector,
-                                    ns_string(key_to_native(&keystroke.key).as_ref()),
+                                    ns_string(key_to_native(&keystroke.display_key).as_ref()),
                                 )
                                 .autorelease();
                             if Self::os_version() >= SemanticVersion::new(12, 0, 0) {
@@ -882,6 +888,10 @@ impl Platform for MacPlatform {
         Box::new(MacKeyboardLayout::new())
     }
 
+    fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
+        self.0.lock().keyboard_mapper.clone()
+    }
+
     fn app_path(&self) -> Result<PathBuf> {
         unsafe {
             let bundle: id = NSBundle::mainBundle();
@@ -1393,6 +1403,8 @@ extern "C" fn will_terminate(this: &mut Object, _: Sel, _: id) {
 extern "C" fn on_keyboard_layout_change(this: &mut Object, _: Sel, _: id) {
     let platform = unsafe { get_mac_platform(this) };
     let mut lock = platform.0.lock();
+    let keyboard_layout = MacKeyboardLayout::new();
+    lock.keyboard_mapper = Rc::new(MacKeyboardMapper::new(keyboard_layout.id()));
     if let Some(mut callback) = lock.on_keyboard_layout_change.take() {
         drop(lock);
         callback();

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

@@ -1,8 +1,9 @@
 use crate::{
     AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels,
-    ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay, PlatformKeyboardLayout,
-    PlatformTextSystem, PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream,
-    SourceMetadata, Task, TestDisplay, TestWindow, WindowAppearance, WindowParams, size,
+    DummyKeyboardMapper, ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay,
+    PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, PromptButton,
+    ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, SourceMetadata, Task,
+    TestDisplay, TestWindow, WindowAppearance, WindowParams, size,
 };
 use anyhow::Result;
 use collections::VecDeque;
@@ -237,6 +238,10 @@ impl Platform for TestPlatform {
         Box::new(TestKeyboardLayout)
     }
 
+    fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
+        Rc::new(DummyKeyboardMapper)
+    }
+
     fn on_keyboard_layout_change(&self, _: Box<dyn FnMut()>) {}
 
     fn run(&self, _on_finish_launching: Box<dyn FnOnce()>) {

crates/gpui/src/platform/windows/keyboard.rs 🔗

@@ -1,22 +1,31 @@
 use anyhow::Result;
+use collections::HashMap;
 use windows::Win32::UI::{
     Input::KeyboardAndMouse::{
-        GetKeyboardLayoutNameW, MAPVK_VK_TO_CHAR, MapVirtualKeyW, ToUnicode, VIRTUAL_KEY, VK_0,
-        VK_1, VK_2, VK_3, VK_4, VK_5, VK_6, VK_7, VK_8, VK_9, VK_ABNT_C1, VK_CONTROL, VK_MENU,
-        VK_OEM_1, VK_OEM_2, VK_OEM_3, VK_OEM_4, VK_OEM_5, VK_OEM_6, VK_OEM_7, VK_OEM_8, VK_OEM_102,
-        VK_OEM_COMMA, VK_OEM_MINUS, VK_OEM_PERIOD, VK_OEM_PLUS, VK_SHIFT,
+        GetKeyboardLayoutNameW, MAPVK_VK_TO_CHAR, MAPVK_VK_TO_VSC, MapVirtualKeyW, ToUnicode,
+        VIRTUAL_KEY, VK_0, VK_1, VK_2, VK_3, VK_4, VK_5, VK_6, VK_7, VK_8, VK_9, VK_ABNT_C1,
+        VK_CONTROL, VK_MENU, VK_OEM_1, VK_OEM_2, VK_OEM_3, VK_OEM_4, VK_OEM_5, VK_OEM_6, VK_OEM_7,
+        VK_OEM_8, VK_OEM_102, VK_OEM_COMMA, VK_OEM_MINUS, VK_OEM_PERIOD, VK_OEM_PLUS, VK_SHIFT,
     },
     WindowsAndMessaging::KL_NAMELENGTH,
 };
 use windows_core::HSTRING;
 
-use crate::{Modifiers, PlatformKeyboardLayout};
+use crate::{
+    KeybindingKeystroke, Keystroke, Modifiers, PlatformKeyboardLayout, PlatformKeyboardMapper,
+};
 
 pub(crate) struct WindowsKeyboardLayout {
     id: String,
     name: String,
 }
 
+pub(crate) struct WindowsKeyboardMapper {
+    key_to_vkey: HashMap<String, (u16, bool)>,
+    vkey_to_key: HashMap<u16, String>,
+    vkey_to_shifted: HashMap<u16, String>,
+}
+
 impl PlatformKeyboardLayout for WindowsKeyboardLayout {
     fn id(&self) -> &str {
         &self.id
@@ -27,6 +36,65 @@ impl PlatformKeyboardLayout for WindowsKeyboardLayout {
     }
 }
 
+impl PlatformKeyboardMapper for WindowsKeyboardMapper {
+    fn map_key_equivalent(
+        &self,
+        mut keystroke: Keystroke,
+        use_key_equivalents: bool,
+    ) -> KeybindingKeystroke {
+        let Some((vkey, shifted_key)) = self.get_vkey_from_key(&keystroke.key, use_key_equivalents)
+        else {
+            return KeybindingKeystroke::from_keystroke(keystroke);
+        };
+        if shifted_key && keystroke.modifiers.shift {
+            log::warn!(
+                "Keystroke '{}' has both shift and a shifted key, this is likely a bug",
+                keystroke.key
+            );
+        }
+
+        let shift = shifted_key || keystroke.modifiers.shift;
+        keystroke.modifiers.shift = false;
+
+        let Some(key) = self.vkey_to_key.get(&vkey).cloned() else {
+            log::error!(
+                "Failed to map key equivalent '{:?}' to a valid key",
+                keystroke
+            );
+            return KeybindingKeystroke::from_keystroke(keystroke);
+        };
+
+        keystroke.key = if shift {
+            let Some(shifted_key) = self.vkey_to_shifted.get(&vkey).cloned() else {
+                log::error!(
+                    "Failed to map keystroke {:?} with virtual key '{:?}' to a shifted key",
+                    keystroke,
+                    vkey
+                );
+                return KeybindingKeystroke::from_keystroke(keystroke);
+            };
+            shifted_key
+        } else {
+            key.clone()
+        };
+
+        let modifiers = Modifiers {
+            shift,
+            ..keystroke.modifiers
+        };
+
+        KeybindingKeystroke {
+            inner: keystroke,
+            display_modifiers: modifiers,
+            display_key: key,
+        }
+    }
+
+    fn get_key_equivalents(&self) -> Option<&HashMap<char, char>> {
+        None
+    }
+}
+
 impl WindowsKeyboardLayout {
     pub(crate) fn new() -> Result<Self> {
         let mut buffer = [0u16; KL_NAMELENGTH as usize];
@@ -48,6 +116,41 @@ impl WindowsKeyboardLayout {
     }
 }
 
+impl WindowsKeyboardMapper {
+    pub(crate) fn new() -> Self {
+        let mut key_to_vkey = HashMap::default();
+        let mut vkey_to_key = HashMap::default();
+        let mut vkey_to_shifted = HashMap::default();
+        for vkey in CANDIDATE_VKEYS {
+            if let Some(key) = get_key_from_vkey(*vkey) {
+                key_to_vkey.insert(key.clone(), (vkey.0, false));
+                vkey_to_key.insert(vkey.0, key);
+            }
+            let scan_code = unsafe { MapVirtualKeyW(vkey.0 as u32, MAPVK_VK_TO_VSC) };
+            if scan_code == 0 {
+                continue;
+            }
+            if let Some(shifted_key) = get_shifted_key(*vkey, scan_code) {
+                key_to_vkey.insert(shifted_key.clone(), (vkey.0, true));
+                vkey_to_shifted.insert(vkey.0, shifted_key);
+            }
+        }
+        Self {
+            key_to_vkey,
+            vkey_to_key,
+            vkey_to_shifted,
+        }
+    }
+
+    fn get_vkey_from_key(&self, key: &str, use_key_equivalents: bool) -> Option<(u16, bool)> {
+        if use_key_equivalents {
+            get_vkey_from_key_with_us_layout(key)
+        } else {
+            self.key_to_vkey.get(key).cloned()
+        }
+    }
+}
+
 pub(crate) fn get_keystroke_key(
     vkey: VIRTUAL_KEY,
     scan_code: u32,
@@ -140,3 +243,134 @@ pub(crate) fn generate_key_char(
         _ => None,
     }
 }
+
+fn get_vkey_from_key_with_us_layout(key: &str) -> Option<(u16, bool)> {
+    match key {
+        // ` => VK_OEM_3
+        "`" => Some((VK_OEM_3.0, false)),
+        "~" => Some((VK_OEM_3.0, true)),
+        "1" => Some((VK_1.0, false)),
+        "!" => Some((VK_1.0, true)),
+        "2" => Some((VK_2.0, false)),
+        "@" => Some((VK_2.0, true)),
+        "3" => Some((VK_3.0, false)),
+        "#" => Some((VK_3.0, true)),
+        "4" => Some((VK_4.0, false)),
+        "$" => Some((VK_4.0, true)),
+        "5" => Some((VK_5.0, false)),
+        "%" => Some((VK_5.0, true)),
+        "6" => Some((VK_6.0, false)),
+        "^" => Some((VK_6.0, true)),
+        "7" => Some((VK_7.0, false)),
+        "&" => Some((VK_7.0, true)),
+        "8" => Some((VK_8.0, false)),
+        "*" => Some((VK_8.0, true)),
+        "9" => Some((VK_9.0, false)),
+        "(" => Some((VK_9.0, true)),
+        "0" => Some((VK_0.0, false)),
+        ")" => Some((VK_0.0, true)),
+        "-" => Some((VK_OEM_MINUS.0, false)),
+        "_" => Some((VK_OEM_MINUS.0, true)),
+        "=" => Some((VK_OEM_PLUS.0, false)),
+        "+" => Some((VK_OEM_PLUS.0, true)),
+        "[" => Some((VK_OEM_4.0, false)),
+        "{" => Some((VK_OEM_4.0, true)),
+        "]" => Some((VK_OEM_6.0, false)),
+        "}" => Some((VK_OEM_6.0, true)),
+        "\\" => Some((VK_OEM_5.0, false)),
+        "|" => Some((VK_OEM_5.0, true)),
+        ";" => Some((VK_OEM_1.0, false)),
+        ":" => Some((VK_OEM_1.0, true)),
+        "'" => Some((VK_OEM_7.0, false)),
+        "\"" => Some((VK_OEM_7.0, true)),
+        "," => Some((VK_OEM_COMMA.0, false)),
+        "<" => Some((VK_OEM_COMMA.0, true)),
+        "." => Some((VK_OEM_PERIOD.0, false)),
+        ">" => Some((VK_OEM_PERIOD.0, true)),
+        "/" => Some((VK_OEM_2.0, false)),
+        "?" => Some((VK_OEM_2.0, true)),
+        _ => None,
+    }
+}
+
+const CANDIDATE_VKEYS: &[VIRTUAL_KEY] = &[
+    VK_OEM_3,
+    VK_OEM_MINUS,
+    VK_OEM_PLUS,
+    VK_OEM_4,
+    VK_OEM_5,
+    VK_OEM_6,
+    VK_OEM_1,
+    VK_OEM_7,
+    VK_OEM_COMMA,
+    VK_OEM_PERIOD,
+    VK_OEM_2,
+    VK_OEM_102,
+    VK_OEM_8,
+    VK_ABNT_C1,
+    VK_0,
+    VK_1,
+    VK_2,
+    VK_3,
+    VK_4,
+    VK_5,
+    VK_6,
+    VK_7,
+    VK_8,
+    VK_9,
+];
+
+#[cfg(test)]
+mod tests {
+    use crate::{Keystroke, Modifiers, PlatformKeyboardMapper, WindowsKeyboardMapper};
+
+    #[test]
+    fn test_keyboard_mapper() {
+        let mapper = WindowsKeyboardMapper::new();
+
+        // Normal case
+        let keystroke = Keystroke {
+            modifiers: Modifiers::control(),
+            key: "a".to_string(),
+            key_char: None,
+        };
+        let mapped = mapper.map_key_equivalent(keystroke.clone(), true);
+        assert_eq!(mapped.inner, keystroke);
+        assert_eq!(mapped.display_key, "a");
+        assert_eq!(mapped.display_modifiers, Modifiers::control());
+
+        // Shifted case, ctrl-$
+        let keystroke = Keystroke {
+            modifiers: Modifiers::control(),
+            key: "$".to_string(),
+            key_char: None,
+        };
+        let mapped = mapper.map_key_equivalent(keystroke.clone(), true);
+        assert_eq!(mapped.inner, keystroke);
+        assert_eq!(mapped.display_key, "4");
+        assert_eq!(mapped.display_modifiers, Modifiers::control_shift());
+
+        // Shifted case, but shift is true
+        let keystroke = Keystroke {
+            modifiers: Modifiers::control_shift(),
+            key: "$".to_string(),
+            key_char: None,
+        };
+        let mapped = mapper.map_key_equivalent(keystroke, true);
+        assert_eq!(mapped.inner.modifiers, Modifiers::control());
+        assert_eq!(mapped.display_key, "4");
+        assert_eq!(mapped.display_modifiers, Modifiers::control_shift());
+
+        // Windows style
+        let keystroke = Keystroke {
+            modifiers: Modifiers::control_shift(),
+            key: "4".to_string(),
+            key_char: None,
+        };
+        let mapped = mapper.map_key_equivalent(keystroke, true);
+        assert_eq!(mapped.inner.modifiers, Modifiers::control());
+        assert_eq!(mapped.inner.key, "$");
+        assert_eq!(mapped.display_key, "4");
+        assert_eq!(mapped.display_modifiers, Modifiers::control_shift());
+    }
+}

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

@@ -351,6 +351,10 @@ impl Platform for WindowsPlatform {
         )
     }
 
+    fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
+        Rc::new(WindowsKeyboardMapper::new())
+    }
+
     fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>) {
         self.state.borrow_mut().callbacks.keyboard_layout_change = Some(callback);
     }

crates/language_tools/src/key_context_view.rs 🔗

@@ -4,7 +4,6 @@ use gpui::{
 };
 use itertools::Itertools;
 use serde_json::json;
-use settings::get_key_equivalents;
 use ui::{Button, ButtonStyle};
 use ui::{
     ButtonCommon, Clickable, Context, FluentBuilder, InteractiveElement, Label, LabelCommon,
@@ -169,7 +168,8 @@ impl Item for KeyContextView {
 impl Render for KeyContextView {
     fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
         use itertools::Itertools;
-        let key_equivalents = get_key_equivalents(cx.keyboard_layout().id());
+
+        let key_equivalents = cx.keyboard_mapper().get_key_equivalents();
         v_flex()
             .id("key-context-view")
             .overflow_scroll()

crates/settings/src/key_equivalents.rs 🔗

@@ -1,1424 +0,0 @@
-use collections::HashMap;
-
-// On some keyboards (e.g. German QWERTZ) it is not possible to type the full ASCII range
-// without using option. This means that some of our built in keyboard shortcuts do not work
-// for those users.
-//
-// The way macOS solves this problem is to move shortcuts around so that they are all reachable,
-// even if the mnemonic changes. https://developer.apple.com/documentation/swiftui/keyboardshortcut/localization-swift.struct
-//
-// For example, cmd-> is the "switch window" shortcut because the > key is right above tab.
-// To ensure this doesn't cause problems for shortcuts defined for a QWERTY layout, apple moves
-// any shortcuts defined as cmd-> to cmd-:. Coincidentally this s also the same keyboard position
-// as cmd-> on a QWERTY layout.
-//
-// Another example is cmd-[ and cmd-], as they cannot be typed without option, those keys are remapped to cmd-ö
-// and cmd-ä. These shortcuts are not in the same position as a QWERTY keyboard, because on a QWERTZ keyboard
-// the + key is in the way; and shortcuts bound to cmd-+ are still typed as cmd-+ on either keyboard (though the
-// specific key moves)
-//
-// As far as I can tell, there's no way to query the mappings Apple uses except by rendering a menu with every
-// possible key combination, and inspecting the UI to see what it rendered. So that's what we did...
-//
-// These mappings were generated by running https://github.com/ConradIrwin/keyboard-inspector, tidying up the
-// output to remove languages with no mappings and other oddities, and converting it to a less verbose representation with:
-//  jq -s 'map(to_entries | map({key: .key, value: [(.value | to_entries | map(.key) | join("")), (.value | to_entries | map(.value) | join(""))]}) | from_entries) | add'
-// From there I used multi-cursor to produce this match statement.
-#[cfg(target_os = "macos")]
-pub fn get_key_equivalents(layout: &str) -> Option<HashMap<char, char>> {
-    let mappings: &[(char, char)] = match layout {
-        "com.apple.keylayout.ABC-AZERTY" => &[
-            ('!', '1'),
-            ('"', '%'),
-            ('#', '3'),
-            ('$', '4'),
-            ('%', '5'),
-            ('&', '7'),
-            ('(', '9'),
-            (')', '0'),
-            ('*', '8'),
-            ('.', ';'),
-            ('/', ':'),
-            ('0', 'à'),
-            ('1', '&'),
-            ('2', 'é'),
-            ('3', '"'),
-            ('4', '\''),
-            ('5', '('),
-            ('6', '§'),
-            ('7', 'è'),
-            ('8', '!'),
-            ('9', 'ç'),
-            (':', '°'),
-            (';', ')'),
-            ('<', '.'),
-            ('>', '/'),
-            ('@', '2'),
-            ('[', '^'),
-            ('\'', 'ù'),
-            ('\\', '`'),
-            (']', '$'),
-            ('^', '6'),
-            ('`', '<'),
-            ('{', '¨'),
-            ('|', '£'),
-            ('}', '*'),
-            ('~', '>'),
-        ],
-        "com.apple.keylayout.ABC-QWERTZ" => &[
-            ('"', '`'),
-            ('#', '§'),
-            ('&', '/'),
-            ('(', ')'),
-            (')', '='),
-            ('*', '('),
-            ('/', 'ß'),
-            (':', 'Ü'),
-            (';', 'ü'),
-            ('<', ';'),
-            ('=', '*'),
-            ('>', ':'),
-            ('@', '"'),
-            ('[', 'ö'),
-            ('\'', '´'),
-            ('\\', '#'),
-            (']', 'ä'),
-            ('^', '&'),
-            ('`', '<'),
-            ('{', 'Ö'),
-            ('|', '\''),
-            ('}', 'Ä'),
-            ('~', '>'),
-        ],
-        "com.apple.keylayout.Albanian" => &[
-            ('"', '\''),
-            (':', 'Ç'),
-            (';', 'ç'),
-            ('<', ';'),
-            ('>', ':'),
-            ('@', '"'),
-            ('\'', '@'),
-            ('\\', 'ë'),
-            ('`', '<'),
-            ('|', 'Ë'),
-            ('~', '>'),
-        ],
-        "com.apple.keylayout.Austrian" => &[
-            ('"', '`'),
-            ('#', '§'),
-            ('&', '/'),
-            ('(', ')'),
-            (')', '='),
-            ('*', '('),
-            ('/', 'ß'),
-            (':', 'Ü'),
-            (';', 'ü'),
-            ('<', ';'),
-            ('=', '*'),
-            ('>', ':'),
-            ('@', '"'),
-            ('[', 'ö'),
-            ('\'', '´'),
-            ('\\', '#'),
-            (']', 'ä'),
-            ('^', '&'),
-            ('`', '<'),
-            ('{', 'Ö'),
-            ('|', '\''),
-            ('}', 'Ä'),
-            ('~', '>'),
-        ],
-        "com.apple.keylayout.Azeri" => &[
-            ('"', 'Ə'),
-            (',', 'ç'),
-            ('.', 'ş'),
-            ('/', '.'),
-            (':', 'I'),
-            (';', 'ı'),
-            ('<', 'Ç'),
-            ('>', 'Ş'),
-            ('?', ','),
-            ('W', 'Ü'),
-            ('[', 'ö'),
-            ('\'', 'ə'),
-            (']', 'ğ'),
-            ('w', 'ü'),
-            ('{', 'Ö'),
-            ('|', '/'),
-            ('}', 'Ğ'),
-        ],
-        "com.apple.keylayout.Belgian" => &[
-            ('!', '1'),
-            ('"', '%'),
-            ('#', '3'),
-            ('$', '4'),
-            ('%', '5'),
-            ('&', '7'),
-            ('(', '9'),
-            (')', '0'),
-            ('*', '8'),
-            ('.', ';'),
-            ('/', ':'),
-            ('0', 'à'),
-            ('1', '&'),
-            ('2', 'é'),
-            ('3', '"'),
-            ('4', '\''),
-            ('5', '('),
-            ('6', '§'),
-            ('7', 'è'),
-            ('8', '!'),
-            ('9', 'ç'),
-            (':', '°'),
-            (';', ')'),
-            ('<', '.'),
-            ('>', '/'),
-            ('@', '2'),
-            ('[', '^'),
-            ('\'', 'ù'),
-            ('\\', '`'),
-            (']', '$'),
-            ('^', '6'),
-            ('`', '<'),
-            ('{', '¨'),
-            ('|', '£'),
-            ('}', '*'),
-            ('~', '>'),
-        ],
-        "com.apple.keylayout.Brazilian-ABNT2" => &[
-            ('"', '`'),
-            ('/', 'ç'),
-            ('?', 'Ç'),
-            ('\'', '´'),
-            ('\\', '~'),
-            ('^', '¨'),
-            ('`', '\''),
-            ('|', '^'),
-            ('~', '"'),
-        ],
-        "com.apple.keylayout.Brazilian-Pro" => &[('^', 'ˆ'), ('~', '˜')],
-        "com.apple.keylayout.British" => &[('#', '£')],
-        "com.apple.keylayout.Canadian-CSA" => &[
-            ('"', 'È'),
-            ('/', 'é'),
-            ('<', '\''),
-            ('>', '"'),
-            ('?', 'É'),
-            ('[', '^'),
-            ('\'', 'è'),
-            ('\\', 'à'),
-            (']', 'ç'),
-            ('`', 'ù'),
-            ('{', '¨'),
-            ('|', 'À'),
-            ('}', 'Ç'),
-            ('~', 'Ù'),
-        ],
-        "com.apple.keylayout.Croatian" => &[
-            ('"', 'Ć'),
-            ('&', '\''),
-            ('(', ')'),
-            (')', '='),
-            ('*', '('),
-            (':', 'Č'),
-            (';', 'č'),
-            ('<', ';'),
-            ('=', '*'),
-            ('>', ':'),
-            ('@', '"'),
-            ('[', 'š'),
-            ('\'', 'ć'),
-            ('\\', 'ž'),
-            (']', 'đ'),
-            ('^', '&'),
-            ('`', '<'),
-            ('{', 'Š'),
-            ('|', 'Ž'),
-            ('}', 'Đ'),
-            ('~', '>'),
-        ],
-        "com.apple.keylayout.Croatian-PC" => &[
-            ('"', 'Ć'),
-            ('&', '/'),
-            ('(', ')'),
-            (')', '='),
-            ('*', '('),
-            ('/', '\''),
-            (':', 'Č'),
-            (';', 'č'),
-            ('<', ';'),
-            ('=', '*'),
-            ('>', ':'),
-            ('@', '"'),
-            ('[', 'š'),
-            ('\'', 'ć'),
-            ('\\', 'ž'),
-            (']', 'đ'),
-            ('^', '&'),
-            ('`', '<'),
-            ('{', 'Š'),
-            ('|', 'Ž'),
-            ('}', 'Đ'),
-            ('~', '>'),
-        ],
-        "com.apple.keylayout.Czech" => &[
-            ('!', '1'),
-            ('"', '!'),
-            ('#', '3'),
-            ('$', '4'),
-            ('%', '5'),
-            ('&', '7'),
-            ('(', '9'),
-            (')', '0'),
-            ('*', '8'),
-            ('+', '%'),
-            ('/', '\''),
-            ('0', 'é'),
-            ('1', '+'),
-            ('2', 'ě'),
-            ('3', 'š'),
-            ('4', 'č'),
-            ('5', 'ř'),
-            ('6', 'ž'),
-            ('7', 'ý'),
-            ('8', 'á'),
-            ('9', 'í'),
-            (':', '"'),
-            (';', 'ů'),
-            ('<', '?'),
-            ('>', ':'),
-            ('?', 'ˇ'),
-            ('@', '2'),
-            ('[', 'ú'),
-            ('\'', '§'),
-            (']', ')'),
-            ('^', '6'),
-            ('`', '¨'),
-            ('{', 'Ú'),
-            ('}', '('),
-            ('~', '`'),
-        ],
-        "com.apple.keylayout.Czech-QWERTY" => &[
-            ('!', '1'),
-            ('"', '!'),
-            ('#', '3'),
-            ('$', '4'),
-            ('%', '5'),
-            ('&', '7'),
-            ('(', '9'),
-            (')', '0'),
-            ('*', '8'),
-            ('+', '%'),
-            ('/', '\''),
-            ('0', 'é'),
-            ('1', '+'),
-            ('2', 'ě'),
-            ('3', 'š'),
-            ('4', 'č'),
-            ('5', 'ř'),
-            ('6', 'ž'),
-            ('7', 'ý'),
-            ('8', 'á'),
-            ('9', 'í'),
-            (':', '"'),
-            (';', 'ů'),
-            ('<', '?'),
-            ('>', ':'),
-            ('?', 'ˇ'),
-            ('@', '2'),
-            ('[', 'ú'),
-            ('\'', '§'),
-            (']', ')'),
-            ('^', '6'),
-            ('`', '¨'),
-            ('{', 'Ú'),
-            ('}', '('),
-            ('~', '`'),
-        ],
-        "com.apple.keylayout.Danish" => &[
-            ('"', '^'),
-            ('$', '€'),
-            ('&', '/'),
-            ('(', ')'),
-            (')', '='),
-            ('*', '('),
-            ('/', '´'),
-            (':', 'Å'),
-            (';', 'å'),
-            ('<', ';'),
-            ('=', '`'),
-            ('>', ':'),
-            ('@', '"'),
-            ('[', 'æ'),
-            ('\'', '¨'),
-            ('\\', '\''),
-            (']', 'ø'),
-            ('^', '&'),
-            ('`', '<'),
-            ('{', 'Æ'),
-            ('|', '*'),
-            ('}', 'Ø'),
-            ('~', '>'),
-        ],
-        "com.apple.keylayout.Faroese" => &[
-            ('"', 'Ø'),
-            ('$', '€'),
-            ('&', '/'),
-            ('(', ')'),
-            (')', '='),
-            ('*', '('),
-            ('/', '´'),
-            (':', 'Æ'),
-            (';', 'æ'),
-            ('<', ';'),
-            ('=', '`'),
-            ('>', ':'),
-            ('@', '"'),
-            ('[', 'å'),
-            ('\'', 'ø'),
-            ('\\', '\''),
-            (']', 'ð'),
-            ('^', '&'),
-            ('`', '<'),
-            ('{', 'Å'),
-            ('|', '*'),
-            ('}', 'Ð'),
-            ('~', '>'),
-        ],
-        "com.apple.keylayout.Finnish" => &[
-            ('"', '^'),
-            ('$', '€'),
-            ('&', '/'),
-            ('(', ')'),
-            (')', '='),
-            ('*', '('),
-            ('/', '´'),
-            (':', 'Å'),
-            (';', 'å'),
-            ('<', ';'),
-            ('=', '`'),
-            ('>', ':'),
-            ('@', '"'),
-            ('[', 'ö'),
-            ('\'', '¨'),
-            ('\\', '\''),
-            (']', 'ä'),
-            ('^', '&'),
-            ('`', '<'),
-            ('{', 'Ö'),
-            ('|', '*'),
-            ('}', 'Ä'),
-            ('~', '>'),
-        ],
-        "com.apple.keylayout.FinnishExtended" => &[
-            ('"', 'ˆ'),
-            ('$', '€'),
-            ('&', '/'),
-            ('(', ')'),
-            (')', '='),
-            ('*', '('),
-            ('/', '´'),
-            (':', 'Å'),
-            (';', 'å'),
-            ('<', ';'),
-            ('=', '`'),
-            ('>', ':'),
-            ('@', '"'),
-            ('[', 'ö'),
-            ('\'', '¨'),
-            ('\\', '\''),
-            (']', 'ä'),
-            ('^', '&'),
-            ('`', '<'),
-            ('{', 'Ö'),
-            ('|', '*'),
-            ('}', 'Ä'),
-            ('~', '>'),
-        ],
-        "com.apple.keylayout.FinnishSami-PC" => &[
-            ('"', 'ˆ'),
-            ('&', '/'),
-            ('(', ')'),
-            (')', '='),
-            ('*', '('),
-            ('/', '´'),
-            (':', 'Å'),
-            (';', 'å'),
-            ('<', ';'),
-            ('=', '`'),
-            ('>', ':'),
-            ('@', '"'),
-            ('[', 'ö'),
-            ('\'', '¨'),
-            ('\\', '@'),
-            (']', 'ä'),
-            ('^', '&'),
-            ('`', '<'),
-            ('{', 'Ö'),
-            ('|', '*'),
-            ('}', 'Ä'),
-            ('~', '>'),
-        ],
-        "com.apple.keylayout.French" => &[
-            ('!', '1'),
-            ('"', '%'),
-            ('#', '3'),
-            ('$', '4'),
-            ('%', '5'),
-            ('&', '7'),
-            ('(', '9'),
-            (')', '0'),
-            ('*', '8'),
-            ('.', ';'),
-            ('/', ':'),
-            ('0', 'à'),
-            ('1', '&'),
-            ('2', 'é'),
-            ('3', '"'),
-            ('4', '\''),
-            ('5', '('),
-            ('6', '§'),
-            ('7', 'è'),
-            ('8', '!'),
-            ('9', 'ç'),
-            (':', '°'),
-            (';', ')'),
-            ('<', '.'),
-            ('>', '/'),
-            ('@', '2'),
-            ('[', '^'),
-            ('\'', 'ù'),
-            ('\\', '`'),
-            (']', '$'),
-            ('^', '6'),
-            ('`', '<'),
-            ('{', '¨'),
-            ('|', '£'),
-            ('}', '*'),
-            ('~', '>'),
-        ],
-        "com.apple.keylayout.French-PC" => &[
-            ('!', '1'),
-            ('"', '%'),
-            ('#', '3'),
-            ('$', '4'),
-            ('%', '5'),
-            ('&', '7'),
-            ('(', '9'),
-            (')', '0'),
-            ('*', '8'),
-            ('-', ')'),
-            ('.', ';'),
-            ('/', ':'),
-            ('0', 'à'),
-            ('1', '&'),
-            ('2', 'é'),
-            ('3', '"'),
-            ('4', '\''),
-            ('5', '('),
-            ('6', '-'),
-            ('7', 'è'),
-            ('8', '_'),
-            ('9', 'ç'),
-            (':', '§'),
-            (';', '!'),
-            ('<', '.'),
-            ('>', '/'),
-            ('@', '2'),
-            ('[', '^'),
-            ('\'', 'ù'),
-            ('\\', '*'),
-            (']', '$'),
-            ('^', '6'),
-            ('_', '°'),
-            ('`', '<'),
-            ('{', '¨'),
-            ('|', 'μ'),
-            ('}', '£'),
-            ('~', '>'),
-        ],
-        "com.apple.keylayout.French-numerical" => &[
-            ('!', '1'),
-            ('"', '%'),
-            ('#', '3'),
-            ('$', '4'),
-            ('%', '5'),
-            ('&', '7'),
-            ('(', '9'),
-            (')', '0'),
-            ('*', '8'),
-            ('.', ';'),
-            ('/', ':'),
-            ('0', 'à'),
-            ('1', '&'),
-            ('2', 'é'),
-            ('3', '"'),
-            ('4', '\''),
-            ('5', '('),
-            ('6', '§'),
-            ('7', 'è'),
-            ('8', '!'),
-            ('9', 'ç'),
-            (':', '°'),
-            (';', ')'),
-            ('<', '.'),
-            ('>', '/'),
-            ('@', '2'),
-            ('[', '^'),
-            ('\'', 'ù'),
-            ('\\', '`'),
-            (']', '$'),
-            ('^', '6'),
-            ('`', '<'),
-            ('{', '¨'),
-            ('|', '£'),
-            ('}', '*'),
-            ('~', '>'),
-        ],
-        "com.apple.keylayout.German" => &[
-            ('"', '`'),
-            ('#', '§'),
-            ('&', '/'),
-            ('(', ')'),
-            (')', '='),
-            ('*', '('),
-            ('/', 'ß'),
-            (':', 'Ü'),
-            (';', 'ü'),
-            ('<', ';'),
-            ('=', '*'),
-            ('>', ':'),
-            ('@', '"'),
-            ('[', 'ö'),
-            ('\'', '´'),
-            ('\\', '#'),
-            (']', 'ä'),
-            ('^', '&'),
-            ('`', '<'),
-            ('{', 'Ö'),
-            ('|', '\''),
-            ('}', 'Ä'),
-            ('~', '>'),
-        ],
-        "com.apple.keylayout.German-DIN-2137" => &[
-            ('"', '`'),
-            ('#', '§'),
-            ('&', '/'),
-            ('(', ')'),
-            (')', '='),
-            ('*', '('),
-            ('/', 'ß'),
-            (':', 'Ü'),
-            (';', 'ü'),
-            ('<', ';'),
-            ('=', '*'),
-            ('>', ':'),
-            ('@', '"'),
-            ('[', 'ö'),
-            ('\'', '´'),
-            ('\\', '#'),
-            (']', 'ä'),
-            ('^', '&'),
-            ('`', '<'),
-            ('{', 'Ö'),
-            ('|', '\''),
-            ('}', 'Ä'),
-            ('~', '>'),
-        ],
-        "com.apple.keylayout.Hawaiian" => &[('\'', 'ʻ')],
-        "com.apple.keylayout.Hungarian" => &[
-            ('!', '\''),
-            ('"', 'Á'),
-            ('#', '+'),
-            ('$', '!'),
-            ('&', '='),
-            ('(', ')'),
-            (')', 'Ö'),
-            ('*', '('),
-            ('+', 'Ó'),
-            ('/', 'ü'),
-            ('0', 'ö'),
-            (':', 'É'),
-            (';', 'é'),
-            ('<', 'Ü'),
-            ('=', 'ó'),
-            ('>', ':'),
-            ('@', '"'),
-            ('[', 'ő'),
-            ('\'', 'á'),
-            ('\\', 'ű'),
-            (']', 'ú'),
-            ('^', '/'),
-            ('`', 'í'),
-            ('{', 'Ő'),
-            ('|', 'Ű'),
-            ('}', 'Ú'),
-            ('~', 'Í'),
-        ],
-        "com.apple.keylayout.Hungarian-QWERTY" => &[
-            ('!', '\''),
-            ('"', 'Á'),
-            ('#', '+'),
-            ('$', '!'),
-            ('&', '='),
-            ('(', ')'),
-            (')', 'Ö'),
-            ('*', '('),
-            ('+', 'Ó'),
-            ('/', 'ü'),
-            ('0', 'ö'),
-            (':', 'É'),
-            (';', 'é'),
-            ('<', 'Ü'),
-            ('=', 'ó'),
-            ('>', ':'),
-            ('@', '"'),
-            ('[', 'ő'),
-            ('\'', 'á'),
-            ('\\', 'ű'),
-            (']', 'ú'),
-            ('^', '/'),
-            ('`', 'í'),
-            ('{', 'Ő'),
-            ('|', 'Ű'),
-            ('}', 'Ú'),
-            ('~', 'Í'),
-        ],
-        "com.apple.keylayout.Icelandic" => &[
-            ('"', 'Ö'),
-            ('&', '/'),
-            ('(', ')'),
-            (')', '='),
-            ('*', '('),
-            ('/', '\''),
-            (':', 'Ð'),
-            (';', 'ð'),
-            ('<', ';'),
-            ('=', '*'),
-            ('>', ':'),
-            ('@', '"'),
-            ('[', 'æ'),
-            ('\'', 'ö'),
-            ('\\', 'þ'),
-            (']', '´'),
-            ('^', '&'),
-            ('`', '<'),
-            ('{', 'Æ'),
-            ('|', 'Þ'),
-            ('}', '´'),
-            ('~', '>'),
-        ],
-        "com.apple.keylayout.Irish" => &[('#', '£')],
-        "com.apple.keylayout.IrishExtended" => &[('#', '£')],
-        "com.apple.keylayout.Italian" => &[
-            ('!', '1'),
-            ('"', '%'),
-            ('#', '3'),
-            ('$', '4'),
-            ('%', '5'),
-            ('&', '7'),
-            ('(', '9'),
-            (')', '0'),
-            ('*', '8'),
-            (',', ';'),
-            ('.', ':'),
-            ('/', ','),
-            ('0', 'é'),
-            ('1', '&'),
-            ('2', '"'),
-            ('3', '\''),
-            ('4', '('),
-            ('5', 'ç'),
-            ('6', 'è'),
-            ('7', ')'),
-            ('8', '£'),
-            ('9', 'à'),
-            (':', '!'),
-            (';', 'ò'),
-            ('<', '.'),
-            ('>', '/'),
-            ('@', '2'),
-            ('[', 'ì'),
-            ('\'', 'ù'),
-            ('\\', '§'),
-            (']', '$'),
-            ('^', '6'),
-            ('`', '<'),
-            ('{', '^'),
-            ('|', '°'),
-            ('}', '*'),
-            ('~', '>'),
-        ],
-        "com.apple.keylayout.Italian-Pro" => &[
-            ('"', '^'),
-            ('#', '£'),
-            ('&', '/'),
-            ('(', ')'),
-            (')', '='),
-            ('*', '('),
-            ('/', '\''),
-            (':', 'é'),
-            (';', 'è'),
-            ('<', ';'),
-            ('=', '*'),
-            ('>', ':'),
-            ('@', '"'),
-            ('[', 'ò'),
-            ('\'', 'ì'),
-            ('\\', 'ù'),
-            (']', 'à'),
-            ('^', '&'),
-            ('`', '<'),
-            ('{', 'ç'),
-            ('|', '§'),
-            ('}', '°'),
-            ('~', '>'),
-        ],
-        "com.apple.keylayout.LatinAmerican" => &[
-            ('"', '¨'),
-            ('&', '/'),
-            ('(', ')'),
-            (')', '='),
-            ('*', '('),
-            ('/', '\''),
-            (':', 'Ñ'),
-            (';', 'ñ'),
-            ('<', ';'),
-            ('=', '*'),
-            ('>', ':'),
-            ('@', '"'),
-            ('[', '{'),
-            ('\'', '´'),
-            ('\\', '¿'),
-            (']', '}'),
-            ('^', '&'),
-            ('`', '<'),
-            ('{', '['),
-            ('|', '¡'),
-            ('}', ']'),
-            ('~', '>'),
-        ],
-        "com.apple.keylayout.Lithuanian" => &[
-            ('!', 'Ą'),
-            ('#', 'Ę'),
-            ('$', 'Ė'),
-            ('%', 'Į'),
-            ('&', 'Ų'),
-            ('*', 'Ū'),
-            ('+', 'Ž'),
-            ('1', 'ą'),
-            ('2', 'č'),
-            ('3', 'ę'),
-            ('4', 'ė'),
-            ('5', 'į'),
-            ('6', 'š'),
-            ('7', 'ų'),
-            ('8', 'ū'),
-            ('=', 'ž'),
-            ('@', 'Č'),
-            ('^', 'Š'),
-        ],
-        "com.apple.keylayout.Maltese" => &[
-            ('#', '£'),
-            ('[', 'ġ'),
-            (']', 'ħ'),
-            ('`', 'ż'),
-            ('{', 'Ġ'),
-            ('}', 'Ħ'),
-            ('~', 'Ż'),
-        ],
-        "com.apple.keylayout.NorthernSami" => &[
-            ('"', 'Ŋ'),
-            ('&', '/'),
-            ('(', ')'),
-            (')', '='),
-            ('*', '('),
-            ('/', '´'),
-            (':', 'Å'),
-            (';', 'å'),
-            ('<', ';'),
-            ('=', '`'),
-            ('>', ':'),
-            ('@', '"'),
-            ('Q', 'Á'),
-            ('W', 'Š'),
-            ('X', 'Č'),
-            ('[', 'ø'),
-            ('\'', 'ŋ'),
-            ('\\', 'đ'),
-            (']', 'æ'),
-            ('^', '&'),
-            ('`', 'ž'),
-            ('q', 'á'),
-            ('w', 'š'),
-            ('x', 'č'),
-            ('{', 'Ø'),
-            ('|', 'Đ'),
-            ('}', 'Æ'),
-            ('~', 'Ž'),
-        ],
-        "com.apple.keylayout.Norwegian" => &[
-            ('"', '^'),
-            ('&', '/'),
-            ('(', ')'),
-            (')', '='),
-            ('*', '('),
-            ('/', '´'),
-            (':', 'Å'),
-            (';', 'å'),
-            ('<', ';'),
-            ('=', '`'),
-            ('>', ':'),
-            ('@', '"'),
-            ('[', 'ø'),
-            ('\'', '¨'),
-            ('\\', '@'),
-            (']', 'æ'),
-            ('^', '&'),
-            ('`', '<'),
-            ('{', 'Ø'),
-            ('|', '*'),
-            ('}', 'Æ'),
-            ('~', '>'),
-        ],
-        "com.apple.keylayout.NorwegianExtended" => &[
-            ('"', 'ˆ'),
-            ('&', '/'),
-            ('(', ')'),
-            (')', '='),
-            ('*', '('),
-            ('/', '´'),
-            (':', 'Å'),
-            (';', 'å'),
-            ('<', ';'),
-            ('=', '`'),
-            ('>', ':'),
-            ('@', '"'),
-            ('[', 'ø'),
-            ('\\', '@'),
-            (']', 'æ'),
-            ('`', '<'),
-            ('}', 'Æ'),
-            ('~', '>'),
-        ],
-        "com.apple.keylayout.NorwegianSami-PC" => &[
-            ('"', 'ˆ'),
-            ('&', '/'),
-            ('(', ')'),
-            (')', '='),
-            ('*', '('),
-            ('/', '´'),
-            (':', 'Å'),
-            (';', 'å'),
-            ('<', ';'),
-            ('=', '`'),
-            ('>', ':'),
-            ('@', '"'),
-            ('[', 'ø'),
-            ('\'', '¨'),
-            ('\\', '@'),
-            (']', 'æ'),
-            ('^', '&'),
-            ('`', '<'),
-            ('{', 'Ø'),
-            ('|', '*'),
-            ('}', 'Æ'),
-            ('~', '>'),
-        ],
-        "com.apple.keylayout.Polish" => &[
-            ('!', '§'),
-            ('"', 'ę'),
-            ('#', '!'),
-            ('$', '?'),
-            ('%', '+'),
-            ('&', ':'),
-            ('(', '/'),
-            (')', '"'),
-            ('*', '_'),
-            ('+', ']'),
-            (',', '.'),
-            ('.', ','),
-            ('/', 'ż'),
-            (':', 'Ł'),
-            (';', 'ł'),
-            ('<', 'ś'),
-            ('=', '['),
-            ('>', 'ń'),
-            ('?', 'Ż'),
-            ('@', '%'),
-            ('[', 'ó'),
-            ('\'', 'ą'),
-            ('\\', ';'),
-            (']', '('),
-            ('^', '='),
-            ('_', 'ć'),
-            ('`', '<'),
-            ('{', 'ź'),
-            ('|', '$'),
-            ('}', ')'),
-            ('~', '>'),
-        ],
-        "com.apple.keylayout.Portuguese" => &[
-            ('"', '`'),
-            ('&', '/'),
-            ('(', ')'),
-            (')', '='),
-            ('*', '('),
-            ('/', '\''),
-            (':', 'ª'),
-            (';', 'º'),
-            ('<', ';'),
-            ('=', '*'),
-            ('>', ':'),
-            ('@', '"'),
-            ('[', 'ç'),
-            ('\'', '´'),
-            (']', '~'),
-            ('^', '&'),
-            ('`', '<'),
-            ('{', 'Ç'),
-            ('}', '^'),
-            ('~', '>'),
-        ],
-        "com.apple.keylayout.Sami-PC" => &[
-            ('"', 'Ŋ'),
-            ('&', '/'),
-            ('(', ')'),
-            (')', '='),
-            ('*', '('),
-            ('/', '´'),
-            (':', 'Å'),
-            (';', 'å'),
-            ('<', ';'),
-            ('=', '`'),
-            ('>', ':'),
-            ('@', '"'),
-            ('Q', 'Á'),
-            ('W', 'Š'),
-            ('X', 'Č'),
-            ('[', 'ø'),
-            ('\'', 'ŋ'),
-            ('\\', 'đ'),
-            (']', 'æ'),
-            ('^', '&'),
-            ('`', 'ž'),
-            ('q', 'á'),
-            ('w', 'š'),
-            ('x', 'č'),
-            ('{', 'Ø'),
-            ('|', 'Đ'),
-            ('}', 'Æ'),
-            ('~', 'Ž'),
-        ],
-        "com.apple.keylayout.Serbian-Latin" => &[
-            ('"', 'Ć'),
-            ('&', '\''),
-            ('(', ')'),
-            (')', '='),
-            ('*', '('),
-            (':', 'Č'),
-            (';', 'č'),
-            ('<', ';'),
-            ('=', '*'),
-            ('>', ':'),
-            ('@', '"'),
-            ('[', 'š'),
-            ('\'', 'ć'),
-            ('\\', 'ž'),
-            (']', 'đ'),
-            ('^', '&'),
-            ('`', '<'),
-            ('{', 'Š'),
-            ('|', 'Ž'),
-            ('}', 'Đ'),
-            ('~', '>'),
-        ],
-        "com.apple.keylayout.Slovak" => &[
-            ('!', '1'),
-            ('"', '!'),
-            ('#', '3'),
-            ('$', '4'),
-            ('%', '5'),
-            ('&', '7'),
-            ('(', '9'),
-            (')', '0'),
-            ('*', '8'),
-            ('+', '%'),
-            ('/', '\''),
-            ('0', 'é'),
-            ('1', '+'),
-            ('2', 'ľ'),
-            ('3', 'š'),
-            ('4', 'č'),
-            ('5', 'ť'),
-            ('6', 'ž'),
-            ('7', 'ý'),
-            ('8', 'á'),
-            ('9', 'í'),
-            (':', '"'),
-            (';', 'ô'),
-            ('<', '?'),
-            ('>', ':'),
-            ('?', 'ˇ'),
-            ('@', '2'),
-            ('[', 'ú'),
-            ('\'', '§'),
-            (']', 'ä'),
-            ('^', '6'),
-            ('`', 'ň'),
-            ('{', 'Ú'),
-            ('}', 'Ä'),
-            ('~', 'Ň'),
-        ],
-        "com.apple.keylayout.Slovak-QWERTY" => &[
-            ('!', '1'),
-            ('"', '!'),
-            ('#', '3'),
-            ('$', '4'),
-            ('%', '5'),
-            ('&', '7'),
-            ('(', '9'),
-            (')', '0'),
-            ('*', '8'),
-            ('+', '%'),
-            ('/', '\''),
-            ('0', 'é'),
-            ('1', '+'),
-            ('2', 'ľ'),
-            ('3', 'š'),
-            ('4', 'č'),
-            ('5', 'ť'),
-            ('6', 'ž'),
-            ('7', 'ý'),
-            ('8', 'á'),
-            ('9', 'í'),
-            (':', '"'),
-            (';', 'ô'),
-            ('<', '?'),
-            ('>', ':'),
-            ('?', 'ˇ'),
-            ('@', '2'),
-            ('[', 'ú'),
-            ('\'', '§'),
-            (']', 'ä'),
-            ('^', '6'),
-            ('`', 'ň'),
-            ('{', 'Ú'),
-            ('}', 'Ä'),
-            ('~', 'Ň'),
-        ],
-        "com.apple.keylayout.Slovenian" => &[
-            ('"', 'Ć'),
-            ('&', '\''),
-            ('(', ')'),
-            (')', '='),
-            ('*', '('),
-            (':', 'Č'),
-            (';', 'č'),
-            ('<', ';'),
-            ('=', '*'),
-            ('>', ':'),
-            ('@', '"'),
-            ('[', 'š'),
-            ('\'', 'ć'),
-            ('\\', 'ž'),
-            (']', 'đ'),
-            ('^', '&'),
-            ('`', '<'),
-            ('{', 'Š'),
-            ('|', 'Ž'),
-            ('}', 'Đ'),
-            ('~', '>'),
-        ],
-        "com.apple.keylayout.Spanish" => &[
-            ('!', '¡'),
-            ('"', '¨'),
-            ('.', 'ç'),
-            ('/', '.'),
-            (':', 'º'),
-            (';', '´'),
-            ('<', '¿'),
-            ('>', 'Ç'),
-            ('@', '!'),
-            ('[', 'ñ'),
-            ('\'', '`'),
-            ('\\', '\''),
-            (']', ';'),
-            ('^', '/'),
-            ('`', '<'),
-            ('{', 'Ñ'),
-            ('|', '"'),
-            ('}', ':'),
-            ('~', '>'),
-        ],
-        "com.apple.keylayout.Spanish-ISO" => &[
-            ('"', '¨'),
-            ('#', '·'),
-            ('&', '/'),
-            ('(', ')'),
-            (')', '='),
-            ('*', '('),
-            ('.', 'ç'),
-            ('/', '.'),
-            (':', 'º'),
-            (';', '´'),
-            ('<', '¿'),
-            ('>', 'Ç'),
-            ('@', '"'),
-            ('[', 'ñ'),
-            ('\'', '`'),
-            ('\\', '\''),
-            (']', ';'),
-            ('^', '&'),
-            ('`', '<'),
-            ('{', 'Ñ'),
-            ('|', '"'),
-            ('}', '`'),
-            ('~', '>'),
-        ],
-        "com.apple.keylayout.Swedish" => &[
-            ('"', '^'),
-            ('$', '€'),
-            ('&', '/'),
-            ('(', ')'),
-            (')', '='),
-            ('*', '('),
-            ('/', '´'),
-            (':', 'Å'),
-            (';', 'å'),
-            ('<', ';'),
-            ('=', '`'),
-            ('>', ':'),
-            ('@', '"'),
-            ('[', 'ö'),
-            ('\'', '¨'),
-            ('\\', '\''),
-            (']', 'ä'),
-            ('^', '&'),
-            ('`', '<'),
-            ('{', 'Ö'),
-            ('|', '*'),
-            ('}', 'Ä'),
-            ('~', '>'),
-        ],
-        "com.apple.keylayout.Swedish-Pro" => &[
-            ('"', '^'),
-            ('$', '€'),
-            ('&', '/'),
-            ('(', ')'),
-            (')', '='),
-            ('*', '('),
-            ('/', '´'),
-            (':', 'Å'),
-            (';', 'å'),
-            ('<', ';'),
-            ('=', '`'),
-            ('>', ':'),
-            ('@', '"'),
-            ('[', 'ö'),
-            ('\'', '¨'),
-            ('\\', '\''),
-            (']', 'ä'),
-            ('^', '&'),
-            ('`', '<'),
-            ('{', 'Ö'),
-            ('|', '*'),
-            ('}', 'Ä'),
-            ('~', '>'),
-        ],
-        "com.apple.keylayout.SwedishSami-PC" => &[
-            ('"', 'ˆ'),
-            ('&', '/'),
-            ('(', ')'),
-            (')', '='),
-            ('*', '('),
-            ('/', '´'),
-            (':', 'Å'),
-            (';', 'å'),
-            ('<', ';'),
-            ('=', '`'),
-            ('>', ':'),
-            ('@', '"'),
-            ('[', 'ö'),
-            ('\'', '¨'),
-            ('\\', '@'),
-            (']', 'ä'),
-            ('^', '&'),
-            ('`', '<'),
-            ('{', 'Ö'),
-            ('|', '*'),
-            ('}', 'Ä'),
-            ('~', '>'),
-        ],
-        "com.apple.keylayout.SwissFrench" => &[
-            ('!', '+'),
-            ('"', '`'),
-            ('#', '*'),
-            ('$', 'ç'),
-            ('&', '/'),
-            ('(', ')'),
-            (')', '='),
-            ('*', '('),
-            ('+', '!'),
-            ('/', '\''),
-            (':', 'ü'),
-            (';', 'è'),
-            ('<', ';'),
-            ('=', '¨'),
-            ('>', ':'),
-            ('@', '"'),
-            ('[', 'é'),
-            ('\'', '^'),
-            ('\\', '$'),
-            (']', 'à'),
-            ('^', '&'),
-            ('`', '<'),
-            ('{', 'ö'),
-            ('|', '£'),
-            ('}', 'ä'),
-            ('~', '>'),
-        ],
-        "com.apple.keylayout.SwissGerman" => &[
-            ('!', '+'),
-            ('"', '`'),
-            ('#', '*'),
-            ('$', 'ç'),
-            ('&', '/'),
-            ('(', ')'),
-            (')', '='),
-            ('*', '('),
-            ('+', '!'),
-            ('/', '\''),
-            (':', 'è'),
-            (';', 'ü'),
-            ('<', ';'),
-            ('=', '¨'),
-            ('>', ':'),
-            ('@', '"'),
-            ('[', 'ö'),
-            ('\'', '^'),
-            ('\\', '$'),
-            (']', 'ä'),
-            ('^', '&'),
-            ('`', '<'),
-            ('{', 'é'),
-            ('|', '£'),
-            ('}', 'à'),
-            ('~', '>'),
-        ],
-        "com.apple.keylayout.Turkish" => &[
-            ('"', '-'),
-            ('#', '"'),
-            ('$', '\''),
-            ('%', '('),
-            ('&', ')'),
-            ('(', '%'),
-            (')', ':'),
-            ('*', '_'),
-            (',', 'ö'),
-            ('-', 'ş'),
-            ('.', 'ç'),
-            ('/', '.'),
-            (':', '$'),
-            ('<', 'Ö'),
-            ('>', 'Ç'),
-            ('@', '*'),
-            ('[', 'ğ'),
-            ('\'', ','),
-            ('\\', 'ü'),
-            (']', 'ı'),
-            ('^', '/'),
-            ('_', 'Ş'),
-            ('`', '<'),
-            ('{', 'Ğ'),
-            ('|', 'Ü'),
-            ('}', 'I'),
-            ('~', '>'),
-        ],
-        "com.apple.keylayout.Turkish-QWERTY-PC" => &[
-            ('"', 'I'),
-            ('#', '^'),
-            ('$', '+'),
-            ('&', '/'),
-            ('(', ')'),
-            (')', '='),
-            ('*', '('),
-            ('+', ':'),
-            (',', 'ö'),
-            ('.', 'ç'),
-            ('/', '*'),
-            (':', 'Ş'),
-            (';', 'ş'),
-            ('<', 'Ö'),
-            ('=', '.'),
-            ('>', 'Ç'),
-            ('@', '\''),
-            ('[', 'ğ'),
-            ('\'', 'ı'),
-            ('\\', ','),
-            (']', 'ü'),
-            ('^', '&'),
-            ('`', '<'),
-            ('{', 'Ğ'),
-            ('|', ';'),
-            ('}', 'Ü'),
-            ('~', '>'),
-        ],
-        "com.apple.keylayout.Turkish-Standard" => &[
-            ('"', 'Ş'),
-            ('#', '^'),
-            ('&', '\''),
-            ('(', ')'),
-            (')', '='),
-            ('*', '('),
-            (',', '.'),
-            ('.', ','),
-            (':', 'Ç'),
-            (';', 'ç'),
-            ('<', ':'),
-            ('=', '*'),
-            ('>', ';'),
-            ('@', '"'),
-            ('[', 'ğ'),
-            ('\'', 'ş'),
-            ('\\', 'ü'),
-            (']', 'ı'),
-            ('^', '&'),
-            ('`', 'ö'),
-            ('{', 'Ğ'),
-            ('|', 'Ü'),
-            ('}', 'I'),
-            ('~', 'Ö'),
-        ],
-        "com.apple.keylayout.Turkmen" => &[
-            ('C', 'Ç'),
-            ('Q', 'Ä'),
-            ('V', 'Ý'),
-            ('X', 'Ü'),
-            ('[', 'ň'),
-            ('\\', 'ş'),
-            (']', 'ö'),
-            ('^', '№'),
-            ('`', 'ž'),
-            ('c', 'ç'),
-            ('q', 'ä'),
-            ('v', 'ý'),
-            ('x', 'ü'),
-            ('{', 'Ň'),
-            ('|', 'Ş'),
-            ('}', 'Ö'),
-            ('~', 'Ž'),
-        ],
-        "com.apple.keylayout.USInternational-PC" => &[('^', 'ˆ'), ('~', '˜')],
-        "com.apple.keylayout.Welsh" => &[('#', '£')],
-
-        _ => return None,
-    };
-
-    Some(HashMap::from_iter(mappings.iter().cloned()))
-}
-
-#[cfg(not(target_os = "macos"))]
-pub fn get_key_equivalents(_layout: &str) -> Option<HashMap<char, char>> {
-    None
-}

crates/settings/src/keymap_file.rs 🔗

@@ -3,7 +3,8 @@ use collections::{BTreeMap, HashMap, IndexMap};
 use fs::Fs;
 use gpui::{
     Action, ActionBuildError, App, InvalidKeystrokeError, KEYSTROKE_PARSE_EXPECTED_MESSAGE,
-    KeyBinding, KeyBindingContextPredicate, KeyBindingMetaIndex, Keystroke, NoAction, SharedString,
+    KeyBinding, KeyBindingContextPredicate, KeyBindingMetaIndex, KeybindingKeystroke, Keystroke,
+    NoAction, SharedString,
 };
 use schemars::{JsonSchema, json_schema};
 use serde::Deserialize;
@@ -211,9 +212,6 @@ impl KeymapFile {
     }
 
     pub fn load(content: &str, cx: &App) -> KeymapFileLoadResult {
-        let key_equivalents =
-            crate::key_equivalents::get_key_equivalents(cx.keyboard_layout().id());
-
         if content.is_empty() {
             return KeymapFileLoadResult::Success {
                 key_bindings: Vec::new(),
@@ -255,12 +253,6 @@ impl KeymapFile {
                 }
             };
 
-            let key_equivalents = if *use_key_equivalents {
-                key_equivalents.as_ref()
-            } else {
-                None
-            };
-
             let mut section_errors = String::new();
 
             if !unrecognized_fields.is_empty() {
@@ -278,7 +270,7 @@ impl KeymapFile {
                         keystrokes,
                         action,
                         context_predicate.clone(),
-                        key_equivalents,
+                        *use_key_equivalents,
                         cx,
                     );
                     match result {
@@ -336,7 +328,7 @@ impl KeymapFile {
         keystrokes: &str,
         action: &KeymapAction,
         context: Option<Rc<KeyBindingContextPredicate>>,
-        key_equivalents: Option<&HashMap<char, char>>,
+        use_key_equivalents: bool,
         cx: &App,
     ) -> std::result::Result<KeyBinding, String> {
         let (build_result, action_input_string) = match &action.0 {
@@ -404,8 +396,9 @@ impl KeymapFile {
             keystrokes,
             action,
             context,
-            key_equivalents,
+            use_key_equivalents,
             action_input_string.map(SharedString::from),
+            cx.keyboard_mapper().as_ref(),
         ) {
             Ok(key_binding) => key_binding,
             Err(InvalidKeystrokeError { keystroke }) => {
@@ -607,6 +600,7 @@ impl KeymapFile {
         mut operation: KeybindUpdateOperation<'a>,
         mut keymap_contents: String,
         tab_size: usize,
+        keyboard_mapper: &dyn gpui::PlatformKeyboardMapper,
     ) -> Result<String> {
         match operation {
             // if trying to replace a keybinding that is not user-defined, treat it as an add operation
@@ -646,7 +640,7 @@ impl KeymapFile {
                 .action_value()
                 .context("Failed to generate target action JSON value")?;
             let Some((index, keystrokes_str)) =
-                find_binding(&keymap, &target, &target_action_value)
+                find_binding(&keymap, &target, &target_action_value, keyboard_mapper)
             else {
                 anyhow::bail!("Failed to find keybinding to remove");
             };
@@ -681,7 +675,7 @@ impl KeymapFile {
                 .context("Failed to generate source action JSON value")?;
 
             if let Some((index, keystrokes_str)) =
-                find_binding(&keymap, &target, &target_action_value)
+                find_binding(&keymap, &target, &target_action_value, keyboard_mapper)
             {
                 if target.context == source.context {
                     // if we are only changing the keybinding (common case)
@@ -781,7 +775,7 @@ impl KeymapFile {
             }
             let use_key_equivalents = from.and_then(|from| {
                 let action_value = from.action_value().context("Failed to serialize action value. `use_key_equivalents` on new keybinding may be incorrect.").log_err()?;
-                let (index, _) = find_binding(&keymap, &from, &action_value)?;
+                let (index, _) = find_binding(&keymap, &from, &action_value, keyboard_mapper)?;
                 Some(keymap.0[index].use_key_equivalents)
             }).unwrap_or(false);
             if use_key_equivalents {
@@ -808,6 +802,7 @@ impl KeymapFile {
             keymap: &'b KeymapFile,
             target: &KeybindUpdateTarget<'a>,
             target_action_value: &Value,
+            keyboard_mapper: &dyn gpui::PlatformKeyboardMapper,
         ) -> Option<(usize, &'b str)> {
             let target_context_parsed =
                 KeyBindingContextPredicate::parse(target.context.unwrap_or("")).ok();
@@ -823,8 +818,11 @@ impl KeymapFile {
                 for (keystrokes_str, action) in bindings {
                     let Ok(keystrokes) = keystrokes_str
                         .split_whitespace()
-                        .map(Keystroke::parse)
-                        .collect::<Result<Vec<_>, _>>()
+                        .map(|source| {
+                            let keystroke = Keystroke::parse(source)?;
+                            Ok(KeybindingKeystroke::new(keystroke, false, keyboard_mapper))
+                        })
+                        .collect::<Result<Vec<_>, InvalidKeystrokeError>>()
                     else {
                         continue;
                     };
@@ -832,7 +830,7 @@ impl KeymapFile {
                         || !keystrokes
                             .iter()
                             .zip(target.keystrokes)
-                            .all(|(a, b)| a.should_match(b))
+                            .all(|(a, b)| a.inner.should_match(b))
                     {
                         continue;
                     }
@@ -847,7 +845,7 @@ impl KeymapFile {
     }
 }
 
-#[derive(Clone)]
+#[derive(Clone, Debug)]
 pub enum KeybindUpdateOperation<'a> {
     Replace {
         /// Describes the keybind to create
@@ -916,7 +914,7 @@ impl<'a> KeybindUpdateOperation<'a> {
 #[derive(Debug, Clone)]
 pub struct KeybindUpdateTarget<'a> {
     pub context: Option<&'a str>,
-    pub keystrokes: &'a [Keystroke],
+    pub keystrokes: &'a [KeybindingKeystroke],
     pub action_name: &'a str,
     pub action_arguments: Option<&'a str>,
 }
@@ -941,6 +939,9 @@ impl<'a> KeybindUpdateTarget<'a> {
     fn keystrokes_unparsed(&self) -> String {
         let mut keystrokes = String::with_capacity(self.keystrokes.len() * 8);
         for keystroke in self.keystrokes {
+            // The reason use `keystroke.unparse()` instead of `keystroke.inner.unparse()`
+            // here is that, we want the user to use `ctrl-shift-4` instead of `ctrl-$`
+            // by default on Windows.
             keystrokes.push_str(&keystroke.unparse());
             keystrokes.push(' ');
         }
@@ -959,7 +960,7 @@ impl<'a> KeybindUpdateTarget<'a> {
     }
 }
 
-#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)]
+#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Debug)]
 pub enum KeybindSource {
     User,
     Vim,
@@ -1020,7 +1021,7 @@ impl From<KeybindSource> for KeyBindingMetaIndex {
 
 #[cfg(test)]
 mod tests {
-    use gpui::Keystroke;
+    use gpui::{DummyKeyboardMapper, KeybindingKeystroke, Keystroke};
     use unindent::Unindent;
 
     use crate::{
@@ -1049,16 +1050,27 @@ mod tests {
         operation: KeybindUpdateOperation,
         expected: impl ToString,
     ) {
-        let result = KeymapFile::update_keybinding(operation, input.to_string(), 4)
-            .expect("Update succeeded");
+        let result = KeymapFile::update_keybinding(
+            operation,
+            input.to_string(),
+            4,
+            &gpui::DummyKeyboardMapper,
+        )
+        .expect("Update succeeded");
         pretty_assertions::assert_eq!(expected.to_string(), result);
     }
 
     #[track_caller]
-    fn parse_keystrokes(keystrokes: &str) -> Vec<Keystroke> {
+    fn parse_keystrokes(keystrokes: &str) -> Vec<KeybindingKeystroke> {
         keystrokes
             .split(' ')
-            .map(|s| Keystroke::parse(s).expect("Keystrokes valid"))
+            .map(|s| {
+                KeybindingKeystroke::new(
+                    Keystroke::parse(s).expect("Keystrokes valid"),
+                    false,
+                    &DummyKeyboardMapper,
+                )
+            })
             .collect()
     }
 

crates/settings/src/settings.rs 🔗

@@ -1,6 +1,5 @@
 mod base_keymap_setting;
 mod editable_setting_control;
-mod key_equivalents;
 mod keymap_file;
 mod settings_file;
 mod settings_json;
@@ -14,7 +13,6 @@ use util::asset_str;
 
 pub use base_keymap_setting::*;
 pub use editable_setting_control::*;
-pub use key_equivalents::*;
 pub use keymap_file::{
     KeyBindingValidator, KeyBindingValidatorRegistration, KeybindSource, KeybindUpdateOperation,
     KeybindUpdateTarget, KeymapFile, KeymapFileLoadResult,
@@ -89,7 +87,10 @@ pub fn default_settings() -> Cow<'static, str> {
 #[cfg(target_os = "macos")]
 pub const DEFAULT_KEYMAP_PATH: &str = "keymaps/default-macos.json";
 
-#[cfg(not(target_os = "macos"))]
+#[cfg(target_os = "windows")]
+pub const DEFAULT_KEYMAP_PATH: &str = "keymaps/default-windows.json";
+
+#[cfg(not(any(target_os = "macos", target_os = "windows")))]
 pub const DEFAULT_KEYMAP_PATH: &str = "keymaps/default-linux.json";
 
 pub fn default_keymap() -> Cow<'static, str> {

crates/settings_ui/src/keybindings.rs 🔗

@@ -14,9 +14,9 @@ use gpui::{
     Action, AppContext as _, AsyncApp, Axis, ClickEvent, Context, DismissEvent, Entity,
     EventEmitter, FocusHandle, Focusable, Global, IsZero,
     KeyBindingContextPredicate::{And, Descendant, Equal, Identifier, Not, NotEqual, Or},
-    KeyContext, Keystroke, MouseButton, Point, ScrollStrategy, ScrollWheelEvent, Stateful,
-    StyledText, Subscription, Task, TextStyleRefinement, WeakEntity, actions, anchored, deferred,
-    div,
+    KeyContext, KeybindingKeystroke, Keystroke, MouseButton, PlatformKeyboardMapper, Point,
+    ScrollStrategy, ScrollWheelEvent, Stateful, StyledText, Subscription, Task,
+    TextStyleRefinement, WeakEntity, actions, anchored, deferred, div,
 };
 use language::{Language, LanguageConfig, ToOffset as _};
 use notifications::status_toast::{StatusToast, ToastIcon};
@@ -174,7 +174,7 @@ impl FilterState {
 
 #[derive(Debug, Default, PartialEq, Eq, Clone, Hash)]
 struct ActionMapping {
-    keystrokes: Vec<Keystroke>,
+    keystrokes: Vec<KeybindingKeystroke>,
     context: Option<SharedString>,
 }
 
@@ -236,7 +236,7 @@ struct ConflictState {
 }
 
 type ConflictKeybindMapping = HashMap<
-    Vec<Keystroke>,
+    Vec<KeybindingKeystroke>,
     Vec<(
         Option<gpui::KeyBindingContextPredicate>,
         Vec<ConflictOrigin>,
@@ -414,12 +414,14 @@ impl Focusable for KeymapEditor {
     }
 }
 /// Helper function to check if two keystroke sequences match exactly
-fn keystrokes_match_exactly(keystrokes1: &[Keystroke], keystrokes2: &[Keystroke]) -> bool {
+fn keystrokes_match_exactly(
+    keystrokes1: &[KeybindingKeystroke],
+    keystrokes2: &[KeybindingKeystroke],
+) -> bool {
     keystrokes1.len() == keystrokes2.len()
-        && keystrokes1
-            .iter()
-            .zip(keystrokes2)
-            .all(|(k1, k2)| k1.key == k2.key && k1.modifiers == k2.modifiers)
+        && keystrokes1.iter().zip(keystrokes2).all(|(k1, k2)| {
+            k1.inner.key == k2.inner.key && k1.inner.modifiers == k2.inner.modifiers
+        })
 }
 
 impl KeymapEditor {
@@ -509,7 +511,7 @@ impl KeymapEditor {
         self.filter_editor.read(cx).text(cx)
     }
 
-    fn current_keystroke_query(&self, cx: &App) -> Vec<Keystroke> {
+    fn current_keystroke_query(&self, cx: &App) -> Vec<KeybindingKeystroke> {
         match self.search_mode {
             SearchMode::KeyStroke { .. } => self.keystroke_editor.read(cx).keystrokes().to_vec(),
             SearchMode::Normal => Default::default(),
@@ -530,7 +532,7 @@ impl KeymapEditor {
 
                 let keystroke_query = keystroke_query
                     .into_iter()
-                    .map(|keystroke| keystroke.unparse())
+                    .map(|keystroke| keystroke.inner.unparse())
                     .collect::<Vec<String>>()
                     .join(" ");
 
@@ -554,7 +556,7 @@ impl KeymapEditor {
     async fn update_matches(
         this: WeakEntity<Self>,
         action_query: String,
-        keystroke_query: Vec<Keystroke>,
+        keystroke_query: Vec<KeybindingKeystroke>,
         cx: &mut AsyncApp,
     ) -> anyhow::Result<()> {
         let action_query = command_palette::normalize_action_query(&action_query);
@@ -603,13 +605,15 @@ impl KeymapEditor {
                                         {
                                             let query = &keystroke_query[query_cursor];
                                             let keystroke = &keystrokes[keystroke_cursor];
-                                            let matches =
-                                                query.modifiers.is_subset_of(&keystroke.modifiers)
-                                                    && ((query.key.is_empty()
-                                                        || query.key == keystroke.key)
-                                                        && query.key_char.as_ref().is_none_or(
-                                                            |q_kc| q_kc == &keystroke.key,
-                                                        ));
+                                            let matches = query
+                                                .inner
+                                                .modifiers
+                                                .is_subset_of(&keystroke.inner.modifiers)
+                                                && ((query.inner.key.is_empty()
+                                                    || query.inner.key == keystroke.inner.key)
+                                                    && query.inner.key_char.as_ref().is_none_or(
+                                                        |q_kc| q_kc == &keystroke.inner.key,
+                                                    ));
                                             if matches {
                                                 found_count += 1;
                                                 query_cursor += 1;
@@ -678,7 +682,7 @@ impl KeymapEditor {
                 .map(KeybindSource::from_meta)
                 .unwrap_or(KeybindSource::Unknown);
 
-            let keystroke_text = ui::text_for_keystrokes(key_binding.keystrokes(), cx);
+            let keystroke_text = ui::text_for_keybinding_keystrokes(key_binding.keystrokes(), cx);
             let ui_key_binding = ui::KeyBinding::new_from_gpui(key_binding.clone(), cx)
                 .vim_mode(source == KeybindSource::Vim);
 
@@ -1202,8 +1206,11 @@ impl KeymapEditor {
                 .read(cx)
                 .get_scrollbar_offset(Axis::Vertical),
         ));
-        cx.spawn(async move |_, _| remove_keybinding(to_remove, &fs, tab_size).await)
-            .detach_and_notify_err(window, cx);
+        let keyboard_mapper = cx.keyboard_mapper().clone();
+        cx.spawn(async move |_, _| {
+            remove_keybinding(to_remove, &fs, tab_size, keyboard_mapper.as_ref()).await
+        })
+        .detach_and_notify_err(window, cx);
     }
 
     fn copy_context_to_clipboard(
@@ -1422,7 +1429,7 @@ impl ProcessedBinding {
             .map(|keybind| keybind.get_action_mapping())
     }
 
-    fn keystrokes(&self) -> Option<&[Keystroke]> {
+    fn keystrokes(&self) -> Option<&[KeybindingKeystroke]> {
         self.ui_key_binding()
             .map(|binding| binding.keystrokes.as_slice())
     }
@@ -2220,7 +2227,7 @@ impl KeybindingEditorModal {
         Ok(action_arguments)
     }
 
-    fn validate_keystrokes(&self, cx: &App) -> anyhow::Result<Vec<Keystroke>> {
+    fn validate_keystrokes(&self, cx: &App) -> anyhow::Result<Vec<KeybindingKeystroke>> {
         let new_keystrokes = self
             .keybind_editor
             .read_with(cx, |editor, _| editor.keystrokes().to_vec());
@@ -2316,6 +2323,7 @@ impl KeybindingEditorModal {
         }).unwrap_or(Ok(()))?;
 
         let create = self.creating;
+        let keyboard_mapper = cx.keyboard_mapper().clone();
 
         cx.spawn(async move |this, cx| {
             let action_name = existing_keybind.action().name;
@@ -2328,6 +2336,7 @@ impl KeybindingEditorModal {
                 new_action_args.as_deref(),
                 &fs,
                 tab_size,
+                keyboard_mapper.as_ref(),
             )
             .await
             {
@@ -2445,11 +2454,21 @@ impl KeybindingEditorModal {
     }
 }
 
-fn remove_key_char(Keystroke { modifiers, key, .. }: Keystroke) -> Keystroke {
-    Keystroke {
-        modifiers,
-        key,
-        ..Default::default()
+fn remove_key_char(
+    KeybindingKeystroke {
+        inner,
+        display_modifiers,
+        display_key,
+    }: KeybindingKeystroke,
+) -> KeybindingKeystroke {
+    KeybindingKeystroke {
+        inner: Keystroke {
+            modifiers: inner.modifiers,
+            key: inner.key,
+            key_char: None,
+        },
+        display_modifiers,
+        display_key,
     }
 }
 
@@ -2992,6 +3011,7 @@ async fn save_keybinding_update(
     new_args: Option<&str>,
     fs: &Arc<dyn Fs>,
     tab_size: usize,
+    keyboard_mapper: &dyn PlatformKeyboardMapper,
 ) -> anyhow::Result<()> {
     let keymap_contents = settings::KeymapFile::load_keymap_file(fs)
         .await
@@ -3034,9 +3054,13 @@ async fn save_keybinding_update(
 
     let (new_keybinding, removed_keybinding, source) = operation.generate_telemetry();
 
-    let updated_keymap_contents =
-        settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size)
-            .map_err(|err| anyhow::anyhow!("Could not save updated keybinding: {}", err))?;
+    let updated_keymap_contents = settings::KeymapFile::update_keybinding(
+        operation,
+        keymap_contents,
+        tab_size,
+        keyboard_mapper,
+    )
+    .map_err(|err| anyhow::anyhow!("Could not save updated keybinding: {}", err))?;
     fs.write(
         paths::keymap_file().as_path(),
         updated_keymap_contents.as_bytes(),
@@ -3057,6 +3081,7 @@ async fn remove_keybinding(
     existing: ProcessedBinding,
     fs: &Arc<dyn Fs>,
     tab_size: usize,
+    keyboard_mapper: &dyn PlatformKeyboardMapper,
 ) -> anyhow::Result<()> {
     let Some(keystrokes) = existing.keystrokes() else {
         anyhow::bail!("Cannot remove a keybinding that does not exist");
@@ -3080,9 +3105,13 @@ async fn remove_keybinding(
     };
 
     let (new_keybinding, removed_keybinding, source) = operation.generate_telemetry();
-    let updated_keymap_contents =
-        settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size)
-            .context("Failed to update keybinding")?;
+    let updated_keymap_contents = settings::KeymapFile::update_keybinding(
+        operation,
+        keymap_contents,
+        tab_size,
+        keyboard_mapper,
+    )
+    .context("Failed to update keybinding")?;
     fs.write(
         paths::keymap_file().as_path(),
         updated_keymap_contents.as_bytes(),

crates/settings_ui/src/ui_components/keystroke_input.rs 🔗

@@ -1,6 +1,6 @@
 use gpui::{
     Animation, AnimationExt, Context, EventEmitter, FocusHandle, Focusable, FontWeight, KeyContext,
-    Keystroke, Modifiers, ModifiersChangedEvent, Subscription, Task, actions,
+    KeybindingKeystroke, Keystroke, Modifiers, ModifiersChangedEvent, Subscription, Task, actions,
 };
 use ui::{
     ActiveTheme as _, Color, IconButton, IconButtonShape, IconName, IconSize, Label, LabelSize,
@@ -42,8 +42,8 @@ impl PartialEq for CloseKeystrokeResult {
 }
 
 pub struct KeystrokeInput {
-    keystrokes: Vec<Keystroke>,
-    placeholder_keystrokes: Option<Vec<Keystroke>>,
+    keystrokes: Vec<KeybindingKeystroke>,
+    placeholder_keystrokes: Option<Vec<KeybindingKeystroke>>,
     outer_focus_handle: FocusHandle,
     inner_focus_handle: FocusHandle,
     intercept_subscription: Option<Subscription>,
@@ -70,7 +70,7 @@ impl KeystrokeInput {
     const KEYSTROKE_COUNT_MAX: usize = 3;
 
     pub fn new(
-        placeholder_keystrokes: Option<Vec<Keystroke>>,
+        placeholder_keystrokes: Option<Vec<KeybindingKeystroke>>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Self {
@@ -97,7 +97,7 @@ impl KeystrokeInput {
         }
     }
 
-    pub fn set_keystrokes(&mut self, keystrokes: Vec<Keystroke>, cx: &mut Context<Self>) {
+    pub fn set_keystrokes(&mut self, keystrokes: Vec<KeybindingKeystroke>, cx: &mut Context<Self>) {
         self.keystrokes = keystrokes;
         self.keystrokes_changed(cx);
     }
@@ -106,7 +106,7 @@ impl KeystrokeInput {
         self.search = search;
     }
 
-    pub fn keystrokes(&self) -> &[Keystroke] {
+    pub fn keystrokes(&self) -> &[KeybindingKeystroke] {
         if let Some(placeholders) = self.placeholder_keystrokes.as_ref()
             && self.keystrokes.is_empty()
         {
@@ -116,18 +116,22 @@ impl KeystrokeInput {
             && self
                 .keystrokes
                 .last()
-                .is_some_and(|last| last.key.is_empty())
+                .is_some_and(|last| last.display_key.is_empty())
         {
             return &self.keystrokes[..self.keystrokes.len() - 1];
         }
         &self.keystrokes
     }
 
-    fn dummy(modifiers: Modifiers) -> Keystroke {
-        Keystroke {
-            modifiers,
-            key: "".to_string(),
-            key_char: None,
+    fn dummy(modifiers: Modifiers) -> KeybindingKeystroke {
+        KeybindingKeystroke {
+            inner: Keystroke {
+                modifiers,
+                key: "".to_string(),
+                key_char: None,
+            },
+            display_modifiers: modifiers,
+            display_key: "".to_string(),
         }
     }
 
@@ -254,7 +258,7 @@ impl KeystrokeInput {
         self.keystrokes_changed(cx);
 
         if let Some(last) = self.keystrokes.last_mut()
-            && last.key.is_empty()
+            && last.display_key.is_empty()
             && keystrokes_len <= Self::KEYSTROKE_COUNT_MAX
         {
             if !self.search && !event.modifiers.modified() {
@@ -263,13 +267,15 @@ impl KeystrokeInput {
             }
             if self.search {
                 if self.previous_modifiers.modified() {
-                    last.modifiers |= event.modifiers;
+                    last.display_modifiers |= event.modifiers;
+                    last.inner.modifiers |= event.modifiers;
                 } else {
                     self.keystrokes.push(Self::dummy(event.modifiers));
                 }
                 self.previous_modifiers |= event.modifiers;
             } else {
-                last.modifiers = event.modifiers;
+                last.display_modifiers = event.modifiers;
+                last.inner.modifiers = event.modifiers;
                 return;
             }
         } else if keystrokes_len < Self::KEYSTROKE_COUNT_MAX {
@@ -297,14 +303,17 @@ impl KeystrokeInput {
             return;
         }
 
-        let mut keystroke = keystroke.clone();
+        let mut keystroke =
+            KeybindingKeystroke::new(keystroke.clone(), false, cx.keyboard_mapper().as_ref());
         if let Some(last) = self.keystrokes.last()
-            && last.key.is_empty()
+            && last.display_key.is_empty()
             && (!self.search || self.previous_modifiers.modified())
         {
-            let key = keystroke.key.clone();
+            let display_key = keystroke.display_key.clone();
+            let inner_key = keystroke.inner.key.clone();
             keystroke = last.clone();
-            keystroke.key = key;
+            keystroke.display_key = display_key;
+            keystroke.inner.key = inner_key;
             self.keystrokes.pop();
         }
 
@@ -324,11 +333,14 @@ impl KeystrokeInput {
         self.keystrokes_changed(cx);
 
         if self.search {
-            self.previous_modifiers = keystroke.modifiers;
+            self.previous_modifiers = keystroke.display_modifiers;
             return;
         }
-        if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX && keystroke.modifiers.modified() {
-            self.keystrokes.push(Self::dummy(keystroke.modifiers));
+        if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX
+            && keystroke.display_modifiers.modified()
+        {
+            self.keystrokes
+                .push(Self::dummy(keystroke.display_modifiers));
         }
     }
 
@@ -364,7 +376,7 @@ impl KeystrokeInput {
             &self.keystrokes
         };
         keystrokes.iter().map(move |keystroke| {
-            h_flex().children(ui::render_keystroke(
+            h_flex().children(ui::render_keybinding_keystroke(
                 keystroke,
                 Some(Color::Default),
                 Some(rems(0.875).into()),
@@ -809,9 +821,13 @@ mod tests {
         /// Verifies that the keystrokes match the expected strings
         #[track_caller]
         pub fn expect_keystrokes(&mut self, expected: &[&str]) -> &mut Self {
-            let actual = self
-                .input
-                .read_with(&self.cx, |input, _| input.keystrokes.clone());
+            let actual: Vec<Keystroke> = self.input.read_with(&self.cx, |input, _| {
+                input
+                    .keystrokes
+                    .iter()
+                    .map(|keystroke| keystroke.inner.clone())
+                    .collect()
+            });
             Self::expect_keystrokes_equal(&actual, expected);
             self
         }
@@ -939,7 +955,7 @@ mod tests {
     }
 
     struct KeystrokeUpdateTracker {
-        initial_keystrokes: Vec<Keystroke>,
+        initial_keystrokes: Vec<KeybindingKeystroke>,
         _subscription: Subscription,
         input: Entity<KeystrokeInput>,
         received_keystrokes_updated: bool,
@@ -983,8 +999,8 @@ mod tests {
                 );
             }
 
-            fn keystrokes_str(ks: &[Keystroke]) -> String {
-                ks.iter().map(|ks| ks.unparse()).join(" ")
+            fn keystrokes_str(ks: &[KeybindingKeystroke]) -> String {
+                ks.iter().map(|ks| ks.inner.unparse()).join(" ")
             }
         }
     }

crates/ui/src/components/keybinding.rs 🔗

@@ -1,8 +1,8 @@
 use crate::PlatformStyle;
 use crate::{Icon, IconName, IconSize, h_flex, prelude::*};
 use gpui::{
-    Action, AnyElement, App, FocusHandle, Global, IntoElement, Keystroke, Modifiers, Window,
-    relative,
+    Action, AnyElement, App, FocusHandle, Global, IntoElement, KeybindingKeystroke, Keystroke,
+    Modifiers, Window, relative,
 };
 use itertools::Itertools;
 
@@ -13,7 +13,7 @@ pub struct KeyBinding {
     /// More than one keystroke produces a chord.
     ///
     /// This should always contain at least one keystroke.
-    pub keystrokes: Vec<Keystroke>,
+    pub keystrokes: Vec<KeybindingKeystroke>,
 
     /// The [`PlatformStyle`] to use when displaying this keybinding.
     platform_style: PlatformStyle,
@@ -59,7 +59,7 @@ impl KeyBinding {
         cx.try_global::<VimStyle>().is_some_and(|g| g.0)
     }
 
-    pub fn new(keystrokes: Vec<Keystroke>, cx: &App) -> Self {
+    pub fn new(keystrokes: Vec<KeybindingKeystroke>, cx: &App) -> Self {
         Self {
             keystrokes,
             platform_style: PlatformStyle::platform(),
@@ -99,16 +99,16 @@ impl KeyBinding {
 }
 
 fn render_key(
-    keystroke: &Keystroke,
+    key: &str,
     color: Option<Color>,
     platform_style: PlatformStyle,
     size: impl Into<Option<AbsoluteLength>>,
 ) -> AnyElement {
-    let key_icon = icon_for_key(keystroke, platform_style);
+    let key_icon = icon_for_key(key, platform_style);
     match key_icon {
         Some(icon) => KeyIcon::new(icon, color).size(size).into_any_element(),
         None => {
-            let key = util::capitalize(&keystroke.key);
+            let key = util::capitalize(key);
             Key::new(&key, color).size(size).into_any_element()
         }
     }
@@ -124,7 +124,7 @@ impl RenderOnce for KeyBinding {
                     "KEY_BINDING-{}",
                     self.keystrokes
                         .iter()
-                        .map(|k| k.key.to_string())
+                        .map(|k| k.display_key.to_string())
                         .collect::<Vec<_>>()
                         .join(" ")
                 )
@@ -137,7 +137,7 @@ impl RenderOnce for KeyBinding {
                     .py_0p5()
                     .rounded_xs()
                     .text_color(cx.theme().colors().text_muted)
-                    .children(render_keystroke(
+                    .children(render_keybinding_keystroke(
                         keystroke,
                         color,
                         self.size,
@@ -148,8 +148,8 @@ impl RenderOnce for KeyBinding {
     }
 }
 
-pub fn render_keystroke(
-    keystroke: &Keystroke,
+pub fn render_keybinding_keystroke(
+    keystroke: &KeybindingKeystroke,
     color: Option<Color>,
     size: impl Into<Option<AbsoluteLength>>,
     platform_style: PlatformStyle,
@@ -163,26 +163,39 @@ pub fn render_keystroke(
     let size = size.into();
 
     if use_text {
-        let element = Key::new(keystroke_text(keystroke, platform_style, vim_mode), color)
-            .size(size)
-            .into_any_element();
+        let element = Key::new(
+            keystroke_text(
+                &keystroke.display_modifiers,
+                &keystroke.display_key,
+                platform_style,
+                vim_mode,
+            ),
+            color,
+        )
+        .size(size)
+        .into_any_element();
         vec![element]
     } else {
         let mut elements = Vec::new();
         elements.extend(render_modifiers(
-            &keystroke.modifiers,
+            &keystroke.display_modifiers,
             platform_style,
             color,
             size,
             true,
         ));
-        elements.push(render_key(keystroke, color, platform_style, size));
+        elements.push(render_key(
+            &keystroke.display_key,
+            color,
+            platform_style,
+            size,
+        ));
         elements
     }
 }
 
-fn icon_for_key(keystroke: &Keystroke, platform_style: PlatformStyle) -> Option<IconName> {
-    match keystroke.key.as_str() {
+fn icon_for_key(key: &str, platform_style: PlatformStyle) -> Option<IconName> {
+    match key {
         "left" => Some(IconName::ArrowLeft),
         "right" => Some(IconName::ArrowRight),
         "up" => Some(IconName::ArrowUp),
@@ -379,7 +392,7 @@ impl KeyIcon {
 /// Returns a textual representation of the key binding for the given [`Action`].
 pub fn text_for_action(action: &dyn Action, window: &Window, cx: &App) -> Option<String> {
     let key_binding = window.highest_precedence_binding_for_action(action)?;
-    Some(text_for_keystrokes(key_binding.keystrokes(), cx))
+    Some(text_for_keybinding_keystrokes(key_binding.keystrokes(), cx))
 }
 
 pub fn text_for_keystrokes(keystrokes: &[Keystroke], cx: &App) -> String {
@@ -387,22 +400,50 @@ pub fn text_for_keystrokes(keystrokes: &[Keystroke], cx: &App) -> String {
     let vim_enabled = cx.try_global::<VimStyle>().is_some();
     keystrokes
         .iter()
-        .map(|keystroke| keystroke_text(keystroke, platform_style, vim_enabled))
+        .map(|keystroke| {
+            keystroke_text(
+                &keystroke.modifiers,
+                &keystroke.key,
+                platform_style,
+                vim_enabled,
+            )
+        })
+        .join(" ")
+}
+
+pub fn text_for_keybinding_keystrokes(keystrokes: &[KeybindingKeystroke], cx: &App) -> String {
+    let platform_style = PlatformStyle::platform();
+    let vim_enabled = cx.try_global::<VimStyle>().is_some();
+    keystrokes
+        .iter()
+        .map(|keystroke| {
+            keystroke_text(
+                &keystroke.display_modifiers,
+                &keystroke.display_key,
+                platform_style,
+                vim_enabled,
+            )
+        })
         .join(" ")
 }
 
-pub fn text_for_keystroke(keystroke: &Keystroke, cx: &App) -> String {
+pub fn text_for_keystroke(modifiers: &Modifiers, key: &str, cx: &App) -> String {
     let platform_style = PlatformStyle::platform();
     let vim_enabled = cx.try_global::<VimStyle>().is_some();
-    keystroke_text(keystroke, platform_style, vim_enabled)
+    keystroke_text(modifiers, key, platform_style, vim_enabled)
 }
 
 /// Returns a textual representation of the given [`Keystroke`].
-fn keystroke_text(keystroke: &Keystroke, platform_style: PlatformStyle, vim_mode: bool) -> String {
+fn keystroke_text(
+    modifiers: &Modifiers,
+    key: &str,
+    platform_style: PlatformStyle,
+    vim_mode: bool,
+) -> String {
     let mut text = String::new();
     let delimiter = '-';
 
-    if keystroke.modifiers.function {
+    if modifiers.function {
         match vim_mode {
             false => text.push_str("Fn"),
             true => text.push_str("fn"),
@@ -411,7 +452,7 @@ fn keystroke_text(keystroke: &Keystroke, platform_style: PlatformStyle, vim_mode
         text.push(delimiter);
     }
 
-    if keystroke.modifiers.control {
+    if modifiers.control {
         match (platform_style, vim_mode) {
             (PlatformStyle::Mac, false) => text.push_str("Control"),
             (PlatformStyle::Linux | PlatformStyle::Windows, false) => text.push_str("Ctrl"),
@@ -421,7 +462,7 @@ fn keystroke_text(keystroke: &Keystroke, platform_style: PlatformStyle, vim_mode
         text.push(delimiter);
     }
 
-    if keystroke.modifiers.platform {
+    if modifiers.platform {
         match (platform_style, vim_mode) {
             (PlatformStyle::Mac, false) => text.push_str("Command"),
             (PlatformStyle::Mac, true) => text.push_str("cmd"),
@@ -434,7 +475,7 @@ fn keystroke_text(keystroke: &Keystroke, platform_style: PlatformStyle, vim_mode
         text.push(delimiter);
     }
 
-    if keystroke.modifiers.alt {
+    if modifiers.alt {
         match (platform_style, vim_mode) {
             (PlatformStyle::Mac, false) => text.push_str("Option"),
             (PlatformStyle::Linux | PlatformStyle::Windows, false) => text.push_str("Alt"),
@@ -444,7 +485,7 @@ fn keystroke_text(keystroke: &Keystroke, platform_style: PlatformStyle, vim_mode
         text.push(delimiter);
     }
 
-    if keystroke.modifiers.shift {
+    if modifiers.shift {
         match (platform_style, vim_mode) {
             (_, false) => text.push_str("Shift"),
             (_, true) => text.push_str("shift"),
@@ -453,9 +494,9 @@ fn keystroke_text(keystroke: &Keystroke, platform_style: PlatformStyle, vim_mode
     }
 
     if vim_mode {
-        text.push_str(&keystroke.key)
+        text.push_str(key)
     } else {
-        let key = match keystroke.key.as_str() {
+        let key = match key {
             "pageup" => "PageUp",
             "pagedown" => "PageDown",
             key => &util::capitalize(key),
@@ -562,9 +603,11 @@ mod tests {
 
     #[test]
     fn test_text_for_keystroke() {
+        let keystroke = Keystroke::parse("cmd-c").unwrap();
         assert_eq!(
             keystroke_text(
-                &Keystroke::parse("cmd-c").unwrap(),
+                &keystroke.modifiers,
+                &keystroke.key,
                 PlatformStyle::Mac,
                 false
             ),
@@ -572,7 +615,8 @@ mod tests {
         );
         assert_eq!(
             keystroke_text(
-                &Keystroke::parse("cmd-c").unwrap(),
+                &keystroke.modifiers,
+                &keystroke.key,
                 PlatformStyle::Linux,
                 false
             ),
@@ -580,16 +624,19 @@ mod tests {
         );
         assert_eq!(
             keystroke_text(
-                &Keystroke::parse("cmd-c").unwrap(),
+                &keystroke.modifiers,
+                &keystroke.key,
                 PlatformStyle::Windows,
                 false
             ),
             "Win-C".to_string()
         );
 
+        let keystroke = Keystroke::parse("ctrl-alt-delete").unwrap();
         assert_eq!(
             keystroke_text(
-                &Keystroke::parse("ctrl-alt-delete").unwrap(),
+                &keystroke.modifiers,
+                &keystroke.key,
                 PlatformStyle::Mac,
                 false
             ),
@@ -597,7 +644,8 @@ mod tests {
         );
         assert_eq!(
             keystroke_text(
-                &Keystroke::parse("ctrl-alt-delete").unwrap(),
+                &keystroke.modifiers,
+                &keystroke.key,
                 PlatformStyle::Linux,
                 false
             ),
@@ -605,16 +653,19 @@ mod tests {
         );
         assert_eq!(
             keystroke_text(
-                &Keystroke::parse("ctrl-alt-delete").unwrap(),
+                &keystroke.modifiers,
+                &keystroke.key,
                 PlatformStyle::Windows,
                 false
             ),
             "Ctrl-Alt-Delete".to_string()
         );
 
+        let keystroke = Keystroke::parse("shift-pageup").unwrap();
         assert_eq!(
             keystroke_text(
-                &Keystroke::parse("shift-pageup").unwrap(),
+                &keystroke.modifiers,
+                &keystroke.key,
                 PlatformStyle::Mac,
                 false
             ),
@@ -622,7 +673,8 @@ mod tests {
         );
         assert_eq!(
             keystroke_text(
-                &Keystroke::parse("shift-pageup").unwrap(),
+                &keystroke.modifiers,
+                &keystroke.key,
                 PlatformStyle::Linux,
                 false,
             ),
@@ -630,7 +682,8 @@ mod tests {
         );
         assert_eq!(
             keystroke_text(
-                &Keystroke::parse("shift-pageup").unwrap(),
+                &keystroke.modifiers,
+                &keystroke.key,
                 PlatformStyle::Windows,
                 false
             ),

crates/zed/src/zed.rs 🔗

@@ -1308,11 +1308,11 @@ pub fn handle_keymap_file_changes(
     })
     .detach();
 
-    let mut current_mapping = settings::get_key_equivalents(cx.keyboard_layout().id());
+    let mut current_layout_id = cx.keyboard_layout().id().to_string();
     cx.on_keyboard_layout_change(move |cx| {
-        let next_mapping = settings::get_key_equivalents(cx.keyboard_layout().id());
-        if next_mapping != current_mapping {
-            current_mapping = next_mapping;
+        let next_layout_id = cx.keyboard_layout().id();
+        if next_layout_id != current_layout_id {
+            current_layout_id = next_layout_id.to_string();
             keyboard_layout_tx.unbounded_send(()).ok();
         }
     })
@@ -4729,7 +4729,7 @@ mod tests {
                 // and key strokes contain the given key
                 bindings
                     .into_iter()
-                    .any(|binding| binding.keystrokes().iter().any(|k| k.key == key)),
+                    .any(|binding| binding.keystrokes().iter().any(|k| k.display_key == key)),
                 "On {} Failed to find {} with key binding {}",
                 line,
                 action.name(),

crates/zed/src/zed/quick_action_bar/preview.rs 🔗

@@ -72,7 +72,10 @@ impl QuickActionBar {
                 Tooltip::with_meta(
                     tooltip_text,
                     Some(open_action_for_tooltip),
-                    format!("{} to open in a split", text_for_keystroke(&alt_click, cx)),
+                    format!(
+                        "{} to open in a split",
+                        text_for_keystroke(&alt_click.modifiers, &alt_click.key, cx)
+                    ),
                     window,
                     cx,
                 )