From f18ea2c94c28c5de9790b1304f5911cf6eadc457 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 24 Jul 2026 10:54:03 +0300 Subject: [PATCH] Split VSCode and Zed keymap files (#61532) Currently, Zed's "VSCode" keymap has certain discrepancies that are not changed for compatibility reasons, e.g. opening inline assistant is different in each editor. To simplify the transition, extract VSCode bindings its own keymap so in the future it's simpler to accept changes to each keymap separately. Caveat: people who had ever changed the keymap, will have VSCode keymap in their settings which will change their bindings slightly after this commit is released. I had to introduce more logic to `null` handling as had to somehow disable `ctrl-enter` on mac from spawning the inline assistant block for VSCode keymap (it's cmd-i there). Release Notes: - Split VSCode and Zed keymap files --- assets/keymaps/linux/vscode.json | 72 ++++++++++++++ assets/keymaps/macos/vscode.json | 82 ++++++++++++++++ assets/settings/default.json | 16 ++-- crates/gpui/src/keymap.rs | 96 ++++++++++++++++--- crates/onboarding/src/basics_page.rs | 22 +++-- crates/settings/src/base_keymap_setting.rs | 22 +++-- .../settings_content/src/settings_content.rs | 4 +- crates/zed/src/zed.rs | 32 +++++++ docs/src/key-bindings.md | 3 +- docs/src/reference/all-settings.md | 48 +++++++--- 10 files changed, 349 insertions(+), 48 deletions(-) create mode 100644 assets/keymaps/linux/vscode.json create mode 100644 assets/keymaps/macos/vscode.json diff --git a/assets/keymaps/linux/vscode.json b/assets/keymaps/linux/vscode.json new file mode 100644 index 0000000000..a333e19ff9 --- /dev/null +++ b/assets/keymaps/linux/vscode.json @@ -0,0 +1,72 @@ +[ + // VS Code for Linux (and Windows). See: https://code.visualstudio.com/docs/reference/default-keybindings + // + // Zed's default keymap is close to VS Code's, so this overlay only contains + // the bindings where the two diverge. + { + "context": "Editor", + "use_key_equivalents": true, + "bindings": { + "shift-alt-f": "editor::Format", // Format Document (Windows-style; Linux `ctrl-shift-i` is a Zed default already) + "ctrl-k ctrl-f": "editor::FormatSelections", // Format Selection + "shift-alt-i": "editor::SplitSelectionIntoLines", // Insert cursor at end of each line selected + "alt-z": "editor::ToggleSoftWrap", // Toggle Word Wrap + "ctrl-shift-space": "editor::ShowSignatureHelp", // Trigger Parameter Hints + "ctrl-shift-a": "editor::ToggleBlockComments", // Toggle Block Comment + "ctrl-k f12": "editor::GoToDefinitionSplit", // Open Definition to the Side + }, + }, + { + "context": "Editor && mode == full", + "use_key_equivalents": true, + "bindings": { + // In VS Code `ctrl-k z` is Zen Mode; the word wrap toggle moves to `alt-z`. + "ctrl-k z": "workspace::ToggleCenteredLayout", + "ctrl-i": "assistant::InlineAssist", // Inline Chat + }, + }, + { + "context": "!AcpThread > Editor && mode == full", + "use_key_equivalents": true, + "bindings": { + "ctrl-enter": "editor::NewlineBelow", // Insert Line Below (Inline Chat lives on `ctrl-i`) + }, + }, + { + "context": "Terminal", + "use_key_equivalents": true, + "bindings": { + "ctrl-enter": null, + }, + }, + { + "context": "Workspace", + "use_key_equivalents": true, + "bindings": { + "f5": "debugger::Start", // Start Debugging + "ctrl-alt-i": "agent::ToggleFocus", // Open Chat + }, + }, + { + "context": "Workspace && debugger_running", + "use_key_equivalents": true, + "bindings": { + "f5": null, + }, + }, + { + "context": "Workspace && debugger_stopped", + "use_key_equivalents": true, + "bindings": { + "f5": "debugger::Continue", + }, + }, + { + "context": "Workspace && debugger_session", + "use_key_equivalents": true, + "bindings": { + "f10": "debugger::StepOver", + "f11": "debugger::StepInto", + }, + }, +] diff --git a/assets/keymaps/macos/vscode.json b/assets/keymaps/macos/vscode.json new file mode 100644 index 0000000000..532671e508 --- /dev/null +++ b/assets/keymaps/macos/vscode.json @@ -0,0 +1,82 @@ +[ + // VS Code for macOS. See: https://code.visualstudio.com/docs/reference/default-keybindings + // + // Zed's default keymap is close to VS Code's, so this overlay only contains + // the bindings where the two diverge. + { + "context": "Editor", + "use_key_equivalents": true, + "bindings": { + "shift-alt-f": "editor::Format", // Format Document + "cmd-k cmd-f": "editor::FormatSelections", // Format Selection + "shift-alt-i": "editor::SplitSelectionIntoLines", // Insert cursor at end of each line selected + "alt-z": "editor::ToggleSoftWrap", // Toggle Word Wrap + "cmd-shift-space": "editor::ShowSignatureHelp", // Trigger Parameter Hints + "ctrl-shift-cmd-right": "editor::SelectLargerSyntaxNode", // Expand AST Selection + "ctrl-shift-cmd-left": "editor::SelectSmallerSyntaxNode", // Shrink AST Selection + "cmd-k cmd-c": ["editor::ToggleComments", { "advance_downwards": false }], // Add Line Comment (closest analog) + "cmd-k f12": "editor::GoToDefinitionSplit", // Open Definition to the Side + }, + }, + { + "context": "Editor && mode == full", + "use_key_equivalents": true, + "bindings": { + // In VS Code `cmd-k z` is Zen Mode; the word wrap toggle moves to `alt-z`. + "cmd-k z": "workspace::ToggleCenteredLayout", + "cmd-i": "assistant::InlineAssist", // Inline Chat + }, + }, + { + "context": "!AcpThread > Editor && mode == full", + "use_key_equivalents": true, + "bindings": { + "ctrl-enter": null, // Inline Chat lives on `cmd-i` in VS Code + }, + }, + { + "context": "Terminal", + "use_key_equivalents": true, + "bindings": { + "cmd-i": "assistant::InlineAssist", // Terminal Inline Chat + "ctrl-enter": null, + }, + }, + { + "context": "BufferSearchBar || ProjectSearchBar", + "use_key_equivalents": true, + "bindings": { + "alt-cmd-r": "search::ToggleRegex", // Toggle Find Regex + }, + }, + { + "context": "Workspace", + "use_key_equivalents": true, + "bindings": { + "f5": "debugger::Start", // Start Debugging + "ctrl-cmd-i": "agent::ToggleFocus", // Open Chat + }, + }, + { + "context": "Workspace && debugger_running", + "use_key_equivalents": true, + "bindings": { + "f5": null, + }, + }, + { + "context": "Workspace && debugger_stopped", + "use_key_equivalents": true, + "bindings": { + "f5": "debugger::Continue", + }, + }, + { + "context": "Workspace && debugger_session", + "use_key_equivalents": true, + "bindings": { + "f10": "debugger::StepOver", + "f11": "debugger::StepInto", + }, + }, +] diff --git a/assets/settings/default.json b/assets/settings/default.json index 0ecebd2dde..98f758c9f3 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -13,16 +13,18 @@ }, "icon_theme": "Zed (Default)", // The name of a base set of key bindings to use. - // This setting can take six values, each named after another - // text editor: + // This setting can take the following values: // - // 1. "VSCode" - // 2. "Atom" - // 3. "JetBrains" - // 4. "None" + // 1. "Zed" + // 2. "VSCode" + // 3. "Atom" + // 4. "JetBrains" // 5. "SublimeText" // 6. "TextMate" - "base_keymap": "VSCode", + // 7. "Emacs" + // 8. "Cursor" + // 9. "None" + "base_keymap": "Zed", // The name of a font to use for rendering text in the editor // ".ZedMono" currently aliases to Lilex // but this may change in the future. diff --git a/crates/gpui/src/keymap.rs b/crates/gpui/src/keymap.rs index ade499b890..bef0d03a2b 100644 --- a/crates/gpui/src/keymap.rs +++ b/crates/gpui/src/keymap.rs @@ -158,9 +158,10 @@ impl Keymap { /// In the case of multiple bindings at the same depth, the ones added to the keymap later take /// precedence. User bindings are added after built-in bindings so that they take precedence. /// - /// If a user has disabled a binding with `"x": null` it will not be returned. Disabled bindings + /// If a binding has been disabled with `"x": null` it will not be returned. Disabled bindings /// are evaluated with the same precedence rules so you can disable a rule in a given context - /// only. + /// only. A disabled binding only suppresses bindings from sources with equal or weaker + /// precedence: a base keymap null hides default bindings, but user bindings still apply. pub fn bindings_for_input( &self, input: &[impl AsKeystroke], @@ -191,20 +192,20 @@ impl Keymap { let mut bindings: SmallVec<[_; 1]> = SmallVec::new(); let mut first_binding_index = None; let mut unbound_bindings: Vec<&KeyBinding> = Vec::new(); + // A `NoAction` binding suppresses out-ranked bindings from sources with + // equal or weaker precedence, while bindings from stronger sources (a + // smaller meta, e.g. a user binding vs a base keymap null) still apply. + // Bindings without a meta are treated as user bindings. + let mut no_action_meta: Option = None; for (_, ix, binding) in matched_bindings { + let meta = binding.meta.map_or(0, |meta| meta.0); if is_no_action(&*binding.action) { - // Only break if this is a user-defined NoAction binding - // This allows user keymaps to override base keymap NoAction bindings - if let Some(meta) = binding.meta { - if meta.0 == 0 { - break; - } - } else { - // If no meta is set, assume it's a user binding for safety - break; - } - // For non-user NoAction bindings, continue searching for user overrides + no_action_meta = Some(no_action_meta.map_or(meta, |existing| existing.min(meta))); + continue; + } + + if no_action_meta.is_some_and(|no_action_meta| meta >= no_action_meta) { continue; } @@ -570,6 +571,75 @@ mod tests { assert!(!pending); } + #[test] + fn test_disable_weaker_sources_only() { + const USER: KeyBindingMetaIndex = KeyBindingMetaIndex(0); + const VIM: KeyBindingMetaIndex = KeyBindingMetaIndex(1); + const BASE: KeyBindingMetaIndex = KeyBindingMetaIndex(2); + const DEFAULT: KeyBindingMetaIndex = KeyBindingMetaIndex(3); + + let editor_context = || [KeyContext::parse("editor").unwrap()]; + let ctrl_x = || [Keystroke::parse("ctrl-x").unwrap()]; + + // A base keymap null disables a default binding in the same context. + let mut keymap = Keymap::default(); + keymap.add_bindings([ + KeyBinding::new("ctrl-x", ActionAlpha {}, Some("editor")).with_meta(DEFAULT), + KeyBinding::new("ctrl-x", NoAction {}, Some("editor")).with_meta(BASE), + ]); + let (result, _) = keymap.bindings_for_input(&ctrl_x(), &editor_context()); + assert!(result.is_empty()); + + // A user binding is not affected by base keymap or default nulls. + let mut keymap = Keymap::default(); + keymap.add_bindings([ + KeyBinding::new("ctrl-x", NoAction {}, Some("editor")).with_meta(DEFAULT), + KeyBinding::new("ctrl-x", NoAction {}, Some("editor")).with_meta(BASE), + KeyBinding::new("ctrl-x", ActionBeta {}, None).with_meta(USER), + ]); + let (result, _) = keymap.bindings_for_input(&ctrl_x(), &editor_context()); + assert_eq!(result.len(), 1); + assert!(result[0].action.partial_eq(&ActionBeta {})); + + // A user binding at a shallower context is not disabled by a deeper + // base keymap null. + let mut keymap = Keymap::default(); + keymap.add_bindings([ + KeyBinding::new("ctrl-x", NoAction {}, Some("editor")).with_meta(BASE), + KeyBinding::new("ctrl-x", ActionBeta {}, Some("workspace")).with_meta(USER), + ]); + let (result, _) = keymap.bindings_for_input( + &ctrl_x(), + &[ + KeyContext::parse("workspace").unwrap(), + KeyContext::parse("editor").unwrap(), + ], + ); + assert_eq!(result.len(), 1); + assert!(result[0].action.partial_eq(&ActionBeta {})); + + // A vim binding survives a base keymap null, and a user null disables + // everything. + let mut keymap = Keymap::default(); + keymap.add_bindings([ + KeyBinding::new("ctrl-x", ActionAlpha {}, Some("editor")).with_meta(DEFAULT), + KeyBinding::new("ctrl-x", NoAction {}, Some("editor")).with_meta(BASE), + KeyBinding::new("ctrl-x", ActionGamma {}, Some("editor")).with_meta(VIM), + ]); + let (result, _) = keymap.bindings_for_input(&ctrl_x(), &editor_context()); + assert_eq!(result.len(), 1); + assert!(result[0].action.partial_eq(&ActionGamma {})); + + let mut keymap = Keymap::default(); + keymap.add_bindings([ + KeyBinding::new("ctrl-x", ActionAlpha {}, Some("editor")).with_meta(DEFAULT), + KeyBinding::new("ctrl-x", ActionGamma {}, Some("editor")).with_meta(VIM), + KeyBinding::new("ctrl-x", NoAction {}, Some("editor")).with_meta(USER), + ]); + let (result, _) = keymap.bindings_for_input(&ctrl_x(), &editor_context()); + assert!(result.is_empty()); + } + #[test] fn test_fail_to_disable() { // disabled at the wrong level diff --git a/crates/onboarding/src/basics_page.rs b/crates/onboarding/src/basics_page.rs index 40fe15c089..f2b1466c71 100644 --- a/crates/onboarding/src/basics_page.rs +++ b/crates/onboarding/src/basics_page.rs @@ -331,19 +331,24 @@ fn render_telemetry_section(tab_index: &mut isize, cx: &App) -> impl IntoElement fn render_base_keymap_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement { let base_keymap = match BaseKeymap::get_global(cx) { - BaseKeymap::VSCode => Some(0), - BaseKeymap::JetBrains => Some(1), - BaseKeymap::SublimeText => Some(2), - BaseKeymap::Atom => Some(3), - BaseKeymap::Emacs => Some(4), - BaseKeymap::Cursor => Some(5), - BaseKeymap::TextMate | BaseKeymap::None => None, + BaseKeymap::Zed => Some(0), + BaseKeymap::VSCode => Some(1), + BaseKeymap::JetBrains => Some(2), + BaseKeymap::SublimeText => Some(3), + BaseKeymap::Atom => Some(4), + BaseKeymap::Emacs => Some(5), + BaseKeymap::Cursor => Some(6), + BaseKeymap::TextMate => Some(7), + BaseKeymap::None => None, }; return v_flex().gap_2().child(Label::new("Base Keymap")).child( ToggleButtonGroup::two_rows( "base_keymap_selection", [ + ToggleButtonWithIcon::new("Zed", IconName::AiZed, |_, _, cx| { + write_keymap_base(BaseKeymap::Zed, cx); + }), ToggleButtonWithIcon::new("VS Code", IconName::EditorVsCode, |_, _, cx| { write_keymap_base(BaseKeymap::VSCode, cx); }), @@ -364,6 +369,9 @@ fn render_base_keymap_section(tab_index: &mut isize, cx: &mut App) -> impl IntoE ToggleButtonWithIcon::new("Cursor", IconName::EditorCursor, |_, _, cx| { write_keymap_base(BaseKeymap::Cursor, cx); }), + ToggleButtonWithIcon::new("TextMate", IconName::Keyboard, |_, _, cx| { + write_keymap_base(BaseKeymap::TextMate, cx); + }), ], ) .when_some(base_keymap, |this, base_keymap| { diff --git a/crates/settings/src/base_keymap_setting.rs b/crates/settings/src/base_keymap_setting.rs index 8e872dae40..28fc665de5 100644 --- a/crates/settings/src/base_keymap_setting.rs +++ b/crates/settings/src/base_keymap_setting.rs @@ -7,12 +7,13 @@ use settings::{RegisterSetting, Settings}; /// Base key bindings scheme. Base keymaps can be overridden with user keymaps. /// -/// Default: VSCode +/// Default: Zed #[derive( Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default, RegisterSetting, )] pub enum BaseKeymap { #[default] + Zed, VSCode, JetBrains, SublimeText, @@ -26,6 +27,7 @@ pub enum BaseKeymap { impl From for BaseKeymap { fn from(value: BaseKeymapContent) -> Self { match value { + BaseKeymapContent::Zed => Self::Zed, BaseKeymapContent::VSCode => Self::VSCode, BaseKeymapContent::JetBrains => Self::JetBrains, BaseKeymapContent::SublimeText => Self::SublimeText, @@ -40,6 +42,7 @@ impl From for BaseKeymap { impl Into for BaseKeymap { fn into(self) -> BaseKeymapContent { match self { + BaseKeymap::Zed => BaseKeymapContent::Zed, BaseKeymap::VSCode => BaseKeymapContent::VSCode, BaseKeymap::JetBrains => BaseKeymapContent::JetBrains, BaseKeymap::SublimeText => BaseKeymapContent::SublimeText, @@ -55,6 +58,7 @@ impl Into for BaseKeymap { impl Display for BaseKeymap { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { + BaseKeymap::Zed => write!(f, "Zed"), BaseKeymap::VSCode => write!(f, "VS Code"), BaseKeymap::JetBrains => write!(f, "JetBrains"), BaseKeymap::SublimeText => write!(f, "Sublime Text"), @@ -69,8 +73,9 @@ impl Display for BaseKeymap { impl BaseKeymap { #[cfg(target_os = "macos")] - pub const OPTIONS: [(&'static str, Self); 7] = [ - ("VS Code (Default)", Self::VSCode), + pub const OPTIONS: [(&'static str, Self); 8] = [ + ("Zed (Default)", Self::Zed), + ("VS Code", Self::VSCode), ("Atom", Self::Atom), ("JetBrains", Self::JetBrains), ("Sublime Text", Self::SublimeText), @@ -80,8 +85,9 @@ impl BaseKeymap { ]; #[cfg(not(target_os = "macos"))] - pub const OPTIONS: [(&'static str, Self); 6] = [ - ("VS Code (Default)", Self::VSCode), + pub const OPTIONS: [(&'static str, Self); 7] = [ + ("Zed (Default)", Self::Zed), + ("VS Code", Self::VSCode), ("Atom", Self::Atom), ("JetBrains", Self::JetBrains), ("Sublime Text", Self::SublimeText), @@ -98,7 +104,8 @@ impl BaseKeymap { BaseKeymap::TextMate => Some("keymaps/macos/textmate.json"), BaseKeymap::Emacs => Some("keymaps/macos/emacs.json"), BaseKeymap::Cursor => Some("keymaps/macos/cursor.json"), - BaseKeymap::VSCode => None, + BaseKeymap::VSCode => Some("keymaps/macos/vscode.json"), + BaseKeymap::Zed => None, BaseKeymap::None => None, } @@ -110,7 +117,8 @@ impl BaseKeymap { BaseKeymap::Emacs => Some("keymaps/linux/emacs.json"), BaseKeymap::Cursor => Some("keymaps/linux/cursor.json"), BaseKeymap::TextMate => None, - BaseKeymap::VSCode => None, + BaseKeymap::VSCode => Some("keymaps/linux/vscode.json"), + BaseKeymap::Zed => None, BaseKeymap::None => None, } } diff --git a/crates/settings_content/src/settings_content.rs b/crates/settings_content/src/settings_content.rs index 42d1a242df..9e97232291 100644 --- a/crates/settings_content/src/settings_content.rs +++ b/crates/settings_content/src/settings_content.rs @@ -459,7 +459,7 @@ pub struct ExtensionsSettingsContent { /// Base key bindings scheme. Base keymaps can be overridden with user keymaps. /// -/// Default: VSCode +/// Default: Zed #[derive( Copy, Clone, @@ -475,6 +475,7 @@ pub struct ExtensionsSettingsContent { )] pub enum BaseKeymapContent { #[default] + Zed, VSCode, JetBrains, SublimeText, @@ -487,6 +488,7 @@ pub enum BaseKeymapContent { impl strum::VariantNames for BaseKeymapContent { const VARIANTS: &'static [&'static str] = &[ + "Zed", "VSCode", "JetBrains", "Sublime Text", diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 19fe94d3ef..dd10791190 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -5287,6 +5287,8 @@ mod tests { use workspace::ActivatePreviousPane; // From the JetBrains keymap use workspace::ActivatePreviousItem; + // From the VSCode keymap + use debugger_ui::Start; app_state .fs @@ -5379,6 +5381,36 @@ mod tests { ], line!(), ); + + // Test the VSCode keymap overlay + app_state + .fs + .save( + paths::settings_file(), + &r#"{"base_keymap": "VSCode"}"#.into(), + Default::default(), + ) + .await + .unwrap(); + + executor.run_until_parked(); + + window + .update(cx, |_, _, cx| { + workspace.update(cx, |workspace, cx| { + workspace.register_action(|_, _: &Start, _window, _cx| {}); + cx.notify(); + }); + }) + .unwrap(); + executor.run_until_parked(); + + assert_key_bindings_for( + window.into(), + cx, + vec![("backspace", &ActionB), ("f5", &Start)], + line!(), + ); } #[gpui::test] diff --git a/docs/src/key-bindings.md b/docs/src/key-bindings.md index 490293c9eb..b137d1217e 100644 --- a/docs/src/key-bindings.md +++ b/docs/src/key-bindings.md @@ -12,7 +12,8 @@ Zed's key binding system is fully customizable. You can rebind any action, creat If you're used to a specific editor's defaults, you can change your `base_keymap` through the settings window ({#kb zed::OpenSettings}) or directly through your `settings.json` file ({#kb zed::OpenSettingsFile}). We currently support: -- VS Code (default) +- Zed (default) +- VS Code - Atom - Emacs (Beta) - JetBrains diff --git a/docs/src/reference/all-settings.md b/docs/src/reference/all-settings.md index d785045fdb..513a41df70 100644 --- a/docs/src/reference/all-settings.md +++ b/docs/src/reference/all-settings.md @@ -304,11 +304,19 @@ Note that a save will be triggered when an unsaved tab is closed, even if this i - Description: Base key bindings scheme. Base keymaps can be overridden with user keymaps. - Setting: `base_keymap` -- Default: `VSCode` +- Default: `Zed` **Options** -1. VS Code +1. Zed + +```json [settings] +{ + "base_keymap": "Zed" +} +``` + +2. VS Code ```json [settings] { @@ -316,7 +324,7 @@ Note that a save will be triggered when an unsaved tab is closed, even if this i } ``` -2. Atom +3. Atom ```json [settings] { @@ -324,7 +332,7 @@ Note that a save will be triggered when an unsaved tab is closed, even if this i } ``` -3. JetBrains +4. JetBrains ```json [settings] { @@ -332,14 +340,6 @@ Note that a save will be triggered when an unsaved tab is closed, even if this i } ``` -4. None - -```json [settings] -{ - "base_keymap": "None" -} -``` - 5. Sublime Text ```json [settings] @@ -356,6 +356,30 @@ Note that a save will be triggered when an unsaved tab is closed, even if this i } ``` +7. Emacs + +```json [settings] +{ + "base_keymap": "Emacs" +} +``` + +8. Cursor + +```json [settings] +{ + "base_keymap": "Cursor" +} +``` + +9. None + +```json [settings] +{ + "base_keymap": "None" +} +``` + ## Buffer Font Family - Description: The name of a font to use for rendering text in the editor.