Merge branch 'main' into reconnections-2

Antonio Scandurra created

Change summary

.github/workflows/ci.yml                                                         |   2 
.github/workflows/release_actions.yml                                            |   4 
Cargo.lock                                                                       |  21 
Cargo.toml                                                                       |   2 
assets/keymaps/default.json                                                      |   9 
assets/settings/default.json                                                     | 445 
crates/auto_update/src/auto_update.rs                                            |   9 
crates/client/src/user.rs                                                        |   1 
crates/collab/Cargo.toml                                                         |   2 
crates/collab/migrations/20221125192125_add_added_to_mailing_list_to_signups.sql |   2 
crates/collab/src/api.rs                                                         |   2 
crates/collab/src/bin/seed.rs                                                    |  20 
crates/collab/src/db.rs                                                          |  70 
crates/collab/src/db/signup.rs                                                   |   4 
crates/collab/src/db/tests.rs                                                    |  48 
crates/collab/src/integration_tests.rs                                           |   7 
crates/diagnostics/src/diagnostics.rs                                            |  33 
crates/drag_and_drop/Cargo.toml                                                  |   2 
crates/drag_and_drop/src/drag_and_drop.rs                                        |  80 
crates/editor/src/display_map/block_map.rs                                       |   6 
crates/editor/src/editor.rs                                                      |  52 
crates/editor/src/editor_tests.rs                                                |  69 
crates/editor/src/element.rs                                                     |  26 
crates/editor/src/hover_popover.rs                                               |   2 
crates/editor/src/items.rs                                                       |  86 
crates/editor/src/multi_buffer.rs                                                | 427 
crates/editor/src/multi_buffer/anchor.rs                                         |  14 
crates/editor/src/selections_collection.rs                                       |  14 
crates/gpui/src/elements/flex.rs                                                 |  12 
crates/gpui/src/elements/list.rs                                                 |   4 
crates/gpui/src/elements/uniform_list.rs                                         |  14 
crates/gpui/src/platform/event.rs                                                |  40 
crates/gpui/src/platform/mac/event.rs                                            |  19 
crates/gpui/src/presenter.rs                                                     |  32 
crates/gpui/src/scene/mouse_event.rs                                             |  22 
crates/gpui/src/scene/mouse_region.rs                                            | 123 
crates/journal/Cargo.toml                                                        |   2 
crates/language/Cargo.toml                                                       |   1 
crates/language/src/buffer.rs                                                    |  31 
crates/language/src/buffer_tests.rs                                              |  63 
crates/language/src/diagnostic_set.rs                                            |   2 
crates/language/src/language.rs                                                  |  67 
crates/language/src/proto.rs                                                     |   9 
crates/language/src/syntax_map.rs                                                | 923 
crates/project/src/lsp_command.rs                                                |   4 
crates/project/src/project.rs                                                    | 259 
crates/project/src/project_tests.rs                                              |   4 
crates/project/src/worktree.rs                                                   |  18 
crates/project_panel/src/project_panel.rs                                        | 104 
crates/project_symbols/Cargo.toml                                                |   2 
crates/rope/Cargo.toml                                                           |   2 
crates/rope/src/rope.rs                                                          | 124 
crates/rope/src/unclipped.rs                                                     |  57 
crates/rpc/proto/zed.proto                                                       |   8 
crates/settings/src/settings.rs                                                  |   9 
crates/terminal/src/mappings/keys.rs                                             |  12 
crates/terminal/src/mappings/mouse.rs                                            |   2 
crates/terminal/src/terminal.rs                                                  |  27 
crates/text/src/locator.rs                                                       |  12 
crates/text/src/text.rs                                                          | 101 
crates/theme_testbench/Cargo.toml                                                |   2 
crates/util/Cargo.toml                                                           |   1 
crates/util/src/lib.rs                                                           |  13 
crates/vim/Cargo.toml                                                            |   2 
crates/vim/src/visual.rs                                                         |   4 
crates/workspace/Cargo.toml                                                      |   2 
crates/workspace/src/pane/dragged_item_receiver.rs                               |   1 
crates/workspace/src/pane_group.rs                                               |  40 
crates/workspace/src/workspace.rs                                                |   1 
crates/zed/Cargo.toml                                                            |   3 
crates/zed/src/languages.rs                                                      |  12 
crates/zed/src/languages/erb/config.toml                                         |   8 
crates/zed/src/languages/erb/highlights.scm                                      |  12 
crates/zed/src/languages/erb/injections.scm                                      |   7 
crates/zed/src/languages/python.rs                                               |  19 
crates/zed/src/languages/ruby.rs                                                 | 145 
crates/zed/src/main.rs                                                           |  15 
script/lib/bump-version.sh                                                       |   1 
78 files changed, 2,591 insertions(+), 1,265 deletions(-)

Detailed changes

.github/workflows/ci.yml 🔗

@@ -49,7 +49,7 @@ jobs:
         run: cargo build -p collab
 
       - name: Build other binaries
-        run: cargo build --bins --all-features 
+        run: cargo build --workspace --bins --all-features
 
   bundle:
     name: Bundle app

.github/workflows/release_actions.yml 🔗

@@ -14,10 +14,10 @@ jobs:
         content: |
           📣 Zed ${{ github.event.release.tag_name }} was just released!
           
-          Restart your Zed or head to https://zed.dev/releases to grab it.
+          Restart your Zed or head to https://zed.dev/releases/latest to grab it.
         
           ```md
-          ### Changelog
+          # Changelog
           
           ${{ github.event.release.body }}
           ```

Cargo.lock 🔗

