emacs: Improve default keymap to better match the emacs behavior (#40631)

Affonso, Guilherme created

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

Change summary

assets/keymaps/linux/emacs.json | 46 +++++++++++++++++++++++++++++-----
assets/keymaps/macos/emacs.json | 42 ++++++++++++++++++++++++++-----
2 files changed, 74 insertions(+), 14 deletions(-)

Detailed changes

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

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