Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions assets/keymaps/linux/vscode.json
Original file line number Diff line number Diff line change
@@ -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 changes: 82 additions & 0 deletions assets/keymaps/macos/vscode.json
Original file line number Diff line number Diff line change
@@ -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",
},
},
]
16 changes: 9 additions & 7 deletions assets/settings/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
96 changes: 83 additions & 13 deletions crates/gpui/src/keymap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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
Expand Down
22 changes: 15 additions & 7 deletions crates/onboarding/src/basics_page.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}),
Expand All @@ -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| {
Expand Down
Loading
Loading