Detailed changes
@@ -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"
+ }
+ }
+]
@@ -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),
};
@@ -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
@@ -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),
@@ -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
@@ -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);
}
@@ -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()
}
@@ -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.
@@ -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
+ }
+}
@@ -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
+}
@@ -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));
}
@@ -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()))
+}
@@ -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();
@@ -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()>) {
@@ -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());
+ }
+}
@@ -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);
}
@@ -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()
@@ -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
-}
@@ -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()
}
@@ -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> {
@@ -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(),
@@ -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(" ")
}
}
}
@@ -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
),
@@ -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(),
@@ -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,
)