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
This commit is contained in:
Kirill Bulatov authored and GitHub committed 2026-07-24 07:54:03 +00:00
1 parent 28605024d7
commit f18ea2c94c
10 files changed
+349 -48

No files matched your search

+72
View File
@@ -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",
},
},
]
+82
View File
@@ -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",
},
},
]
+9 -7
View File
@@ -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.
+83 -13
View File
@@ -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<u32> = 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
+15 -7
View File
@@ -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| {
+15 -7
View File
@@ -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<BaseKeymapContent> 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<BaseKeymapContent> for BaseKeymap {
impl Into<BaseKeymapContent> 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<BaseKeymapContent> 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,
}
}
@@ -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",
+32
View File
@@ -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]
+2 -1
View File
@@ -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
+36 -12
View File
@@ -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.