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.