@@ -1121,7 +1121,7 @@ dependencies = [
 
 [[package]]
 name = "collab"
-version = "0.2.2"
+version = "0.2.5"
 dependencies = [
  "anyhow",
  "async-tungstenite",
@@ -3125,6 +3125,7 @@ dependencies = [
  "text",
  "theme",
  "tree-sitter",
+ "tree-sitter-embedded-template",
  "tree-sitter-html",
  "tree-sitter-javascript",
  "tree-sitter-json 0.19.0",
@@ -6729,8 +6730,8 @@ dependencies = [
 
 [[package]]
 name = "tree-sitter"
-version = "0.20.8"
-source = "git+https://github.com/tree-sitter/tree-sitter?rev=366210ae925d7ea0891bc7a0c738f60c77c04d7b#366210ae925d7ea0891bc7a0c738f60c77c04d7b"
+version = "0.20.9"
+source = "git+https://github.com/tree-sitter/tree-sitter?rev=36b5b6c89e55ad1a502f8b3234bb3e12ec83a5da#36b5b6c89e55ad1a502f8b3234bb3e12ec83a5da"
 dependencies = [
  "cc",
  "regex",
@@ -6774,6 +6775,16 @@ dependencies = [
  "tree-sitter",
 ]
 
+[[package]]
+name = "tree-sitter-embedded-template"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33817ade928c73a32d4f904a602321e09de9fc24b71d106f3b4b3f8ab30dcc38"
+dependencies = [
+ "cc",
+ "tree-sitter",
+]
+
 [[package]]
 name = "tree-sitter-go"
 version = "0.19.1"
@@ -7100,6 +7111,7 @@ name = "util"
 version = "0.1.0"
 dependencies = [
  "anyhow",
+ "backtrace",
  "futures 0.3.24",
  "git2",
  "lazy_static",
@@ -7989,7 +8001,7 @@ dependencies = [
 
 [[package]]
 name = "zed"
-version = "0.65.0"
+version = "0.67.0"
 dependencies = [
  "activity_indicator",
  "anyhow",
@@ -8068,6 +8080,7 @@ dependencies = [
  "tree-sitter-cpp",
  "tree-sitter-css",
  "tree-sitter-elixir",
+ "tree-sitter-embedded-template",
  "tree-sitter-go",
  "tree-sitter-html",
  "tree-sitter-json 0.20.0",

Cargo.toml 🔗

@@ -65,7 +65,7 @@ serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] }
 rand = { version = "0.8" }
 
 [patch.crates-io]
-tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "366210ae925d7ea0891bc7a0c738f60c77c04d7b" }
+tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "36b5b6c89e55ad1a502f8b3234bb3e12ec83a5da" }
 async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" }
 sqlx = { git = "https://github.com/launchbadge/sqlx", rev = "4b7053807c705df312bcb9b6281e184bf7534eb3" }
 

assets/keymaps/default.json 🔗

@@ -472,6 +472,15 @@
                 "terminal::SendText",
                 "\u0001"
             ],
+            // Terminal.app compatability
+            "alt-left": [
+                "terminal::SendText",
+                "\u001bb"
+            ],
+            "alt-right": [
+                "terminal::SendText",
+                "\u001bf"
+            ],
             // There are conflicting bindings for these keys in the global context.
             // these bindings override them, remove at your own risk:
             "up": [

assets/settings/default.json 🔗

@@ -1,230 +1,233 @@
 {
-  // The name of the Zed theme to use for the UI
-  "theme": "One Dark",
-  // The name of a font to use for rendering text in the editor
-  "buffer_font_family": "Zed Mono",
-  // The default font size for text in the editor
-  "buffer_font_size": 15,
-  // Whether to enable vim modes and key bindings
-  "vim_mode": false,
-  // Whether to show the informational hover box when moving the mouse
-  // over symbols in the editor.
-  "hover_popover_enabled": true,
-  // Whether the cursor blinks in the editor.
-  "cursor_blink": true,
-  // Whether to pop the completions menu while typing in an editor without
-  // explicitly requesting it.
-  "show_completions_on_input": true,
-  // Whether new projects should start out 'online'. Online projects
-  // appear in the contacts panel under your name, so that your contacts
-  // can see which projects you are working on. Regardless of this
-  // setting, projects keep their last online status when you reopen them.
-  "projects_online_by_default": true,
-  // Whether to use language servers to provide code intelligence.
-  "enable_language_server": true,
-  // When to automatically save edited buffers. This setting can
-  // take four values.
-  //
-  // 1. Never automatically save:
-  //     "autosave": "off",
-  // 2. Save when changing focus away from the Zed window:
-  //     "autosave": "on_window_change",
-  // 3. Save when changing focus away from a specific buffer:
-  //     "autosave": "on_focus_change",
-  // 4. Save when idle for a certain amount of time:
-  //     "autosave": { "after_delay": {"milliseconds": 500} },
-  "autosave": "off",
-  // Where to place the dock by default. This setting can take three
-  // values:
-  //
-  // 1. Position the dock attached to the bottom of the workspace
-  //     "default_dock_anchor": "bottom"
-  // 2. Position the dock to the right of the workspace like a side panel
-  //     "default_dock_anchor": "right"
-  // 3. Position the dock full screen over the entire workspace"
-  //     "default_dock_anchor": "expanded"
-  "default_dock_anchor": "right",
-  // Whether or not to perform a buffer format before saving
-  "format_on_save": "on",
-  // How to perform a buffer format. This setting can take two values:
-  //
-  // 1. Format code using the current language server:
-  //     "format_on_save": "language_server"
-  // 2. Format code using an external command:
-  //     "format_on_save": {
-  //       "external": {
-  //         "command": "prettier",
-  //         "arguments": ["--stdin-filepath", "{buffer_path}"]
-  //       }
-  //     }
-  "formatter": "language_server",
-  // How to soft-wrap long lines of text. This setting can take
-  // three values:
-  //
-  // 1. Do not soft wrap.
-  //      "soft_wrap": "none",
-  // 2. Soft wrap lines that overflow the editor:
-  //      "soft_wrap": "editor_width",
-  // 3. Soft wrap lines at the preferred line length
-  //      "soft_wrap": "preferred_line_length",
-  "soft_wrap": "none",
-  // The column at which to soft-wrap lines, for buffers where soft-wrap
-  // is enabled.
-  "preferred_line_length": 80,
-  // Whether to indent lines using tab characters, as opposed to multiple
-  // spaces.
-  "hard_tabs": false,
-  // How many columns a tab should occupy.
-  "tab_size": 4,
-  // Git gutter behavior configuration.
-  "git": {
-    // Control whether the git gutter is shown. May take 2 values:
-    // 1. Show the gutter
-    //      "git_gutter": "tracked_files"
-    // 2. Hide the gutter
-    //      "git_gutter": "hide"
-    "git_gutter": "tracked_files"
-  },
-  // Settings specific to journaling
-  "journal": {
-    // The path of the directory where journal entries are stored
-    "path": "~",
-    // What format to display the hours in
-    // May take 2 values:
-    // 1. hour12
-    // 2. hour24
-    "hour_format": "hour12"
-  },
-  // Settings specific to the terminal
-  "terminal": {
-    // What shell to use when opening a terminal. May take 3 values:
-    // 1. Use the system's default terminal configuration (e.g. $TERM).
-    //      "shell": "system"
-    // 2. A program:
-    //      "shell": {
-    //        "program": "sh"
-    //      }
-    // 3. A program with arguments:
-    //     "shell": {
-    //         "with_arguments": {
-    //           "program": "/bin/bash",
-    //           "arguments": ["--login"]
-    //         }
-    //     }
-    "shell": "system",
-    // What working directory to use when launching the terminal.
-    // May take 4 values:
-    // 1. Use the current file's project directory.  Will Fallback to the
-    //    first project directory strategy if unsuccessful
-    //      "working_directory": "current_project_directory"
-    // 2. Use the first project in this workspace's directory
-    //      "working_directory": "first_project_directory"
-    // 3. Always use this platform's home directory (if we can find it)
-    //     "working_directory": "always_home"
-    // 4. Always use a specific directory. This value will be shell expanded.
-    //    If this path is not a valid directory the terminal will default to
-    //    this platform's home directory  (if we can find it)
-    //      "working_directory": {
-    //        "always": {
-    //          "directory": "~/zed/projects/"
-    //        }
-    //      }
+    // The name of the Zed theme to use for the UI
+    "theme": "One Dark",
+    // The name of a font to use for rendering text in the editor
+    "buffer_font_family": "Zed Mono",
+    // The default font size for text in the editor
+    "buffer_font_size": 15,
+    // The factor to grow the active pane by. Defaults to 1.0
+    // which gives the same size as all other panes.
+    "active_pane_magnification": 1.0,
+    // Whether to enable vim modes and key bindings
+    "vim_mode": false,
+    // Whether to show the informational hover box when moving the mouse
+    // over symbols in the editor.
+    "hover_popover_enabled": true,
+    // Whether the cursor blinks in the editor.
+    "cursor_blink": true,
+    // Whether to pop the completions menu while typing in an editor without
+    // explicitly requesting it.
+    "show_completions_on_input": true,
+    // Whether new projects should start out 'online'. Online projects
+    // appear in the contacts panel under your name, so that your contacts
+    // can see which projects you are working on. Regardless of this
+    // setting, projects keep their last online status when you reopen them.
+    "projects_online_by_default": true,
+    // Whether to use language servers to provide code intelligence.
+    "enable_language_server": true,
+    // When to automatically save edited buffers. This setting can
+    // take four values.
     //
+    // 1. Never automatically save:
+    //     "autosave": "off",
+    // 2. Save when changing focus away from the Zed window:
+    //     "autosave": "on_window_change",
+    // 3. Save when changing focus away from a specific buffer:
+    //     "autosave": "on_focus_change",
+    // 4. Save when idle for a certain amount of time:
+    //     "autosave": { "after_delay": {"milliseconds": 500} },
+    "autosave": "off",
+    // Where to place the dock by default. This setting can take three
+    // values:
     //
-    "working_directory": "current_project_directory",
-    // Set the cursor blinking behavior in the terminal.
-    // May take 4 values:
-    //  1. Never blink the cursor, ignoring the terminal mode
-    //         "blinking": "off",
-    //  2. Default the cursor blink to off, but allow the terminal to
-    //     set blinking
-    //         "blinking": "terminal_controlled",
-    //  3. Always blink the cursor, ignoring the terminal mode
-    //         "blinking": "on",
-    "blinking": "terminal_controlled",
-    // Set whether Alternate Scroll mode (code: ?1007) is active by default.
-    // Alternate Scroll mode converts mouse scroll events into up / down key
-    // presses when in the alternate screen (e.g. when running applications
-    // like vim or  less). The terminal can still set and unset this mode.
-    // May take 2 values:
-    //  1. Default alternate scroll mode to on
-    //         "alternate_scroll": "on",
-    //  2. Default alternate scroll mode to off
-    //         "alternate_scroll": "off",
-    "alternate_scroll": "off",
-    // Set whether the option key behaves as the meta key.
-    // May take 2 values:
-    //  1. Rely on default platform handling of option key, on macOS
-    //     this means generating certain unicode characters
-    //         "option_to_meta": false,
-    //  2. Make the option keys behave as a 'meta' key, e.g. for emacs
-    //         "option_to_meta": true,
-    "option_as_meta": false,
-    // Whether or not selecting text in the terminal will automatically
-    // copy to the system clipboard.
-    "copy_on_select": false,
-    // Any key-value pairs added to this list will be added to the terminal's
-    // enviroment. Use `:` to seperate multiple values.
-    "env": {
-      // "KEY": "value1:value2"
-    }
-    // Set the terminal's font size. If this option is not included,
-    // the terminal will default to matching the buffer's font size.
-    // "font_size": "15"
-    // Set the terminal's font family. If this option is not included,
-    // the terminal will default to matching the buffer's font family.
-    // "font_family": "Zed Mono"
-  },
-  // Different settings for specific languages.
-  "languages": {
-    "Plain Text": {
-      "soft_wrap": "preferred_line_length"
-    },
-    "C": {
-      "tab_size": 2
-    },
-    "C++": {
-      "tab_size": 2
-    },
-    "Elixir": {
-      "tab_size": 2
-    },
-    "Go": {
-      "tab_size": 4,
-      "hard_tabs": true
-    },
-    "Markdown": {
-      "soft_wrap": "preferred_line_length"
+    // 1. Position the dock attached to the bottom of the workspace
+    //     "default_dock_anchor": "bottom"
+    // 2. Position the dock to the right of the workspace like a side panel
+    //     "default_dock_anchor": "right"
+    // 3. Position the dock full screen over the entire workspace"
+    //     "default_dock_anchor": "expanded"
+    "default_dock_anchor": "right",
+    // Whether or not to perform a buffer format before saving
+    "format_on_save": "on",
+    // How to perform a buffer format. This setting can take two values:
+    //
+    // 1. Format code using the current language server:
+    //     "format_on_save": "language_server"
+    // 2. Format code using an external command:
+    //     "format_on_save": {
+    //       "external": {
+    //         "command": "prettier",
+    //         "arguments": ["--stdin-filepath", "{buffer_path}"]
+    //       }
+    //     }
+    "formatter": "language_server",
+    // How to soft-wrap long lines of text. This setting can take
+    // three values:
+    //
+    // 1. Do not soft wrap.
+    //      "soft_wrap": "none",
+    // 2. Soft wrap lines that overflow the editor:
+    //      "soft_wrap": "editor_width",
+    // 3. Soft wrap lines at the preferred line length
+    //      "soft_wrap": "preferred_line_length",
+    "soft_wrap": "none",
+    // The column at which to soft-wrap lines, for buffers where soft-wrap
+    // is enabled.
+    "preferred_line_length": 80,
+    // Whether to indent lines using tab characters, as opposed to multiple
+    // spaces.
+    "hard_tabs": false,
+    // How many columns a tab should occupy.
+    "tab_size": 4,
+    // Git gutter behavior configuration.
+    "git": {
+        // Control whether the git gutter is shown. May take 2 values:
+        // 1. Show the gutter
+        //      "git_gutter": "tracked_files"
+        // 2. Hide the gutter
+        //      "git_gutter": "hide"
+        "git_gutter": "tracked_files"
     },
-    "Rust": {
-      "tab_size": 4
+    // Settings specific to journaling
+    "journal": {
+        // The path of the directory where journal entries are stored
+        "path": "~",
+        // What format to display the hours in
+        // May take 2 values:
+        // 1. hour12
+        // 2. hour24
+        "hour_format": "hour12"
     },
-    "JavaScript": {
-      "tab_size": 2
+    // Settings specific to the terminal
+    "terminal": {
+        // What shell to use when opening a terminal. May take 3 values:
+        // 1. Use the system's default terminal configuration (e.g. $TERM).
+        //      "shell": "system"
+        // 2. A program:
+        //      "shell": {
+        //        "program": "sh"
+        //      }
+        // 3. A program with arguments:
+        //     "shell": {
+        //         "with_arguments": {
+        //           "program": "/bin/bash",
+        //           "arguments": ["--login"]
+        //         }
+        //     }
+        "shell": "system",
+        // What working directory to use when launching the terminal.
+        // May take 4 values:
+        // 1. Use the current file's project directory.  Will Fallback to the
+        //    first project directory strategy if unsuccessful
+        //      "working_directory": "current_project_directory"
+        // 2. Use the first project in this workspace's directory
+        //      "working_directory": "first_project_directory"
+        // 3. Always use this platform's home directory (if we can find it)
+        //     "working_directory": "always_home"
+        // 4. Always use a specific directory. This value will be shell expanded.
+        //    If this path is not a valid directory the terminal will default to
+        //    this platform's home directory  (if we can find it)
+        //      "working_directory": {
+        //        "always": {
+        //          "directory": "~/zed/projects/"
+        //        }
+        //      }
+        //
+        //
+        "working_directory": "current_project_directory",
+        // Set the cursor blinking behavior in the terminal.
+        // May take 4 values:
+        //  1. Never blink the cursor, ignoring the terminal mode
+        //         "blinking": "off",
+        //  2. Default the cursor blink to off, but allow the terminal to
+        //     set blinking
+        //         "blinking": "terminal_controlled",
+        //  3. Always blink the cursor, ignoring the terminal mode
+        //         "blinking": "on",
+        "blinking": "terminal_controlled",
+        // Set whether Alternate Scroll mode (code: ?1007) is active by default.
+        // Alternate Scroll mode converts mouse scroll events into up / down key
+        // presses when in the alternate screen (e.g. when running applications
+        // like vim or  less). The terminal can still set and unset this mode.
+        // May take 2 values:
+        //  1. Default alternate scroll mode to on
+        //         "alternate_scroll": "on",
+        //  2. Default alternate scroll mode to off
+        //         "alternate_scroll": "off",
+        "alternate_scroll": "off",
+        // Set whether the option key behaves as the meta key.
+        // May take 2 values:
+        //  1. Rely on default platform handling of option key, on macOS
+        //     this means generating certain unicode characters
+        //         "option_to_meta": false,
+        //  2. Make the option keys behave as a 'meta' key, e.g. for emacs
+        //         "option_to_meta": true,
+        "option_as_meta": false,
+        // Whether or not selecting text in the terminal will automatically
+        // copy to the system clipboard.
+        "copy_on_select": false,
+        // Any key-value pairs added to this list will be added to the terminal's
+        // enviroment. Use `:` to seperate multiple values.
+        "env": {
+            // "KEY": "value1:value2"
+        }
+        // Set the terminal's font size. If this option is not included,
+        // the terminal will default to matching the buffer's font size.
+        // "font_size": "15"
+        // Set the terminal's font family. If this option is not included,
+        // the terminal will default to matching the buffer's font family.
+        // "font_family": "Zed Mono"
     },
-    "TypeScript": {
-      "tab_size": 2
+    // Different settings for specific languages.
+    "languages": {
+        "Plain Text": {
+            "soft_wrap": "preferred_line_length"
+        },
+        "C": {
+            "tab_size": 2
+        },
+        "C++": {
+            "tab_size": 2
+        },
+        "Elixir": {
+            "tab_size": 2
+        },
+        "Go": {
+            "tab_size": 4,
+            "hard_tabs": true
+        },
+        "Markdown": {
+            "soft_wrap": "preferred_line_length"
+        },
+        "Rust": {
+            "tab_size": 4
+        },
+        "JavaScript": {
+            "tab_size": 2
+        },
+        "TypeScript": {
+            "tab_size": 2
+        },
+        "TSX": {
+            "tab_size": 2
+        }
     },
-    "TSX": {
-      "tab_size": 2
+    // LSP Specific settings.
+    "lsp": {
+        // Specify the LSP name as a key here.
+        // As of 8/10/22, supported LSPs are:
+        // pyright
+        // gopls
+        // rust-analyzer
+        // typescript-language-server
+        // vscode-json-languageserver
+        // "rust_analyzer": {
+        //     //These initialization options are merged into Zed's defaults
+        //     "initialization_options": {
+        //         "checkOnSave": {
+        //             "command": "clippy"
+        //         }
+        //     }
+        // }
     }
-  },
-  // LSP Specific settings.
-  "lsp": {
-    // Specify the LSP name as a key here.
-    // As of 8/10/22, supported LSPs are:
-    // pyright
-    // gopls
-    // rust-analyzer
-    // typescript-language-server
-    // vscode-json-languageserver
-    // "rust_analyzer": {
-    //     //These initialization options are merged into Zed's defaults
-    //     "initialization_options": {
-    //         "checkOnSave": {
-    //             "command": "clippy"
-    //         }
-    //     }
-    // }
-  }
 }

crates/auto_update/src/auto_update.rs 🔗

@@ -70,7 +70,14 @@ pub fn init(db: project::Db, http_client: Arc<dyn HttpClient>, cx: &mut MutableA
             }
         });
         cx.add_global_action(move |_: &ViewReleaseNotes, cx| {
-            cx.platform().open_url(&format!("{server_url}/releases"));
+            let latest_release_url = if cx.has_global::<ReleaseChannel>()
+                && *cx.global::<ReleaseChannel>() == ReleaseChannel::Preview
+            {
+                format!("{server_url}/releases/preview/latest")
+            } else {
+                format!("{server_url}/releases/latest")
+            };
+            cx.platform().open_url(&latest_release_url);
         });
         cx.add_action(UpdateNotification::dismiss);
     }

crates/client/src/user.rs 🔗

@@ -150,7 +150,6 @@ impl UserStore {
                                     client.telemetry.set_authenticated_user_info(None, false);
                                 }
 
-                                client.telemetry.report_event("sign in", Default::default());
                                 current_user_tx.send(user).await.ok();
                             }
                         }

crates/collab/Cargo.toml 🔗

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
 default-run = "collab"
 edition = "2021"
 name = "collab"
-version = "0.2.2"
+version = "0.2.5"
 
 [[bin]]
 name = "collab"

crates/collab/src/api.rs 🔗

@@ -338,7 +338,7 @@ async fn create_signup(
     Json(params): Json<NewSignup>,
     Extension(app): Extension<Arc<AppState>>,
 ) -> Result<()> {
-    app.db.create_signup(params).await?;
+    app.db.create_signup(&params).await?;
     Ok(())
 }
 

crates/collab/src/bin/seed.rs 🔗

@@ -57,16 +57,14 @@ async fn main() {
         }
     }
 
-    let mut zed_user_ids = Vec::<UserId>::new();
     for (github_user, admin) in zed_users {
-        if let Some(user) = db
+        if db
             .get_user_by_github_account(&github_user.login, Some(github_user.id))
             .await
             .expect("failed to fetch user")
+            .is_none()
         {
-            zed_user_ids.push(user.id);
-        } else if let Some(email) = &github_user.email {
-            zed_user_ids.push(
+            if let Some(email) = &github_user.email {
                 db.create_user(
                     email,
                     admin,
@@ -77,11 +75,8 @@ async fn main() {
                     },
                 )
                 .await
-                .expect("failed to insert user")
-                .user_id,
-            );
-        } else if admin {
-            zed_user_ids.push(
+                .expect("failed to insert user");
+            } else if admin {
                 db.create_user(
                     &format!("{}@zed.dev", github_user.login),
                     admin,
@@ -92,9 +87,8 @@ async fn main() {
                     },
                 )
                 .await
-                .expect("failed to insert user")
-                .user_id,
-            );
+                .expect("failed to insert user");
+            }
         }
     }
 }

crates/collab/src/db.rs 🔗

@@ -660,9 +660,9 @@ impl Database {
 
     // signups
 
-    pub async fn create_signup(&self, signup: NewSignup) -> Result<()> {
+    pub async fn create_signup(&self, signup: &NewSignup) -> Result<()> {
         self.transact(|tx| async {
-            signup::ActiveModel {
+            signup::Entity::insert(signup::ActiveModel {
                 email_address: ActiveValue::set(signup.email_address.clone()),
                 email_confirmation_code: ActiveValue::set(random_email_confirmation_code()),
                 email_confirmation_sent: ActiveValue::set(false),
@@ -673,9 +673,15 @@ impl Database {
                 editor_features: ActiveValue::set(Some(signup.editor_features.clone())),
                 programming_languages: ActiveValue::set(Some(signup.programming_languages.clone())),
                 device_id: ActiveValue::set(signup.device_id.clone()),
+                added_to_mailing_list: ActiveValue::set(signup.added_to_mailing_list),
                 ..Default::default()
-            }
-            .insert(&tx)
+            })
+            .on_conflict(
+                OnConflict::column(signup::Column::EmailAddress)
+                    .update_column(signup::Column::EmailAddress)
+                    .to_owned(),
+            )
+            .exec(&tx)
             .await?;
             tx.commit().await?;
             Ok(())
@@ -746,6 +752,7 @@ impl Database {
                             .or(signup::Column::PlatformUnknown.eq(true)),
                     ),
                 )
+                .order_by_asc(signup::Column::CreatedAt)
                 .limit(count as u64)
                 .into_model()
                 .all(&tx)
@@ -772,32 +779,41 @@ impl Database {
                 Err(anyhow!("email address is already in use"))?;
             }
 
-            let inviter = match user::Entity::find()
-                .filter(user::Column::InviteCode.eq(code))
+            let inviting_user_with_invites = match user::Entity::find()
+                .filter(
+                    user::Column::InviteCode
+                        .eq(code)
+                        .and(user::Column::InviteCount.gt(0)),
+                )
                 .one(&tx)
                 .await?
             {
-                Some(inviter) => inviter,
+                Some(inviting_user) => inviting_user,
                 None => {
                     return Err(Error::Http(
-                        StatusCode::NOT_FOUND,
-                        "invite code not found".to_string(),
+                        StatusCode::UNAUTHORIZED,
+                        "unable to find an invite code with invites remaining".to_string(),
                     ))?
                 }
             };
-
-            if inviter.invite_count == 0 {
-                Err(Error::Http(
-                    StatusCode::UNAUTHORIZED,
-                    "no invites remaining".to_string(),
-                ))?;
-            }
+            user::Entity::update_many()
+                .filter(
+                    user::Column::Id
+                        .eq(inviting_user_with_invites.id)
+                        .and(user::Column::InviteCount.gt(0)),
+                )
+                .col_expr(
+                    user::Column::InviteCount,
+                    Expr::col(user::Column::InviteCount).sub(1),
+                )
+                .exec(&tx)
+                .await?;
 
             let signup = signup::Entity::insert(signup::ActiveModel {
                 email_address: ActiveValue::set(email_address.into()),
                 email_confirmation_code: ActiveValue::set(random_email_confirmation_code()),
                 email_confirmation_sent: ActiveValue::set(false),
-                inviting_user_id: ActiveValue::set(Some(inviter.id)),
+                inviting_user_id: ActiveValue::set(Some(inviting_user_with_invites.id)),
                 platform_linux: ActiveValue::set(false),
                 platform_mac: ActiveValue::set(false),
                 platform_windows: ActiveValue::set(false),
@@ -873,26 +889,6 @@ impl Database {
             let signup = signup.update(&tx).await?;
 
             if let Some(inviting_user_id) = signup.inviting_user_id {
-                let result = user::Entity::update_many()
-                    .filter(
-                        user::Column::Id
-                            .eq(inviting_user_id)
-                            .and(user::Column::InviteCount.gt(0)),
-                    )
-                    .col_expr(
-                        user::Column::InviteCount,
-                        Expr::col(user::Column::InviteCount).sub(1),
-                    )
-                    .exec(&tx)
-                    .await?;
-
-                if result.rows_affected == 0 {
-                    Err(Error::Http(
-                        StatusCode::UNAUTHORIZED,
-                        "no invites remaining".to_string(),
-                    ))?;
-                }
-
                 contact::Entity::insert(contact::ActiveModel {
                     user_id_a: ActiveValue::set(inviting_user_id),
                     user_id_b: ActiveValue::set(user.id),

crates/collab/src/db/signup.rs 🔗

@@ -20,6 +20,7 @@ pub struct Model {
     pub platform_unknown: bool,
     pub editor_features: Option<Vec<String>>,
     pub programming_languages: Option<Vec<String>>,
+    pub added_to_mailing_list: bool,
 }
 
 #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
@@ -27,7 +28,7 @@ pub enum Relation {}
 
 impl ActiveModelBehavior for ActiveModel {}
 
-#[derive(Debug, PartialEq, Eq, FromQueryResult, Serialize, Deserialize)]
+#[derive(Clone, Debug, PartialEq, Eq, FromQueryResult, Serialize, Deserialize)]
 pub struct Invite {
     pub email_address: String,
     pub email_confirmation_code: String,
@@ -42,6 +43,7 @@ pub struct NewSignup {
     pub editor_features: Vec<String>,
     pub programming_languages: Vec<String>,
     pub device_id: Option<String>,
+    pub added_to_mailing_list: bool,
 }
 
 #[derive(Clone, Debug, PartialEq, Deserialize, Serialize, FromQueryResult)]

crates/collab/src/db/tests.rs 🔗

@@ -667,19 +667,29 @@ async fn test_signups() {
     let test_db = TestDb::postgres(build_background_executor());
     let db = test_db.db();
 
-    // people sign up on the waitlist
-    for i in 0..8 {
-        db.create_signup(NewSignup {
-            email_address: format!("person-{i}@example.com"),
+    let usernames = (0..8).map(|i| format!("person-{i}")).collect::<Vec<_>>();
+
+    let all_signups = usernames
+        .iter()
+        .enumerate()
+        .map(|(i, username)| NewSignup {
+            email_address: format!("{username}@example.com"),
             platform_mac: true,
             platform_linux: i % 2 == 0,
             platform_windows: i % 4 == 0,
             editor_features: vec!["speed".into()],
             programming_languages: vec!["rust".into(), "c".into()],
             device_id: Some(format!("device_id_{i}")),
+            added_to_mailing_list: i != 0, // One user failed to subscribe
         })
-        .await
-        .unwrap();
+        .collect::<Vec<NewSignup>>();
+
+    // people sign up on the waitlist
+    for signup in &all_signups {
+        // users can sign up multiple times without issues
+        for _ in 0..2 {
+            db.create_signup(&signup).await.unwrap();
+        }
     }
 
     assert_eq!(
@@ -702,9 +712,9 @@ async fn test_signups() {
     assert_eq!(
         addresses,
         &[
-            "person-0@example.com",
-            "person-1@example.com",
-            "person-2@example.com"
+            all_signups[0].email_address.as_str(),
+            all_signups[1].email_address.as_str(),
+            all_signups[2].email_address.as_str()
         ]
     );
     assert_ne!(
@@ -728,9 +738,9 @@ async fn test_signups() {
     assert_eq!(
         addresses,
         &[
-            "person-3@example.com",
-            "person-4@example.com",
-            "person-5@example.com"
+            all_signups[3].email_address.as_str(),
+            all_signups[4].email_address.as_str(),
+            all_signups[5].email_address.as_str()
         ]
     );
 
@@ -756,11 +766,10 @@ async fn test_signups() {
     } = db
         .create_user_from_invite(
             &Invite {
-                email_address: signups_batch1[0].email_address.clone(),
-                email_confirmation_code: signups_batch1[0].email_confirmation_code.clone(),
+                ..signups_batch1[0].clone()
             },
             NewUserParams {
-                github_login: "person-0".into(),
+                github_login: usernames[0].clone(),
                 github_user_id: 0,
                 invite_count: 5,
             },
@@ -770,8 +779,11 @@ async fn test_signups() {
         .unwrap();
     let user = db.get_user_by_id(user_id).await.unwrap().unwrap();
     assert!(inviting_user_id.is_none());
-    assert_eq!(user.github_login, "person-0");
-    assert_eq!(user.email_address.as_deref(), Some("person-0@example.com"));
+    assert_eq!(user.github_login, usernames[0]);
+    assert_eq!(
+        user.email_address,
+        Some(all_signups[0].email_address.clone())
+    );
     assert_eq!(user.invite_count, 5);
     assert_eq!(signup_device_id.unwrap(), "device_id_0");
 
@@ -799,7 +811,7 @@ async fn test_signups() {
             email_confirmation_code: "the-wrong-code".to_string(),
         },
         NewUserParams {
-            github_login: "person-1".into(),
+            github_login: usernames[1].clone(),
             github_user_id: 2,
             invite_count: 5,
         },

crates/collab/src/integration_tests.rs 🔗

@@ -5566,6 +5566,13 @@ async fn test_random_collaboration(
                                 guest_client.username,
                                 id
                             );
+                            assert_eq!(
+                                guest_snapshot.abs_path(),
+                                host_snapshot.abs_path(),
+                                "{} has different abs path than the host for worktree {}",
+                                guest_client.username,
+                                id
+                            );
                             assert_eq!(
                                 guest_snapshot.entries(false).collect::<Vec<_>>(),
                                 host_snapshot.entries(false).collect::<Vec<_>>(),

crates/diagnostics/src/diagnostics.rs 🔗

@@ -322,7 +322,7 @@ impl ProjectDiagnosticsEditor {
                             );
                             let excerpt_id = excerpts
                                 .insert_excerpts_after(
-                                    &prev_excerpt_id,
+                                    prev_excerpt_id,
                                     buffer.clone(),
                                     [ExcerptRange {
                                         context: excerpt_start..excerpt_end,
@@ -384,7 +384,7 @@ impl ProjectDiagnosticsEditor {
 
                     groups_to_add.push(group_state);
                 } else if let Some((group_ix, group_state)) = to_remove {
-                    excerpts.remove_excerpts(group_state.excerpts.iter(), excerpts_cx);
+                    excerpts.remove_excerpts(group_state.excerpts.iter().copied(), excerpts_cx);
                     group_ixs_to_remove.push(group_ix);
                     blocks_to_remove.extend(group_state.blocks.iter().copied());
                 } else if let Some((_, group)) = to_keep {
@@ -457,10 +457,15 @@ impl ProjectDiagnosticsEditor {
             }
 
             // If any selection has lost its position, move it to start of the next primary diagnostic.
+            let snapshot = editor.snapshot(cx);
             for selection in &mut selections {
                 if let Some(new_excerpt_id) = new_excerpt_ids_by_selection_id.get(&selection.id) {
                     let group_ix = match groups.binary_search_by(|probe| {
-                        probe.excerpts.last().unwrap().cmp(new_excerpt_id)
+                        probe
+                            .excerpts
+                            .last()
+                            .unwrap()
+                            .cmp(new_excerpt_id, &snapshot.buffer_snapshot)
                     }) {
                         Ok(ix) | Err(ix) => ix,
                     };
@@ -738,7 +743,7 @@ mod tests {
         DisplayPoint,
     };
     use gpui::TestAppContext;
-    use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16};
+    use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16, Unclipped};
     use serde_json::json;
     use unindent::Unindent as _;
     use workspace::AppState;
@@ -788,7 +793,7 @@ mod tests {
                     None,
                     vec![
                         DiagnosticEntry {
-                            range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9),
+                            range: Unclipped(PointUtf16::new(1, 8))..Unclipped(PointUtf16::new(1, 9)),
                             diagnostic: Diagnostic {
                                 message:
                                     "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
@@ -801,7 +806,7 @@ mod tests {
                             },
                         },
                         DiagnosticEntry {
-                            range: PointUtf16::new(2, 8)..PointUtf16::new(2, 9),
+                            range: Unclipped(PointUtf16::new(2, 8))..Unclipped(PointUtf16::new(2, 9)),
                             diagnostic: Diagnostic {
                                 message:
                                     "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
@@ -814,7 +819,7 @@ mod tests {
                             },
                         },
                         DiagnosticEntry {
-                            range: PointUtf16::new(3, 6)..PointUtf16::new(3, 7),
+                            range: Unclipped(PointUtf16::new(3, 6))..Unclipped(PointUtf16::new(3, 7)),
                             diagnostic: Diagnostic {
                                 message: "value moved here".to_string(),
                                 severity: DiagnosticSeverity::INFORMATION,
@@ -825,7 +830,7 @@ mod tests {
                             },
                         },
                         DiagnosticEntry {
-                            range: PointUtf16::new(4, 6)..PointUtf16::new(4, 7),
+                            range: Unclipped(PointUtf16::new(4, 6))..Unclipped(PointUtf16::new(4, 7)),
                             diagnostic: Diagnostic {
                                 message: "value moved here".to_string(),
                                 severity: DiagnosticSeverity::INFORMATION,
@@ -836,7 +841,7 @@ mod tests {
                             },
                         },
                         DiagnosticEntry {
-                            range: PointUtf16::new(7, 6)..PointUtf16::new(7, 7),
+                            range: Unclipped(PointUtf16::new(7, 6))..Unclipped(PointUtf16::new(7, 7)),
                             diagnostic: Diagnostic {
                                 message: "use of moved value\nvalue used here after move".to_string(),
                                 severity: DiagnosticSeverity::ERROR,
@@ -847,7 +852,7 @@ mod tests {
                             },
                         },
                         DiagnosticEntry {
-                            range: PointUtf16::new(8, 6)..PointUtf16::new(8, 7),
+                            range: Unclipped(PointUtf16::new(8, 6))..Unclipped(PointUtf16::new(8, 7)),
                             diagnostic: Diagnostic {
                                 message: "use of moved value\nvalue used here after move".to_string(),
                                 severity: DiagnosticSeverity::ERROR,
@@ -939,7 +944,7 @@ mod tests {
                     PathBuf::from("/test/consts.rs"),
                     None,
                     vec![DiagnosticEntry {
-                        range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
+                        range: Unclipped(PointUtf16::new(0, 15))..Unclipped(PointUtf16::new(0, 15)),
                         diagnostic: Diagnostic {
                             message: "mismatched types\nexpected `usize`, found `char`".to_string(),
                             severity: DiagnosticSeverity::ERROR,
@@ -1040,7 +1045,8 @@ mod tests {
                     None,
                     vec![
                         DiagnosticEntry {
-                            range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
+                            range: Unclipped(PointUtf16::new(0, 15))
+                                ..Unclipped(PointUtf16::new(0, 15)),
                             diagnostic: Diagnostic {
                                 message: "mismatched types\nexpected `usize`, found `char`"
                                     .to_string(),
@@ -1052,7 +1058,8 @@ mod tests {
                             },
                         },
                         DiagnosticEntry {
-                            range: PointUtf16::new(1, 15)..PointUtf16::new(1, 15),
+                            range: Unclipped(PointUtf16::new(1, 15))
+                                ..Unclipped(PointUtf16::new(1, 15)),
                             diagnostic: Diagnostic {
                                 message: "unresolved name `c`".to_string(),
                                 severity: DiagnosticSeverity::ERROR,

crates/drag_and_drop/Cargo.toml 🔗

@@ -12,4 +12,4 @@ collections = { path = "../collections" }
 gpui = { path = "../gpui" }
 
 [dev-dependencies]
-gpui = { path = "../gpui", features = ["test-support"] }
+gpui = { path = "../gpui", features = ["test-support"] }

crates/drag_and_drop/src/drag_and_drop.rs 🔗

@@ -4,12 +4,16 @@ use collections::HashSet;
 use gpui::{
     elements::{Empty, MouseEventHandler, Overlay},
     geometry::{rect::RectF, vector::Vector2F},
-    scene::MouseDrag,
+    scene::{MouseDown, MouseDrag},
     CursorStyle, Element, ElementBox, EventContext, MouseButton, MutableAppContext, RenderContext,
     View, WeakViewHandle,
 };
 
 enum State<V: View> {
+    Down {
+        region_offset: Vector2F,
+        region: RectF,
+    },
     Dragging {
         window_id: usize,
         position: Vector2F,
@@ -24,6 +28,13 @@ enum State<V: View> {
 impl<V: View> Clone for State<V> {
     fn clone(&self) -> Self {
         match self {
+            &State::Down {
+                region_offset,
+                region,
+            } => State::Down {
+                region_offset,
+                region,
+            },
             State::Dragging {
                 window_id,
                 position,
@@ -87,6 +98,15 @@ impl<V: View> DragAndDrop<V> {
         })
     }
 
+    pub fn drag_started(event: MouseDown, cx: &mut EventContext) {
+        cx.update_global(|this: &mut Self, _| {
+            this.currently_dragged = Some(State::Down {
+                region_offset: event.region.origin() - event.position,
+                region: event.region,
+            });
+        })
+    }
+
     pub fn dragging<T: Any>(
         event: MouseDrag,
         payload: Rc<T>,
@@ -94,37 +114,32 @@ impl<V: View> DragAndDrop<V> {
         render: Rc<impl 'static + Fn(&T, &mut RenderContext<V>) -> ElementBox>,
     ) {
         let window_id = cx.window_id();
-        cx.update_global::<Self, _, _>(|this, cx| {
+        cx.update_global(|this: &mut Self, cx| {
             this.notify_containers_for_window(window_id, cx);
 
-            if matches!(this.currently_dragged, Some(State::Canceled)) {
-                return;
+            match this.currently_dragged.as_ref() {
+                Some(&State::Down {
+                    region_offset,
+                    region,
+                })
+                | Some(&State::Dragging {
+                    region_offset,
+                    region,
+                    ..
+                }) => {
+                    this.currently_dragged = Some(State::Dragging {
+                        window_id,
+                        region_offset,
+                        region,
+                        position: event.position,
+                        payload,
+                        render: Rc::new(move |payload, cx| {
+                            render(payload.downcast_ref::<T>().unwrap(), cx)
+                        }),
+                    });
+                }
+                _ => {}
             }
-
-            let (region_offset, region) = if let Some(State::Dragging {
-                region_offset,
-                region,
-                ..
-            }) = this.currently_dragged.as_ref()
-            {
-                (*region_offset, *region)
-            } else {
-                (
-                    event.region.origin() - event.prev_mouse_position,
-                    event.region,
-                )
-            };
-
-            this.currently_dragged = Some(State::Dragging {
-                window_id,
-                region_offset,
-                region,
-                position: event.position,
-                payload,
-                render: Rc::new(move |payload, cx| {
-                    render(payload.downcast_ref::<T>().unwrap(), cx)
-                }),
-            });
         });
     }
 
@@ -135,6 +150,7 @@ impl<V: View> DragAndDrop<V> {
             .clone()
             .and_then(|state| {
                 match state {
+                    State::Down { .. } => None,
                     State::Dragging {
                         window_id,
                         region_offset,
@@ -263,7 +279,11 @@ impl<Tag> Draggable for MouseEventHandler<Tag> {
     {
         let payload = Rc::new(payload);
         let render = Rc::new(render);
-        self.on_drag(MouseButton::Left, move |e, cx| {
+        self.on_down(MouseButton::Left, move |e, cx| {
+            cx.propagate_event();
+            DragAndDrop::<V>::drag_started(e, cx);
+        })
+        .on_drag(MouseButton::Left, move |e, cx| {
             let payload = payload.clone();
             let render = render.clone();
             DragAndDrop::<V>::dragging(e, payload, cx, render)

crates/editor/src/display_map/block_map.rs 🔗

@@ -2,7 +2,7 @@ use super::{
     wrap_map::{self, WrapEdit, WrapPoint, WrapSnapshot},
     TextHighlights,
 };
-use crate::{Anchor, ExcerptRange, ToPoint as _};
+use crate::{Anchor, ExcerptId, ExcerptRange, ToPoint as _};
 use collections::{Bound, HashMap, HashSet};
 use gpui::{ElementBox, RenderContext};
 use language::{BufferSnapshot, Chunk, Patch, Point};
@@ -107,7 +107,7 @@ struct Transform {
 pub enum TransformBlock {
     Custom(Arc<Block>),
     ExcerptHeader {
-        key: usize,
+        id: ExcerptId,
         buffer: BufferSnapshot,
         range: ExcerptRange<text::Anchor>,
         height: u8,
@@ -371,7 +371,7 @@ impl BlockMap {
                                 .make_wrap_point(Point::new(excerpt_boundary.row, 0), Bias::Left)
                                 .row(),
                             TransformBlock::ExcerptHeader {
-                                key: excerpt_boundary.key,
+                                id: excerpt_boundary.id,
                                 buffer: excerpt_boundary.buffer,
                                 range: excerpt_boundary.range,
                                 height: if excerpt_boundary.starts_new_buffer {

crates/editor/src/editor.rs 🔗

@@ -73,6 +73,7 @@ use std::{
     mem,
     num::NonZeroU32,
     ops::{Deref, DerefMut, Range, RangeInclusive},
+    path::Path,
     sync::Arc,
     time::{Duration, Instant},
 };
@@ -1161,7 +1162,7 @@ impl Editor {
         });
         clone.selections.set_state(&self.selections);
         clone.scroll_position = self.scroll_position;
-        clone.scroll_top_anchor = self.scroll_top_anchor.clone();
+        clone.scroll_top_anchor = self.scroll_top_anchor;
         clone.searchable = self.searchable;
         clone
     }
@@ -1304,7 +1305,7 @@ impl Editor {
             display_snapshot: self.display_map.update(cx, |map, cx| map.snapshot(cx)),
             ongoing_scroll: self.ongoing_scroll,
             scroll_position: self.scroll_position,
-            scroll_top_anchor: self.scroll_top_anchor.clone(),
+            scroll_top_anchor: self.scroll_top_anchor,
             placeholder_text: self.placeholder_text.clone(),
             is_focused: self
                 .handle
@@ -1790,17 +1791,15 @@ impl Editor {
             .pending_anchor()
             .expect("extend_selection not called with pending selection");
         if position >= tail {
-            pending_selection.start = tail_anchor.clone();
+            pending_selection.start = tail_anchor;
         } else {
-            pending_selection.end = tail_anchor.clone();
+            pending_selection.end = tail_anchor;
             pending_selection.reversed = true;
         }
 
         let mut pending_mode = self.selections.pending_mode().unwrap();
         match &mut pending_mode {
-            SelectMode::Word(range) | SelectMode::Line(range) => {
-                *range = tail_anchor.clone()..tail_anchor
-            }
+            SelectMode::Word(range) | SelectMode::Line(range) => *range = tail_anchor..tail_anchor,
             _ => {}
         }
 
@@ -2144,10 +2143,9 @@ impl Editor {
                                     ));
                             if following_text_allows_autoclose && preceding_text_matches_prefix {
                                 let anchor = snapshot.anchor_before(selection.end);
-                                new_selections
-                                    .push((selection.map(|_| anchor.clone()), text.len()));
+                                new_selections.push((selection.map(|_| anchor), text.len()));
                                 new_autoclose_regions.push((
-                                    anchor.clone(),
+                                    anchor,
                                     text.len(),
                                     selection.id,
                                     bracket_pair.clone(),
@@ -2168,10 +2166,8 @@ impl Editor {
                                 && text.as_ref() == region.pair.end.as_str();
                             if should_skip {
                                 let anchor = snapshot.anchor_after(selection.end);
-                                new_selections.push((
-                                    selection.map(|_| anchor.clone()),
-                                    region.pair.end.len(),
-                                ));
+                                new_selections
+                                    .push((selection.map(|_| anchor), region.pair.end.len()));
                                 continue;
                             }
                         }
@@ -2203,7 +2199,7 @@ impl Editor {
             // text with the given input and move the selection to the end of the
             // newly inserted text.
             let anchor = snapshot.anchor_after(selection.end);
-            new_selections.push((selection.map(|_| anchor.clone()), 0));
+            new_selections.push((selection.map(|_| anchor), 0));
             edits.push((selection.start..selection.end, text.clone()));
         }
 
@@ -2305,7 +2301,7 @@ impl Editor {
                         }
 
                         let anchor = buffer.anchor_after(end);
-                        let new_selection = selection.map(|_| anchor.clone());
+                        let new_selection = selection.map(|_| anchor);
                         (
                             (start..end, new_text),
                             (insert_extra_newline, new_selection),
@@ -2385,7 +2381,7 @@ impl Editor {
                         .iter()
                         .map(|s| {
                             let anchor = snapshot.anchor_after(s.end);
-                            s.map(|_| anchor.clone())
+                            s.map(|_| anchor)
                         })
                         .collect::<Vec<_>>()
                 };
@@ -3649,7 +3645,7 @@ impl Editor {
                         String::new(),
                     ));
                     let insertion_anchor = buffer.anchor_after(insertion_point);
-                    edits.push((insertion_anchor.clone()..insertion_anchor, text));
+                    edits.push((insertion_anchor..insertion_anchor, text));
 
                     let row_delta = range_to_move.start.row - insertion_point.row + 1;
 
@@ -3754,7 +3750,7 @@ impl Editor {
                         String::new(),
                     ));
                     let insertion_anchor = buffer.anchor_after(insertion_point);
-                    edits.push((insertion_anchor.clone()..insertion_anchor, text));
+                    edits.push((insertion_anchor..insertion_anchor, text));
 
                     let row_delta = insertion_point.row - range_to_move.end.row + 1;
 
@@ -4624,7 +4620,7 @@ impl Editor {
                     cursor_anchor: position,
                     cursor_position: point,
                     scroll_position: self.scroll_position,
-                    scroll_top_anchor: self.scroll_top_anchor.clone(),
+                    scroll_top_anchor: self.scroll_top_anchor,
                     scroll_top_row,
                 }),
                 cx,
@@ -6536,15 +6532,13 @@ impl Editor {
                 .as_singleton()
                 .and_then(|b| b.read(cx).file()),
         ) {
-            project.read(cx).client().report_event(
-                name,
-                json!({
-                    "File Extension": file
-                        .path()
-                        .extension()
-                        .and_then(|e| e.to_str())
-                }),
-            );
+            let extension = Path::new(file.file_name(cx))
+                .extension()
+                .and_then(|e| e.to_str());
+            project
+                .read(cx)
+                .client()
+                .report_event(name, json!({ "File Extension": extension }));
         }
     }
 }

crates/editor/src/editor_tests.rs 🔗

@@ -542,7 +542,7 @@ fn test_navigation_history(cx: &mut gpui::MutableAppContext) {
         // Set scroll position to check later
         editor.set_scroll_position(Vector2F::new(5.5, 5.5), cx);
         let original_scroll_position = editor.scroll_position;
-        let original_scroll_top_anchor = editor.scroll_top_anchor.clone();
+        let original_scroll_top_anchor = editor.scroll_top_anchor;
 
         // Jump to the end of the document and adjust scroll
         editor.move_to_end(&MoveToEnd, cx);
@@ -556,12 +556,12 @@ fn test_navigation_history(cx: &mut gpui::MutableAppContext) {
         assert_eq!(editor.scroll_top_anchor, original_scroll_top_anchor);
 
         // Ensure we don't panic when navigation data contains invalid anchors *and* points.
-        let mut invalid_anchor = editor.scroll_top_anchor.clone();
+        let mut invalid_anchor = editor.scroll_top_anchor;
         invalid_anchor.text_anchor.buffer_id = Some(999);
         let invalid_point = Point::new(9999, 0);
         editor.navigate(
             Box::new(NavigationData {
-                cursor_anchor: invalid_anchor.clone(),
+                cursor_anchor: invalid_anchor,
                 cursor_position: invalid_point,
                 scroll_top_anchor: invalid_anchor,
                 scroll_top_row: invalid_point.row,
@@ -4146,14 +4146,26 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
 
     handle_resolve_completion_request(
         &mut cx,
-        Some((
-            indoc! {"
-                one.second_completion
-                two
-                threeˇ
-            "},
-            "\nadditional edit",
-        )),
+        Some(vec![
+            (
+                //This overlaps with the primary completion edit which is
+                //misbehavior from the LSP spec, test that we filter it out
+                indoc! {"
+                    one.second_ˇcompletion
+                    two
+                    threeˇ
+                "},
+                "overlapping aditional edit",
+            ),
+            (
+                indoc! {"
+                    one.second_completion
+                    two
+                    threeˇ
+                "},
+                "\nadditional edit",
+            ),
+        ]),
     )
     .await;
     apply_additional_edits.await.unwrap();
@@ -4303,19 +4315,24 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
 
     async fn handle_resolve_completion_request<'a>(
         cx: &mut EditorLspTestContext<'a>,
-        edit: Option<(&'static str, &'static str)>,
+        edits: Option<Vec<(&'static str, &'static str)>>,
     ) {
-        let edit = edit.map(|(marked_string, new_text)| {
-            let (_, marked_ranges) = marked_text_ranges(marked_string, false);
-            let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
-            vec![lsp::TextEdit::new(replace_range, new_text.to_string())]
+        let edits = edits.map(|edits| {
+            edits
+                .iter()
+                .map(|(marked_string, new_text)| {
+                    let (_, marked_ranges) = marked_text_ranges(marked_string, false);
+                    let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
+                    lsp::TextEdit::new(replace_range, new_text.to_string())
+                })
+                .collect::<Vec<_>>()
         });
 
         cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
-            let edit = edit.clone();
+            let edits = edits.clone();
             async move {
                 Ok(lsp::CompletionItem {
-                    additional_text_edits: edit,
+                    additional_text_edits: edits,
                     ..Default::default()
                 })
             }
@@ -4701,9 +4718,7 @@ fn test_refresh_selections(cx: &mut gpui::MutableAppContext) {
 
     // Refreshing selections is a no-op when excerpts haven't changed.
     editor.update(cx, |editor, cx| {
-        editor.change_selections(None, cx, |s| {
-            s.refresh();
-        });
+        editor.change_selections(None, cx, |s| s.refresh());
         assert_eq!(
             editor.selections.ranges(cx),
             [
@@ -4714,7 +4729,7 @@ fn test_refresh_selections(cx: &mut gpui::MutableAppContext) {
     });
 
     multibuffer.update(cx, |multibuffer, cx| {
-        multibuffer.remove_excerpts([&excerpt1_id.unwrap()], cx);
+        multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
     });
     editor.update(cx, |editor, cx| {
         // Removing an excerpt causes the first selection to become degenerate.
@@ -4728,9 +4743,7 @@ fn test_refresh_selections(cx: &mut gpui::MutableAppContext) {
 
         // Refreshing selections will relocate the first selection to the original buffer
         // location.
-        editor.change_selections(None, cx, |s| {
-            s.refresh();
-        });
+        editor.change_selections(None, cx, |s| s.refresh());
         assert_eq!(
             editor.selections.ranges(cx),
             [
@@ -4784,7 +4797,7 @@ fn test_refresh_selections_while_selecting_with_mouse(cx: &mut gpui::MutableAppC
     });
 
     multibuffer.update(cx, |multibuffer, cx| {
-        multibuffer.remove_excerpts([&excerpt1_id.unwrap()], cx);
+        multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
     });
     editor.update(cx, |editor, cx| {
         assert_eq!(
@@ -4793,9 +4806,7 @@ fn test_refresh_selections_while_selecting_with_mouse(cx: &mut gpui::MutableAppC
         );
 
         // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
-        editor.change_selections(None, cx, |s| {
-            s.refresh();
-        });
+        editor.change_selections(None, cx, |s| s.refresh());
         assert_eq!(
             editor.selections.ranges(cx),
             [Point::new(0, 3)..Point::new(0, 3)]

crates/editor/src/element.rs 🔗

@@ -192,8 +192,14 @@ impl EditorElement {
                 .on_scroll({
                     let position_map = position_map.clone();
                     move |e, cx| {
-                        if !Self::scroll(e.position, e.delta, e.precise, &position_map, bounds, cx)
-                        {
+                        if !Self::scroll(
+                            e.position,
+                            *e.delta.raw(),
+                            e.delta.precise(),
+                            &position_map,
+                            bounds,
+                            cx,
+                        ) {
                             cx.propagate_event()
                         }
                     }
@@ -1328,12 +1334,13 @@ impl EditorElement {
                     })
                 }
                 TransformBlock::ExcerptHeader {
-                    key,
+                    id,
                     buffer,
                     range,
                     starts_new_buffer,
                     ..
                 } => {
+                    let id = *id;
                     let jump_icon = project::File::from_dyn(buffer.file()).map(|file| {
                         let jump_position = range
                             .primary
@@ -1350,7 +1357,7 @@ impl EditorElement {
 
                         enum JumpIcon {}
                         cx.render(&editor, |_, cx| {
-                            MouseEventHandler::<JumpIcon>::new(*key, cx, |state, _| {
+                            MouseEventHandler::<JumpIcon>::new(id.into(), cx, |state, _| {
                                 let style = style.jump_icon.style_for(state, false);
                                 Svg::new("icons/arrow_up_right_8.svg")
                                     .with_color(style.color)
@@ -1369,7 +1376,7 @@ impl EditorElement {
                                 cx.dispatch_action(jump_action.clone())
                             })
                             .with_tooltip::<JumpIcon, _>(
-                                *key,
+                                id.into(),
                                 "Jump to Buffer".to_string(),
                                 Some(Box::new(crate::OpenExcerpts)),
                                 tooltip_style.clone(),
@@ -1600,16 +1607,13 @@ impl Element for EditorElement {
 
             highlighted_rows = view.highlighted_rows();
             let theme = cx.global::<Settings>().theme.as_ref();
-            highlighted_ranges = view.background_highlights_in_range(
-                start_anchor.clone()..end_anchor.clone(),
-                &display_map,
-                theme,
-            );
+            highlighted_ranges =
+                view.background_highlights_in_range(start_anchor..end_anchor, &display_map, theme);
 
             let mut remote_selections = HashMap::default();
             for (replica_id, line_mode, cursor_shape, selection) in display_map
                 .buffer_snapshot
-                .remote_selections_in_range(&(start_anchor.clone()..end_anchor.clone()))
+                .remote_selections_in_range(&(start_anchor..end_anchor))
             {
                 // The local selections match the leader's selections.
                 if Some(replica_id) == view.leader_replica_id {

crates/editor/src/items.rs 🔗

@@ -819,11 +819,20 @@ impl StatusItemView for CursorPosition {
 
 fn path_for_buffer<'a>(
     buffer: &ModelHandle<MultiBuffer>,
-    mut height: usize,
+    height: usize,
     include_filename: bool,
     cx: &'a AppContext,
 ) -> Option<Cow<'a, Path>> {
     let file = buffer.read(cx).as_singleton()?.read(cx).file()?;
+    path_for_file(file, height, include_filename, cx)
+}
+
+fn path_for_file<'a>(
+    file: &'a dyn language::File,
+    mut height: usize,
+    include_filename: bool,
+    cx: &'a AppContext,
+) -> Option<Cow<'a, Path>> {
     // Ensure we always render at least the filename.
     height += 1;
 
@@ -845,13 +854,82 @@ fn path_for_buffer<'a>(
         if include_filename {
             Some(full_path.into())
         } else {
-            Some(full_path.parent().unwrap().to_path_buf().into())
+            Some(full_path.parent()?.to_path_buf().into())
         }
     } else {
-        let mut path = file.path().strip_prefix(prefix).unwrap();
+        let mut path = file.path().strip_prefix(prefix).ok()?;
         if !include_filename {
-            path = path.parent().unwrap();
+            path = path.parent()?;
         }
         Some(path.into())
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use gpui::MutableAppContext;
+    use std::{
+        path::{Path, PathBuf},
+        sync::Arc,
+    };
+
+    #[gpui::test]
+    fn test_path_for_file(cx: &mut MutableAppContext) {
+        let file = TestFile {
+            path: Path::new("").into(),
+            full_path: PathBuf::from(""),
+        };
+        assert_eq!(path_for_file(&file, 0, false, cx), None);
+    }
+
+    struct TestFile {
+        path: Arc<Path>,
+        full_path: PathBuf,
+    }
+
+    impl language::File for TestFile {
+        fn path(&self) -> &Arc<Path> {
+            &self.path
+        }
+
+        fn full_path(&self, _: &gpui::AppContext) -> PathBuf {
+            self.full_path.clone()
+        }
+
+        fn as_local(&self) -> Option<&dyn language::LocalFile> {
+            todo!()
+        }
+
+        fn mtime(&self) -> std::time::SystemTime {
+            todo!()
+        }
+
+        fn file_name<'a>(&'a self, _: &'a gpui::AppContext) -> &'a std::ffi::OsStr {
+            todo!()
+        }
+
+        fn is_deleted(&self) -> bool {
+            todo!()
+        }
+
+        fn save(
+            &self,
+            _: u64,
+            _: language::Rope,
+            _: clock::Global,
+            _: project::LineEnding,
+            _: &mut MutableAppContext,
+        ) -> gpui::Task<anyhow::Result<(clock::Global, String, std::time::SystemTime)>> {
+            todo!()
+        }
+
+        fn as_any(&self) -> &dyn std::any::Any {
+            todo!()
+        }
+
+        fn to_proto(&self) -> rpc::proto::File {
+            todo!()
+        }
+    }
+}

crates/editor/src/multi_buffer.rs 🔗

@@ -11,7 +11,7 @@ use language::{
     char_kind, AutoindentMode, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk, CursorShape,
     DiagnosticEntry, Event, File, IndentSize, Language, OffsetRangeExt, OffsetUtf16, Outline,
     OutlineItem, Point, PointUtf16, Selection, TextDimension, ToOffset as _, ToOffsetUtf16 as _,
-    ToPoint as _, ToPointUtf16 as _, TransactionId,
+    ToPoint as _, ToPointUtf16 as _, TransactionId, Unclipped,
 };
 use smallvec::SmallVec;
 use std::{
@@ -36,13 +36,13 @@ use util::post_inc;
 
 const NEWLINES: &[u8] = &[b'\n'; u8::MAX as usize];
 
-pub type ExcerptId = Locator;
+#[derive(Debug, Default, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
+pub struct ExcerptId(usize);
 
 pub struct MultiBuffer {
     snapshot: RefCell<MultiBufferSnapshot>,
     buffers: RefCell<HashMap<usize, BufferState>>,
-    used_excerpt_ids: SumTree<ExcerptId>,
-    next_excerpt_key: usize,
+    next_excerpt_id: usize,
     subscriptions: Topic,
     singleton: bool,
     replica_id: ReplicaId,
@@ -92,7 +92,7 @@ struct BufferState {
     last_diagnostics_update_count: usize,
     last_file_update_count: usize,
     last_git_diff_update_count: usize,
-    excerpts: Vec<ExcerptId>,
+    excerpts: Vec<Locator>,
     _subscriptions: [gpui::Subscription; 2],
 }
 
@@ -100,6 +100,7 @@ struct BufferState {
 pub struct MultiBufferSnapshot {
     singleton: bool,
     excerpts: SumTree<Excerpt>,
+    excerpt_ids: SumTree<ExcerptIdMapping>,
     parse_count: usize,
     diagnostics_update_count: usize,
     trailing_excerpt_update_count: usize,
@@ -111,7 +112,6 @@ pub struct MultiBufferSnapshot {
 
 pub struct ExcerptBoundary {
     pub id: ExcerptId,
-    pub key: usize,
     pub row: u32,
     pub buffer: BufferSnapshot,
     pub range: ExcerptRange<text::Anchor>,
@@ -121,7 +121,7 @@ pub struct ExcerptBoundary {
 #[derive(Clone)]
 struct Excerpt {
     id: ExcerptId,
-    key: usize,
+    locator: Locator,
     buffer_id: usize,
     buffer: BufferSnapshot,
     range: ExcerptRange<text::Anchor>,
@@ -130,6 +130,12 @@ struct Excerpt {
     has_trailing_newline: bool,
 }
 
+#[derive(Clone, Debug)]
+struct ExcerptIdMapping {
+    id: ExcerptId,
+    locator: Locator,
+}
+
 #[derive(Clone, Debug, Eq, PartialEq)]
 pub struct ExcerptRange<T> {
     pub context: Range<T>,
@@ -139,6 +145,7 @@ pub struct ExcerptRange<T> {
 #[derive(Clone, Debug, Default)]
 struct ExcerptSummary {
     excerpt_id: ExcerptId,
+    excerpt_locator: Locator,
     max_buffer_row: u32,
     text: TextSummary,
 }
@@ -178,8 +185,7 @@ impl MultiBuffer {
         Self {
             snapshot: Default::default(),
             buffers: Default::default(),
-            used_excerpt_ids: Default::default(),
-            next_excerpt_key: Default::default(),
+            next_excerpt_id: 1,
             subscriptions: Default::default(),
             singleton: false,
             replica_id,
@@ -218,8 +224,7 @@ impl MultiBuffer {
         Self {
             snapshot: RefCell::new(self.snapshot.borrow().clone()),
             buffers: RefCell::new(buffers),
-            used_excerpt_ids: self.used_excerpt_ids.clone(),
-            next_excerpt_key: self.next_excerpt_key,
+            next_excerpt_id: 1,
             subscriptions: Default::default(),
             singleton: self.singleton,
             replica_id: self.replica_id,
@@ -610,11 +615,14 @@ impl MultiBuffer {
         let mut selections_by_buffer: HashMap<usize, Vec<Selection<text::Anchor>>> =
             Default::default();
         let snapshot = self.read(cx);
-        let mut cursor = snapshot.excerpts.cursor::<Option<&ExcerptId>>();
+        let mut cursor = snapshot.excerpts.cursor::<Option<&Locator>>();
         for selection in selections {
-            cursor.seek(&Some(&selection.start.excerpt_id), Bias::Left, &());
+            let start_locator = snapshot.excerpt_locator_for_id(selection.start.excerpt_id);
+            let end_locator = snapshot.excerpt_locator_for_id(selection.end.excerpt_id);
+
+            cursor.seek(&Some(start_locator), Bias::Left, &());
             while let Some(excerpt) = cursor.item() {
-                if excerpt.id > selection.end.excerpt_id {
+                if excerpt.locator > *end_locator {
                     break;
                 }
 
@@ -745,7 +753,7 @@ impl MultiBuffer {
     where
         O: text::ToOffset,
     {
-        self.insert_excerpts_after(&ExcerptId::max(), buffer, ranges, cx)
+        self.insert_excerpts_after(ExcerptId::max(), buffer, ranges, cx)
     }
 
     pub fn push_excerpts_with_context_lines<O>(
@@ -818,7 +826,7 @@ impl MultiBuffer {
 
     pub fn insert_excerpts_after<O>(
         &mut self,
-        prev_excerpt_id: &ExcerptId,
+        prev_excerpt_id: ExcerptId,
         buffer: ModelHandle<Buffer>,
         ranges: impl IntoIterator<Item = ExcerptRange<O>>,
         cx: &mut ModelContext<Self>,
@@ -854,8 +862,12 @@ impl MultiBuffer {
         });
 
         let mut snapshot = self.snapshot.borrow_mut();
-        let mut cursor = snapshot.excerpts.cursor::<Option<&ExcerptId>>();
-        let mut new_excerpts = cursor.slice(&Some(prev_excerpt_id), Bias::Right, &());
+
+        let mut prev_locator = snapshot.excerpt_locator_for_id(prev_excerpt_id).clone();
+        let mut new_excerpt_ids = mem::take(&mut snapshot.excerpt_ids);
+        let mut cursor = snapshot.excerpts.cursor::<Option<&Locator>>();
+        let mut new_excerpts = cursor.slice(&prev_locator, Bias::Right, &());
+        prev_locator = cursor.start().unwrap_or(Locator::min_ref()).clone();
 
         let edit_start = new_excerpts.summary().text.len;
         new_excerpts.update_last(
@@ -865,25 +877,17 @@ impl MultiBuffer {
             &(),
         );
 
-        let mut used_cursor = self.used_excerpt_ids.cursor::<Locator>();
-        used_cursor.seek(prev_excerpt_id, Bias::Right, &());
-        let mut prev_id = if let Some(excerpt_id) = used_cursor.prev_item() {
-            excerpt_id.clone()
-        } else {
-            ExcerptId::min()
-        };
-        let next_id = if let Some(excerpt_id) = used_cursor.item() {
-            excerpt_id.clone()
+        let next_locator = if let Some(excerpt) = cursor.item() {
+            excerpt.locator.clone()
         } else {
-            ExcerptId::max()
+            Locator::max()
         };
-        drop(used_cursor);
 
         let mut ids = Vec::new();
         while let Some(range) = ranges.next() {
-            let id = ExcerptId::between(&prev_id, &next_id);
-            if let Err(ix) = buffer_state.excerpts.binary_search(&id) {
-                buffer_state.excerpts.insert(ix, id.clone());
+            let locator = Locator::between(&prev_locator, &next_locator);
+            if let Err(ix) = buffer_state.excerpts.binary_search(&locator) {
+                buffer_state.excerpts.insert(ix, locator.clone());
             }
             let range = ExcerptRange {
                 context: buffer_snapshot.anchor_before(&range.context.start)
@@ -893,22 +897,20 @@ impl MultiBuffer {
                         ..buffer_snapshot.anchor_after(&primary.end)
                 }),
             };
+            let id = ExcerptId(post_inc(&mut self.next_excerpt_id));
             let excerpt = Excerpt::new(
-                id.clone(),
-                post_inc(&mut self.next_excerpt_key),
+                id,
+                locator.clone(),
                 buffer_id,
                 buffer_snapshot.clone(),
                 range,
                 ranges.peek().is_some() || cursor.item().is_some(),
             );
             new_excerpts.push(excerpt, &());
-            prev_id = id.clone();
+            prev_locator = locator.clone();
+            new_excerpt_ids.push(ExcerptIdMapping { id, locator }, &());
             ids.push(id);
         }
-        self.used_excerpt_ids.edit(
-            ids.iter().cloned().map(sum_tree::Edit::Insert).collect(),
-            &(),
-        );
 
         let edit_end = new_excerpts.summary().text.len;
 
@@ -917,6 +919,7 @@ impl MultiBuffer {
         new_excerpts.push_tree(suffix, &());
         drop(cursor);
         snapshot.excerpts = new_excerpts;
+        snapshot.excerpt_ids = new_excerpt_ids;
         if changed_trailing_excerpt {
             snapshot.trailing_excerpt_update_count += 1;
         }
@@ -956,16 +959,16 @@ impl MultiBuffer {
         let mut excerpts = Vec::new();
         let snapshot = self.read(cx);
         let buffers = self.buffers.borrow();
-        let mut cursor = snapshot.excerpts.cursor::<Option<&ExcerptId>>();
-        for excerpt_id in buffers
+        let mut cursor = snapshot.excerpts.cursor::<Option<&Locator>>();
+        for locator in buffers
             .get(&buffer.id())
             .map(|state| &state.excerpts)
             .into_iter()
             .flatten()
         {
-            cursor.seek_forward(&Some(excerpt_id), Bias::Left, &());
+            cursor.seek_forward(&Some(locator), Bias::Left, &());
             if let Some(excerpt) = cursor.item() {
-                if excerpt.id == *excerpt_id {
+                if excerpt.locator == *locator {
                     excerpts.push((excerpt.id.clone(), excerpt.range.clone()));
                 }
             }
@@ -975,10 +978,11 @@ impl MultiBuffer {
     }
 
     pub fn excerpt_ids(&self) -> Vec<ExcerptId> {
-        self.buffers
+        self.snapshot
             .borrow()
-            .values()
-            .flat_map(|state| state.excerpts.iter().cloned())
+            .excerpts
+            .iter()
+            .map(|entry| entry.id)
             .collect()
     }
 
@@ -1061,32 +1065,34 @@ impl MultiBuffer {
         result
     }
 
-    pub fn remove_excerpts<'a>(
+    pub fn remove_excerpts(
         &mut self,
-        excerpt_ids: impl IntoIterator<Item = &'a ExcerptId>,
+        excerpt_ids: impl IntoIterator<Item = ExcerptId>,
         cx: &mut ModelContext<Self>,
     ) {
         self.sync(cx);
         let mut buffers = self.buffers.borrow_mut();
         let mut snapshot = self.snapshot.borrow_mut();
         let mut new_excerpts = SumTree::new();
-        let mut cursor = snapshot.excerpts.cursor::<(Option<&ExcerptId>, usize)>();
+        let mut cursor = snapshot.excerpts.cursor::<(Option<&Locator>, usize)>();
         let mut edits = Vec::new();
         let mut excerpt_ids = excerpt_ids.into_iter().peekable();
 
-        while let Some(mut excerpt_id) = excerpt_ids.next() {
+        while let Some(excerpt_id) = excerpt_ids.next() {
             // Seek to the next excerpt to remove, preserving any preceding excerpts.
-            new_excerpts.push_tree(cursor.slice(&Some(excerpt_id), Bias::Left, &()), &());
+            let locator = snapshot.excerpt_locator_for_id(excerpt_id);
+            new_excerpts.push_tree(cursor.slice(&Some(locator), Bias::Left, &()), &());
+
             if let Some(mut excerpt) = cursor.item() {
-                if excerpt.id != *excerpt_id {
+                if excerpt.id != excerpt_id {
                     continue;
                 }
                 let mut old_start = cursor.start().1;
 
                 // Skip over the removed excerpt.
-                loop {
+                'remove_excerpts: loop {
                     if let Some(buffer_state) = buffers.get_mut(&excerpt.buffer_id) {
-                        buffer_state.excerpts.retain(|id| id != excerpt_id);
+                        buffer_state.excerpts.retain(|l| l != &excerpt.locator);
                         if buffer_state.excerpts.is_empty() {
                             buffers.remove(&excerpt.buffer_id);
                         }
@@ -1094,14 +1100,16 @@ impl MultiBuffer {
                     cursor.next(&());
 
                     // Skip over any subsequent excerpts that are also removed.
-                    if let Some(&next_excerpt_id) = excerpt_ids.peek() {
+                    while let Some(&next_excerpt_id) = excerpt_ids.peek() {
+                        let next_locator = snapshot.excerpt_locator_for_id(next_excerpt_id);
                         if let Some(next_excerpt) = cursor.item() {
-                            if next_excerpt.id == *next_excerpt_id {
+                            if next_excerpt.locator == *next_locator {
+                                excerpt_ids.next();
                                 excerpt = next_excerpt;
-                                excerpt_id = excerpt_ids.next().unwrap();
-                                continue;
+                                continue 'remove_excerpts;
                             }
                         }
+                        break;
                     }
 
                     break;
@@ -1128,6 +1136,7 @@ impl MultiBuffer {
         new_excerpts.push_tree(suffix, &());
         drop(cursor);
         snapshot.excerpts = new_excerpts;
+
         if changed_trailing_excerpt {
             snapshot.trailing_excerpt_update_count += 1;
         }
@@ -1307,7 +1316,7 @@ impl MultiBuffer {
                     buffer_state
                         .excerpts
                         .iter()
-                        .map(|excerpt_id| (excerpt_id, buffer_state.buffer.clone(), buffer_edited)),
+                        .map(|locator| (locator, buffer_state.buffer.clone(), buffer_edited)),
                 );
             }
 
@@ -1333,14 +1342,14 @@ impl MultiBuffer {
         snapshot.is_dirty = is_dirty;
         snapshot.has_conflict = has_conflict;
 
-        excerpts_to_edit.sort_unstable_by_key(|(excerpt_id, _, _)| *excerpt_id);
+        excerpts_to_edit.sort_unstable_by_key(|(locator, _, _)| *locator);
 
         let mut edits = Vec::new();
         let mut new_excerpts = SumTree::new();
-        let mut cursor = snapshot.excerpts.cursor::<(Option<&ExcerptId>, usize)>();
+        let mut cursor = snapshot.excerpts.cursor::<(Option<&Locator>, usize)>();
 
-        for (id, buffer, buffer_edited) in excerpts_to_edit {
-            new_excerpts.push_tree(cursor.slice(&Some(id), Bias::Left, &()), &());
+        for (locator, buffer, buffer_edited) in excerpts_to_edit {
+            new_excerpts.push_tree(cursor.slice(&Some(locator), Bias::Left, &()), &());
             let old_excerpt = cursor.item().unwrap();
             let buffer_id = buffer.id();
             let buffer = buffer.read(cx);
@@ -1365,8 +1374,8 @@ impl MultiBuffer {
                 );
 
                 new_excerpt = Excerpt::new(
-                    id.clone(),
-                    old_excerpt.key,
+                    old_excerpt.id,
+                    locator.clone(),
                     buffer_id,
                     buffer.snapshot(),
                     old_excerpt.range.clone(),
@@ -1467,13 +1476,7 @@ impl MultiBuffer {
                 continue;
             }
 
-            let excerpt_ids = self
-                .buffers
-                .borrow()
-                .values()
-                .flat_map(|b| &b.excerpts)
-                .cloned()
-                .collect::<Vec<_>>();
+            let excerpt_ids = self.excerpt_ids();
             if excerpt_ids.is_empty() || (rng.gen() && excerpt_ids.len() < max_excerpts) {
                 let buffer_handle = if rng.gen() || self.buffers.borrow().is_empty() {
                     let text = RandomCharIter::new(&mut *rng).take(10).collect::<String>();
@@ -1511,24 +1514,26 @@ impl MultiBuffer {
                 log::info!(
                     "Inserting excerpts from buffer {} and ranges {:?}: {:?}",
                     buffer_handle.id(),
-                    ranges,
+                    ranges.iter().map(|r| &r.context).collect::<Vec<_>>(),
                     ranges
                         .iter()
-                        .map(|range| &buffer_text[range.context.clone()])
+                        .map(|r| &buffer_text[r.context.clone()])
                         .collect::<Vec<_>>()
                 );
 
                 let excerpt_id = self.push_excerpts(buffer_handle.clone(), ranges, cx);
-                log::info!("Inserted with id: {:?}", excerpt_id);
+                log::info!("Inserted with ids: {:?}", excerpt_id);
             } else {
                 let remove_count = rng.gen_range(1..=excerpt_ids.len());
                 let mut excerpts_to_remove = excerpt_ids
                     .choose_multiple(rng, remove_count)
                     .cloned()
                     .collect::<Vec<_>>();
-                excerpts_to_remove.sort();
+                let snapshot = self.snapshot.borrow();
+                excerpts_to_remove.sort_unstable_by(|a, b| a.cmp(b, &*snapshot));
+                drop(snapshot);
                 log::info!("Removing excerpts {:?}", excerpts_to_remove);
-                self.remove_excerpts(&excerpts_to_remove, cx);
+                self.remove_excerpts(excerpts_to_remove, cx);
             }
         }
     }
@@ -1563,6 +1568,38 @@ impl MultiBuffer {
         } else {
             self.randomly_edit_excerpts(rng, mutation_count, cx);
         }
+
+        self.check_invariants(cx);
+    }
+
+    fn check_invariants(&self, cx: &mut ModelContext<Self>) {
+        let snapshot = self.read(cx);
+        let excerpts = snapshot.excerpts.items(&());
+        let excerpt_ids = snapshot.excerpt_ids.items(&());
+
+        for (ix, excerpt) in excerpts.iter().enumerate() {
+            if ix == 0 {
+                if excerpt.locator <= Locator::min() {
+                    panic!("invalid first excerpt locator {:?}", excerpt.locator);
+                }
+            } else {
+                if excerpt.locator <= excerpts[ix - 1].locator {
+                    panic!("excerpts are out-of-order: {:?}", excerpts);
+                }
+            }
+        }
+
+        for (ix, entry) in excerpt_ids.iter().enumerate() {
+            if ix == 0 {
+                if entry.id.cmp(&ExcerptId::min(), &*snapshot).is_le() {
+                    panic!("invalid first excerpt id {:?}", entry.id);
+                }
+            } else {
+                if entry.id <= excerpt_ids[ix - 1].id {
+                    panic!("excerpt ids are out-of-order: {:?}", excerpt_ids);
+                }
+            }
+        }
     }
 }
 
@@ -1749,20 +1786,20 @@ impl MultiBufferSnapshot {
         *cursor.start() + overshoot
     }
 
-    pub fn clip_point_utf16(&self, point: PointUtf16, bias: Bias) -> PointUtf16 {
+    pub fn clip_point_utf16(&self, point: Unclipped<PointUtf16>, bias: Bias) -> PointUtf16 {
         if let Some((_, _, buffer)) = self.as_singleton() {
             return buffer.clip_point_utf16(point, bias);
         }
 
         let mut cursor = self.excerpts.cursor::<PointUtf16>();
-        cursor.seek(&point, Bias::Right, &());
+        cursor.seek(&point.0, Bias::Right, &());
         let overshoot = if let Some(excerpt) = cursor.item() {
             let excerpt_start = excerpt
                 .buffer
                 .offset_to_point_utf16(excerpt.range.context.start.to_offset(&excerpt.buffer));
             let buffer_point = excerpt
                 .buffer
-                .clip_point_utf16(excerpt_start + (point - cursor.start()), bias);
+                .clip_point_utf16(Unclipped(excerpt_start + (point.0 - cursor.start())), bias);
             buffer_point.saturating_sub(excerpt_start)
         } else {
             PointUtf16::zero()
@@ -2151,7 +2188,9 @@ impl MultiBufferSnapshot {
         D: TextDimension + Ord + Sub<D, Output = D>,
     {
         let mut cursor = self.excerpts.cursor::<ExcerptSummary>();
-        cursor.seek(&Some(&anchor.excerpt_id), Bias::Left, &());
+        let locator = self.excerpt_locator_for_id(anchor.excerpt_id);
+
+        cursor.seek(locator, Bias::Left, &());
         if cursor.item().is_none() {
             cursor.next(&());
         }
@@ -2189,24 +2228,25 @@ impl MultiBufferSnapshot {
         let mut cursor = self.excerpts.cursor::<ExcerptSummary>();
         let mut summaries = Vec::new();
         while let Some(anchor) = anchors.peek() {
-            let excerpt_id = &anchor.excerpt_id;
+            let excerpt_id = anchor.excerpt_id;
             let excerpt_anchors = iter::from_fn(|| {
                 let anchor = anchors.peek()?;
-                if anchor.excerpt_id == *excerpt_id {
+                if anchor.excerpt_id == excerpt_id {
                     Some(&anchors.next().unwrap().text_anchor)
                 } else {
                     None
                 }
             });
 
-            cursor.seek_forward(&Some(excerpt_id), Bias::Left, &());
+            let locator = self.excerpt_locator_for_id(excerpt_id);
+            cursor.seek_forward(locator, Bias::Left, &());
             if cursor.item().is_none() {
                 cursor.next(&());
             }
 
             let position = D::from_text_summary(&cursor.start().text);
             if let Some(excerpt) = cursor.item() {
-                if excerpt.id == *excerpt_id {
+                if excerpt.id == excerpt_id {
                     let excerpt_buffer_start =
                         excerpt.range.context.start.summary::<D>(&excerpt.buffer);
                     let excerpt_buffer_end =
@@ -2240,13 +2280,18 @@ impl MultiBufferSnapshot {
         I: 'a + IntoIterator<Item = &'a Anchor>,
     {
         let mut anchors = anchors.into_iter().enumerate().peekable();
-        let mut cursor = self.excerpts.cursor::<Option<&ExcerptId>>();
+        let mut cursor = self.excerpts.cursor::<Option<&Locator>>();
+        cursor.next(&());
+
         let mut result = Vec::new();
+
         while let Some((_, anchor)) = anchors.peek() {
-            let old_excerpt_id = &anchor.excerpt_id;
+            let old_excerpt_id = anchor.excerpt_id;
 
             // Find the location where this anchor's excerpt should be.
-            cursor.seek_forward(&Some(old_excerpt_id), Bias::Left, &());
+            let old_locator = self.excerpt_locator_for_id(old_excerpt_id);
+            cursor.seek_forward(&Some(old_locator), Bias::Left, &());
+
             if cursor.item().is_none() {
                 cursor.next(&());
             }
@@ -2256,27 +2301,22 @@ impl MultiBufferSnapshot {
 
             // Process all of the anchors for this excerpt.
             while let Some((_, anchor)) = anchors.peek() {
-                if anchor.excerpt_id != *old_excerpt_id {
+                if anchor.excerpt_id != old_excerpt_id {
                     break;
                 }
-                let mut kept_position = false;
                 let (anchor_ix, anchor) = anchors.next().unwrap();
-                let mut anchor = anchor.clone();
-
-                let id_invalid =
-                    *old_excerpt_id == ExcerptId::max() || *old_excerpt_id == ExcerptId::min();
-                let still_exists = next_excerpt.map_or(false, |excerpt| {
-                    excerpt.id == *old_excerpt_id && excerpt.contains(&anchor)
-                });
+                let mut anchor = *anchor;
 
                 // Leave min and max anchors unchanged if invalid or
                 // if the old excerpt still exists at this location
-                if id_invalid || still_exists {
-                    kept_position = true;
-                }
+                let mut kept_position = next_excerpt
+                    .map_or(false, |e| e.id == old_excerpt_id && e.contains(&anchor))
+                    || old_excerpt_id == ExcerptId::max()
+                    || old_excerpt_id == ExcerptId::min();
+
                 // If the old excerpt no longer exists at this location, then attempt to
                 // find an equivalent position for this anchor in an adjacent excerpt.
-                else {
+                if !kept_position {
                     for excerpt in [next_excerpt, prev_excerpt].iter().filter_map(|e| *e) {
                         if excerpt.contains(&anchor) {
                             anchor.excerpt_id = excerpt.id.clone();
@@ -2285,6 +2325,7 @@ impl MultiBufferSnapshot {
                         }
                     }
                 }
+
                 // If there's no adjacent excerpt that contains the anchor's position,
                 // then report that the anchor has lost its position.
                 if !kept_position {
@@ -2354,7 +2395,7 @@ impl MultiBufferSnapshot {
             };
         }
 
-        let mut cursor = self.excerpts.cursor::<(usize, Option<&ExcerptId>)>();
+        let mut cursor = self.excerpts.cursor::<(usize, Option<ExcerptId>)>();
         cursor.seek(&offset, Bias::Right, &());
         if cursor.item().is_none() && offset == cursor.start().0 && bias == Bias::Left {
             cursor.prev(&());
@@ -2382,8 +2423,9 @@ impl MultiBufferSnapshot {
     }
 
     pub fn anchor_in_excerpt(&self, excerpt_id: ExcerptId, text_anchor: text::Anchor) -> Anchor {
-        let mut cursor = self.excerpts.cursor::<Option<&ExcerptId>>();
-        cursor.seek(&Some(&excerpt_id), Bias::Left, &());
+        let locator = self.excerpt_locator_for_id(excerpt_id);
+        let mut cursor = self.excerpts.cursor::<Option<&Locator>>();
+        cursor.seek(locator, Bias::Left, &());
         if let Some(excerpt) = cursor.item() {
             if excerpt.id == excerpt_id {
                 let text_anchor = excerpt.clip_anchor(text_anchor);
@@ -2401,7 +2443,7 @@ impl MultiBufferSnapshot {
     pub fn can_resolve(&self, anchor: &Anchor) -> bool {
         if anchor.excerpt_id == ExcerptId::min() || anchor.excerpt_id == ExcerptId::max() {
             true
-        } else if let Some(excerpt) = self.excerpt(&anchor.excerpt_id) {
+        } else if let Some(excerpt) = self.excerpt(anchor.excerpt_id) {
             excerpt.buffer.can_resolve(&anchor.text_anchor)
         } else {
             false
@@ -2456,7 +2498,6 @@ impl MultiBufferSnapshot {
                 let starts_new_buffer = Some(excerpt.buffer_id) != prev_buffer_id;
                 let boundary = ExcerptBoundary {
                     id: excerpt.id.clone(),
-                    key: excerpt.key,
                     row: cursor.start().1.row,
                     buffer: excerpt.buffer.clone(),
                     range: excerpt.range.clone(),
@@ -2678,8 +2719,8 @@ impl MultiBufferSnapshot {
                 .flatten()
                 .map(|item| OutlineItem {
                     depth: item.depth,
-                    range: self.anchor_in_excerpt(excerpt_id.clone(), item.range.start)
-                        ..self.anchor_in_excerpt(excerpt_id.clone(), item.range.end),
+                    range: self.anchor_in_excerpt(excerpt_id, item.range.start)
+                        ..self.anchor_in_excerpt(excerpt_id, item.range.end),
                     text: item.text,
                     highlight_ranges: item.highlight_ranges,
                     name_ranges: item.name_ranges,
@@ -2688,11 +2729,29 @@ impl MultiBufferSnapshot {
         ))
     }
 
-    fn excerpt<'a>(&'a self, excerpt_id: &'a ExcerptId) -> Option<&'a Excerpt> {
-        let mut cursor = self.excerpts.cursor::<Option<&ExcerptId>>();
-        cursor.seek(&Some(excerpt_id), Bias::Left, &());
+    fn excerpt_locator_for_id<'a>(&'a self, id: ExcerptId) -> &'a Locator {
+        if id == ExcerptId::min() {
+            Locator::min_ref()
+        } else if id == ExcerptId::max() {
+            Locator::max_ref()
+        } else {
+            let mut cursor = self.excerpt_ids.cursor::<ExcerptId>();
+            cursor.seek(&id, Bias::Left, &());
+            if let Some(entry) = cursor.item() {
+                if entry.id == id {
+                    return &entry.locator;
+                }
+            }
+            panic!("invalid excerpt id {:?}", id)
+        }
+    }
+
+    fn excerpt<'a>(&'a self, excerpt_id: ExcerptId) -> Option<&'a Excerpt> {
+        let mut cursor = self.excerpts.cursor::<Option<&Locator>>();
+        let locator = self.excerpt_locator_for_id(excerpt_id);
+        cursor.seek(&Some(locator), Bias::Left, &());
         if let Some(excerpt) = cursor.item() {
-            if excerpt.id == *excerpt_id {
+            if excerpt.id == excerpt_id {
                 return Some(excerpt);
             }
         }
@@ -2703,10 +2762,12 @@ impl MultiBufferSnapshot {
         &'a self,
         range: &'a Range<Anchor>,
     ) -> impl 'a + Iterator<Item = (ReplicaId, bool, CursorShape, Selection<Anchor>)> {
-        let mut cursor = self.excerpts.cursor::<Option<&ExcerptId>>();
-        cursor.seek(&Some(&range.start.excerpt_id), Bias::Left, &());
+        let mut cursor = self.excerpts.cursor::<ExcerptSummary>();
+        let start_locator = self.excerpt_locator_for_id(range.start.excerpt_id);
+        let end_locator = self.excerpt_locator_for_id(range.end.excerpt_id);
+        cursor.seek(start_locator, Bias::Left, &());
         cursor
-            .take_while(move |excerpt| excerpt.id <= range.end.excerpt_id)
+            .take_while(move |excerpt| excerpt.locator <= *end_locator)
             .flat_map(move |excerpt| {
                 let mut query_range = excerpt.range.context.start..excerpt.range.context.end;
                 if excerpt.id == range.start.excerpt_id {
@@ -2916,7 +2977,7 @@ impl History {
 impl Excerpt {
     fn new(
         id: ExcerptId,
-        key: usize,
+        locator: Locator,
         buffer_id: usize,
         buffer: BufferSnapshot,
         range: ExcerptRange<text::Anchor>,
@@ -2924,7 +2985,7 @@ impl Excerpt {
     ) -> Self {
         Excerpt {
             id,
-            key,
+            locator,
             max_buffer_row: range.context.end.to_point(&buffer).row,
             text_summary: buffer
                 .text_summary_for_range::<TextSummary, _>(range.context.to_offset(&buffer)),
@@ -3010,10 +3071,33 @@ impl Excerpt {
     }
 }
 
+impl ExcerptId {
+    pub fn min() -> Self {
+        Self(0)
+    }
+
+    pub fn max() -> Self {
+        Self(usize::MAX)
+    }
+
+    pub fn cmp(&self, other: &Self, snapshot: &MultiBufferSnapshot) -> cmp::Ordering {
+        let a = snapshot.excerpt_locator_for_id(*self);
+        let b = snapshot.excerpt_locator_for_id(*other);
+        a.cmp(&b).then_with(|| self.0.cmp(&other.0))
+    }
+}
+
+impl Into<usize> for ExcerptId {
+    fn into(self) -> usize {
+        self.0
+    }
+}
+
 impl fmt::Debug for Excerpt {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         f.debug_struct("Excerpt")
             .field("id", &self.id)
+            .field("locator", &self.locator)
             .field("buffer_id", &self.buffer_id)
             .field("range", &self.range)
             .field("text_summary", &self.text_summary)
@@ -3031,19 +3115,44 @@ impl sum_tree::Item for Excerpt {
             text += TextSummary::from("\n");
         }
         ExcerptSummary {
-            excerpt_id: self.id.clone(),
+            excerpt_id: self.id,
+            excerpt_locator: self.locator.clone(),
             max_buffer_row: self.max_buffer_row,
             text,
         }
     }
 }
 
+impl sum_tree::Item for ExcerptIdMapping {
+    type Summary = ExcerptId;
+
+    fn summary(&self) -> Self::Summary {
+        self.id
+    }
+}
+
+impl sum_tree::KeyedItem for ExcerptIdMapping {
+    type Key = ExcerptId;
+
+    fn key(&self) -> Self::Key {
+        self.id
+    }
+}
+
+impl sum_tree::Summary for ExcerptId {
+    type Context = ();
+
+    fn add_summary(&mut self, other: &Self, _: &()) {
+        *self = *other;
+    }
+}
+
 impl sum_tree::Summary for ExcerptSummary {
     type Context = ();
 
     fn add_summary(&mut self, summary: &Self, _: &()) {
-        debug_assert!(summary.excerpt_id > self.excerpt_id);
-        self.excerpt_id = summary.excerpt_id.clone();
+        debug_assert!(summary.excerpt_locator > self.excerpt_locator);
+        self.excerpt_locator = summary.excerpt_locator.clone();
         self.text.add_summary(&summary.text, &());
         self.max_buffer_row = cmp::max(self.max_buffer_row, summary.max_buffer_row);
     }
@@ -3067,9 +3176,15 @@ impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, ExcerptSummary> for usize {
     }
 }
 
-impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, ExcerptSummary> for Option<&'a ExcerptId> {
+impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, Option<&'a Locator>> for Locator {
+    fn cmp(&self, cursor_location: &Option<&'a Locator>, _: &()) -> cmp::Ordering {
+        Ord::cmp(&Some(self), cursor_location)
+    }
+}
+
+impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, ExcerptSummary> for Locator {
     fn cmp(&self, cursor_location: &ExcerptSummary, _: &()) -> cmp::Ordering {
-        Ord::cmp(self, &Some(&cursor_location.excerpt_id))
+        Ord::cmp(self, &cursor_location.excerpt_locator)
     }
 }
 
@@ -3091,9 +3206,15 @@ impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for PointUtf16 {
     }
 }
 
-impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for Option<&'a ExcerptId> {
+impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for Option<&'a Locator> {
+    fn add_summary(&mut self, summary: &'a ExcerptSummary, _: &()) {
+        *self = Some(&summary.excerpt_locator);
+    }
+}
+
+impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for Option<ExcerptId> {
     fn add_summary(&mut self, summary: &'a ExcerptSummary, _: &()) {
-        *self = Some(&summary.excerpt_id);
+        *self = Some(summary.excerpt_id);
     }
 }
 
@@ -3274,12 +3395,6 @@ impl ToOffset for Point {
     }
 }
 
-impl ToOffset for PointUtf16 {
-    fn to_offset<'a>(&self, snapshot: &MultiBufferSnapshot) -> usize {
-        snapshot.point_utf16_to_offset(*self)
-    }
-}
-
 impl ToOffset for usize {
     fn to_offset<'a>(&self, snapshot: &MultiBufferSnapshot) -> usize {
         assert!(*self <= snapshot.len(), "offset is out of range");
@@ -3293,6 +3408,12 @@ impl ToOffset for OffsetUtf16 {
     }
 }
 
+impl ToOffset for PointUtf16 {
+    fn to_offset<'a>(&self, snapshot: &MultiBufferSnapshot) -> usize {
+        snapshot.point_utf16_to_offset(*self)
+    }
+}
+
 impl ToOffsetUtf16 for OffsetUtf16 {
     fn to_offset_utf16(&self, _snapshot: &MultiBufferSnapshot) -> OffsetUtf16 {
         *self
@@ -3591,7 +3712,7 @@ mod tests {
         let snapshot = multibuffer.update(cx, |multibuffer, cx| {
             let (buffer_2_excerpt_id, _) =
                 multibuffer.excerpts_for_buffer(&buffer_2, cx)[0].clone();
-            multibuffer.remove_excerpts(&[buffer_2_excerpt_id], cx);
+            multibuffer.remove_excerpts([buffer_2_excerpt_id], cx);
             multibuffer.snapshot(cx)
         });
 
@@ -3780,7 +3901,7 @@ mod tests {
 
         // Replace the buffer 1 excerpt with new excerpts from buffer 2.
         let (excerpt_id_2, excerpt_id_3) = multibuffer.update(cx, |multibuffer, cx| {
-            multibuffer.remove_excerpts([&excerpt_id_1], cx);
+            multibuffer.remove_excerpts([excerpt_id_1], cx);
             let mut ids = multibuffer
                 .push_excerpts(
                     buffer_2.clone(),
@@ -3810,9 +3931,8 @@ mod tests {
         assert_ne!(excerpt_id_2, excerpt_id_1);
 
         // Resolve some anchors from the previous snapshot in the new snapshot.
-        // Although there is still an excerpt with the same id, it is for
-        // a different buffer, so we don't attempt to resolve the old text
-        // anchor in the new buffer.
+        // The current excerpts are from a different buffer, so we don't attempt to
+        // resolve the old text anchor in the new buffer.
         assert_eq!(
             snapshot_2.summary_for_anchor::<usize>(&snapshot_1.anchor_before(2)),
             0
@@ -3824,6 +3944,9 @@ mod tests {
             ]),
             vec![0, 0]
         );
+
+        // Refresh anchors from the old snapshot. The return value indicates that both
+        // anchors lost their original excerpt.
         let refresh =
             snapshot_2.refresh_anchors(&[snapshot_1.anchor_before(2), snapshot_1.anchor_after(3)]);
         assert_eq!(
@@ -3837,10 +3960,10 @@ mod tests {
         // Replace the middle excerpt with a smaller excerpt in buffer 2,
         // that intersects the old excerpt.
         let excerpt_id_5 = multibuffer.update(cx, |multibuffer, cx| {
-            multibuffer.remove_excerpts([&excerpt_id_3], cx);
+            multibuffer.remove_excerpts([excerpt_id_3], cx);
             multibuffer
                 .insert_excerpts_after(
-                    &excerpt_id_3,
+                    excerpt_id_2,
                     buffer_2.clone(),
                     [ExcerptRange {
                         context: 5..8,
@@ -3857,8 +3980,8 @@ mod tests {
         assert_ne!(excerpt_id_5, excerpt_id_3);
 
         // Resolve some anchors from the previous snapshot in the new snapshot.
-        // The anchor in the middle excerpt snaps to the beginning of the
-        // excerpt, since it is not
+        // The third anchor can't be resolved, since its excerpt has been removed,
+        // so it resolves to the same position as its predecessor.
         let anchors = [
             snapshot_2.anchor_before(0),
             snapshot_2.anchor_after(2),
@@ -3867,7 +3990,7 @@ mod tests {
         ];
         assert_eq!(
             snapshot_3.summaries_for_anchors::<usize, _>(&anchors),
-            &[0, 2, 5, 13]
+            &[0, 2, 9, 13]
         );
 
         let new_anchors = snapshot_3.refresh_anchors(&anchors);
@@ -3889,7 +4012,7 @@ mod tests {
 
         let mut buffers: Vec<ModelHandle<Buffer>> = Vec::new();
         let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
-        let mut excerpt_ids = Vec::new();
+        let mut excerpt_ids = Vec::<ExcerptId>::new();
         let mut expected_excerpts = Vec::<(ModelHandle<Buffer>, Range<text::Anchor>)>::new();
         let mut anchors = Vec::new();
         let mut old_versions = Vec::new();
@@ -3919,9 +4042,11 @@ mod tests {
                                 .collect::<String>(),
                         );
                     }
-                    ids_to_remove.sort_unstable();
+                    let snapshot = multibuffer.read(cx).read(cx);
+                    ids_to_remove.sort_unstable_by(|a, b| a.cmp(&b, &snapshot));
+                    drop(snapshot);
                     multibuffer.update(cx, |multibuffer, cx| {
-                        multibuffer.remove_excerpts(&ids_to_remove, cx)
+                        multibuffer.remove_excerpts(ids_to_remove, cx)
                     });
                 }
                 30..=39 if !expected_excerpts.is_empty() => {
@@ -3945,7 +4070,6 @@ mod tests {
                     // Ensure the newly-refreshed anchors point to a valid excerpt and don't
                     // overshoot its boundaries.
                     assert_eq!(anchors.len(), prev_len);
-                    let mut cursor = multibuffer.excerpts.cursor::<Option<&ExcerptId>>();
                     for anchor in &anchors {
                         if anchor.excerpt_id == ExcerptId::min()
                             || anchor.excerpt_id == ExcerptId::max()
@@ -3953,8 +4077,7 @@ mod tests {
                             continue;
                         }
 
-                        cursor.seek_forward(&Some(&anchor.excerpt_id), Bias::Left, &());
-                        let excerpt = cursor.item().unwrap();
+                        let excerpt = multibuffer.excerpt(anchor.excerpt_id).unwrap();
                         assert_eq!(excerpt.id, anchor.excerpt_id);
                         assert!(excerpt.contains(anchor));
                     }
@@ -3994,7 +4117,7 @@ mod tests {
                     let excerpt_id = multibuffer.update(cx, |multibuffer, cx| {
                         multibuffer
                             .insert_excerpts_after(
-                                &prev_excerpt_id,
+                                prev_excerpt_id,
                                 buffer_handle.clone(),
                                 [ExcerptRange {
                                     context: start_ix..end_ix,
@@ -4158,12 +4281,14 @@ mod tests {
                     }
 
                     for _ in 0..ch.len_utf16() {
-                        let left_point_utf16 = snapshot.clip_point_utf16(point_utf16, Bias::Left);
-                        let right_point_utf16 = snapshot.clip_point_utf16(point_utf16, Bias::Right);
+                        let left_point_utf16 =
+                            snapshot.clip_point_utf16(Unclipped(point_utf16), Bias::Left);
+                        let right_point_utf16 =
+                            snapshot.clip_point_utf16(Unclipped(point_utf16), Bias::Right);
                         let buffer_left_point_utf16 =
-                            buffer.clip_point_utf16(buffer_point_utf16, Bias::Left);
+                            buffer.clip_point_utf16(Unclipped(buffer_point_utf16), Bias::Left);
                         let buffer_right_point_utf16 =
-                            buffer.clip_point_utf16(buffer_point_utf16, Bias::Right);
+                            buffer.clip_point_utf16(Unclipped(buffer_point_utf16), Bias::Right);
                         assert_eq!(
                             left_point_utf16,
                             excerpt_start.lines_utf16()

crates/editor/src/multi_buffer/anchor.rs 🔗

@@ -6,7 +6,7 @@ use std::{
 };
 use sum_tree::Bias;
 
-#[derive(Clone, Eq, PartialEq, Debug, Hash)]
+#[derive(Clone, Copy, Eq, PartialEq, Debug, Hash)]
 pub struct Anchor {
     pub(crate) buffer_id: Option<usize>,
     pub(crate) excerpt_id: ExcerptId,
@@ -30,16 +30,16 @@ impl Anchor {
         }
     }
 
-    pub fn excerpt_id(&self) -> &ExcerptId {
-        &self.excerpt_id
+    pub fn excerpt_id(&self) -> ExcerptId {
+        self.excerpt_id
     }
 
     pub fn cmp(&self, other: &Anchor, snapshot: &MultiBufferSnapshot) -> Ordering {
-        let excerpt_id_cmp = self.excerpt_id.cmp(&other.excerpt_id);
+        let excerpt_id_cmp = self.excerpt_id.cmp(&other.excerpt_id, snapshot);
         if excerpt_id_cmp.is_eq() {
             if self.excerpt_id == ExcerptId::min() || self.excerpt_id == ExcerptId::max() {
                 Ordering::Equal
-            } else if let Some(excerpt) = snapshot.excerpt(&self.excerpt_id) {
+            } else if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) {
                 self.text_anchor.cmp(&other.text_anchor, &excerpt.buffer)
             } else {
                 Ordering::Equal
@@ -51,7 +51,7 @@ impl Anchor {
 
     pub fn bias_left(&self, snapshot: &MultiBufferSnapshot) -> Anchor {
         if self.text_anchor.bias != Bias::Left {
-            if let Some(excerpt) = snapshot.excerpt(&self.excerpt_id) {
+            if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) {
                 return Self {
                     buffer_id: self.buffer_id,
                     excerpt_id: self.excerpt_id.clone(),
@@ -64,7 +64,7 @@ impl Anchor {
 
     pub fn bias_right(&self, snapshot: &MultiBufferSnapshot) -> Anchor {
         if self.text_anchor.bias != Bias::Right {
-            if let Some(excerpt) = snapshot.excerpt(&self.excerpt_id) {
+            if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) {
                 return Self {
                     buffer_id: self.buffer_id,
                     excerpt_id: self.excerpt_id.clone(),

crates/editor/src/selections_collection.rs 🔗

@@ -544,11 +544,21 @@ impl<'a> MutableSelectionsCollection<'a> {
         T: ToOffset,
     {
         let buffer = self.buffer.read(self.cx).snapshot(self.cx);
+        let ranges = ranges
+            .into_iter()
+            .map(|range| range.start.to_offset(&buffer)..range.end.to_offset(&buffer));
+        self.select_offset_ranges(ranges);
+    }
+
+    fn select_offset_ranges<I>(&mut self, ranges: I)
+    where
+        I: IntoIterator<Item = Range<usize>>,
+    {
         let selections = ranges
             .into_iter()
             .map(|range| {
-                let mut start = range.start.to_offset(&buffer);
-                let mut end = range.end.to_offset(&buffer);
+                let mut start = range.start;
+                let mut end = range.end;
                 let reversed = if start > end {
                     mem::swap(&mut start, &mut end);
                     true

crates/gpui/src/elements/flex.rs 🔗

@@ -257,17 +257,19 @@ impl Element for Flex {
                         let axis = self.axis;
                         move |e, cx| {
                             if remaining_space < 0. {
+                                let scroll_delta = e.delta.raw();
+
                                 let mut delta = match axis {
                                     Axis::Horizontal => {
-                                        if e.delta.x().abs() >= e.delta.y().abs() {
-                                            e.delta.x()
+                                        if scroll_delta.x().abs() >= scroll_delta.y().abs() {
+                                            scroll_delta.x()
                                         } else {
-                                            e.delta.y()
+                                            scroll_delta.y()
                                         }
                                     }
-                                    Axis::Vertical => e.delta.y(),
+                                    Axis::Vertical => scroll_delta.y(),
                                 };
-                                if !e.precise {
+                                if !e.delta.precise() {
                                     delta *= 20.;
                                 }
 

crates/gpui/src/elements/list.rs 🔗

@@ -258,8 +258,8 @@ impl Element for List {
                     state.0.borrow_mut().scroll(
                         &scroll_top,
                         height,
-                        e.platform_event.delta,
-                        e.platform_event.precise,
+                        *e.platform_event.delta.raw(),
+                        e.platform_event.delta.precise(),
                         cx,
                     )
                 }

crates/gpui/src/elements/uniform_list.rs 🔗

@@ -295,15 +295,19 @@ impl Element for UniformList {
                 move |MouseScrollWheel {
                           platform_event:
                               ScrollWheelEvent {
-                                  position,
-                                  delta,
-                                  precise,
-                                  ..
+                                  position, delta, ..
                               },
                           ..
                       },
                       cx| {
-                    if !Self::scroll(state.clone(), position, delta, precise, scroll_max, cx) {
+                    if !Self::scroll(
+                        state.clone(),
+                        position,
+                        *delta.raw(),
+                        delta.precise(),
+                        scroll_max,
+                        cx,
+                    ) {
                         cx.propagate_event();
                     }
                 }

crates/gpui/src/platform/event.rs 🔗

@@ -1,5 +1,7 @@
 use std::ops::Deref;
 
+use pathfinder_geometry::vector::vec2f;
+
 use crate::{geometry::vector::Vector2F, keymap::Keystroke};
 
 #[derive(Clone, Debug)]
@@ -44,11 +46,45 @@ pub enum TouchPhase {
     Ended,
 }
 
+#[derive(Clone, Copy, Debug)]
+pub enum ScrollDelta {
+    Pixels(Vector2F),
+    Lines(Vector2F),
+}
+
+impl Default for ScrollDelta {
+    fn default() -> Self {
+        Self::Lines(Default::default())
+    }
+}
+
+impl ScrollDelta {
+    pub fn raw(&self) -> &Vector2F {
+        match self {
+            ScrollDelta::Pixels(v) => v,
+            ScrollDelta::Lines(v) => v,
+        }
+    }
+
+    pub fn precise(&self) -> bool {
+        match self {
+            ScrollDelta::Pixels(_) => true,
+            ScrollDelta::Lines(_) => false,
+        }
+    }
+
+    pub fn pixel_delta(&self, line_height: f32) -> Vector2F {
+        match self {
+            ScrollDelta::Pixels(delta) => *delta,
+            ScrollDelta::Lines(delta) => vec2f(delta.x() * line_height, delta.y() * line_height),
+        }
+    }
+}
+
 #[derive(Clone, Copy, Debug, Default)]
 pub struct ScrollWheelEvent {
     pub position: Vector2F,
-    pub delta: Vector2F,
-    pub precise: bool,
+    pub delta: ScrollDelta,
     pub modifiers: Modifiers,
     /// If the platform supports returning the phase of a scroll wheel event, it will be stored here
     pub phase: Option<TouchPhase>,

crates/gpui/src/platform/mac/event.rs 🔗

@@ -3,7 +3,7 @@ use crate::{
     keymap::Keystroke,
     platform::{Event, NavigationDirection},
     KeyDownEvent, KeyUpEvent, Modifiers, ModifiersChangedEvent, MouseButton, MouseButtonEvent,
-    MouseMovedEvent, ScrollWheelEvent, TouchPhase,
+    MouseMovedEvent, ScrollDelta, ScrollWheelEvent, TouchPhase,
 };
 use cocoa::{
     appkit::{NSEvent, NSEventModifierFlags, NSEventPhase, NSEventType},
@@ -164,17 +164,24 @@ impl Event {
                     _ => Some(TouchPhase::Moved),
                 };
 
+                let raw_data = vec2f(
+                    native_event.scrollingDeltaX() as f32,
+                    native_event.scrollingDeltaY() as f32,
+                );
+
+                let delta = if native_event.hasPreciseScrollingDeltas() == YES {
+                    ScrollDelta::Pixels(raw_data)
+                } else {
+                    ScrollDelta::Lines(raw_data)
+                };
+
                 Self::ScrollWheel(ScrollWheelEvent {
                     position: vec2f(
                         native_event.locationInWindow().x as f32,
                         window_height - native_event.locationInWindow().y as f32,
                     ),
-                    delta: vec2f(
-                        native_event.scrollingDeltaX() as f32,
-                        native_event.scrollingDeltaY() as f32,
-                    ),
+                    delta,
                     phase,
-                    precise: native_event.hasPreciseScrollingDeltas() == YES,
                     modifiers: read_modifiers(native_event),
                 })
             }),

crates/gpui/src/presenter.rs 🔗

@@ -475,27 +475,35 @@ impl Presenter {
                 if let MouseEvent::Down(e) = &mouse_event {
                     if valid_region
                         .handlers
-                        .contains_handler(MouseEvent::click_disc(), Some(e.button))
+                        .contains(MouseEvent::click_disc(), Some(e.button))
                         || valid_region
                             .handlers
-                            .contains_handler(MouseEvent::drag_disc(), Some(e.button))
+                            .contains(MouseEvent::drag_disc(), Some(e.button))
                     {
                         event_cx.handled = true;
                     }
                 }
 
-                if let Some(callback) = valid_region.handlers.get(&mouse_event.handler_key()) {
-                    event_cx.handled = true;
-                    event_cx.with_current_view(valid_region.id().view_id(), {
-                        let region_event = mouse_event.clone();
-                        |cx| callback(region_event, cx)
-                    });
+                // `event_consumed` should only be true if there are any handlers for this event.
+                let mut event_consumed = event_cx.handled;
+                if let Some(callbacks) = valid_region.handlers.get(&mouse_event.handler_key()) {
+                    event_consumed = true;
+                    for callback in callbacks {
+                        event_cx.handled = true;
+                        event_cx.with_current_view(valid_region.id().view_id(), {
+                            let region_event = mouse_event.clone();
+                            |cx| callback(region_event, cx)
+                        });
+                        event_consumed &= event_cx.handled;
+                        any_event_handled |= event_cx.handled;
+                    }
                 }
 
-                any_event_handled = any_event_handled || event_cx.handled;
-                // For bubbling events, if the event was handled, don't continue dispatching
-                // This only makes sense for local events.
-                if event_cx.handled && mouse_event.is_capturable() {
+                any_event_handled |= event_cx.handled;
+
+                // For bubbling events, if the event was handled, don't continue dispatching.
+                // This only makes sense for local events which return false from is_capturable.
+                if event_consumed && mouse_event.is_capturable() {
                     break;
                 }
             }

crates/gpui/src/scene/mouse_event.rs 🔗

@@ -5,7 +5,7 @@ use std::{
 
 use pathfinder_geometry::{rect::RectF, vector::Vector2F};
 
-use crate::{MouseButton, MouseButtonEvent, MouseMovedEvent, ScrollWheelEvent};
+use crate::{scene::mouse_region::HandlerKey, MouseButtonEvent, MouseMovedEvent, ScrollWheelEvent};
 
 #[derive(Debug, Default, Clone)]
 pub struct MouseMove {
@@ -217,17 +217,17 @@ impl MouseEvent {
         discriminant(&MouseEvent::ScrollWheel(Default::default()))
     }
 
-    pub fn handler_key(&self) -> (Discriminant<MouseEvent>, Option<MouseButton>) {
+    pub fn handler_key(&self) -> HandlerKey {
         match self {
-            MouseEvent::Move(_) => (Self::move_disc(), None),
-            MouseEvent::Drag(e) => (Self::drag_disc(), e.pressed_button),
-            MouseEvent::Hover(_) => (Self::hover_disc(), None),
-            MouseEvent::Down(e) => (Self::down_disc(), Some(e.button)),
-            MouseEvent::Up(e) => (Self::up_disc(), Some(e.button)),
-            MouseEvent::Click(e) => (Self::click_disc(), Some(e.button)),
-            MouseEvent::UpOut(e) => (Self::up_out_disc(), Some(e.button)),
-            MouseEvent::DownOut(e) => (Self::down_out_disc(), Some(e.button)),
-            MouseEvent::ScrollWheel(_) => (Self::scroll_wheel_disc(), None),
+            MouseEvent::Move(_) => HandlerKey::new(Self::move_disc(), None),
+            MouseEvent::Drag(e) => HandlerKey::new(Self::drag_disc(), e.pressed_button),
+            MouseEvent::Hover(_) => HandlerKey::new(Self::hover_disc(), None),
+            MouseEvent::Down(e) => HandlerKey::new(Self::down_disc(), Some(e.button)),
+            MouseEvent::Up(e) => HandlerKey::new(Self::up_disc(), Some(e.button)),
+            MouseEvent::Click(e) => HandlerKey::new(Self::click_disc(), Some(e.button)),
+            MouseEvent::UpOut(e) => HandlerKey::new(Self::up_out_disc(), Some(e.button)),
+            MouseEvent::DownOut(e) => HandlerKey::new(Self::down_out_disc(), Some(e.button)),
+            MouseEvent::ScrollWheel(_) => HandlerKey::new(Self::scroll_wheel_disc(), None),
         }
     }
 }

crates/gpui/src/scene/mouse_region.rs 🔗

@@ -3,6 +3,7 @@ use std::{any::TypeId, fmt::Debug, mem::Discriminant, rc::Rc};
 use collections::HashMap;
 
 use pathfinder_geometry::rect::RectF;
+use smallvec::SmallVec;
 
 use crate::{EventContext, MouseButton};
 
@@ -177,61 +178,105 @@ impl MouseRegionId {
     }
 }
 
+pub type HandlerCallback = Rc<dyn Fn(MouseEvent, &mut EventContext)>;
+
+#[derive(Clone, PartialEq, Eq, Hash)]
+pub struct HandlerKey {
+    event_kind: Discriminant<MouseEvent>,
+    button: Option<MouseButton>,
+}
+
+impl HandlerKey {
+    pub fn new(event_kind: Discriminant<MouseEvent>, button: Option<MouseButton>) -> HandlerKey {
+        HandlerKey { event_kind, button }
+    }
+}
+
 #[derive(Clone, Default)]
 pub struct HandlerSet {
-    #[allow(clippy::type_complexity)]
-    pub set: HashMap<
-        (Discriminant<MouseEvent>, Option<MouseButton>),
-        Rc<dyn Fn(MouseEvent, &mut EventContext)>,
-    >,
+    set: HashMap<HandlerKey, SmallVec<[HandlerCallback; 1]>>,
 }
 
 impl HandlerSet {
     pub fn capture_all() -> Self {
-        #[allow(clippy::type_complexity)]
-        let mut set: HashMap<
-            (Discriminant<MouseEvent>, Option<MouseButton>),
-            Rc<dyn Fn(MouseEvent, &mut EventContext)>,
-        > = Default::default();
-
-        set.insert((MouseEvent::move_disc(), None), Rc::new(|_, _| {}));
-        set.insert((MouseEvent::hover_disc(), None), Rc::new(|_, _| {}));
+        let mut set: HashMap<HandlerKey, SmallVec<[HandlerCallback; 1]>> = HashMap::default();
+
+        set.insert(
+            HandlerKey::new(MouseEvent::move_disc(), None),
+            SmallVec::from_buf([Rc::new(|_, _| {})]),
+        );
+        set.insert(
+            HandlerKey::new(MouseEvent::hover_disc(), None),
+            SmallVec::from_buf([Rc::new(|_, _| {})]),
+        );
         for button in MouseButton::all() {
-            set.insert((MouseEvent::drag_disc(), Some(button)), Rc::new(|_, _| {}));
-            set.insert((MouseEvent::down_disc(), Some(button)), Rc::new(|_, _| {}));
-            set.insert((MouseEvent::up_disc(), Some(button)), Rc::new(|_, _| {}));
-            set.insert((MouseEvent::click_disc(), Some(button)), Rc::new(|_, _| {}));
             set.insert(
-                (MouseEvent::down_out_disc(), Some(button)),
-                Rc::new(|_, _| {}),
+                HandlerKey::new(MouseEvent::drag_disc(), Some(button)),
+                SmallVec::from_buf([Rc::new(|_, _| {})]),
+            );
+            set.insert(
+                HandlerKey::new(MouseEvent::down_disc(), Some(button)),
+                SmallVec::from_buf([Rc::new(|_, _| {})]),
+            );
+            set.insert(
+                HandlerKey::new(MouseEvent::up_disc(), Some(button)),
+                SmallVec::from_buf([Rc::new(|_, _| {})]),
             );
             set.insert(
-                (MouseEvent::up_out_disc(), Some(button)),
-                Rc::new(|_, _| {}),
+                HandlerKey::new(MouseEvent::click_disc(), Some(button)),
+                SmallVec::from_buf([Rc::new(|_, _| {})]),
+            );
+            set.insert(
+                HandlerKey::new(MouseEvent::down_out_disc(), Some(button)),
+                SmallVec::from_buf([Rc::new(|_, _| {})]),
+            );
+            set.insert(
+                HandlerKey::new(MouseEvent::up_out_disc(), Some(button)),
+                SmallVec::from_buf([Rc::new(|_, _| {})]),
             );
         }
-        set.insert((MouseEvent::scroll_wheel_disc(), None), Rc::new(|_, _| {}));
+        set.insert(
+            HandlerKey::new(MouseEvent::scroll_wheel_disc(), None),
+            SmallVec::from_buf([Rc::new(|_, _| {})]),
+        );
 
         HandlerSet { set }
     }
 
-    pub fn get(
-        &self,
-        key: &(Discriminant<MouseEvent>, Option<MouseButton>),
-    ) -> Option<Rc<dyn Fn(MouseEvent, &mut EventContext)>> {
-        self.set.get(key).cloned()
+    pub fn get(&self, key: &HandlerKey) -> Option<&[HandlerCallback]> {
+        self.set.get(key).map(|vec| vec.as_slice())
     }
 
-    pub fn contains_handler(
+    pub fn contains(
         &self,
-        event: Discriminant<MouseEvent>,
+        discriminant: Discriminant<MouseEvent>,
         button: Option<MouseButton>,
     ) -> bool {
-        self.set.contains_key(&(event, button))
+        self.set
+            .contains_key(&HandlerKey::new(discriminant, button))
+    }
+
+    fn insert(
+        &mut self,
+        event_kind: Discriminant<MouseEvent>,
+        button: Option<MouseButton>,
+        callback: HandlerCallback,
+    ) {
+        use std::collections::hash_map::Entry;
+
+        match self.set.entry(HandlerKey::new(event_kind, button)) {
+            Entry::Occupied(mut vec) => {
+                vec.get_mut().push(callback);
+            }
+
+            Entry::Vacant(entry) => {
+                entry.insert(SmallVec::from_buf([callback]));
+            }
+        }
     }
 
     pub fn on_move(mut self, handler: impl Fn(MouseMove, &mut EventContext) + 'static) -> Self {
-        self.set.insert((MouseEvent::move_disc(), None),
+        self.insert(MouseEvent::move_disc(), None,
             Rc::new(move |region_event, cx| {
                 if let MouseEvent::Move(e) = region_event {
                     handler(e, cx);
@@ -249,7 +294,7 @@ impl HandlerSet {
         button: MouseButton,
         handler: impl Fn(MouseDown, &mut EventContext) + 'static,
     ) -> Self {
-        self.set.insert((MouseEvent::down_disc(), Some(button)),
+        self.insert(MouseEvent::down_disc(), Some(button),
             Rc::new(move |region_event, cx| {
                 if let MouseEvent::Down(e) = region_event {
                     handler(e, cx);
@@ -267,7 +312,7 @@ impl HandlerSet {
         button: MouseButton,
         handler: impl Fn(MouseUp, &mut EventContext) + 'static,
     ) -> Self {
-        self.set.insert((MouseEvent::up_disc(), Some(button)),
+        self.insert(MouseEvent::up_disc(), Some(button),
             Rc::new(move |region_event, cx| {
                 if let MouseEvent::Up(e) = region_event {
                     handler(e, cx);
@@ -285,7 +330,7 @@ impl HandlerSet {
         button: MouseButton,
         handler: impl Fn(MouseClick, &mut EventContext) + 'static,
     ) -> Self {
-        self.set.insert((MouseEvent::click_disc(), Some(button)),
+        self.insert(MouseEvent::click_disc(), Some(button),
             Rc::new(move |region_event, cx| {
                 if let MouseEvent::Click(e) = region_event {
                     handler(e, cx);
@@ -303,7 +348,7 @@ impl HandlerSet {
         button: MouseButton,
         handler: impl Fn(MouseDownOut, &mut EventContext) + 'static,
     ) -> Self {
-        self.set.insert((MouseEvent::down_out_disc(), Some(button)),
+        self.insert(MouseEvent::down_out_disc(), Some(button),
             Rc::new(move |region_event, cx| {
                 if let MouseEvent::DownOut(e) = region_event {
                     handler(e, cx);
@@ -321,7 +366,7 @@ impl HandlerSet {
         button: MouseButton,
         handler: impl Fn(MouseUpOut, &mut EventContext) + 'static,
     ) -> Self {
-        self.set.insert((MouseEvent::up_out_disc(), Some(button)),
+        self.insert(MouseEvent::up_out_disc(), Some(button),
             Rc::new(move |region_event, cx| {
                 if let MouseEvent::UpOut(e) = region_event {
                     handler(e, cx);
@@ -339,7 +384,7 @@ impl HandlerSet {
         button: MouseButton,
         handler: impl Fn(MouseDrag, &mut EventContext) + 'static,
     ) -> Self {
-        self.set.insert((MouseEvent::drag_disc(), Some(button)),
+        self.insert(MouseEvent::drag_disc(), Some(button),
             Rc::new(move |region_event, cx| {
                 if let MouseEvent::Drag(e) = region_event {
                     handler(e, cx);
@@ -353,7 +398,7 @@ impl HandlerSet {
     }
 
     pub fn on_hover(mut self, handler: impl Fn(MouseHover, &mut EventContext) + 'static) -> Self {
-        self.set.insert((MouseEvent::hover_disc(), None),
+        self.insert(MouseEvent::hover_disc(), None,
             Rc::new(move |region_event, cx| {
                 if let MouseEvent::Hover(e) = region_event {
                     handler(e, cx);
@@ -370,7 +415,7 @@ impl HandlerSet {
         mut self,
         handler: impl Fn(MouseScrollWheel, &mut EventContext) + 'static,
     ) -> Self {
-        self.set.insert((MouseEvent::scroll_wheel_disc(), None),
+        self.insert(MouseEvent::scroll_wheel_disc(), None,
             Rc::new(move |region_event, cx| {
                 if let MouseEvent::ScrollWheel(e) = region_event {
                     handler(e, cx);

crates/journal/Cargo.toml 🔗

@@ -16,4 +16,4 @@ chrono = "0.4"
 dirs = "4.0"
 log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 settings = { path = "../settings" }
-shellexpand = "2.1.0"
+shellexpand = "2.1.0"

crates/language/Cargo.toml 🔗

@@ -72,4 +72,5 @@ tree-sitter-rust = "*"
 tree-sitter-python = "*"
 tree-sitter-typescript = "*"
 tree-sitter-ruby = "*"
+tree-sitter-embedded-template = "*"
 unindent = "0.1.7"

crates/language/src/buffer.rs 🔗

@@ -2225,11 +2225,12 @@ impl BufferSnapshot {
         range: Range<T>,
     ) -> Option<(Range<usize>, Range<usize>)> {
         // Find bracket pairs that *inclusively* contain the given range.
-        let range = range.start.to_offset(self).saturating_sub(1)
-            ..self.len().min(range.end.to_offset(self) + 1);
-        let mut matches = self.syntax.matches(range, &self.text, |grammar| {
-            grammar.brackets_config.as_ref().map(|c| &c.query)
-        });
+        let range = range.start.to_offset(self)..range.end.to_offset(self);
+        let mut matches = self.syntax.matches(
+            range.start.saturating_sub(1)..self.len().min(range.end + 1),
+            &self.text,
+            |grammar| grammar.brackets_config.as_ref().map(|c| &c.query),
+        );
         let configs = matches
             .grammars()
             .iter()
@@ -2252,18 +2253,20 @@ impl BufferSnapshot {
 
             matches.advance();
 
-            if let Some((open, close)) = open.zip(close) {
-                let len = close.end - open.start;
+            let Some((open, close)) = open.zip(close) else { continue };
+            if open.start > range.start || close.end < range.end {
+                continue;
+            }
+            let len = close.end - open.start;
 
-                if let Some((existing_open, existing_close)) = &result {
-                    let existing_len = existing_close.end - existing_open.start;
-                    if len > existing_len {
-                        continue;
-                    }
+            if let Some((existing_open, existing_close)) = &result {
+                let existing_len = existing_close.end - existing_open.start;
+                if len > existing_len {
+                    continue;
                 }
-
-                result = Some((open, close));
             }
+
+            result = Some((open, close));
         }
 
         result

crates/language/src/buffer_tests.rs 🔗

@@ -573,14 +573,72 @@ fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) {
         ))
     );
 
-    // Regression test: avoid crash when querying at the end of the buffer.
     assert_eq!(
-        buffer.enclosing_bracket_point_ranges(buffer.len() - 1..buffer.len()),
+        buffer.enclosing_bracket_point_ranges(Point::new(4, 1)..Point::new(4, 1)),
         Some((
             Point::new(0, 6)..Point::new(0, 7),
             Point::new(4, 0)..Point::new(4, 1)
         ))
     );
+
+    // Regression test: avoid crash when querying at the end of the buffer.
+    assert_eq!(
+        buffer.enclosing_bracket_point_ranges(Point::new(4, 1)..Point::new(5, 0)),
+        None
+    );
+}
+
+#[gpui::test]
+fn test_enclosing_bracket_ranges_where_brackets_are_not_outermost_children(
+    cx: &mut MutableAppContext,
+) {
+    let javascript_language = Arc::new(
+        Language::new(
+            LanguageConfig {
+                name: "JavaScript".into(),
+                ..Default::default()
+            },
+            Some(tree_sitter_javascript::language()),
+        )
+        .with_brackets_query(
+            r#"
+            ("{" @open "}" @close)
+            ("(" @open ")" @close)
+            "#,
+        )
+        .unwrap(),
+    );
+
+    cx.set_global(Settings::test(cx));
+    let buffer = cx.add_model(|cx| {
+        let text = "
+            for (const a in b) {
+                // a comment that's longer than the for-loop header
+            }
+        "
+        .unindent();
+        Buffer::new(0, text, cx).with_language(javascript_language, cx)
+    });
+
+    let buffer = buffer.read(cx);
+    assert_eq!(
+        buffer.enclosing_bracket_point_ranges(Point::new(0, 18)..Point::new(0, 18)),
+        Some((
+            Point::new(0, 4)..Point::new(0, 5),
+            Point::new(0, 17)..Point::new(0, 18)
+        ))
+    );
+
+    // Regression test: even though the parent node of the parentheses (the for loop) does
+    // intersect the given range, the parentheses themselves do not contain the range, so
+    // they should not be returned. Only the curly braces contain the range.
+    assert_eq!(
+        buffer.enclosing_bracket_point_ranges(Point::new(0, 20)..Point::new(0, 20)),
+        Some((
+            Point::new(0, 19)..Point::new(0, 20),
+            Point::new(2, 0)..Point::new(2, 1)
+        ))
+    );
 }
 
 #[gpui::test]
@@ -1337,6 +1395,7 @@ fn test_random_collaboration(cx: &mut MutableAppContext, mut rng: StdRng) {
                         (0..entry_count).map(|_| {
                             let range = buffer.random_byte_range(0, &mut rng);
                             let range = range.to_point_utf16(buffer);
+                            let range = range.start..range.end;
                             DiagnosticEntry {
                                 range,
                                 diagnostic: Diagnostic {

crates/language/src/diagnostic_set.rs 🔗

@@ -71,7 +71,7 @@ impl DiagnosticSet {
             diagnostics: SumTree::from_iter(
                 entries.into_iter().map(|entry| DiagnosticEntry {
                     range: buffer.anchor_before(entry.range.start)
-                        ..buffer.anchor_after(entry.range.end),
+                        ..buffer.anchor_before(entry.range.end),
                     diagnostic: entry.diagnostic,
                 }),
                 buffer,

crates/language/src/language.rs 🔗

@@ -28,6 +28,7 @@ use std::{
     any::Any,
     cell::RefCell,
     fmt::Debug,
+    hash::Hash,
     mem,
     ops::Range,
     path::{Path, PathBuf},
@@ -134,6 +135,10 @@ impl CachedLspAdapter {
         self.adapter.process_diagnostics(params).await
     }
 
+    pub async fn process_completion(&self, completion_item: &mut lsp::CompletionItem) {
+        self.adapter.process_completion(completion_item).await
+    }
+
     pub async fn label_for_completion(
         &self,
         completion_item: &lsp::CompletionItem,
@@ -174,6 +179,8 @@ pub trait LspAdapter: 'static + Send + Sync {
 
     async fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
 
+    async fn process_completion(&self, _: &mut lsp::CompletionItem) {}
+
     async fn label_for_completion(
         &self,
         _: &lsp::CompletionItem,
@@ -326,7 +333,13 @@ struct InjectionConfig {
     query: Query,
     content_capture_ix: u32,
     language_capture_ix: Option<u32>,
-    languages_by_pattern_ix: Vec<Option<Box<str>>>,
+    patterns: Vec<InjectionPatternConfig>,
+}
+
+#[derive(Default, Clone)]
+struct InjectionPatternConfig {
+    language: Option<Box<str>>,
+    combined: bool,
 }
 
 struct BracketConfig {
@@ -637,6 +650,10 @@ impl Language {
         self.adapter.clone()
     }
 
+    pub fn id(&self) -> Option<usize> {
+        self.grammar.as_ref().map(|g| g.id)
+    }
+
     pub fn with_highlights_query(mut self, source: &str) -> Result<Self> {
         let grammar = self.grammar_mut();
         grammar.highlights_query = Some(Query::new(grammar.ts_language, source)?);
@@ -730,15 +747,21 @@ impl Language {
                 ("content", &mut content_capture_ix),
             ],
         );
-        let languages_by_pattern_ix = (0..query.pattern_count())
+        let patterns = (0..query.pattern_count())
             .map(|ix| {
-                query.property_settings(ix).iter().find_map(|setting| {
-                    if setting.key.as_ref() == "language" {
-                        return setting.value.clone();
-                    } else {
-                        None
+                let mut config = InjectionPatternConfig::default();
+                for setting in query.property_settings(ix) {
+                    match setting.key.as_ref() {
+                        "language" => {
+                            config.language = setting.value.clone();
+                        }
+                        "combined" => {
+                            config.combined = true;
+                        }
+                        _ => {}
                     }
-                })
+                }
+                config
             })
             .collect();
         if let Some(content_capture_ix) = content_capture_ix {
@@ -746,7 +769,7 @@ impl Language {
                 query,
                 language_capture_ix,
                 content_capture_ix,
-                languages_by_pattern_ix,
+                patterns,
             });
         }
         Ok(self)
@@ -809,6 +832,12 @@ impl Language {
         }
     }
 
+    pub async fn process_completion(self: &Arc<Self>, completion: &mut lsp::CompletionItem) {
+        if let Some(adapter) = self.adapter.as_ref() {
+            adapter.process_completion(completion).await;
+        }
+    }
+
     pub async fn label_for_completion(
         self: &Arc<Self>,
         completion: &lsp::CompletionItem,
@@ -883,6 +912,20 @@ impl Language {
     }
 }
 
+impl Hash for Language {
+    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+        self.id().hash(state)
+    }
+}
+
+impl PartialEq for Language {
+    fn eq(&self, other: &Self) -> bool {
+        self.id().eq(&other.id())
+    }
+}
+
+impl Eq for Language {}
+
 impl Debug for Language {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         f.debug_struct("Language")
@@ -1010,8 +1053,8 @@ pub fn point_to_lsp(point: PointUtf16) -> lsp::Position {
     lsp::Position::new(point.row, point.column)
 }
 
-pub fn point_from_lsp(point: lsp::Position) -> PointUtf16 {
-    PointUtf16::new(point.line, point.character)
+pub fn point_from_lsp(point: lsp::Position) -> Unclipped<PointUtf16> {
+    Unclipped(PointUtf16::new(point.line, point.character))
 }
 
 pub fn range_to_lsp(range: Range<PointUtf16>) -> lsp::Range {
@@ -1021,7 +1064,7 @@ pub fn range_to_lsp(range: Range<PointUtf16>) -> lsp::Range {
     }
 }
 
-pub fn range_from_lsp(range: lsp::Range) -> Range<PointUtf16> {
+pub fn range_from_lsp(range: lsp::Range) -> Range<Unclipped<PointUtf16>> {
     let mut start = point_from_lsp(range.start);
     let mut end = point_from_lsp(range.end);
     if start > end {

crates/language/src/proto.rs 🔗

@@ -426,10 +426,11 @@ pub async fn deserialize_completion(
         .and_then(deserialize_anchor)
         .ok_or_else(|| anyhow!("invalid old end"))?;
     let lsp_completion = serde_json::from_slice(&completion.lsp_completion)?;
-    let label = match language {
-        Some(l) => l.label_for_completion(&lsp_completion).await,
-        None => None,
-    };
+
+    let mut label = None;
+    if let Some(language) = language {
+        label = language.label_for_completion(&lsp_completion).await;
+    }
 
     Ok(Completion {
         old_range: old_start..old_end,

crates/language/src/syntax_map.rs 🔗

@@ -1,4 +1,5 @@
 use crate::{Grammar, InjectionConfig, Language, LanguageRegistry};
+use collections::HashMap;
 use lazy_static::lazy_static;
 use parking_lot::Mutex;
 use std::{
@@ -105,22 +106,42 @@ struct SyntaxLayerSummary {
     max_depth: usize,
     range: Range<Anchor>,
     last_layer_range: Range<Anchor>,
+    last_layer_language: Option<usize>,
 }
 
 #[derive(Clone, Debug)]
-struct DepthAndRange(usize, Range<Anchor>);
+struct SyntaxLayerPosition {
+    depth: usize,
+    range: Range<Anchor>,
+    language: Option<usize>,
+}
 
 #[derive(Clone, Debug)]
-struct DepthAndMaxPosition(usize, Anchor);
+struct ChangeStartPosition {
+    depth: usize,
+    position: Anchor,
+}
 
 #[derive(Clone, Debug)]
-struct DepthAndRangeOrMaxPosition(DepthAndRange, DepthAndMaxPosition);
+struct SyntaxLayerPositionBeforeChange {
+    position: SyntaxLayerPosition,
+    change: ChangeStartPosition,
+}
 
-struct ReparseStep {
+struct ParseStep {
     depth: usize,
     language: Arc<Language>,
-    ranges: Vec<tree_sitter::Range>,
     range: Range<Anchor>,
+    included_ranges: Vec<tree_sitter::Range>,
+    mode: ParseMode,
+}
+
+enum ParseMode {
+    Single,
+    Combined {
+        parent_layer_range: Range<usize>,
+        parent_layer_changed_ranges: Vec<Range<usize>>,
+    },
 }
 
 #[derive(Debug, PartialEq, Eq)]
@@ -215,7 +236,10 @@ impl SyntaxSnapshot {
 
             // Preserve any layers at this depth that precede the first edit.
             if let Some((_, edit_range)) = edits.get(first_edit_ix_for_depth) {
-                let target = DepthAndMaxPosition(depth, edit_range.start);
+                let target = ChangeStartPosition {
+                    depth,
+                    position: edit_range.start,
+                };
                 if target.cmp(&cursor.start(), text).is_gt() {
                     let slice = cursor.slice(&target, Bias::Left, text);
                     layers.push_tree(slice, text);
@@ -225,7 +249,11 @@ impl SyntaxSnapshot {
             // subsequent layers at this same depth.
             else if cursor.item().is_some() {
                 let slice = cursor.slice(
-                    &DepthAndRange(depth + 1, Anchor::MIN..Anchor::MAX),
+                    &SyntaxLayerPosition {
+                        depth: depth + 1,
+                        range: Anchor::MIN..Anchor::MAX,
+                        language: None,
+                    },
                     Bias::Left,
                     text,
                 );
@@ -233,24 +261,17 @@ impl SyntaxSnapshot {
                 continue;
             };
 
-            let layer = if let Some(layer) = cursor.item() {
-                layer
-            } else {
-                break;
-            };
+            let Some(layer) = cursor.item() else { break };
             let (start_byte, start_point) = layer.range.start.summary::<(usize, Point)>(text);
 
             // Ignore edits that end before the start of this layer, and don't consider them
             // for any subsequent layers at this same depth.
             loop {
-                if let Some((_, edit_range)) = edits.get(first_edit_ix_for_depth) {
-                    if edit_range.end.cmp(&layer.range.start, text).is_le() {
-                        first_edit_ix_for_depth += 1;
-                    } else {
-                        break;
-                    }
+                let Some((_, edit_range)) = edits.get(first_edit_ix_for_depth) else { continue 'outer };
+                if edit_range.end.cmp(&layer.range.start, text).is_le() {
+                    first_edit_ix_for_depth += 1;
                 } else {
-                    continue 'outer;
+                    break;
                 }
             }
 
@@ -310,7 +331,7 @@ impl SyntaxSnapshot {
         from_version: &clock::Global,
         text: &BufferSnapshot,
         registry: Option<Arc<LanguageRegistry>>,
-        language: Arc<Language>,
+        root_language: Arc<Language>,
     ) {
         let edits = text.edits_since::<usize>(from_version).collect::<Vec<_>>();
         let max_depth = self.layers.summary().max_depth;
@@ -320,30 +341,46 @@ impl SyntaxSnapshot {
 
         let mut changed_regions = ChangeRegionSet::default();
         let mut queue = BinaryHeap::new();
-        queue.push(ReparseStep {
+        let mut combined_injection_ranges = HashMap::default();
+        queue.push(ParseStep {
             depth: 0,
-            language: language.clone(),
-            ranges: Vec::new(),
+            language: root_language.clone(),
+            included_ranges: vec![tree_sitter::Range {
+                start_byte: 0,
+                end_byte: text.len(),
+                start_point: Point::zero().to_ts_point(),
+                end_point: text.max_point().to_ts_point(),
+            }],
             range: Anchor::MIN..Anchor::MAX,
+            mode: ParseMode::Single,
         });
 
         loop {
             let step = queue.pop();
-            let (depth, range) = if let Some(step) = &step {
-                (step.depth, step.range.clone())
+            let position = if let Some(step) = &step {
+                SyntaxLayerPosition {
+                    depth: step.depth,
+                    range: step.range.clone(),
+                    language: step.language.id(),
+                }
             } else {
-                (max_depth + 1, Anchor::MAX..Anchor::MAX)
+                SyntaxLayerPosition {
+                    depth: max_depth + 1,
+                    range: Anchor::MAX..Anchor::MAX,
+                    language: None,
+                }
             };
 
-            let target = DepthAndRange(depth, range.clone());
             let mut done = cursor.item().is_none();
-            while !done && target.cmp(&cursor.end(text), &text).is_gt() {
+            while !done && position.cmp(&cursor.end(text), &text).is_gt() {
                 done = true;
 
-                let bounded_target =
-                    DepthAndRangeOrMaxPosition(target.clone(), changed_regions.start_position());
-                if bounded_target.cmp(&cursor.start(), &text).is_gt() {
-                    let slice = cursor.slice(&bounded_target, Bias::Left, text);
+                let bounded_position = SyntaxLayerPositionBeforeChange {
+                    position: position.clone(),
+                    change: changed_regions.start_position(),
+                };
+                if bounded_position.cmp(&cursor.start(), &text).is_gt() {
+                    let slice = cursor.slice(&bounded_position, Bias::Left, text);
                     if !slice.is_empty() {
                         layers.push_tree(slice, &text);
                         if changed_regions.prune(cursor.end(text), text) {
@@ -352,12 +389,8 @@ impl SyntaxSnapshot {
                     }
                 }
 
-                while target.cmp(&cursor.end(text), text).is_gt() {
-                    let layer = if let Some(layer) = cursor.item() {
-                        layer
-                    } else {
-                        break;
-                    };
+                while position.cmp(&cursor.end(text), text).is_gt() {
+                    let Some(layer) = cursor.item() else { break };
 
                     if changed_regions.intersects(&layer, text) {
                         changed_regions.insert(
@@ -378,70 +411,74 @@ impl SyntaxSnapshot {
                 }
             }
 
-            let (ranges, language) = if let Some(step) = step {
-                (step.ranges, step.language)
-            } else {
-                break;
-            };
-
-            let start_point;
-            let start_byte;
-            let end_byte;
-            if let Some((first, last)) = ranges.first().zip(ranges.last()) {
-                start_point = first.start_point;
-                start_byte = first.start_byte;
-                end_byte = last.end_byte;
-            } else {
-                start_point = Point::zero().to_ts_point();
-                start_byte = 0;
-                end_byte = text.len();
-            };
+            let Some(step) = step else { break };
+            let (step_start_byte, step_start_point) =
+                step.range.start.summary::<(usize, Point)>(text);
+            let step_end_byte = step.range.end.to_offset(text);
+            let Some(grammar) = step.language.grammar.as_deref() else { continue };
 
             let mut old_layer = cursor.item();
             if let Some(layer) = old_layer {
-                if layer.range.to_offset(text) == (start_byte..end_byte) {
+                if layer.range.to_offset(text) == (step_start_byte..step_end_byte)
+                    && layer.language.id() == step.language.id()
+                {
                     cursor.next(&text);
                 } else {
                     old_layer = None;
                 }
             }
 
-            let grammar = if let Some(grammar) = language.grammar.as_deref() {
-                grammar
-            } else {
-                continue;
-            };
-
             let tree;
             let changed_ranges;
+            let mut included_ranges = step.included_ranges;
             if let Some(old_layer) = old_layer {
+                if let ParseMode::Combined {
+                    parent_layer_changed_ranges,
+                    ..
+                } = step.mode
+                {
+                    included_ranges = splice_included_ranges(
+                        old_layer.tree.included_ranges(),
+                        &parent_layer_changed_ranges,
+                        &included_ranges,
+                    );
+                }
+
                 tree = parse_text(
                     grammar,
                     text.as_rope(),
+                    step_start_byte,
+                    step_start_point,
+                    included_ranges,
                     Some(old_layer.tree.clone()),
-                    ranges,
                 );
                 changed_ranges = join_ranges(
-                    edits
-                        .iter()
-                        .map(|e| e.new.clone())
-                        .filter(|range| range.start < end_byte && range.end > start_byte),
+                    edits.iter().map(|e| e.new.clone()).filter(|range| {
+                        range.start <= step_end_byte && range.end >= step_start_byte
+                    }),
                     old_layer
                         .tree
                         .changed_ranges(&tree)
-                        .map(|r| start_byte + r.start_byte..start_byte + r.end_byte),
+                        .map(|r| step_start_byte + r.start_byte..step_start_byte + r.end_byte),
                 );
             } else {
-                tree = parse_text(grammar, text.as_rope(), None, ranges);
-                changed_ranges = vec![start_byte..end_byte];
+                tree = parse_text(
+                    grammar,
+                    text.as_rope(),
+                    step_start_byte,
+                    step_start_point,
+                    included_ranges,
+                    None,
+                );
+                changed_ranges = vec![step_start_byte..step_end_byte];
             }
 
             layers.push(
                 SyntaxLayer {
-                    depth,
-                    range,
+                    depth: step.depth,
+                    range: step.range,
                     tree: tree.clone(),
-                    language: language.clone(),
+                    language: step.language.clone(),
                 },
                 &text,
             );
@@ -450,11 +487,10 @@ impl SyntaxSnapshot {
                 grammar.injection_config.as_ref().zip(registry.as_ref()),
                 changed_ranges.is_empty(),
             ) {
-                let depth = depth + 1;
                 for range in &changed_ranges {
                     changed_regions.insert(
                         ChangedRegion {
-                            depth,
+                            depth: step.depth + 1,
                             range: text.anchor_before(range.start)..text.anchor_after(range.end),
                         },
                         text,
@@ -463,10 +499,11 @@ impl SyntaxSnapshot {
                 get_injections(
                     config,
                     text,
-                    tree.root_node_with_offset(start_byte, start_point),
+                    tree.root_node_with_offset(step_start_byte, step_start_point.to_ts_point()),
                     registry,
-                    depth,
+                    step.depth + 1,
                     &changed_ranges,
+                    &mut combined_injection_ranges,
                     &mut queue,
                 );
             }
@@ -547,7 +584,6 @@ impl SyntaxSnapshot {
             }
         });
 
-        // let mut result = Vec::new();
         cursor.next(buffer);
         std::iter::from_fn(move || {
             if let Some(layer) = cursor.item() {
@@ -565,8 +601,6 @@ impl SyntaxSnapshot {
                 None
             }
         })
-
-        // result
     }
 }
 
@@ -892,14 +926,11 @@ fn join_ranges(
 fn parse_text(
     grammar: &Grammar,
     text: &Rope,
-    old_tree: Option<Tree>,
+    start_byte: usize,
+    start_point: Point,
     mut ranges: Vec<tree_sitter::Range>,
+    old_tree: Option<Tree>,
 ) -> Tree {
-    let (start_byte, start_point) = ranges
-        .first()
-        .map(|range| (range.start_byte, Point::from_ts_point(range.start_point)))
-        .unwrap_or_default();
-
     for range in &mut ranges {
         range.start_byte -= start_byte;
         range.end_byte -= start_byte;
@@ -934,14 +965,25 @@ fn get_injections(
     node: Node,
     language_registry: &LanguageRegistry,
     depth: usize,
-    query_ranges: &[Range<usize>],
-    queue: &mut BinaryHeap<ReparseStep>,
+    changed_ranges: &[Range<usize>],
+    combined_injection_ranges: &mut HashMap<Arc<Language>, Vec<tree_sitter::Range>>,
+    queue: &mut BinaryHeap<ParseStep>,
 ) -> bool {
     let mut result = false;
     let mut query_cursor = QueryCursorHandle::new();
     let mut prev_match = None;
-    for query_range in query_ranges {
-        query_cursor.set_byte_range(query_range.start.saturating_sub(1)..query_range.end);
+
+    combined_injection_ranges.clear();
+    for pattern in &config.patterns {
+        if let (Some(language_name), true) = (pattern.language.as_ref(), pattern.combined) {
+            if let Some(language) = language_registry.get_language(language_name) {
+                combined_injection_ranges.insert(language, Vec::new());
+            }
+        }
+    }
+
+    for query_range in changed_ranges {
+        query_cursor.set_byte_range(query_range.start.saturating_sub(1)..query_range.end + 1);
         for mat in query_cursor.matches(&config.query, node, TextProvider(text.as_rope())) {
             let content_ranges = mat
                 .nodes_for_capture_index(config.content_capture_ix)
@@ -961,7 +1003,9 @@ fn get_injections(
             }
             prev_match = Some((mat.pattern_index, content_range.clone()));
 
-            let language_name = config.languages_by_pattern_ix[mat.pattern_index]
+            let combined = config.patterns[mat.pattern_index].combined;
+            let language_name = config.patterns[mat.pattern_index]
+                .language
                 .as_ref()
                 .map(|s| Cow::Borrowed(s.as_ref()))
                 .or_else(|| {
@@ -975,19 +1019,114 @@ fn get_injections(
                     result = true;
                     let range = text.anchor_before(content_range.start)
                         ..text.anchor_after(content_range.end);
-                    queue.push(ReparseStep {
-                        depth,
-                        language,
-                        ranges: content_ranges,
-                        range,
-                    })
+                    if combined {
+                        combined_injection_ranges
+                            .get_mut(&language.clone())
+                            .unwrap()
+                            .extend(content_ranges);
+                    } else {
+                        queue.push(ParseStep {
+                            depth,
+                            language,
+                            included_ranges: content_ranges,
+                            range,
+                            mode: ParseMode::Single,
+                        });
+                    }
                 }
             }
         }
     }
+
+    for (language, mut included_ranges) in combined_injection_ranges.drain() {
+        included_ranges.sort_unstable();
+        let range = text.anchor_before(node.start_byte())..text.anchor_after(node.end_byte());
+        queue.push(ParseStep {
+            depth,
+            language,
+            range,
+            included_ranges,
+            mode: ParseMode::Combined {
+                parent_layer_range: node.start_byte()..node.end_byte(),
+                parent_layer_changed_ranges: changed_ranges.to_vec(),
+            },
+        })
+    }
+
     result
 }
 
+fn splice_included_ranges(
+    mut ranges: Vec<tree_sitter::Range>,
+    changed_ranges: &[Range<usize>],
+    new_ranges: &[tree_sitter::Range],
+) -> Vec<tree_sitter::Range> {
+    let mut changed_ranges = changed_ranges.into_iter().peekable();
+    let mut new_ranges = new_ranges.into_iter().peekable();
+    let mut ranges_ix = 0;
+    loop {
+        let new_range = new_ranges.peek();
+        let mut changed_range = changed_ranges.peek();
+
+        // Remove ranges that have changed before inserting any new ranges
+        // into those ranges.
+        if let Some((changed, new)) = changed_range.zip(new_range) {
+            if new.end_byte < changed.start {
+                changed_range = None;
+            }
+        }
+
+        if let Some(changed) = changed_range {
+            let mut start_ix = ranges_ix
+                + match ranges[ranges_ix..].binary_search_by_key(&changed.start, |r| r.end_byte) {
+                    Ok(ix) | Err(ix) => ix,
+                };
+            let mut end_ix = ranges_ix
+                + match ranges[ranges_ix..].binary_search_by_key(&changed.end, |r| r.start_byte) {
+                    Ok(ix) => ix + 1,
+                    Err(ix) => ix,
+                };
+
+            // If there are empty ranges, then there may be multiple ranges with the same
+            // start or end. Expand the splice to include any adjacent ranges that touch
+            // the changed range.
+            while start_ix > 0 {
+                if ranges[start_ix - 1].end_byte == changed.start {
+                    start_ix -= 1;
+                } else {
+                    break;
+                }
+            }
+            while let Some(range) = ranges.get(end_ix) {
+                if range.start_byte == changed.end {
+                    end_ix += 1;
+                } else {
+                    break;
+                }
+            }
+
+            if end_ix > start_ix {
+                ranges.splice(start_ix..end_ix, []);
+            }
+            changed_ranges.next();
+            ranges_ix = start_ix;
+        } else if let Some(new_range) = new_range {
+            let ix = ranges_ix
+                + match ranges[ranges_ix..]
+                    .binary_search_by_key(&new_range.start_byte, |r| r.start_byte)
+                {
+                    Ok(ix) | Err(ix) => ix,
+                };
+            ranges.insert(ix, **new_range);
+            new_ranges.next();
+            ranges_ix = ix + 1;
+        } else {
+            break;
+        }
+    }
+    ranges
+}
+
 impl std::ops::Deref for SyntaxMap {
     type Target = SyntaxSnapshot;
 
@@ -996,35 +1135,43 @@ impl std::ops::Deref for SyntaxMap {
     }
 }
 
-impl PartialEq for ReparseStep {
+impl PartialEq for ParseStep {
     fn eq(&self, _: &Self) -> bool {
         false
     }
 }
 
-impl Eq for ReparseStep {}
+impl Eq for ParseStep {}
 
-impl PartialOrd for ReparseStep {
+impl PartialOrd for ParseStep {
     fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
         Some(self.cmp(&other))
     }
 }
 
-impl Ord for ReparseStep {
+impl Ord for ParseStep {
     fn cmp(&self, other: &Self) -> Ordering {
         let range_a = self.range();
         let range_b = other.range();
         Ord::cmp(&other.depth, &self.depth)
             .then_with(|| Ord::cmp(&range_b.start, &range_a.start))
             .then_with(|| Ord::cmp(&range_a.end, &range_b.end))
+            .then_with(|| self.language.id().cmp(&other.language.id()))
     }
 }
 
-impl ReparseStep {
+impl ParseStep {
     fn range(&self) -> Range<usize> {
-        let start = self.ranges.first().map_or(0, |r| r.start_byte);
-        let end = self.ranges.last().map_or(0, |r| r.end_byte);
-        start..end
+        if let ParseMode::Combined {
+            parent_layer_range, ..
+        } = &self.mode
+        {
+            parent_layer_range.clone()
+        } else {
+            let start = self.included_ranges.first().map_or(0, |r| r.start_byte);
+            let end = self.included_ranges.last().map_or(0, |r| r.end_byte);
+            start..end
+        }
     }
 }
 
@@ -1039,12 +1186,17 @@ impl ChangedRegion {
 }
 
 impl ChangeRegionSet {
-    fn start_position(&self) -> DepthAndMaxPosition {
-        self.0
-            .first()
-            .map_or(DepthAndMaxPosition(usize::MAX, Anchor::MAX), |region| {
-                DepthAndMaxPosition(region.depth, region.range.start)
-            })
+    fn start_position(&self) -> ChangeStartPosition {
+        self.0.first().map_or(
+            ChangeStartPosition {
+                depth: usize::MAX,
+                position: Anchor::MAX,
+            },
+            |region| ChangeStartPosition {
+                depth: region.depth,
+                position: region.range.start,
+            },
+        )
     }
 
     fn intersects(&self, layer: &SyntaxLayer, text: &BufferSnapshot) -> bool {
@@ -1094,6 +1246,7 @@ impl Default for SyntaxLayerSummary {
             min_depth: 0,
             range: Anchor::MAX..Anchor::MIN,
             last_layer_range: Anchor::MIN..Anchor::MAX,
+            last_layer_language: None,
         }
     }
 }
@@ -1114,14 +1267,15 @@ impl sum_tree::Summary for SyntaxLayerSummary {
             }
         }
         self.last_layer_range = other.last_layer_range.clone();
+        self.last_layer_language = other.last_layer_language;
     }
 }
 
-impl<'a> SeekTarget<'a, SyntaxLayerSummary, SyntaxLayerSummary> for DepthAndRange {
+impl<'a> SeekTarget<'a, SyntaxLayerSummary, SyntaxLayerSummary> for SyntaxLayerPosition {
     fn cmp(&self, cursor_location: &SyntaxLayerSummary, buffer: &BufferSnapshot) -> Ordering {
-        Ord::cmp(&self.0, &cursor_location.max_depth)
+        Ord::cmp(&self.depth, &cursor_location.max_depth)
             .then_with(|| {
-                self.1
+                self.range
                     .start
                     .cmp(&cursor_location.last_layer_range.start, buffer)
             })
@@ -1129,24 +1283,27 @@ impl<'a> SeekTarget<'a, SyntaxLayerSummary, SyntaxLayerSummary> for DepthAndRang
                 cursor_location
                     .last_layer_range
                     .end
-                    .cmp(&self.1.end, buffer)
+                    .cmp(&self.range.end, buffer)
             })
+            .then_with(|| self.language.cmp(&cursor_location.last_layer_language))
     }
 }
 
-impl<'a> SeekTarget<'a, SyntaxLayerSummary, SyntaxLayerSummary> for DepthAndMaxPosition {
+impl<'a> SeekTarget<'a, SyntaxLayerSummary, SyntaxLayerSummary> for ChangeStartPosition {
     fn cmp(&self, cursor_location: &SyntaxLayerSummary, text: &BufferSnapshot) -> Ordering {
-        Ord::cmp(&self.0, &cursor_location.max_depth)
-            .then_with(|| self.1.cmp(&cursor_location.range.end, text))
+        Ord::cmp(&self.depth, &cursor_location.max_depth)
+            .then_with(|| self.position.cmp(&cursor_location.range.end, text))
     }
 }
 
-impl<'a> SeekTarget<'a, SyntaxLayerSummary, SyntaxLayerSummary> for DepthAndRangeOrMaxPosition {
+impl<'a> SeekTarget<'a, SyntaxLayerSummary, SyntaxLayerSummary>
+    for SyntaxLayerPositionBeforeChange
+{
     fn cmp(&self, cursor_location: &SyntaxLayerSummary, buffer: &BufferSnapshot) -> Ordering {
-        if self.1.cmp(cursor_location, buffer).is_le() {
+        if self.change.cmp(cursor_location, buffer).is_le() {
             return Ordering::Less;
         } else {
-            self.0.cmp(cursor_location, buffer)
+            self.position.cmp(cursor_location, buffer)
         }
     }
 }
@@ -1160,6 +1317,7 @@ impl sum_tree::Item for SyntaxLayer {
             max_depth: self.depth,
             range: self.range.clone(),
             last_layer_range: self.range.clone(),
+            last_layer_language: self.language.id(),
         }
     }
 }
@@ -1246,6 +1404,63 @@ mod tests {
     use unindent::Unindent as _;
     use util::test::marked_text_ranges;
 
+    #[test]
+    fn test_splice_included_ranges() {
+        let ranges = vec![ts_range(20..30), ts_range(50..60), ts_range(80..90)];
+
+        let new_ranges = splice_included_ranges(
+            ranges.clone(),
+            &[54..56, 58..68],
+            &[ts_range(50..54), ts_range(59..67)],
+        );
+        assert_eq!(
+            new_ranges,
+            &[
+                ts_range(20..30),
+                ts_range(50..54),
+                ts_range(59..67),
+                ts_range(80..90),
+            ]
+        );
+
+        let new_ranges = splice_included_ranges(ranges.clone(), &[70..71, 91..100], &[]);
+        assert_eq!(
+            new_ranges,
+            &[ts_range(20..30), ts_range(50..60), ts_range(80..90)]
+        );
+
+        let new_ranges =
+            splice_included_ranges(ranges.clone(), &[], &[ts_range(0..2), ts_range(70..75)]);
+        assert_eq!(
+            new_ranges,
+            &[
+                ts_range(0..2),
+                ts_range(20..30),
+                ts_range(50..60),
+                ts_range(70..75),
+                ts_range(80..90)
+            ]
+        );
+
+        let new_ranges = splice_included_ranges(ranges.clone(), &[30..50], &[ts_range(25..55)]);
+        assert_eq!(new_ranges, &[ts_range(25..55), ts_range(80..90)]);
+
+        fn ts_range(range: Range<usize>) -> tree_sitter::Range {
+            tree_sitter::Range {
+                start_byte: range.start,
+                start_point: tree_sitter::Point {
+                    row: 0,
+                    column: range.start,
+                },
+                end_byte: range.end,
+                end_point: tree_sitter::Point {
+                    row: 0,
+                    column: range.end,
+                },
+            }
+        }
+    }
+
     #[gpui::test]
     fn test_syntax_map_layers_for_range() {
         let registry = Arc::new(LanguageRegistry::test());
@@ -1345,21 +1560,24 @@ mod tests {
 
     #[gpui::test]
     fn test_typing_multiple_new_injections() {
-        let (buffer, syntax_map) = test_edit_sequence(&[
-            "fn a() { dbg }",
-            "fn a() { dbg«!» }",
-            "fn a() { dbg!«()» }",
-            "fn a() { dbg!(«b») }",
-            "fn a() { dbg!(b«.») }",
-            "fn a() { dbg!(b.«c») }",
-            "fn a() { dbg!(b.c«()») }",
-            "fn a() { dbg!(b.c(«vec»)) }",
-            "fn a() { dbg!(b.c(vec«!»)) }",
-            "fn a() { dbg!(b.c(vec!«[]»)) }",
-            "fn a() { dbg!(b.c(vec![«d»])) }",
-            "fn a() { dbg!(b.c(vec![d«.»])) }",
-            "fn a() { dbg!(b.c(vec![d.«e»])) }",
-        ]);
+        let (buffer, syntax_map) = test_edit_sequence(
+            "Rust",
+            &[
+                "fn a() { dbg }",
+                "fn a() { dbg«!» }",
+                "fn a() { dbg!«()» }",
+                "fn a() { dbg!(«b») }",
+                "fn a() { dbg!(b«.») }",
+                "fn a() { dbg!(b.«c») }",
+                "fn a() { dbg!(b.c«()») }",
+                "fn a() { dbg!(b.c(«vec»)) }",
+                "fn a() { dbg!(b.c(vec«!»)) }",
+                "fn a() { dbg!(b.c(vec!«[]»)) }",
+                "fn a() { dbg!(b.c(vec![«d»])) }",
+                "fn a() { dbg!(b.c(vec![d«.»])) }",
+                "fn a() { dbg!(b.c(vec![d.«e»])) }",
+            ],
+        );
 
         assert_capture_ranges(
             &syntax_map,
@@ -1371,29 +1589,32 @@ mod tests {
 
     #[gpui::test]
     fn test_pasting_new_injection_line_between_others() {
-        let (buffer, syntax_map) = test_edit_sequence(&[
-            "
-                fn a() {
-                    b!(B {});
-                    c!(C {});
-                    d!(D {});
-                    e!(E {});
-                    f!(F {});
-                    g!(G {});
-                }
-            ",
-            "
-                fn a() {
-                    b!(B {});
-                    c!(C {});
-                    d!(D {});
-                «    h!(H {});
-                »    e!(E {});
-                    f!(F {});
-                    g!(G {});
-                }
-            ",
-        ]);
+        let (buffer, syntax_map) = test_edit_sequence(
+            "Rust",
+            &[
+                "
+                    fn a() {
+                        b!(B {});
+                        c!(C {});
+                        d!(D {});
+                        e!(E {});
+                        f!(F {});
+                        g!(G {});
+                    }
+                ",
+                "
+                    fn a() {
+                        b!(B {});
+                        c!(C {});
+                        d!(D {});
+                    «    h!(H {});
+                    »    e!(E {});
+                        f!(F {});
+                        g!(G {});
+                    }
+                ",
+            ],
+        );
 
         assert_capture_ranges(
             &syntax_map,
@@ -1415,28 +1636,31 @@ mod tests {
 
     #[gpui::test]
     fn test_joining_injections_with_child_injections() {
-        let (buffer, syntax_map) = test_edit_sequence(&[
-            "
-                fn a() {
-                    b!(
-                        c![one.two.three],
-                        d![four.five.six],
-                    );
-                    e!(
-                        f![seven.eight],
-                    );
-                }
-            ",
-            "
-                fn a() {
-                    b!(
-                        c![one.two.three],
-                        d![four.five.six],
-                    ˇ    f![seven.eight],
-                    );
-                }
-            ",
-        ]);
+        let (buffer, syntax_map) = test_edit_sequence(
+            "Rust",
+            &[
+                "
+                    fn a() {
+                        b!(
+                            c![one.two.three],
+                            d![four.five.six],
+                        );
+                        e!(
+                            f![seven.eight],
+                        );
+                    }
+                ",
+                "
+                    fn a() {
+                        b!(
+                            c![one.two.three],
+                            d![four.five.six],
+                        ˇ    f![seven.eight],
+                        );
+                    }
+                ",
+            ],
+        );
 
         assert_capture_ranges(
             &syntax_map,
@@ -1456,131 +1680,270 @@ mod tests {
 
     #[gpui::test]
     fn test_editing_edges_of_injection() {
-        test_edit_sequence(&[
-            "
-                fn a() {
-                    b!(c!())
-                }
-            ",
-            "
-                fn a() {
-                    «d»!(c!())
-                }
-            ",
-            "
-                fn a() {
-                    «e»d!(c!())
-                }
-            ",
-            "
-                fn a() {
-                    ed!«[»c!()«]»
-                }
+        test_edit_sequence(
+            "Rust",
+            &[
+                "
+                    fn a() {
+                        b!(c!())
+                    }
+                ",
+                "
+                    fn a() {
+                        «d»!(c!())
+                    }
+                ",
+                "
+                    fn a() {
+                        «e»d!(c!())
+                    }
+                ",
+                "
+                    fn a() {
+                        ed!«[»c!()«]»
+                    }
             ",
-        ]);
+            ],
+        );
     }
 
     #[gpui::test]
     fn test_edits_preceding_and_intersecting_injection() {
-        test_edit_sequence(&[
-            //
-            "const aaaaaaaaaaaa: B = c!(d(e.f));",
-            "const aˇa: B = c!(d(eˇ));",
-        ]);
+        test_edit_sequence(
+            "Rust",
+            &[
+                //
+                "const aaaaaaaaaaaa: B = c!(d(e.f));",
+                "const aˇa: B = c!(d(eˇ));",
+            ],
+        );
     }
 
     #[gpui::test]
     fn test_non_local_changes_create_injections() {
-        test_edit_sequence(&[
-            "
-                // a! {
-                    static B: C = d;
-                // }
-            ",
-            "
-                ˇa! {
-                    static B: C = d;
-                ˇ}
-            ",
-        ]);
+        test_edit_sequence(
+            "Rust",
+            &[
+                "
+                    // a! {
+                        static B: C = d;
+                    // }
+                ",
+                "
+                    ˇa! {
+                        static B: C = d;
+                    ˇ}
+                ",
+            ],
+        );
     }
 
     #[gpui::test]
     fn test_creating_many_injections_in_one_edit() {
-        test_edit_sequence(&[
-            "
-                fn a() {
-                    one(Two::three(3));
-                    four(Five::six(6));
-                    seven(Eight::nine(9));
-                }
-            ",
-            "
-                fn a() {
-                    one«!»(Two::three(3));
-                    four«!»(Five::six(6));
-                    seven«!»(Eight::nine(9));
-                }
-            ",
-            "
-                fn a() {
-                    one!(Two::three«!»(3));
-                    four!(Five::six«!»(6));
-                    seven!(Eight::nine«!»(9));
-                }
-            ",
-        ]);
+        test_edit_sequence(
+            "Rust",
+            &[
+                "
+                    fn a() {
+                        one(Two::three(3));
+                        four(Five::six(6));
+                        seven(Eight::nine(9));
+                    }
+                ",
+                "
+                    fn a() {
+                        one«!»(Two::three(3));
+                        four«!»(Five::six(6));
+                        seven«!»(Eight::nine(9));
+                    }
+                ",
+                "
+                    fn a() {
+                        one!(Two::three«!»(3));
+                        four!(Five::six«!»(6));
+                        seven!(Eight::nine«!»(9));
+                    }
+                ",
+            ],
+        );
     }
 
     #[gpui::test]
     fn test_editing_across_injection_boundary() {
-        test_edit_sequence(&[
-            "
-                fn one() {
-                    two();
-                    three!(
-                        three.four,
-                        five.six,
-                    );
-                }
-            ",
-            "
-                fn one() {
-                    two();
-                    th«irty_five![»
-                        three.four,
-                        five.six,
-                    «   seven.eight,
-                    ];»
-                }
-            ",
-        ]);
+        test_edit_sequence(
+            "Rust",
+            &[
+                "
+                    fn one() {
+                        two();
+                        three!(
+                            three.four,
+                            five.six,
+                        );
+                    }
+                ",
+                "
+                    fn one() {
+                        two();
+                        th«irty_five![»
+                            three.four,
+                            five.six,
+                        «   seven.eight,
+                        ];»
+                    }
+                ",
+            ],
+        );
     }
 
     #[gpui::test]
     fn test_removing_injection_by_replacing_across_boundary() {
-        test_edit_sequence(&[
+        test_edit_sequence(
+            "Rust",
+            &[
+                "
+                    fn one() {
+                        two!(
+                            three.four,
+                        );
+                    }
+                ",
+                "
+                    fn one() {
+                        t«en
+                            .eleven(
+                            twelve,
+                        »
+                            three.four,
+                        );
+                    }
+                ",
+            ],
+        );
+    }
+
+    #[gpui::test]
+    fn test_combined_injections() {
+        let (buffer, syntax_map) = test_edit_sequence(
+            "ERB",
+            &[
+                "
+                    <body>
+                        <% if @one %>
+                            <div class=one>
+                        <% else %>
+                            <div class=two>
+                        <% end %>
+                        </div>
+                    </body>
+                ",
+                "
+                    <body>
+                        <% if @one %>
+                            <div class=one>
+                        ˇ else ˇ
+                            <div class=two>
+                        <% end %>
+                        </div>
+                    </body>
+                ",
+                "
+                    <body>
+                        <% if @one «;» end %>
+                        </div>
+                    </body>
+                ",
+            ],
+        );
+
+        assert_capture_ranges(
+            &syntax_map,
+            &buffer,
+            &["tag", "ivar"],
             "
-                fn one() {
-                    two!(
-                        three.four,
-                    );
-                }
+                <«body»>
+                    <% if «@one» ; end %>
+                    </«div»>
+                </«body»>
             ",
+        );
+    }
+
+    #[gpui::test]
+    fn test_combined_injections_empty_ranges() {
+        test_edit_sequence(
+            "ERB",
+            &[
+                "
+                    <% if @one %>
+                    <% else %>
+                    <% end %>
+                ",
+                "
+                    <% if @one %>
+                    ˇ<% end %>
+                ",
+            ],
+        );
+    }
+
+    #[gpui::test]
+    fn test_combined_injections_edit_edges_of_ranges() {
+        let (buffer, syntax_map) = test_edit_sequence(
+            "ERB",
+            &[
+                "
+                    <%= one @two %>
+                    <%= three @four %>
+                ",
+                "
+                    <%= one @two %ˇ
+                    <%= three @four %>
+                ",
+                "
+                    <%= one @two %«>»
+                    <%= three @four %>
+                ",
+            ],
+        );
+
+        assert_capture_ranges(
+            &syntax_map,
+            &buffer,
+            &["tag", "ivar"],
             "
-                fn one() {
-                    t«en
-                        .eleven(
-                        twelve,
-                    »
-                        three.four,
-                    );
-                }
+                <%= one «@two» %>
+                <%= three «@four» %>
             ",
-        ]);
+        );
     }
 
-    #[gpui::test(iterations = 100)]
+    #[gpui::test]
+    fn test_combined_injections_splitting_some_injections() {
+        let (_buffer, _syntax_map) = test_edit_sequence(
+            "ERB",
+            &[
+                r#"
+                      <%A if b(:c) %>
+                        d
+                      <% end %>
+                      eee
+                      <% f %>
+                "#,
+                r#"
+                      <%« AAAAAAA %>
+                        hhhhhhh
+                      <%=» if b(:c) %>
+                        d
+                      <% end %>
+                      eee
+                      <% f %>
+                "#,
+            ],
+        );
+    }
+
+    #[gpui::test(iterations = 50)]
     fn test_random_syntax_map_edits(mut rng: StdRng) {
         let operations = env::var("OPERATIONS")
             .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))

crates/project/src/lsp_command.rs 🔗

@@ -128,8 +128,8 @@ impl LspCommand for PrepareRename {
             ) = message
             {
                 let Range { start, end } = range_from_lsp(range);
-                if buffer.clip_point_utf16(start, Bias::Left) == start
-                    && buffer.clip_point_utf16(end, Bias::Left) == end
+                if buffer.clip_point_utf16(start, Bias::Left) == start.0
+                    && buffer.clip_point_utf16(end, Bias::Left) == end.0
                 {
                     return Ok(Some(buffer.anchor_after(start)..buffer.anchor_before(end)));
                 }

crates/project/src/project.rs 🔗

@@ -30,6 +30,7 @@ use language::{
     CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Event as BufferEvent,
     File as _, Language, LanguageRegistry, LanguageServerName, LocalFile, OffsetRangeExt,
     Operation, Patch, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction,
+    Unclipped,
 };
 use lsp::{
     DiagnosticSeverity, DiagnosticTag, DocumentHighlightKind, LanguageServer, LanguageString,
@@ -251,7 +252,7 @@ pub struct Symbol {
     pub label: CodeLabel,
     pub name: String,
     pub kind: lsp::SymbolKind,
-    pub range: Range<PointUtf16>,
+    pub range: Range<Unclipped<PointUtf16>>,
     pub signature: [u8; 32],
 }
 
@@ -2582,7 +2583,7 @@ impl Project {
         language_server_id: usize,
         abs_path: PathBuf,
         version: Option<i32>,
-        diagnostics: Vec<DiagnosticEntry<PointUtf16>>,
+        diagnostics: Vec<DiagnosticEntry<Unclipped<PointUtf16>>>,
         cx: &mut ModelContext<Project>,
     ) -> Result<(), anyhow::Error> {
         let (worktree, relative_path) = self
@@ -2620,7 +2621,7 @@ impl Project {
     fn update_buffer_diagnostics(
         &mut self,
         buffer: &ModelHandle<Buffer>,
-        mut diagnostics: Vec<DiagnosticEntry<PointUtf16>>,
+        mut diagnostics: Vec<DiagnosticEntry<Unclipped<PointUtf16>>>,
         version: Option<i32>,
         cx: &mut ModelContext<Self>,
     ) -> Result<()> {
@@ -2644,7 +2645,7 @@ impl Project {
         let mut sanitized_diagnostics = Vec::new();
         let edits_since_save = Patch::new(
             snapshot
-                .edits_since::<PointUtf16>(buffer.read(cx).saved_version())
+                .edits_since::<Unclipped<PointUtf16>>(buffer.read(cx).saved_version())
                 .collect(),
         );
         for entry in diagnostics {
@@ -2664,13 +2665,14 @@ impl Project {
             let mut range = snapshot.clip_point_utf16(start, Bias::Left)
                 ..snapshot.clip_point_utf16(end, Bias::Right);
 
-            // Expand empty ranges by one character
+            // Expand empty ranges by one codepoint
             if range.start == range.end {
+                // This will be go to the next boundary when being clipped
                 range.end.column += 1;
-                range.end = snapshot.clip_point_utf16(range.end, Bias::Right);
+                range.end = snapshot.clip_point_utf16(Unclipped(range.end), Bias::Right);
                 if range.start == range.end && range.end.column > 0 {
                     range.start.column -= 1;
-                    range.start = snapshot.clip_point_utf16(range.start, Bias::Left);
+                    range.end = snapshot.clip_point_utf16(Unclipped(range.end), Bias::Left);
                 }
             }
 
@@ -3273,7 +3275,7 @@ impl Project {
             return Task::ready(Ok(Default::default()));
         };
 
-        let position = position.to_point_utf16(source_buffer);
+        let position = Unclipped(position.to_point_utf16(source_buffer));
         let anchor = source_buffer.anchor_after(position);
 
         if worktree.read(cx).as_local().is_some() {
@@ -3292,7 +3294,7 @@ impl Project {
                             lsp::TextDocumentIdentifier::new(
                                 lsp::Url::from_file_path(buffer_abs_path).unwrap(),
                             ),
-                            point_to_lsp(position),
+                            point_to_lsp(position.0),
                         ),
                         context: Default::default(),
                         work_done_progress_params: Default::default(),
@@ -3314,88 +3316,91 @@ impl Project {
                     let snapshot = this.snapshot();
                     let clipped_position = this.clip_point_utf16(position, Bias::Left);
                     let mut range_for_token = None;
-                    completions.into_iter().filter_map(move |lsp_completion| {
-                        // For now, we can only handle additional edits if they are returned
-                        // when resolving the completion, not if they are present initially.
-                        if lsp_completion
-                            .additional_text_edits
-                            .as_ref()
-                            .map_or(false, |edits| !edits.is_empty())
-                        {
-                            return None;
-                        }
+                    completions
+                        .into_iter()
+                        .filter_map(move |mut lsp_completion| {
+                            // For now, we can only handle additional edits if they are returned
+                            // when resolving the completion, not if they are present initially.
+                            if lsp_completion
+                                .additional_text_edits
+                                .as_ref()
+                                .map_or(false, |edits| !edits.is_empty())
+                            {
+                                return None;
+                            }
 
-                        let (old_range, mut new_text) = match lsp_completion.text_edit.as_ref() {
-                            // If the language server provides a range to overwrite, then
-                            // check that the range is valid.
-                            Some(lsp::CompletionTextEdit::Edit(edit)) => {
-                                let range = range_from_lsp(edit.range);
-                                let start = snapshot.clip_point_utf16(range.start, Bias::Left);
-                                let end = snapshot.clip_point_utf16(range.end, Bias::Left);
-                                if start != range.start || end != range.end {
-                                    log::info!("completion out of expected range");
-                                    return None;
+                            let (old_range, mut new_text) = match lsp_completion.text_edit.as_ref()
+                            {
+                                // If the language server provides a range to overwrite, then
+                                // check that the range is valid.
+                                Some(lsp::CompletionTextEdit::Edit(edit)) => {
+                                    let range = range_from_lsp(edit.range);
+                                    let start = snapshot.clip_point_utf16(range.start, Bias::Left);
+                                    let end = snapshot.clip_point_utf16(range.end, Bias::Left);
+                                    if start != range.start.0 || end != range.end.0 {
+                                        log::info!("completion out of expected range");
+                                        return None;
+                                    }
+                                    (
+                                        snapshot.anchor_before(start)..snapshot.anchor_after(end),
+                                        edit.new_text.clone(),
+                                    )
                                 }
-                                (
-                                    snapshot.anchor_before(start)..snapshot.anchor_after(end),
-                                    edit.new_text.clone(),
-                                )
-                            }
-                            // If the language server does not provide a range, then infer
-                            // the range based on the syntax tree.
-                            None => {
-                                if position != clipped_position {
-                                    log::info!("completion out of expected range");
+                                // If the language server does not provide a range, then infer
+                                // the range based on the syntax tree.
+                                None => {
+                                    if position.0 != clipped_position {
+                                        log::info!("completion out of expected range");
+                                        return None;
+                                    }
+                                    let Range { start, end } = range_for_token
+                                        .get_or_insert_with(|| {
+                                            let offset = position.to_offset(&snapshot);
+                                            let (range, kind) = snapshot.surrounding_word(offset);
+                                            if kind == Some(CharKind::Word) {
+                                                range
+                                            } else {
+                                                offset..offset
+                                            }
+                                        })
+                                        .clone();
+                                    let text = lsp_completion
+                                        .insert_text
+                                        .as_ref()
+                                        .unwrap_or(&lsp_completion.label)
+                                        .clone();
+                                    (
+                                        snapshot.anchor_before(start)..snapshot.anchor_after(end),
+                                        text,
+                                    )
+                                }
+                                Some(lsp::CompletionTextEdit::InsertAndReplace(_)) => {
+                                    log::info!("unsupported insert/replace completion");
                                     return None;
                                 }
-                                let Range { start, end } = range_for_token
-                                    .get_or_insert_with(|| {
-                                        let offset = position.to_offset(&snapshot);
-                                        let (range, kind) = snapshot.surrounding_word(offset);
-                                        if kind == Some(CharKind::Word) {
-                                            range
-                                        } else {
-                                            offset..offset
-                                        }
-                                    })
-                                    .clone();
-                                let text = lsp_completion
-                                    .insert_text
-                                    .as_ref()
-                                    .unwrap_or(&lsp_completion.label)
-                                    .clone();
-                                (
-                                    snapshot.anchor_before(start)..snapshot.anchor_after(end),
-                                    text,
-                                )
-                            }
-                            Some(lsp::CompletionTextEdit::InsertAndReplace(_)) => {
-                                log::info!("unsupported insert/replace completion");
-                                return None;
-                            }
-                        };
-
-                        LineEnding::normalize(&mut new_text);
-                        let language = language.clone();
-                        Some(async move {
-                            let label = if let Some(language) = language {
-                                language.label_for_completion(&lsp_completion).await
-                            } else {
-                                None
                             };
-                            Completion {
-                                old_range,
-                                new_text,
-                                label: label.unwrap_or_else(|| {
-                                    CodeLabel::plain(
-                                        lsp_completion.label.clone(),
-                                        lsp_completion.filter_text.as_deref(),
-                                    )
-                                }),
-                                lsp_completion,
-                            }
+
+                            LineEnding::normalize(&mut new_text);
+                            let language = language.clone();
+                            Some(async move {
+                                let mut label = None;
+                                if let Some(language) = language {
+                                    language.process_completion(&mut lsp_completion).await;
+                                    label = language.label_for_completion(&lsp_completion).await;
+                                }
+                                Completion {
+                                    old_range,
+                                    new_text,
+                                    label: label.unwrap_or_else(|| {
+                                        CodeLabel::plain(
+                                            lsp_completion.label.clone(),
+                                            lsp_completion.filter_text.as_deref(),
+                                        )
+                                    }),
+                                    lsp_completion,
+                                }
+                            })
                         })
-                    })
                 });
 
                 Ok(futures::future::join_all(completions).await)
@@ -3448,29 +3453,41 @@ impl Project {
         let buffer_id = buffer.remote_id();
 
         if self.is_local() {
-            let lang_server = if let Some((_, server)) = self.language_server_for_buffer(buffer, cx)
-            {
-                server.clone()
-            } else {
-                return Task::ready(Ok(Default::default()));
+            let lang_server = match self.language_server_for_buffer(buffer, cx) {
+                Some((_, server)) => server.clone(),
+                _ => return Task::ready(Ok(Default::default())),
             };
 
             cx.spawn(|this, mut cx| async move {
                 let resolved_completion = lang_server
                     .request::<lsp::request::ResolveCompletionItem>(completion.lsp_completion)
                     .await?;
+
                 if let Some(edits) = resolved_completion.additional_text_edits {
                     let edits = this
                         .update(&mut cx, |this, cx| {
                             this.edits_from_lsp(&buffer_handle, edits, None, cx)
                         })
                         .await?;
+
                     buffer_handle.update(&mut cx, |buffer, cx| {
                         buffer.finalize_last_transaction();
                         buffer.start_transaction();
+
                         for (range, text) in edits {
-                            buffer.edit([(range, text)], None, cx);
+                            let primary = &completion.old_range;
+                            let start_within = primary.start.cmp(&range.start, buffer).is_le()
+                                && primary.end.cmp(&range.start, buffer).is_ge();
+                            let end_within = range.start.cmp(&primary.end, buffer).is_le()
+                                && range.end.cmp(&primary.end, buffer).is_ge();
+
+                            //Skip addtional edits which overlap with the primary completion edit
+                            //https://github.com/zed-industries/zed/pull/1871
+                            if !start_within && !end_within {
+                                buffer.edit([(range, text)], None, cx);
+                            }
                         }
+
                         let transaction = if buffer.end_transaction(cx).is_some() {
                             let transaction = buffer.finalize_last_transaction().unwrap().clone();
                             if !push_to_history {
@@ -3568,7 +3585,13 @@ impl Project {
                         partial_result_params: Default::default(),
                         context: lsp::CodeActionContext {
                             diagnostics: relevant_diagnostics,
-                            only: None,
+                            only: Some(vec![
+                                lsp::CodeActionKind::EMPTY,
+                                lsp::CodeActionKind::QUICKFIX,
+                                lsp::CodeActionKind::REFACTOR,
+                                lsp::CodeActionKind::REFACTOR_EXTRACT,
+                                lsp::CodeActionKind::SOURCE,
+                            ]),
                         },
                     })
                     .await?
@@ -5111,22 +5134,30 @@ impl Project {
         _: Arc<Client>,
         mut cx: AsyncAppContext,
     ) -> Result<proto::GetCompletionsResponse> {
-        let position = envelope
-            .payload
-            .position
-            .and_then(language::proto::deserialize_anchor)
-            .ok_or_else(|| anyhow!("invalid position"))?;
-        let version = deserialize_version(envelope.payload.version);
         let buffer = this.read_with(&cx, |this, cx| {
             this.opened_buffers
                 .get(&envelope.payload.buffer_id)
                 .and_then(|buffer| buffer.upgrade(cx))
                 .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id))
         })?;
+
+        let position = envelope
+            .payload
+            .position
+            .and_then(language::proto::deserialize_anchor)
+            .map(|p| {
+                buffer.read_with(&cx, |buffer, _| {
+                    buffer.clip_point_utf16(Unclipped(p.to_point_utf16(buffer)), Bias::Left)
+                })
+            })
+            .ok_or_else(|| anyhow!("invalid position"))?;
+
+        let version = deserialize_version(envelope.payload.version);
         buffer
             .update(&mut cx, |buffer, _| buffer.wait_for_version(version))
             .await;
         let version = buffer.read_with(&cx, |buffer, _| buffer.version());
+
         let completions = this
             .update(&mut cx, |this, cx| this.completions(&buffer, position, cx))
             .await?;
@@ -5613,8 +5644,8 @@ impl Project {
                 },
 
                 name: serialized_symbol.name,
-                range: PointUtf16::new(start.row, start.column)
-                    ..PointUtf16::new(end.row, end.column),
+                range: Unclipped(PointUtf16::new(start.row, start.column))
+                    ..Unclipped(PointUtf16::new(end.row, end.column)),
                 kind,
                 signature: serialized_symbol
                     .signature
@@ -5700,10 +5731,10 @@ impl Project {
 
             let mut lsp_edits = lsp_edits.into_iter().peekable();
             let mut edits = Vec::new();
-            while let Some((mut range, mut new_text)) = lsp_edits.next() {
+            while let Some((range, mut new_text)) = lsp_edits.next() {
                 // Clip invalid ranges provided by the language server.
-                range.start = snapshot.clip_point_utf16(range.start, Bias::Left);
-                range.end = snapshot.clip_point_utf16(range.end, Bias::Left);
+                let mut range = snapshot.clip_point_utf16(range.start, Bias::Left)
+                    ..snapshot.clip_point_utf16(range.end, Bias::Left);
 
                 // Combine any LSP edits that are adjacent.
                 //
@@ -5715,11 +5746,11 @@ impl Project {
                 // In order for the diffing logic below to work properly, any edits that
                 // cancel each other out must be combined into one.
                 while let Some((next_range, next_text)) = lsp_edits.peek() {
-                    if next_range.start > range.end {
-                        if next_range.start.row > range.end.row + 1
-                            || next_range.start.column > 0
+                    if next_range.start.0 > range.end {
+                        if next_range.start.0.row > range.end.row + 1
+                            || next_range.start.0.column > 0
                             || snapshot.clip_point_utf16(
-                                PointUtf16::new(range.end.row, u32::MAX),
+                                Unclipped(PointUtf16::new(range.end.row, u32::MAX)),
                                 Bias::Left,
                             ) > range.end
                         {
@@ -5727,7 +5758,7 @@ impl Project {
                         }
                         new_text.push('\n');
                     }
-                    range.end = next_range.end;
+                    range.end = snapshot.clip_point_utf16(next_range.end, Bias::Left);
                     new_text.push_str(next_text);
                     lsp_edits.next();
                 }
@@ -6000,13 +6031,13 @@ fn serialize_symbol(symbol: &Symbol) -> proto::Symbol {
         path: symbol.path.path.to_string_lossy().to_string(),
         name: symbol.name.clone(),
         kind: unsafe { mem::transmute(symbol.kind) },
-        start: Some(proto::Point {
-            row: symbol.range.start.row,
-            column: symbol.range.start.column,
+        start: Some(proto::PointUtf16 {
+            row: symbol.range.start.0.row,
+            column: symbol.range.start.0.column,
         }),
-        end: Some(proto::Point {
-            row: symbol.range.end.row,
-            column: symbol.range.end.column,
+        end: Some(proto::PointUtf16 {
+            row: symbol.range.end.0.row,
+            column: symbol.range.end.0.column,
         }),
         signature: symbol.signature.to_vec(),
     }

crates/project/src/project_tests.rs 🔗

@@ -1239,7 +1239,7 @@ async fn test_empty_diagnostic_ranges(cx: &mut gpui::TestAppContext) {
                 &buffer,
                 vec![
                     DiagnosticEntry {
-                        range: PointUtf16::new(0, 10)..PointUtf16::new(0, 10),
+                        range: Unclipped(PointUtf16::new(0, 10))..Unclipped(PointUtf16::new(0, 10)),
                         diagnostic: Diagnostic {
                             severity: DiagnosticSeverity::ERROR,
                             message: "syntax error 1".to_string(),
@@ -1247,7 +1247,7 @@ async fn test_empty_diagnostic_ranges(cx: &mut gpui::TestAppContext) {
                         },
                     },
                     DiagnosticEntry {
-                        range: PointUtf16::new(1, 10)..PointUtf16::new(1, 10),
+                        range: Unclipped(PointUtf16::new(1, 10))..Unclipped(PointUtf16::new(1, 10)),
                         diagnostic: Diagnostic {
                             severity: DiagnosticSeverity::ERROR,
                             message: "syntax error 2".to_string(),

crates/project/src/worktree.rs 🔗

@@ -20,6 +20,7 @@ use gpui::{
     executor, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext,
     Task,
 };
+use language::Unclipped;
 use language::{
     proto::{deserialize_version, serialize_line_ending, serialize_version},
     Buffer, DiagnosticEntry, PointUtf16, Rope,
@@ -64,7 +65,7 @@ pub struct LocalWorktree {
     _background_scanner_task: Option<Task<()>>,
     poll_task: Option<Task<()>>,
     share: Option<ShareState>,
-    diagnostics: HashMap<Arc<Path>, Vec<DiagnosticEntry<PointUtf16>>>,
+    diagnostics: HashMap<Arc<Path>, Vec<DiagnosticEntry<Unclipped<PointUtf16>>>>,
     diagnostic_summaries: TreeMap<PathKey, DiagnosticSummary>,
     client: Arc<Client>,
     fs: Arc<dyn Fs>,
@@ -502,7 +503,10 @@ impl LocalWorktree {
         })
     }
 
-    pub fn diagnostics_for_path(&self, path: &Path) -> Option<Vec<DiagnosticEntry<PointUtf16>>> {
+    pub fn diagnostics_for_path(
+        &self,
+        path: &Path,
+    ) -> Option<Vec<DiagnosticEntry<Unclipped<PointUtf16>>>> {
         self.diagnostics.get(path).cloned()
     }
 
@@ -510,7 +514,7 @@ impl LocalWorktree {
         &mut self,
         language_server_id: usize,
         worktree_path: Arc<Path>,
-        diagnostics: Vec<DiagnosticEntry<PointUtf16>>,
+        diagnostics: Vec<DiagnosticEntry<Unclipped<PointUtf16>>>,
         _: &mut ModelContext<Worktree>,
     ) -> Result<bool> {
         self.diagnostics.remove(&worktree_path);
@@ -1168,6 +1172,10 @@ impl Snapshot {
         self.id
     }
 
+    pub fn abs_path(&self) -> &Arc<Path> {
+        &self.abs_path
+    }
+
     pub fn contains_entry(&self, entry_id: ProjectEntryId) -> bool {
         self.entries_by_id.get(&entry_id, &()).is_some()
     }
@@ -1359,10 +1367,6 @@ impl Snapshot {
 }
 
 impl LocalSnapshot {
-    pub fn abs_path(&self) -> &Arc<Path> {
-        &self.abs_path
-    }
-
     pub fn extension_counts(&self) -> &HashMap<OsString, usize> {
         &self.extension_counts
     }

crates/project_panel/src/project_panel.rs 🔗

@@ -43,6 +43,7 @@ pub struct ProjectPanel {
     filename_editor: ViewHandle<Editor>,
     clipboard_entry: Option<ClipboardEntry>,
     context_menu: ViewHandle<ContextMenu>,
+    dragged_entry_destination: Option<Arc<Path>>,
 }
 
 #[derive(Copy, Clone)]
@@ -95,6 +96,13 @@ pub struct Open {
     pub change_focus: bool,
 }
 
+#[derive(Clone, PartialEq)]
+pub struct MoveProjectEntry {
+    pub entry_to_move: ProjectEntryId,
+    pub destination: ProjectEntryId,
+    pub destination_is_file: bool,
+}
+
 #[derive(Clone, PartialEq)]
 pub struct DeployContextMenu {
     pub position: Vector2F,
@@ -117,7 +125,10 @@ actions!(
         ToggleFocus
     ]
 );
-impl_internal_actions!(project_panel, [Open, ToggleExpanded, DeployContextMenu]);
+impl_internal_actions!(
+    project_panel,
+    [Open, ToggleExpanded, DeployContextMenu, MoveProjectEntry]
+);
 
 pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(ProjectPanel::deploy_context_menu);
@@ -141,6 +152,7 @@ pub fn init(cx: &mut MutableAppContext) {
             this.paste(action, cx);
         },
     );
+    cx.add_action(ProjectPanel::move_entry);
 }
 
 pub enum Event {
@@ -219,6 +231,7 @@ impl ProjectPanel {
                 filename_editor,
                 clipboard_entry: None,
                 context_menu: cx.add_view(ContextMenu::new),
+                dragged_entry_destination: None,
             };
             this.update_visible_entries(None, cx);
             this
@@ -774,6 +787,39 @@ impl ProjectPanel {
         }
     }
 
+    fn move_entry(
+        &mut self,
+        &MoveProjectEntry {
+            entry_to_move,
+            destination,
+            destination_is_file,
+        }: &MoveProjectEntry,
+        cx: &mut ViewContext<Self>,
+    ) {
+        let destination_worktree = self.project.update(cx, |project, cx| {
+            let entry_path = project.path_for_entry(entry_to_move, cx)?;
+            let destination_entry_path = project.path_for_entry(destination, cx)?.path.clone();
+
+            let mut destination_path = destination_entry_path.as_ref();
+            if destination_is_file {
+                destination_path = destination_path.parent()?;
+            }
+
+            let mut new_path = destination_path.to_path_buf();
+            new_path.push(entry_path.path.file_name()?);
+            if new_path != entry_path.path.as_ref() {
+                let task = project.rename_entry(entry_to_move, new_path, cx)?;
+                cx.foreground().spawn(task).detach_and_log_err(cx);
+            }
+
+            Some(project.worktree_id_for_entry(destination, cx)?)
+        });
+
+        if let Some(destination_worktree) = destination_worktree {
+            self.expand_entry(destination_worktree, destination, cx);
+        }
+    }
+
     fn index_for_selection(&self, selection: Selection) -> Option<(usize, usize, usize)> {
         let mut entry_index = 0;
         let mut visible_entries_index = 0;
@@ -1079,10 +1125,13 @@ impl ProjectPanel {
         entry_id: ProjectEntryId,
         details: EntryDetails,
         editor: &ViewHandle<Editor>,
+        dragged_entry_destination: &mut Option<Arc<Path>>,
         theme: &theme::ProjectPanel,
         cx: &mut RenderContext<Self>,
     ) -> ElementBox {
+        let this = cx.handle();
         let kind = details.kind;
+        let path = details.path.clone();
         let padding = theme.container.padding.left + details.depth as f32 * theme.indent_width;
 
         let entry_style = if details.is_cut {
@@ -1096,7 +1145,20 @@ impl ProjectPanel {
         let show_editor = details.is_editing && !details.is_processing;
 
         MouseEventHandler::<Self>::new(entry_id.to_usize(), cx, |state, cx| {
-            let style = entry_style.style_for(state, details.is_selected).clone();
+            let mut style = entry_style.style_for(state, details.is_selected).clone();
+
+            if cx
+                .global::<DragAndDrop<Workspace>>()
+                .currently_dragged::<ProjectEntryId>(cx.window_id())
+                .is_some()
+                && dragged_entry_destination
+                    .as_ref()
+                    .filter(|destination| details.path.starts_with(destination))
+                    .is_some()
+            {
+                style = entry_style.active.clone().unwrap();
+            }
+
             let row_container_style = if show_editor {
                 theme.filename_editor.container
             } else {
@@ -1128,6 +1190,35 @@ impl ProjectPanel {
                 position: e.position,
             })
         })
+        .on_up(MouseButton::Left, move |_, cx| {
+            if let Some((_, dragged_entry)) = cx
+                .global::<DragAndDrop<Workspace>>()
+                .currently_dragged::<ProjectEntryId>(cx.window_id())
+            {
+                cx.dispatch_action(MoveProjectEntry {
+                    entry_to_move: *dragged_entry,
+                    destination: entry_id,
+                    destination_is_file: matches!(details.kind, EntryKind::File(_)),
+                });
+            }
+        })
+        .on_move(move |_, cx| {
+            if cx
+                .global::<DragAndDrop<Workspace>>()
+                .currently_dragged::<ProjectEntryId>(cx.window_id())
+                .is_some()
+            {
+                if let Some(this) = this.upgrade(cx.app) {
+                    this.update(cx.app, |this, _| {
+                        this.dragged_entry_destination = if matches!(kind, EntryKind::File(_)) {
+                            path.parent().map(|parent| Arc::from(parent))
+                        } else {
+                            Some(path.clone())
+                        };
+                    })
+                }
+            }
+        })
         .as_draggable(entry_id, {
             let row_container_style = theme.dragged_entry.container;
 
@@ -1154,14 +1245,15 @@ impl View for ProjectPanel {
     }
 
     fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox {
-        enum Tag {}
+        enum ProjectPanel {}
         let theme = &cx.global::<Settings>().theme.project_panel;
         let mut container_style = theme.container;
         let padding = std::mem::take(&mut container_style.padding);
         let last_worktree_root_id = self.last_worktree_root_id;
+
         Stack::new()
             .with_child(
-                MouseEventHandler::<Tag>::new(0, cx, |_, cx| {
+                MouseEventHandler::<ProjectPanel>::new(0, cx, |_, cx| {
                     UniformList::new(
                         self.list.clone(),
                         self.visible_entries
@@ -1171,15 +1263,19 @@ impl View for ProjectPanel {
                         cx,
                         move |this, range, items, cx| {
                             let theme = cx.global::<Settings>().theme.clone();
+                            let mut dragged_entry_destination =
+                                this.dragged_entry_destination.clone();
                             this.for_each_visible_entry(range, cx, |id, details, cx| {
                                 items.push(Self::render_entry(
                                     id,
                                     details,
                                     &this.filename_editor,
+                                    &mut dragged_entry_destination,
                                     &theme.project_panel,
                                     cx,
                                 ));
                             });
+                            this.dragged_entry_destination = dragged_entry_destination;
                         },
                     )
                     .with_padding_top(padding.top)

crates/project_symbols/Cargo.toml 🔗

@@ -28,4 +28,4 @@ settings = { path = "../settings", features = ["test-support"] }
 gpui = { path = "../gpui", features = ["test-support"] }
 language = { path = "../language", features = ["test-support"] }
 lsp = { path = "../lsp", features = ["test-support"] }
-project = { path = "../project", features = ["test-support"] }
+project = { path = "../project", features = ["test-support"] }

crates/rope/Cargo.toml 🔗

@@ -12,7 +12,7 @@ smallvec = { version = "1.6", features = ["union"] }
 sum_tree = { path = "../sum_tree" }
 arrayvec = "0.7.1"
 log = { version = "0.4.16", features = ["kv_unstable_serde"] }
-
+util = { path = "../util" }
 
 [dev-dependencies]
 rand = "0.8.3"

crates/rope/src/rope.rs 🔗

@@ -1,16 +1,23 @@
 mod offset_utf16;
 mod point;
 mod point_utf16;
+mod unclipped;
 
 use arrayvec::ArrayString;
 use bromberg_sl2::{DigestString, HashMatrix};
 use smallvec::SmallVec;
-use std::{cmp, fmt, io, mem, ops::Range, str};
+use std::{
+    cmp, fmt, io, mem,
+    ops::{AddAssign, Range},
+    str,
+};
 use sum_tree::{Bias, Dimension, SumTree};
+use util::debug_panic;
 
 pub use offset_utf16::OffsetUtf16;
 pub use point::Point;
 pub use point_utf16::PointUtf16;
+pub use unclipped::Unclipped;
 
 #[cfg(test)]
 const CHUNK_BASE: usize = 6;
@@ -260,6 +267,14 @@ impl Rope {
     }
 
     pub fn point_utf16_to_offset(&self, point: PointUtf16) -> usize {
+        self.point_utf16_to_offset_impl(point, false)
+    }
+
+    pub fn unclipped_point_utf16_to_offset(&self, point: Unclipped<PointUtf16>) -> usize {
+        self.point_utf16_to_offset_impl(point.0, true)
+    }
+
+    fn point_utf16_to_offset_impl(&self, point: PointUtf16, clip: bool) -> usize {
         if point >= self.summary().lines_utf16() {
             return self.summary().len;
         }
@@ -269,20 +284,20 @@ impl Rope {
         cursor.start().1
             + cursor
                 .item()
-                .map_or(0, |chunk| chunk.point_utf16_to_offset(overshoot))
+                .map_or(0, |chunk| chunk.point_utf16_to_offset(overshoot, clip))
     }
 
-    pub fn point_utf16_to_point(&self, point: PointUtf16) -> Point {
-        if point >= self.summary().lines_utf16() {
+    pub fn unclipped_point_utf16_to_point(&self, point: Unclipped<PointUtf16>) -> Point {
+        if point.0 >= self.summary().lines_utf16() {
             return self.summary().lines;
         }
         let mut cursor = self.chunks.cursor::<(PointUtf16, Point)>();
-        cursor.seek(&point, Bias::Left, &());
-        let overshoot = point - cursor.start().0;
+        cursor.seek(&point.0, Bias::Left, &());
+        let overshoot = Unclipped(point.0 - cursor.start().0);
         cursor.start().1
-            + cursor
-                .item()
-                .map_or(Point::zero(), |chunk| chunk.point_utf16_to_point(overshoot))
+            + cursor.item().map_or(Point::zero(), |chunk| {
+                chunk.unclipped_point_utf16_to_point(overshoot)
+            })
     }
 
     pub fn clip_offset(&self, mut offset: usize, bias: Bias) -> usize {
@@ -330,11 +345,11 @@ impl Rope {
         }
     }
 
-    pub fn clip_point_utf16(&self, point: PointUtf16, bias: Bias) -> PointUtf16 {
+    pub fn clip_point_utf16(&self, point: Unclipped<PointUtf16>, bias: Bias) -> PointUtf16 {
         let mut cursor = self.chunks.cursor::<PointUtf16>();
-        cursor.seek(&point, Bias::Right, &());
+        cursor.seek(&point.0, Bias::Right, &());
         if let Some(chunk) = cursor.item() {
-            let overshoot = point - cursor.start();
+            let overshoot = Unclipped(point.0 - cursor.start());
             *cursor.start() + chunk.clip_point_utf16(overshoot, bias)
         } else {
             self.summary().lines_utf16()
@@ -665,28 +680,33 @@ impl Chunk {
     fn point_to_offset(&self, target: Point) -> usize {
         let mut offset = 0;
         let mut point = Point::new(0, 0);
+
         for ch in self.0.chars() {
             if point >= target {
                 if point > target {
-                    panic!("point {:?} is inside of character {:?}", target, ch);
+                    debug_panic!("point {target:?} is inside of character {ch:?}");
                 }
                 break;
             }
 
             if ch == '\n' {
                 point.row += 1;
+                point.column = 0;
+
                 if point.row > target.row {
-                    panic!(
-                        "point {:?} is beyond the end of a line with length {}",
-                        target, point.column
+                    debug_panic!(
+                        "point {target:?} is beyond the end of a line with length {}",
+                        point.column
                     );
+                    break;
                 }
-                point.column = 0;
             } else {
                 point.column += ch.len_utf8() as u32;
             }
+
             offset += ch.len_utf8();
         }
+
         offset
     }
 
@@ -711,45 +731,62 @@ impl Chunk {
         point_utf16
     }
 
-    fn point_utf16_to_offset(&self, target: PointUtf16) -> usize {
+    fn point_utf16_to_offset(&self, target: PointUtf16, clip: bool) -> usize {
         let mut offset = 0;
         let mut point = PointUtf16::new(0, 0);
+
         for ch in self.0.chars() {
-            if point >= target {
-                if point > target {
-                    panic!("point {:?} is inside of character {:?}", target, ch);
-                }
+            if point == target {
                 break;
             }
 
             if ch == '\n' {
                 point.row += 1;
+                point.column = 0;
+
                 if point.row > target.row {
-                    panic!(
-                        "point {:?} is beyond the end of a line with length {}",
-                        target, point.column
-                    );
+                    if !clip {
+                        debug_panic!(
+                            "point {target:?} is beyond the end of a line with length {}",
+                            point.column
+                        );
+                    }
+                    // Return the offset of the newline
+                    return offset;
                 }
-                point.column = 0;
             } else {
                 point.column += ch.len_utf16() as u32;
             }
+
+            if point > target {
+                if !clip {
+                    debug_panic!("point {target:?} is inside of codepoint {ch:?}");
+                }
+                // Return the offset of the codepoint which we have landed within, bias left
+                return offset;
+            }
+
             offset += ch.len_utf8();
         }
+
         offset
     }
 
-    fn point_utf16_to_point(&self, target: PointUtf16) -> Point {
+    fn unclipped_point_utf16_to_point(&self, target: Unclipped<PointUtf16>) -> Point {
         let mut point = Point::zero();
         let mut point_utf16 = PointUtf16::zero();
+
         for ch in self.0.chars() {
-            if point_utf16 >= target {
-                if point_utf16 > target {
-                    panic!("point {:?} is inside of character {:?}", target, ch);
-                }
+            if point_utf16 == target.0 {
                 break;
             }
 
+            if point_utf16 > target.0 {
+                // If the point is past the end of a line or inside of a code point,
+                // return the last valid point before the target.
+                return point;
+            }
+
             if ch == '\n' {
                 point_utf16 += PointUtf16::new(1, 0);
                 point += Point::new(1, 0);
@@ -758,6 +795,7 @@ impl Chunk {
                 point += Point::new(0, ch.len_utf8() as u32);
             }
         }
+
         point
     }
 
@@ -777,11 +815,11 @@ impl Chunk {
         unreachable!()
     }
 
-    fn clip_point_utf16(&self, target: PointUtf16, bias: Bias) -> PointUtf16 {
+    fn clip_point_utf16(&self, target: Unclipped<PointUtf16>, bias: Bias) -> PointUtf16 {
         for (row, line) in self.0.split('\n').enumerate() {
-            if row == target.row as usize {
+            if row == target.0.row as usize {
                 let mut code_units = line.encode_utf16();
-                let mut column = code_units.by_ref().take(target.column as usize).count();
+                let mut column = code_units.by_ref().take(target.0.column as usize).count();
                 if char::decode_utf16(code_units).next().transpose().is_err() {
                     match bias {
                         Bias::Left => column -= 1,
@@ -917,7 +955,7 @@ impl std::ops::Add<Self> for TextSummary {
     type Output = Self;
 
     fn add(mut self, rhs: Self) -> Self::Output {
-        self.add_assign(&rhs);
+        AddAssign::add_assign(&mut self, &rhs);
         self
     }
 }
@@ -1114,15 +1152,15 @@ mod tests {
         );
 
         assert_eq!(
-            rope.clip_point_utf16(PointUtf16::new(0, 1), Bias::Left),
+            rope.clip_point_utf16(Unclipped(PointUtf16::new(0, 1)), Bias::Left),
             PointUtf16::new(0, 0)
         );
         assert_eq!(
-            rope.clip_point_utf16(PointUtf16::new(0, 1), Bias::Right),
+            rope.clip_point_utf16(Unclipped(PointUtf16::new(0, 1)), Bias::Right),
             PointUtf16::new(0, 2)
         );
         assert_eq!(
-            rope.clip_point_utf16(PointUtf16::new(0, 3), Bias::Right),
+            rope.clip_point_utf16(Unclipped(PointUtf16::new(0, 3)), Bias::Right),
             PointUtf16::new(0, 2)
         );
 
@@ -1238,7 +1276,7 @@ mod tests {
             }
 
             let mut offset_utf16 = OffsetUtf16(0);
-            let mut point_utf16 = PointUtf16::zero();
+            let mut point_utf16 = Unclipped(PointUtf16::zero());
             for unit in expected.encode_utf16() {
                 let left_offset = actual.clip_offset_utf16(offset_utf16, Bias::Left);
                 let right_offset = actual.clip_offset_utf16(offset_utf16, Bias::Right);
@@ -1250,15 +1288,15 @@ mod tests {
                 let left_point = actual.clip_point_utf16(point_utf16, Bias::Left);
                 let right_point = actual.clip_point_utf16(point_utf16, Bias::Right);
                 assert!(right_point >= left_point);
-                // Ensure translating UTF-16 points to offsets doesn't panic.
+                // Ensure translating valid UTF-16 points to offsets doesn't panic.
                 actual.point_utf16_to_offset(left_point);
                 actual.point_utf16_to_offset(right_point);
 
                 offset_utf16.0 += 1;
                 if unit == b'\n' as u16 {
-                    point_utf16 += PointUtf16::new(1, 0);
+                    point_utf16.0 += PointUtf16::new(1, 0);
                 } else {
-                    point_utf16 += PointUtf16::new(0, 1);
+                    point_utf16.0 += PointUtf16::new(0, 1);
                 }
             }
 

crates/rope/src/unclipped.rs 🔗

@@ -0,0 +1,57 @@
+use crate::{ChunkSummary, TextDimension, TextSummary};
+use std::ops::{Add, AddAssign, Sub, SubAssign};
+
+#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct Unclipped<T>(pub T);
+
+impl<T> From<T> for Unclipped<T> {
+    fn from(value: T) -> Self {
+        Unclipped(value)
+    }
+}
+
+impl<'a, T: sum_tree::Dimension<'a, ChunkSummary>> sum_tree::Dimension<'a, ChunkSummary>
+    for Unclipped<T>
+{
+    fn add_summary(&mut self, summary: &'a ChunkSummary, _: &()) {
+        self.0.add_summary(summary, &());
+    }
+}
+
+impl<T: TextDimension> TextDimension for Unclipped<T> {
+    fn from_text_summary(summary: &TextSummary) -> Self {
+        Unclipped(T::from_text_summary(summary))
+    }
+
+    fn add_assign(&mut self, other: &Self) {
+        TextDimension::add_assign(&mut self.0, &other.0);
+    }
+}
+
+impl<T: Add<T, Output = T>> Add<Unclipped<T>> for Unclipped<T> {
+    type Output = Unclipped<T>;
+
+    fn add(self, rhs: Unclipped<T>) -> Self::Output {
+        Unclipped(self.0 + rhs.0)
+    }
+}
+
+impl<T: Sub<T, Output = T>> Sub<Unclipped<T>> for Unclipped<T> {
+    type Output = Unclipped<T>;
+
+    fn sub(self, rhs: Unclipped<T>) -> Self::Output {
+        Unclipped(self.0 - rhs.0)
+    }
+}
+
+impl<T: AddAssign<T>> AddAssign<Unclipped<T>> for Unclipped<T> {
+    fn add_assign(&mut self, rhs: Unclipped<T>) {
+        self.0 += rhs.0;
+    }
+}
+
+impl<T: SubAssign<T>> SubAssign<Unclipped<T>> for Unclipped<T> {
+    fn sub_assign(&mut self, rhs: Unclipped<T>) {
+        self.0 -= rhs.0;
+    }
+}

crates/rpc/proto/zed.proto 🔗

@@ -403,8 +403,10 @@ message Symbol {
     string name = 4;
     int32 kind = 5;
     string path = 6;
-    Point start = 7;
-    Point end = 8;
+    // Cannot use generate anchors for unopend files,
+    // so we are forced to use point coords instead
+    PointUtf16 start = 7;
+    PointUtf16 end = 8;
     bytes signature = 9;
 }
 
@@ -1033,7 +1035,7 @@ message Range {
     uint64 end = 2;
 }
 
-message Point {
+message PointUtf16 {
     uint32 row = 1;
     uint32 column = 2;
 }

crates/settings/src/settings.rs 🔗

@@ -28,6 +28,7 @@ pub struct Settings {
     pub buffer_font_family: FamilyId,
     pub default_buffer_font_size: f32,
     pub buffer_font_size: f32,
+    pub active_pane_magnification: f32,
     pub cursor_blink: bool,
     pub hover_popover_enabled: bool,
     pub show_completions_on_input: bool,
@@ -253,6 +254,8 @@ pub struct SettingsFileContent {
     #[serde(default)]
     pub buffer_font_size: Option<f32>,
     #[serde(default)]
+    pub active_pane_magnification: Option<f32>,
+    #[serde(default)]
     pub cursor_blink: Option<bool>,
     #[serde(default)]
     pub hover_popover_enabled: Option<bool>,
@@ -312,6 +315,7 @@ impl Settings {
                 .load_family(&[defaults.buffer_font_family.as_ref().unwrap()])
                 .unwrap(),
             buffer_font_size: defaults.buffer_font_size.unwrap(),
+            active_pane_magnification: defaults.active_pane_magnification.unwrap(),
             default_buffer_font_size: defaults.buffer_font_size.unwrap(),
             cursor_blink: defaults.cursor_blink.unwrap(),
             hover_popover_enabled: defaults.hover_popover_enabled.unwrap(),
@@ -367,6 +371,10 @@ impl Settings {
             data.projects_online_by_default,
         );
         merge(&mut self.buffer_font_size, data.buffer_font_size);
+        merge(
+            &mut self.active_pane_magnification,
+            data.active_pane_magnification,
+        );
         merge(&mut self.default_buffer_font_size, data.buffer_font_size);
         merge(&mut self.cursor_blink, data.cursor_blink);
         merge(&mut self.hover_popover_enabled, data.hover_popover_enabled);
@@ -458,6 +466,7 @@ impl Settings {
             experiments: FeatureFlags::default(),
             buffer_font_family: cx.font_cache().load_family(&["Monaco"]).unwrap(),
             buffer_font_size: 14.,
+            active_pane_magnification: 1.,
             default_buffer_font_size: 14.,
             cursor_blink: true,
             hover_popover_enabled: true,

crates/terminal/src/mappings/keys.rs 🔗

@@ -36,18 +36,6 @@ impl Modifiers {
     }
 }
 
-///This function checks if to_esc_str would work, assuming all terminal settings are off.
-///Note that this function is conservative. It can fail in cases where the actual to_esc_str succeeds.
-///This is unavoidable for our use case. GPUI cannot wait until we acquire the terminal
-///lock to determine whether we could actually send the keystroke with the current settings. Therefore,
-///This conservative guess is used instead. Note that in practice the case where this method
-///Returns false when the actual terminal would consume the keystroke never happens. All keystrokes
-///that depend on terminal modes also have a mapping that doesn't depend on the terminal mode.
-///This is fragile, but as these mappings are locked up in legacy compatibility, it's probably good enough
-pub fn might_convert(keystroke: &Keystroke) -> bool {
-    to_esc_str(keystroke, &TermMode::NONE, false).is_some()
-}
-
 pub fn to_esc_str(keystroke: &Keystroke, mode: &TermMode, alt_is_meta: bool) -> Option<String> {
     let modifiers = Modifiers::new(keystroke);
 

crates/terminal/src/mappings/mouse.rs 🔗

@@ -97,7 +97,7 @@ impl MouseButton {
     }
 
     fn from_scroll(e: &ScrollWheelEvent) -> Self {
-        if e.delta.y() > 0. {
+        if e.delta.raw().y() > 0. {
             MouseButton::ScrollUp
         } else {
             MouseButton::ScrollDown

crates/terminal/src/terminal.rs 🔗

@@ -407,13 +407,18 @@ impl TerminalBuilder {
                 'outer: loop {
                     let mut events = vec![];
                     let mut timer = cx.background().timer(Duration::from_millis(4)).fuse();
-
+                    let mut wakeup = false;
                     loop {
                         futures::select_biased! {
                             _ = timer => break,
                             event = self.events_rx.next() => {
                                 if let Some(event) = event {
-                                    events.push(event);
+                                    if matches!(event, AlacTermEvent::Wakeup) {
+                                        wakeup = true;
+                                    } else {
+                                        events.push(event);
+                                    }
+
                                     if events.len() > 100 {
                                         break;
                                     }
@@ -424,11 +429,15 @@ impl TerminalBuilder {
                         }
                     }
 
-                    if events.is_empty() {
+                    if events.is_empty() && wakeup == false {
                         smol::future::yield_now().await;
                         break 'outer;
                     } else {
                         this.upgrade(&cx)?.update(&mut cx, |this, cx| {
+                            if wakeup {
+                                this.process_event(&AlacTermEvent::Wakeup, cx);
+                            }
+
                             for event in events {
                                 this.process_event(&event, cx);
                             }
@@ -627,7 +636,7 @@ impl Terminal {
                 term.grid_mut().reset_region(..cursor.line);
 
                 // Copy the current line up
-                let line = term.grid()[cursor.line][..cursor.column]
+                let line = term.grid()[cursor.line][..Column(term.grid().columns())]
                     .iter()
                     .cloned()
                     .enumerate()
@@ -1136,7 +1145,7 @@ impl Terminal {
 
     fn determine_scroll_lines(&mut self, e: &MouseScrollWheel, mouse_mode: bool) -> Option<i32> {
         let scroll_multiplier = if mouse_mode { 1. } else { SCROLL_MULTIPLIER };
-
+        let line_height = self.last_content.size.line_height;
         match e.phase {
             /* Reset scroll state on started */
             Some(gpui::TouchPhase::Started) => {
@@ -1145,11 +1154,11 @@ impl Terminal {
             }
             /* Calculate the appropriate scroll lines */
             Some(gpui::TouchPhase::Moved) => {
-                let old_offset = (self.scroll_px / self.last_content.size.line_height) as i32;
+                let old_offset = (self.scroll_px / line_height) as i32;
 
-                self.scroll_px += e.delta.y() * scroll_multiplier;
+                self.scroll_px += e.delta.pixel_delta(line_height).y() * scroll_multiplier;
 
-                let new_offset = (self.scroll_px / self.last_content.size.line_height) as i32;
+                let new_offset = (self.scroll_px / line_height) as i32;
 
                 // Whenever we hit the edges, reset our stored scroll to 0
                 // so we can respond to changes in direction quickly
@@ -1159,7 +1168,7 @@ impl Terminal {
             }
             /* Fall back to delta / line_height */
             None => Some(
-                ((e.delta.y() * scroll_multiplier) / self.last_content.size.line_height) as i32,
+                ((e.delta.pixel_delta(line_height).y() * scroll_multiplier) / line_height) as i32,
             ),
             _ => None,
         }

crates/text/src/locator.rs 🔗

@@ -3,8 +3,8 @@ use smallvec::{smallvec, SmallVec};
 use std::iter;
 
 lazy_static! {
-    pub static ref MIN: Locator = Locator::min();
-    pub static ref MAX: Locator = Locator::max();
+    static ref MIN: Locator = Locator::min();
+    static ref MAX: Locator = Locator::max();
 }
 
 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
@@ -19,6 +19,14 @@ impl Locator {
         Self(smallvec![u64::MAX])
     }
 
+    pub fn min_ref() -> &'static Self {
+        &*MIN
+    }
+
+    pub fn max_ref() -> &'static Self {
+        &*MAX
+    }
+
     pub fn assign(&mut self, other: &Self) {
         self.0.resize(other.0.len(), 0);
         self.0.copy_from_slice(&other.0);

crates/text/src/text.rs 🔗

@@ -1594,8 +1594,12 @@ impl BufferSnapshot {
         self.visible_text.point_utf16_to_offset(point)
     }
 
-    pub fn point_utf16_to_point(&self, point: PointUtf16) -> Point {
-        self.visible_text.point_utf16_to_point(point)
+    pub fn unclipped_point_utf16_to_offset(&self, point: Unclipped<PointUtf16>) -> usize {
+        self.visible_text.unclipped_point_utf16_to_offset(point)
+    }
+
+    pub fn unclipped_point_utf16_to_point(&self, point: Unclipped<PointUtf16>) -> Point {
+        self.visible_text.unclipped_point_utf16_to_point(point)
     }
 
     pub fn offset_utf16_to_offset(&self, offset: OffsetUtf16) -> usize {
@@ -1766,9 +1770,9 @@ impl BufferSnapshot {
 
     fn fragment_id_for_anchor(&self, anchor: &Anchor) -> &Locator {
         if *anchor == Anchor::MIN {
-            &locator::MIN
+            Locator::min_ref()
         } else if *anchor == Anchor::MAX {
-            &locator::MAX
+            Locator::max_ref()
         } else {
             let anchor_key = InsertionFragmentKey {
                 timestamp: anchor.timestamp,
@@ -1803,7 +1807,10 @@ impl BufferSnapshot {
     }
 
     pub fn anchor_at<T: ToOffset>(&self, position: T, bias: Bias) -> Anchor {
-        let offset = position.to_offset(self);
+        self.anchor_at_offset(position.to_offset(self), bias)
+    }
+
+    fn anchor_at_offset(&self, offset: usize, bias: Bias) -> Anchor {
         if bias == Bias::Left && offset == 0 {
             Anchor::MIN
         } else if bias == Bias::Right && offset == self.len() {
@@ -1840,7 +1847,7 @@ impl BufferSnapshot {
         self.visible_text.clip_offset_utf16(offset, bias)
     }
 
-    pub fn clip_point_utf16(&self, point: PointUtf16, bias: Bias) -> PointUtf16 {
+    pub fn clip_point_utf16(&self, point: Unclipped<PointUtf16>, bias: Bias) -> PointUtf16 {
         self.visible_text.clip_point_utf16(point, bias)
     }
 
@@ -2354,32 +2361,20 @@ pub trait ToOffset {
 }
 
 impl ToOffset for Point {
-    fn to_offset<'a>(&self, snapshot: &BufferSnapshot) -> usize {
+    fn to_offset(&self, snapshot: &BufferSnapshot) -> usize {
         snapshot.point_to_offset(*self)
     }
 }
 
-impl ToOffset for PointUtf16 {
-    fn to_offset<'a>(&self, snapshot: &BufferSnapshot) -> usize {
-        snapshot.point_utf16_to_offset(*self)
-    }
-}
-
 impl ToOffset for usize {
-    fn to_offset<'a>(&self, snapshot: &BufferSnapshot) -> usize {
+    fn to_offset(&self, snapshot: &BufferSnapshot) -> usize {
         assert!(*self <= snapshot.len(), "offset {self} is out of range");
         *self
     }
 }
 
-impl ToOffset for OffsetUtf16 {
-    fn to_offset<'a>(&self, snapshot: &BufferSnapshot) -> usize {
-        snapshot.offset_utf16_to_offset(*self)
-    }
-}
-
 impl ToOffset for Anchor {
-    fn to_offset<'a>(&self, snapshot: &BufferSnapshot) -> usize {
+    fn to_offset(&self, snapshot: &BufferSnapshot) -> usize {
         snapshot.summary_for_anchor(self)
     }
 }
@@ -2390,31 +2385,43 @@ impl<'a, T: ToOffset> ToOffset for &'a T {
     }
 }
 
+impl ToOffset for PointUtf16 {
+    fn to_offset(&self, snapshot: &BufferSnapshot) -> usize {
+        snapshot.point_utf16_to_offset(*self)
+    }
+}
+
+impl ToOffset for Unclipped<PointUtf16> {
+    fn to_offset(&self, snapshot: &BufferSnapshot) -> usize {
+        snapshot.unclipped_point_utf16_to_offset(*self)
+    }
+}
+
 pub trait ToPoint {
     fn to_point(&self, snapshot: &BufferSnapshot) -> Point;
 }
 
 impl ToPoint for Anchor {
-    fn to_point<'a>(&self, snapshot: &BufferSnapshot) -> Point {
+    fn to_point(&self, snapshot: &BufferSnapshot) -> Point {
         snapshot.summary_for_anchor(self)
     }
 }
 
 impl ToPoint for usize {
-    fn to_point<'a>(&self, snapshot: &BufferSnapshot) -> Point {
+    fn to_point(&self, snapshot: &BufferSnapshot) -> Point {
         snapshot.offset_to_point(*self)
     }
 }
 
-impl ToPoint for PointUtf16 {
-    fn to_point<'a>(&self, snapshot: &BufferSnapshot) -> Point {
-        snapshot.point_utf16_to_point(*self)
+impl ToPoint for Point {
+    fn to_point(&self, _: &BufferSnapshot) -> Point {
+        *self
     }
 }
 
-impl ToPoint for Point {
-    fn to_point<'a>(&self, _: &BufferSnapshot) -> Point {
-        *self
+impl ToPoint for Unclipped<PointUtf16> {
+    fn to_point(&self, snapshot: &BufferSnapshot) -> Point {
+        snapshot.unclipped_point_utf16_to_point(*self)
     }
 }
 
@@ -2423,25 +2430,25 @@ pub trait ToPointUtf16 {
 }
 
 impl ToPointUtf16 for Anchor {
-    fn to_point_utf16<'a>(&self, snapshot: &BufferSnapshot) -> PointUtf16 {
+    fn to_point_utf16(&self, snapshot: &BufferSnapshot) -> PointUtf16 {
         snapshot.summary_for_anchor(self)
     }
 }
 
 impl ToPointUtf16 for usize {
-    fn to_point_utf16<'a>(&self, snapshot: &BufferSnapshot) -> PointUtf16 {
+    fn to_point_utf16(&self, snapshot: &BufferSnapshot) -> PointUtf16 {
         snapshot.offset_to_point_utf16(*self)
     }
 }
 
 impl ToPointUtf16 for PointUtf16 {
-    fn to_point_utf16<'a>(&self, _: &BufferSnapshot) -> PointUtf16 {
+    fn to_point_utf16(&self, _: &BufferSnapshot) -> PointUtf16 {
         *self
     }
 }
 
 impl ToPointUtf16 for Point {
-    fn to_point_utf16<'a>(&self, snapshot: &BufferSnapshot) -> PointUtf16 {
+    fn to_point_utf16(&self, snapshot: &BufferSnapshot) -> PointUtf16 {
         snapshot.point_to_point_utf16(*self)
     }
 }
@@ -2451,45 +2458,23 @@ pub trait ToOffsetUtf16 {
 }
 
 impl ToOffsetUtf16 for Anchor {
-    fn to_offset_utf16<'a>(&self, snapshot: &BufferSnapshot) -> OffsetUtf16 {
+    fn to_offset_utf16(&self, snapshot: &BufferSnapshot) -> OffsetUtf16 {
         snapshot.summary_for_anchor(self)
     }
 }
 
 impl ToOffsetUtf16 for usize {
-    fn to_offset_utf16<'a>(&self, snapshot: &BufferSnapshot) -> OffsetUtf16 {
+    fn to_offset_utf16(&self, snapshot: &BufferSnapshot) -> OffsetUtf16 {
         snapshot.offset_to_offset_utf16(*self)
     }
 }
 
 impl ToOffsetUtf16 for OffsetUtf16 {
-    fn to_offset_utf16<'a>(&self, _snapshot: &BufferSnapshot) -> OffsetUtf16 {
+    fn to_offset_utf16(&self, _snapshot: &BufferSnapshot) -> OffsetUtf16 {
         *self
     }
 }
 
-pub trait Clip {
-    fn clip(&self, bias: Bias, snapshot: &BufferSnapshot) -> Self;
-}
-
-impl Clip for usize {
-    fn clip(&self, bias: Bias, snapshot: &BufferSnapshot) -> Self {
-        snapshot.clip_offset(*self, bias)
-    }
-}
-
-impl Clip for Point {
-    fn clip(&self, bias: Bias, snapshot: &BufferSnapshot) -> Self {
-        snapshot.clip_point(*self, bias)
-    }
-}
-
-impl Clip for PointUtf16 {
-    fn clip(&self, bias: Bias, snapshot: &BufferSnapshot) -> Self {
-        snapshot.clip_point_utf16(*self, bias)
-    }
-}
-
 pub trait FromAnchor {
     fn from_anchor(anchor: &Anchor, snapshot: &BufferSnapshot) -> Self;
 }

crates/theme_testbench/Cargo.toml 🔗

@@ -15,4 +15,4 @@ settings = { path = "../settings" }
 workspace = { path = "../workspace" }
 project = { path = "../project" }
 
-smallvec = { version = "1.6", features = ["union"] }
+smallvec = { version = "1.6", features = ["union"] }

crates/util/Cargo.toml 🔗

@@ -11,6 +11,7 @@ test-support = ["serde_json", "tempdir", "git2"]
 
 [dependencies]
 anyhow = "1.0.38"
+backtrace = "0.3"
 futures = "0.3"
 log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 lazy_static = "1.4.0"

crates/util/src/lib.rs 🔗

@@ -1,6 +1,7 @@
 #[cfg(any(test, feature = "test-support"))]
 pub mod test;
 
+pub use backtrace::Backtrace;
 use futures::Future;
 use rand::{seq::SliceRandom, Rng};
 use std::{
@@ -10,6 +11,18 @@ use std::{
     task::{Context, Poll},
 };
 
+#[macro_export]
+macro_rules! debug_panic {
+    ( $($fmt_arg:tt)* ) => {
+        if cfg!(debug_assertions) {
+            panic!( $($fmt_arg)* );
+        } else {
+            let backtrace = $crate::Backtrace::new();
+            log::error!("{}\n{:?}", format_args!($($fmt_arg)*), backtrace);
+        }
+    };
+}
+
 pub fn truncate(s: &str, max_chars: usize) -> &str {
     match s.char_indices().nth(max_chars) {
         None => s,

crates/vim/Cargo.toml 🔗

@@ -42,4 +42,4 @@ language = { path = "../language", features = ["test-support"] }
 project = { path = "../project", features = ["test-support"] }
 util = { path = "../util", features = ["test-support"] }
 settings = { path = "../settings" }
-workspace = { path = "../workspace", features = ["test-support"] }
+workspace = { path = "../workspace", features = ["test-support"] }

crates/vim/src/visual.rs 🔗

@@ -114,12 +114,12 @@ pub fn change(_: &mut Workspace, _: &VisualChange, cx: &mut ViewContext<Workspac
                         };
 
                         edits.push((expanded_range, "\n"));
-                        new_selections.push(selection.map(|_| anchor.clone()));
+                        new_selections.push(selection.map(|_| anchor));
                     } else {
                         let range = selection.map(|p| p.to_point(map)).range();
                         let anchor = map.buffer_snapshot.anchor_after(range.end);
                         edits.push((range, ""));
-                        new_selections.push(selection.map(|_| anchor.clone()));
+                        new_selections.push(selection.map(|_| anchor));
                     }
                     selection.goal = SelectionGoal::None;
                 });

crates/workspace/Cargo.toml 🔗

@@ -46,4 +46,4 @@ client = { path = "../client", features = ["test-support"] }
 gpui = { path = "../gpui", features = ["test-support"] }
 project = { path = "../project", features = ["test-support"] }
 settings = { path = "../settings", features = ["test-support"] }
-fs = { path = "../fs", features = ["test-support"] }
+fs = { path = "../fs", features = ["test-support"] }

crates/workspace/src/pane_group.rs 🔗

@@ -8,6 +8,7 @@ use gpui::{
 };
 use project::Project;
 use serde::Deserialize;
+use settings::Settings;
 use theme::Theme;
 
 #[derive(Clone, Debug, Eq, PartialEq)]
@@ -63,10 +64,17 @@ impl PaneGroup {
         theme: &Theme,
         follower_states: &FollowerStatesByLeader,
         active_call: Option<&ModelHandle<ActiveCall>>,
+        active_pane: &ViewHandle<Pane>,
         cx: &mut RenderContext<Workspace>,
     ) -> ElementBox {
-        self.root
-            .render(project, theme, follower_states, active_call, cx)
+        self.root.render(
+            project,
+            theme,
+            follower_states,
+            active_call,
+            active_pane,
+            cx,
+        )
     }
 
     pub(crate) fn panes(&self) -> Vec<&ViewHandle<Pane>> {
@@ -104,12 +112,20 @@ impl Member {
         Member::Axis(PaneAxis { axis, members })
     }
 
+    fn contains(&self, needle: &ViewHandle<Pane>) -> bool {
+        match self {
+            Member::Axis(axis) => axis.members.iter().any(|member| member.contains(needle)),
+            Member::Pane(pane) => pane == needle,
+        }
+    }
+
     pub fn render(
         &self,
         project: &ModelHandle<Project>,
         theme: &Theme,
         follower_states: &FollowerStatesByLeader,
         active_call: Option<&ModelHandle<ActiveCall>>,
+        active_pane: &ViewHandle<Pane>,
         cx: &mut RenderContext<Workspace>,
     ) -> ElementBox {
         enum FollowIntoExternalProject {}
@@ -236,7 +252,14 @@ impl Member {
                     .with_children(prompt)
                     .boxed()
             }
-            Member::Axis(axis) => axis.render(project, theme, follower_states, active_call, cx),
+            Member::Axis(axis) => axis.render(
+                project,
+                theme,
+                follower_states,
+                active_call,
+                active_pane,
+                cx,
+            ),
         }
     }
 
@@ -337,12 +360,19 @@ impl PaneAxis {
         theme: &Theme,
         follower_state: &FollowerStatesByLeader,
         active_call: Option<&ModelHandle<ActiveCall>>,
+        active_pane: &ViewHandle<Pane>,
         cx: &mut RenderContext<Workspace>,
     ) -> ElementBox {
         let last_member_ix = self.members.len() - 1;
         Flex::new(self.axis)
             .with_children(self.members.iter().enumerate().map(|(ix, member)| {
-                let mut member = member.render(project, theme, follower_state, active_call, cx);
+                let mut flex = 1.0;
+                if member.contains(active_pane) {
+                    flex = cx.global::<Settings>().active_pane_magnification;
+                }
+
+                let mut member =
+                    member.render(project, theme, follower_state, active_call, active_pane, cx);
                 if ix < last_member_ix {
                     let mut border = theme.workspace.pane_divider;
                     border.left = false;
@@ -356,7 +386,7 @@ impl PaneAxis {
                     member = Container::new(member).with_border(border).boxed();
                 }
 
-                FlexItem::new(member).flex(1.0, true).boxed()
+                FlexItem::new(member).flex(flex, true).boxed()
             }))
             .boxed()
     }

crates/workspace/src/workspace.rs 🔗

@@ -2784,6 +2784,7 @@ impl View for Workspace {
                                                         &theme,
                                                         &self.follower_states_by_leader,
                                                         self.active_call(),
+                                                        self.active_pane(),
                                                         cx,
                                                     ))
                                                     .flex(1., true)

crates/zed/Cargo.toml 🔗

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
 description = "The fast, collaborative code editor."
 edition = "2021"
 name = "zed"
-version = "0.65.0"
+version = "0.67.0"
 
 [lib]
 name = "zed"
@@ -95,6 +95,7 @@ tree-sitter-c = "0.20.1"
 tree-sitter-cpp = "0.20.0"
 tree-sitter-css = { git = "https://github.com/tree-sitter/tree-sitter-css", rev = "769203d0f9abe1a9a691ac2b9fe4bb4397a73c51" }
 tree-sitter-elixir = { git = "https://github.com/elixir-lang/tree-sitter-elixir", rev = "05e3631c6a0701c1fa518b0fee7be95a2ceef5e2" }
+tree-sitter-embedded-template = "0.20.0"
 tree-sitter-go = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "aeb2f33b366fd78d5789ff104956ce23508b85db" }
 tree-sitter-json = { git = "https://github.com/tree-sitter/tree-sitter-json", rev = "137e1ce6a02698fc246cdb9c6b886ed1de9a1ed8" }
 tree-sitter-rust = "0.20.3"

crates/zed/src/languages.rs 🔗

@@ -12,6 +12,7 @@ mod installation;
 mod json;
 mod language_plugin;
 mod python;
+mod ruby;
 mod rust;
 mod typescript;
 
@@ -116,7 +117,16 @@ pub async fn init(languages: Arc<LanguageRegistry>, _executor: Arc<Background>)
             tree_sitter_html::language(),
             Some(CachedLspAdapter::new(html::HtmlLspAdapter).await),
         ),
-        ("ruby", tree_sitter_ruby::language(), None),
+        (
+            "ruby",
+            tree_sitter_ruby::language(),
+            Some(CachedLspAdapter::new(ruby::RubyLanguageServer).await),
+        ),
+        (
+            "erb",
+            tree_sitter_embedded_template::language(),
+            Some(CachedLspAdapter::new(ruby::RubyLanguageServer).await),
+        ),
     ] {
         languages.add(language(name, grammar, lsp_adapter));
     }

crates/zed/src/languages/erb/config.toml 🔗

@@ -0,0 +1,8 @@
+name = "ERB"
+path_suffixes = ["erb"]
+autoclose_before = ">})"
+brackets = [
+    { start = "<", end = ">", close = true, newline = true },
+]
+
+block_comment = ["<%#", "%>"]

crates/zed/src/languages/python.rs 🔗

@@ -87,6 +87,25 @@ impl LspAdapter for PythonLspAdapter {
         .log_err()
     }
 
+    async fn process_completion(&self, item: &mut lsp::CompletionItem) {
+        // Pyright assigns each completion item a `sortText` of the form `XX.YYYY.name`.
+        // Where `XX` is the sorting category, `YYYY` is based on most recent usage,
+        // and `name` is the symbol name itself.
+        //
+        // Because the the symbol name is included, there generally are not ties when
+        // sorting by the `sortText`, so the symbol's fuzzy match score is not taken
+        // into account. Here, we remove the symbol name from the sortText in order
+        // to allow our own fuzzy score to be used to break ties.
+        //
+        // see https://github.com/microsoft/pyright/blob/95ef4e103b9b2f129c9320427e51b73ea7cf78bd/packages/pyright-internal/src/languageService/completionProvider.ts#LL2873
+        let Some(sort_text) = &mut item.sort_text else { return };
+        let mut parts = sort_text.split('.');
+        let Some(first) = parts.next() else { return };
+        let Some(second) = parts.next() else { return };
+        let Some(_) = parts.next() else { return };
+        sort_text.replace_range(first.len() + second.len() + 1.., "");
+    }
+
     async fn label_for_completion(
         &self,
         item: &lsp::CompletionItem,

crates/zed/src/languages/ruby.rs 🔗

@@ -0,0 +1,145 @@
+use anyhow::{anyhow, Result};
+use async_trait::async_trait;
+use client::http::HttpClient;
+use language::{LanguageServerName, LspAdapter};
+use std::{any::Any, path::PathBuf, sync::Arc};
+
+pub struct RubyLanguageServer;
+
+#[async_trait]
+impl LspAdapter for RubyLanguageServer {
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("solargraph".into())
+    }
+
+    async fn server_args(&self) -> Vec<String> {
+        vec!["stdio".into()]
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        _: Arc<dyn HttpClient>,
+    ) -> Result<Box<dyn 'static + Any + Send>> {
+        Ok(Box::new(()))
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        _version: Box<dyn 'static + Send + Any>,
+        _: Arc<dyn HttpClient>,
+        _container_dir: PathBuf,
+    ) -> Result<PathBuf> {
+        Err(anyhow!("solargraph must be installed manually"))
+    }
+
+    async fn cached_server_binary(&self, _container_dir: PathBuf) -> Option<PathBuf> {
+        Some("solargraph".into())
+    }
+
+    async fn label_for_completion(
+        &self,
+        item: &lsp::CompletionItem,
+        language: &Arc<language::Language>,
+    ) -> Option<language::CodeLabel> {
+        let label = &item.label;
+        let grammar = language.grammar()?;
+        let highlight_id = match item.kind? {
+            lsp::CompletionItemKind::METHOD => grammar.highlight_id_for_name("function.method")?,
+            lsp::CompletionItemKind::CONSTANT => grammar.highlight_id_for_name("constant")?,
+            lsp::CompletionItemKind::CLASS | lsp::CompletionItemKind::MODULE => {
+                grammar.highlight_id_for_name("type")?
+            }
+            lsp::CompletionItemKind::KEYWORD => {
+                if label.starts_with(":") {
+                    grammar.highlight_id_for_name("string.special.symbol")?
+                } else {
+                    grammar.highlight_id_for_name("keyword")?
+                }
+            }
+            lsp::CompletionItemKind::VARIABLE => {
+                if label.starts_with("@") {
+                    grammar.highlight_id_for_name("property")?
+                } else {
+                    return None;
+                }
+            }
+            _ => return None,
+        };
+        Some(language::CodeLabel {
+            text: label.clone(),
+            runs: vec![(0..label.len(), highlight_id)],
+            filter_range: 0..label.len(),
+        })
+    }
+
+    async fn label_for_symbol(
+        &self,
+        label: &str,
+        kind: lsp::SymbolKind,
+        language: &Arc<language::Language>,
+    ) -> Option<language::CodeLabel> {
+        let grammar = language.grammar()?;
+        match kind {
+            lsp::SymbolKind::METHOD => {
+                let mut parts = label.split('#');
+                let classes = parts.next()?;
+                let method = parts.next()?;
+                if parts.next().is_some() {
+                    return None;
+                }
+
+                let class_id = grammar.highlight_id_for_name("type")?;
+                let method_id = grammar.highlight_id_for_name("function.method")?;
+
+                let mut ix = 0;
+                let mut runs = Vec::new();
+                for (i, class) in classes.split("::").enumerate() {
+                    if i > 0 {
+                        ix += 2;
+                    }
+                    let end_ix = ix + class.len();
+                    runs.push((ix..end_ix, class_id));
+                    ix = end_ix;
+                }
+
+                ix += 1;
+                let end_ix = ix + method.len();
+                runs.push((ix..end_ix, method_id));
+                Some(language::CodeLabel {
+                    text: label.to_string(),
+                    runs,
+                    filter_range: 0..label.len(),
+                })
+            }
+            lsp::SymbolKind::CONSTANT => {
+                let constant_id = grammar.highlight_id_for_name("constant")?;
+                Some(language::CodeLabel {
+                    text: label.to_string(),
+                    runs: vec![(0..label.len(), constant_id)],
+                    filter_range: 0..label.len(),
+                })
+            }
+            lsp::SymbolKind::CLASS | lsp::SymbolKind::MODULE => {
+                let class_id = grammar.highlight_id_for_name("type")?;
+
+                let mut ix = 0;
+                let mut runs = Vec::new();
+                for (i, class) in label.split("::").enumerate() {
+                    if i > 0 {
+                        ix += "::".len();
+                    }
+                    let end_ix = ix + class.len();
+                    runs.push((ix..end_ix, class_id));
+                    ix = end_ix;
+                }
+
+                Some(language::CodeLabel {
+                    text: label.to_string(),
+                    runs,
+                    filter_range: 0..label.len(),
+                })
+            }
+            _ => return None,
+        }
+    }
+}

crates/zed/src/main.rs 🔗

@@ -211,21 +211,6 @@ fn init_paths() {
     std::fs::create_dir_all(&*zed::paths::LANGUAGES_DIR).expect("could not create languages path");
     std::fs::create_dir_all(&*zed::paths::DB_DIR).expect("could not create database path");
     std::fs::create_dir_all(&*zed::paths::LOGS_DIR).expect("could not create logs path");
-
-    // Copy setting files from legacy locations. TODO: remove this after a few releases.
-    thread::spawn(|| {
-        if std::fs::metadata(&*zed::paths::legacy::SETTINGS).is_ok()
-            && std::fs::metadata(&*zed::paths::SETTINGS).is_err()
-        {
-            std::fs::copy(&*zed::paths::legacy::SETTINGS, &*zed::paths::SETTINGS).log_err();
-        }
-
-        if std::fs::metadata(&*zed::paths::legacy::KEYMAP).is_ok()
-            && std::fs::metadata(&*zed::paths::KEYMAP).is_err()
-        {
-            std::fs::copy(&*zed::paths::legacy::KEYMAP, &*zed::paths::KEYMAP).log_err();
-        }
-    });
 }
 
 fn init_logger() {

script/lib/bump-version.sh 🔗

@@ -13,6 +13,7 @@ if [[ -n $(git status --short --untracked-files=no) ]]; then
 fi
 
 which cargo-set-version > /dev/null || cargo install cargo-edit
+which jq > /dev/null || brew install jq
 cargo set-version --package $package --bump $version_increment
 cargo check --quiet