From 23e9e32d657e4eb0a4f0e650d92e580368446abc Mon Sep 17 00:00:00 2001 From: "Affonso, Guilherme" Date: Thu, 23 Oct 2025 01:37:00 +0900 Subject: [PATCH] emacs: Improve default keymap to better match the emacs behavior (#40631) Hello, I am having a great time setting up the editor, but with a few problems related to the Emacs keymap. In this PR I have compiled changes in the default `emacs.json` that I believe make the onboarding smoother for incoming emacs users. This includes points that may need further discussion and some breaking changes, although nothing that cannot be reverted with a quick `keymap.json` overwrite. (Please let me know if it is better to split up the PR) ### 1. Avoid fallbacks to the default keymap all platforms: - `ctrl-g` activating `go_to_line::Toggle` when there is nothing to cancel linux / windows: - `ctrl-x` activating `editor::Cut` on the 1 second timeout - `ctrl-p` activating `file_finder::Toggle` when the cursor is on the first character of the buffer - `ctrl-n` activating `workspace::NewFile` when the cursor is on the last character of the buffer ### 2. Make all move commands operate on full words In the current Zed implementation some commands run on full words and others on subwords. Although ultimately a matter of user preference, I think it is sensible to use full words as the default, since that is what is shipped with emacs. ### ~~3. Cancel selections after copy/cut commands~~ Moved to #40904 Canceling the selection is the default emacs behavior, but the way to achieve it might need some brushing. Currently I am using `workspace::SendKeystrokes` to copy -> cancel(`ctrl-g`), but this has the following problems: - can only be used in the main buffer (since `editor::Cancel` would typically close secondary buffers) - may cause problems downstream if the user overwrites the `ctrl-g` binding ### ~~4. Replace killring with normal cut/paste commands~~ Moved to #40905 Ideally Zed would support emacs-like killrings (#25270 and #22490). However, I understand that making an emacs emulator is not a project goal, and the Zed team should have a bunch of tasks with higher priority. By using a unified clipboard and standard cut/paste commands, we can provide an experience that is closer to the out-of-the-box emacs behavior (#33351) while also avoiding some pitfalls of the current killring implementation (#28715). ### 5. Promote some bindings to workspace commands - `alt-x` as `command_palette::Toggle` - `ctrl-x b` and `ctrl-x ctrl-b` as `tab_switcher::Toggle` --- Release Notes: - emacs: Fixed a problem where keys would fallback to their default keymap binding on certain conditions - emacs: Changed `alt-f` and `alt-b` to operate on full words, as in the emacs default - emacs: `alt-x`, `ctrl-x b`, and `ctrl-x ctrl-b` are now Workspace bindings --- assets/keymaps/linux/emacs.json | 46 ++++++++++++++++++++++++++++----- assets/keymaps/macos/emacs.json | 42 +++++++++++++++++++++++++----- 2 files changed, 74 insertions(+), 14 deletions(-) diff --git a/assets/keymaps/linux/emacs.json b/assets/keymaps/linux/emacs.json index 5b8a8e5879cf21100895e4ea1ae7896d62b45d98..c5cf22c81220bf286187252394f8fde26bdd6509 100755 --- a/assets/keymaps/linux/emacs.json +++ b/assets/keymaps/linux/emacs.json @@ -8,13 +8,23 @@ "ctrl-g": "menu::Cancel" } }, + { + // Workaround to avoid falling back to default bindings. + // Unbind so Zed ignores these keys and lets emacs handle them. + // NOTE: must be declared before the `Editor` override. + // NOTE: in macos the 'ctrl-x' 'ctrl-p' and 'ctrl-n' rebindings are not needed, since they default to 'cmd'. + "context": "Editor", + "bindings": { + "ctrl-g": null, // currently activates `go_to_line::Toggle` when there is nothing to cancel + "ctrl-x": null, // currently activates `editor::Cut` if no following key is pressed for 1 second + "ctrl-p": null, // currently activates `file_finder::Toggle` when the cursor is on the first character of the buffer + "ctrl-n": null // currently activates `workspace::NewFile` when the cursor is on the last character of the buffer + } + }, { "context": "Editor", "bindings": { - "alt-x": "command_palette::Toggle", "ctrl-g": "editor::Cancel", - "ctrl-x b": "tab_switcher::Toggle", // switch-to-buffer - "ctrl-x ctrl-b": "tab_switcher::Toggle", // list-buffers "alt-g g": "go_to_line::Toggle", // goto-line "alt-g alt-g": "go_to_line::Toggle", // goto-line "ctrl-space": "editor::SetMark", // set-mark @@ -33,8 +43,8 @@ "alt-m": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false, "stop_at_indent": true }], // back-to-indentation "alt-left": "editor::MoveToPreviousWordStart", // left-word "alt-right": "editor::MoveToNextWordEnd", // right-word - "alt-f": "editor::MoveToNextSubwordEnd", // forward-word - "alt-b": "editor::MoveToPreviousSubwordStart", // backward-word + "alt-f": "editor::MoveToNextWordEnd", // forward-word + "alt-b": "editor::MoveToPreviousWordStart", // backward-word "alt-u": "editor::ConvertToUpperCase", // upcase-word "alt-l": "editor::ConvertToLowerCase", // downcase-word "alt-c": "editor::ConvertToUpperCamelCase", // capitalize-word @@ -98,7 +108,7 @@ "ctrl-e": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": false }], "alt-m": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": false, "stop_at_indent": true }], "alt-f": "editor::SelectToNextWordEnd", - "alt-b": "editor::SelectToPreviousSubwordStart", + "alt-b": "editor::SelectToPreviousWordStart", "alt-{": "editor::SelectToStartOfParagraph", "alt-}": "editor::SelectToEndOfParagraph", "ctrl-up": "editor::SelectToStartOfParagraph", @@ -126,15 +136,28 @@ "ctrl-n": "editor::SignatureHelpNext" } }, + // Example setting for using emacs-style tab + // (i.e. indent the current line / selection or perform symbol completion depending on context) + // { + // "context": "Editor && !showing_code_actions && !showing_completions", + // "bindings": { + // "tab": "editor::AutoIndent" // indent-for-tab-command + // } + // }, { "context": "Workspace", "bindings": { + "alt-x": "command_palette::Toggle", // execute-extended-command + "ctrl-x b": "tab_switcher::Toggle", // switch-to-buffer + "ctrl-x ctrl-b": "tab_switcher::Toggle", // list-buffers + // "ctrl-x ctrl-c": "workspace::CloseWindow" // in case you only want to exit the current Zed instance "ctrl-x ctrl-c": "zed::Quit", // save-buffers-kill-terminal "ctrl-x 5 0": "workspace::CloseWindow", // delete-frame "ctrl-x 5 2": "workspace::NewWindow", // make-frame-command "ctrl-x o": "workspace::ActivateNextPane", // other-window "ctrl-x k": "pane::CloseActiveItem", // kill-buffer "ctrl-x 0": "pane::CloseActiveItem", // delete-window + // "ctrl-x 1": "pane::JoinAll", // in case you prefer to delete the splits but keep the buffers open "ctrl-x 1": "pane::CloseOtherItems", // delete-other-windows "ctrl-x 2": "pane::SplitDown", // split-window-below "ctrl-x 3": "pane::SplitRight", // split-window-right @@ -145,10 +168,19 @@ } }, { - // Workaround to enable using emacs in the Zed terminal. + // Workaround to enable using native emacs from the Zed terminal. // Unbind so Zed ignores these keys and lets emacs handle them. + // NOTE: + // "terminal::SendKeystroke" only works for a single key stroke (e.g. ctrl-x), + // so override with null for compound sequences (e.g. ctrl-x ctrl-c). "context": "Terminal", "bindings": { + // If you want to perfect your emacs-in-zed setup, also consider the following. + // You may need to enable "option_as_meta" from the Zed settings for "alt-x" to work. + // "alt-x": ["terminal::SendKeystroke", "alt-x"], + // "ctrl-x": ["terminal::SendKeystroke", "ctrl-x"], + // "ctrl-n": ["terminal::SendKeystroke", "ctrl-n"], + // ... "ctrl-x ctrl-c": null, // save-buffers-kill-terminal "ctrl-x ctrl-f": null, // find-file "ctrl-x ctrl-s": null, // save-buffer diff --git a/assets/keymaps/macos/emacs.json b/assets/keymaps/macos/emacs.json index da815f14cecb2dd3fee403c5e1e54a383dd564a4..ea831c0c059ea082d002f3af01b8d97be9e86616 100755 --- a/assets/keymaps/macos/emacs.json +++ b/assets/keymaps/macos/emacs.json @@ -9,13 +9,19 @@ "ctrl-g": "menu::Cancel" } }, + { + // Workaround to avoid falling back to default bindings. + // Unbind so Zed ignores these keys and lets emacs handle them. + // NOTE: must be declared before the `Editor` override. + "context": "Editor", + "bindings": { + "ctrl-g": null // currently activates `go_to_line::Toggle` when there is nothing to cancel + } + }, { "context": "Editor", "bindings": { - "alt-x": "command_palette::Toggle", "ctrl-g": "editor::Cancel", - "ctrl-x b": "tab_switcher::Toggle", // switch-to-buffer - "ctrl-x ctrl-b": "tab_switcher::Toggle", // list-buffers "alt-g g": "go_to_line::Toggle", // goto-line "alt-g alt-g": "go_to_line::Toggle", // goto-line "ctrl-space": "editor::SetMark", // set-mark @@ -34,8 +40,8 @@ "alt-m": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false, "stop_at_indent": true }], // back-to-indentation "alt-left": "editor::MoveToPreviousWordStart", // left-word "alt-right": "editor::MoveToNextWordEnd", // right-word - "alt-f": "editor::MoveToNextSubwordEnd", // forward-word - "alt-b": "editor::MoveToPreviousSubwordStart", // backward-word + "alt-f": "editor::MoveToNextWordEnd", // forward-word + "alt-b": "editor::MoveToPreviousWordStart", // backward-word "alt-u": "editor::ConvertToUpperCase", // upcase-word "alt-l": "editor::ConvertToLowerCase", // downcase-word "alt-c": "editor::ConvertToUpperCamelCase", // capitalize-word @@ -99,7 +105,7 @@ "ctrl-e": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": false }], "alt-m": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": false, "stop_at_indent": true }], "alt-f": "editor::SelectToNextWordEnd", - "alt-b": "editor::SelectToPreviousSubwordStart", + "alt-b": "editor::SelectToPreviousWordStart", "alt-{": "editor::SelectToStartOfParagraph", "alt-}": "editor::SelectToEndOfParagraph", "ctrl-up": "editor::SelectToStartOfParagraph", @@ -127,15 +133,28 @@ "ctrl-n": "editor::SignatureHelpNext" } }, + // Example setting for using emacs-style tab + // (i.e. indent the current line / selection or perform symbol completion depending on context) + // { + // "context": "Editor && !showing_code_actions && !showing_completions", + // "bindings": { + // "tab": "editor::AutoIndent" // indent-for-tab-command + // } + // }, { "context": "Workspace", "bindings": { + "alt-x": "command_palette::Toggle", // execute-extended-command + "ctrl-x b": "tab_switcher::Toggle", // switch-to-buffer + "ctrl-x ctrl-b": "tab_switcher::Toggle", // list-buffers + // "ctrl-x ctrl-c": "workspace::CloseWindow" // in case you only want to exit the current Zed instance "ctrl-x ctrl-c": "zed::Quit", // save-buffers-kill-terminal "ctrl-x 5 0": "workspace::CloseWindow", // delete-frame "ctrl-x 5 2": "workspace::NewWindow", // make-frame-command "ctrl-x o": "workspace::ActivateNextPane", // other-window "ctrl-x k": "pane::CloseActiveItem", // kill-buffer "ctrl-x 0": "pane::CloseActiveItem", // delete-window + // "ctrl-x 1": "pane::JoinAll", // in case you prefer to delete the splits but keep the buffers open "ctrl-x 1": "pane::CloseOtherItems", // delete-other-windows "ctrl-x 2": "pane::SplitDown", // split-window-below "ctrl-x 3": "pane::SplitRight", // split-window-right @@ -146,10 +165,19 @@ } }, { - // Workaround to enable using emacs in the Zed terminal. + // Workaround to enable using native emacs from the Zed terminal. // Unbind so Zed ignores these keys and lets emacs handle them. + // NOTE: + // "terminal::SendKeystroke" only works for a single key stroke (e.g. ctrl-x), + // so override with null for compound sequences (e.g. ctrl-x ctrl-c). "context": "Terminal", "bindings": { + // If you want to perfect your emacs-in-zed setup, also consider the following. + // You may need to enable "option_as_meta" from the Zed settings for "alt-x" to work. + // "alt-x": ["terminal::SendKeystroke", "alt-x"], + // "ctrl-x": ["terminal::SendKeystroke", "ctrl-x"], + // "ctrl-n": ["terminal::SendKeystroke", "ctrl-n"], + // ... "ctrl-x ctrl-c": null, // save-buffers-kill-terminal "ctrl-x ctrl-f": null, // find-file "ctrl-x ctrl-s": null, // save-buffer