Merge branch 'main' into finally-fix-terminal-line-height

Mikayla Maki created

Change summary

Cargo.lock                                   |    9 
Cargo.toml                                   |    1 
assets/keymaps/default.json                  | 1064 +++++++++++-----------
assets/keymaps/vim.json                      |  622 ++++++------
assets/settings/default.json                 |  511 +++++-----
assets/settings/initial_user_settings.json   |    2 
crates/activity_indicator/Cargo.toml         |    2 
crates/clock/Cargo.toml                      |    2 
crates/collab/Cargo.toml                     |    2 
crates/collab/src/tests/integration_tests.rs |    7 
crates/context_menu/Cargo.toml               |    2 
crates/copilot/Cargo.toml                    |    5 
crates/copilot/src/copilot.rs                |  800 +++++++++++++---
crates/copilot/src/request.rs                |   60 +
crates/copilot/src/sign_in.rs                |   72 
crates/diagnostics/Cargo.toml                |    4 
crates/diagnostics/src/diagnostics.rs        |  364 ++++++
crates/diagnostics/src/items.rs              |    3 
crates/editor/Cargo.toml                     |    2 
crates/editor/src/editor.rs                  |   69 
crates/editor/src/editor_tests.rs            |    6 
crates/editor/src/hover_popover.rs           |    3 
crates/editor/src/multi_buffer.rs            |   13 
crates/gpui/Cargo.toml                       |    2 
crates/language/Cargo.toml                   |    2 
crates/language/src/buffer.rs                |   98 +
crates/language/src/buffer_tests.rs          |    2 
crates/language/src/diagnostic_set.rs        |   26 
crates/language/src/language.rs              |   97 +
crates/language/src/proto.rs                 |   15 
crates/lsp/src/lsp.rs                        |   23 
crates/project/Cargo.toml                    |    1 
crates/project/src/lsp_command.rs            |   34 
crates/project/src/project.rs                |  927 ++++++++++--------
crates/project/src/project_tests.rs          |   97 +
crates/project/src/worktree.rs               |  107 +
crates/rope/Cargo.toml                       |    2 
crates/rpc/proto/zed.proto                   |   10 
crates/rpc/src/rpc.rs                        |    2 
crates/search/Cargo.toml                     |    2 
crates/snippet/Cargo.toml                    |    2 
crates/terminal/Cargo.toml                   |    2 
crates/terminal_view/Cargo.toml              |    2 
crates/text/Cargo.toml                       |    2 
crates/theme_testbench/Cargo.toml            |    2 
crates/workspace/Cargo.toml                  |    2 
crates/zed/Cargo.toml                        |    4 
crates/zed/src/languages.rs                  |   84 -
crates/zed/src/languages/typescript.rs       |  169 +++
crates/zed/src/main.rs                       |   99 +
50 files changed, 3,409 insertions(+), 2,031 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1192,7 +1192,7 @@ dependencies = [
 
 [[package]]
 name = "collab"
-version = "0.8.3"
+version = "0.9.0"
 dependencies = [
  "anyhow",
  "async-tungstenite",
@@ -1341,14 +1341,17 @@ dependencies = [
  "anyhow",
  "async-compression",
  "async-tar",
+ "clock",
  "collections",
  "context_menu",
+ "fs",
  "futures 0.3.25",
  "gpui",
  "language",
  "log",
  "lsp",
  "node_runtime",
+ "rpc",
  "serde",
  "serde_derive",
  "settings",
@@ -1831,6 +1834,7 @@ dependencies = [
  "editor",
  "gpui",
  "language",
+ "lsp",
  "postage",
  "project",
  "serde_json",
@@ -4687,6 +4691,7 @@ dependencies = [
  "client",
  "clock",
  "collections",
+ "copilot",
  "ctor",
  "db",
  "env_logger",
@@ -8517,7 +8522,7 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
 
 [[package]]
 name = "zed"
-version = "0.83.0"
+version = "0.84.0"
 dependencies = [
  "activity_indicator",
  "anyhow",

Cargo.toml 🔗

@@ -76,6 +76,7 @@ serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
 serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] }
 rand = { version = "0.8" }
 postage = { version = "0.5", features = ["futures-traits"] }
+smallvec = { version = "1.6", features = ["union"] }
 
 [patch.crates-io]
 tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "c51896d32dcc11a38e41f36e3deb1a6a9c4f4b14" }

assets/keymaps/default.json 🔗

@@ -1,548 +1,548 @@
 [
-    // Standard macOS bindings
-    {
-        "bindings": {
-            "up": "menu::SelectPrev",
-            "pageup": "menu::SelectFirst",
-            "shift-pageup": "menu::SelectFirst",
-            "ctrl-p": "menu::SelectPrev",
-            "down": "menu::SelectNext",
-            "pagedown": "menu::SelectLast",
-            "shift-pagedown": "menu::SelectFirst",
-            "ctrl-n": "menu::SelectNext",
-            "cmd-up": "menu::SelectFirst",
-            "cmd-down": "menu::SelectLast",
-            "enter": "menu::Confirm",
-            "escape": "menu::Cancel",
-            "ctrl-c": "menu::Cancel",
-            "cmd-{": "pane::ActivatePrevItem",
-            "cmd-}": "pane::ActivateNextItem",
-            "alt-cmd-left": "pane::ActivatePrevItem",
-            "alt-cmd-right": "pane::ActivateNextItem",
-            "cmd-w": "pane::CloseActiveItem",
-            "alt-cmd-t": "pane::CloseInactiveItems",
-            "cmd-k u": "pane::CloseCleanItems",
-            "cmd-k cmd-w": "pane::CloseAllItems",
-            "cmd-shift-w": "workspace::CloseWindow",
-            "cmd-s": "workspace::Save",
-            "cmd-shift-s": "workspace::SaveAs",
-            "cmd-=": "zed::IncreaseBufferFontSize",
-            "cmd--": "zed::DecreaseBufferFontSize",
-            "cmd-0": "zed::ResetBufferFontSize",
-            "cmd-,": "zed::OpenSettings",
-            "cmd-q": "zed::Quit",
-            "cmd-h": "zed::Hide",
-            "alt-cmd-h": "zed::HideOthers",
-            "cmd-m": "zed::Minimize",
-            "ctrl-cmd-f": "zed::ToggleFullScreen",
-            "cmd-n": "workspace::NewFile",
-            "cmd-shift-n": "workspace::NewWindow",
-            "cmd-o": "workspace::Open",
-            "alt-cmd-o": "projects::OpenRecent",
-            "ctrl-`": "workspace::NewTerminal"
-        }
-    },
-    {
-        "context": "Editor",
-        "bindings": {
-            "escape": "editor::Cancel",
-            "backspace": "editor::Backspace",
-            "shift-backspace": "editor::Backspace",
-            "ctrl-h": "editor::Backspace",
-            "delete": "editor::Delete",
-            "ctrl-d": "editor::Delete",
-            "tab": "editor::Tab",
-            "shift-tab": "editor::TabPrev",
-            "ctrl-k": "editor::CutToEndOfLine",
-            "ctrl-t": "editor::Transpose",
-            "cmd-backspace": "editor::DeleteToBeginningOfLine",
-            "cmd-delete": "editor::DeleteToEndOfLine",
-            "alt-backspace": "editor::DeleteToPreviousWordStart",
-            "alt-delete": "editor::DeleteToNextWordEnd",
-            "alt-h": "editor::DeleteToPreviousWordStart",
-            "alt-d": "editor::DeleteToNextWordEnd",
-            "cmd-x": "editor::Cut",
-            "cmd-c": "editor::Copy",
-            "cmd-v": "editor::Paste",
-            "cmd-z": "editor::Undo",
-            "cmd-shift-z": "editor::Redo",
-            "up": "editor::MoveUp",
-            "pageup": "editor::PageUp",
-            "shift-pageup": "editor::MovePageUp",
-            "home": "editor::MoveToBeginningOfLine",
-            "down": "editor::MoveDown",
-            "pagedown": "editor::PageDown",
-            "shift-pagedown": "editor::MovePageDown",
-            "end": "editor::MoveToEndOfLine",
-            "left": "editor::MoveLeft",
-            "right": "editor::MoveRight",
-            "ctrl-p": "editor::MoveUp",
-            "ctrl-n": "editor::MoveDown",
-            "ctrl-b": "editor::MoveLeft",
-            "ctrl-f": "editor::MoveRight",
-            "ctrl-l": "editor::NextScreen",
-            "alt-left": "editor::MoveToPreviousWordStart",
-            "alt-b": "editor::MoveToPreviousWordStart",
-            "alt-right": "editor::MoveToNextWordEnd",
-            "alt-f": "editor::MoveToNextWordEnd",
-            "cmd-left": "editor::MoveToBeginningOfLine",
-            "ctrl-a": "editor::MoveToBeginningOfLine",
-            "cmd-right": "editor::MoveToEndOfLine",
-            "ctrl-e": "editor::MoveToEndOfLine",
-            "cmd-up": "editor::MoveToBeginning",
-            "cmd-down": "editor::MoveToEnd",
-            "shift-up": "editor::SelectUp",
-            "ctrl-shift-p": "editor::SelectUp",
-            "shift-down": "editor::SelectDown",
-            "ctrl-shift-n": "editor::SelectDown",
-            "shift-left": "editor::SelectLeft",
-            "ctrl-shift-b": "editor::SelectLeft",
-            "shift-right": "editor::SelectRight",
-            "ctrl-shift-f": "editor::SelectRight",
-            "alt-shift-left": "editor::SelectToPreviousWordStart",
-            "alt-shift-b": "editor::SelectToPreviousWordStart",
-            "alt-shift-right": "editor::SelectToNextWordEnd",
-            "alt-shift-f": "editor::SelectToNextWordEnd",
-            "cmd-shift-up": "editor::SelectToBeginning",
-            "cmd-shift-down": "editor::SelectToEnd",
-            "cmd-a": "editor::SelectAll",
-            "cmd-l": "editor::SelectLine",
-            "cmd-shift-i": "editor::Format",
-            "cmd-shift-left": [
-                "editor::SelectToBeginningOfLine",
-                {
-                    "stop_at_soft_wraps": true
-                }
-            ],
-            "shift-home": [
-                "editor::SelectToBeginningOfLine",
-                {
-                    "stop_at_soft_wraps": true
-                }
-            ],
-            "ctrl-shift-a": [
-                "editor::SelectToBeginningOfLine",
-                {
-                    "stop_at_soft_wraps": true
-                }
-            ],
-            "cmd-shift-right": [
-                "editor::SelectToEndOfLine",
-                {
-                    "stop_at_soft_wraps": true
-                }
-            ],
-            "shift-end": [
-                "editor::SelectToEndOfLine",
-                {
-                    "stop_at_soft_wraps": true
-                }
-            ],
-            "ctrl-shift-e": [
-                "editor::SelectToEndOfLine",
-                {
-                    "stop_at_soft_wraps": true
-                }
-            ],
-            "ctrl-v": [
-                "editor::MovePageDown",
-                {
-                    "center_cursor": true
-                }
-            ],
-            "alt-v": [
-                "editor::MovePageUp",
-                {
-                    "center_cursor": true
-                }
-            ],
-            "ctrl-cmd-space": "editor::ShowCharacterPalette"
-        }
-    },
-    {
-        "context": "Editor && mode == full",
-        "bindings": {
-            "enter": "editor::Newline",
-            "cmd-shift-enter": "editor::NewlineAbove",
-            "cmd-enter": "editor::NewlineBelow",
-            "alt-z": "editor::ToggleSoftWrap",
-            "cmd-f": [
-                "buffer_search::Deploy",
-                {
-                    "focus": true
-                }
-            ],
-            "cmd-e": [
-                "buffer_search::Deploy",
-                {
-                    "focus": false
-                }
-            ],
-            "alt-\\": "copilot::Suggest",
-            "alt-]": "copilot::NextSuggestion",
-            "alt-[": "copilot::PreviousSuggestion"
-        }
-    },
-    {
-        "context": "Editor && mode == auto_height",
-        "bindings": {
-            "alt-enter": "editor::Newline",
-            "cmd-alt-enter": "editor::NewlineBelow"
-        }
-    },
-    {
-        "context": "BufferSearchBar > Editor",
-        "bindings": {
-            "escape": "buffer_search::Dismiss",
-            "tab": "buffer_search::FocusEditor",
-            "enter": "search::SelectNextMatch",
-            "shift-enter": "search::SelectPrevMatch"
-        }
-    },
-    {
-        "context": "Pane",
-        "bindings": {
-            "cmd-f": "project_search::ToggleFocus",
-            "cmd-g": "search::SelectNextMatch",
-            "cmd-shift-g": "search::SelectPrevMatch",
-            "alt-cmd-c": "search::ToggleCaseSensitive",
-            "alt-cmd-w": "search::ToggleWholeWord",
-            "alt-cmd-r": "search::ToggleRegex"
-        }
-    },
-    // Bindings from VS Code
-    {
-        "context": "Editor",
-        "bindings": {
-            "cmd-[": "editor::Outdent",
-            "cmd-]": "editor::Indent",
-            "cmd-alt-up": "editor::AddSelectionAbove",
-            "cmd-ctrl-p": "editor::AddSelectionAbove",
-            "cmd-alt-down": "editor::AddSelectionBelow",
-            "cmd-ctrl-n": "editor::AddSelectionBelow",
-            "cmd-d": [
-                "editor::SelectNext",
-                {
-                    "replace_newest": false
-                }
-            ],
-            "cmd-k cmd-d": [
-                "editor::SelectNext",
-                {
-                    "replace_newest": true
-                }
-            ],
-            "cmd-k cmd-i": "editor::Hover",
-            "cmd-/": [
-                "editor::ToggleComments",
-                {
-                    "advance_downwards": false
-                }
-            ],
-            "alt-up": "editor::SelectLargerSyntaxNode",
-            "alt-down": "editor::SelectSmallerSyntaxNode",
-            "cmd-u": "editor::UndoSelection",
-            "cmd-shift-u": "editor::RedoSelection",
-            "f8": "editor::GoToDiagnostic",
-            "shift-f8": "editor::GoToPrevDiagnostic",
-            "f2": "editor::Rename",
-            "f12": "editor::GoToDefinition",
-            "cmd-f12": "editor::GoToTypeDefinition",
-            "alt-shift-f12": "editor::FindAllReferences",
-            "ctrl-m": "editor::MoveToEnclosingBracket",
-            "alt-cmd-[": "editor::Fold",
-            "alt-cmd-]": "editor::UnfoldLines",
-            "ctrl-space": "editor::ShowCompletions",
-            "cmd-.": "editor::ToggleCodeActions",
-            "alt-cmd-r": "editor::RevealInFinder"
-        }
-    },
-    {
-        "context": "Editor && mode == full",
-        "bindings": {
-            "cmd-shift-o": "outline::Toggle",
-            "ctrl-g": "go_to_line::Toggle"
-        }
-    },
-    {
-        "context": "Pane",
-        "bindings": {
-            "ctrl-1": [
-                "pane::ActivateItem",
-                0
-            ],
-            "ctrl-2": [
-                "pane::ActivateItem",
-                1
-            ],
-            "ctrl-3": [
-                "pane::ActivateItem",
-                2
-            ],
-            "ctrl-4": [
-                "pane::ActivateItem",
-                3
-            ],
-            "ctrl-5": [
-                "pane::ActivateItem",
-                4
-            ],
-            "ctrl-6": [
-                "pane::ActivateItem",
-                5
-            ],
-            "ctrl-7": [
-                "pane::ActivateItem",
-                6
-            ],
-            "ctrl-8": [
-                "pane::ActivateItem",
-                7
-            ],
-            "ctrl-9": [
-                "pane::ActivateItem",
-                8
-            ],
-            "ctrl-0": "pane::ActivateLastItem",
-            "ctrl--": "pane::GoBack",
-            "ctrl-_": "pane::GoForward",
-            "cmd-shift-t": "pane::ReopenClosedItem",
-            "cmd-shift-f": "project_search::ToggleFocus"
-        }
-    },
-    {
-        "context": "Workspace",
-        "bindings": {
-            "cmd-1": [
-                "workspace::ActivatePane",
-                0
-            ],
-            "cmd-2": [
-                "workspace::ActivatePane",
-                1
-            ],
-            "cmd-3": [
-                "workspace::ActivatePane",
-                2
-            ],
-            "cmd-4": [
-                "workspace::ActivatePane",
-                3
-            ],
-            "cmd-5": [
-                "workspace::ActivatePane",
-                4
-            ],
-            "cmd-6": [
-                "workspace::ActivatePane",
-                5
-            ],
-            "cmd-7": [
-                "workspace::ActivatePane",
-                6
-            ],
-            "cmd-8": [
-                "workspace::ActivatePane",
-                7
-            ],
-            "cmd-9": [
-                "workspace::ActivatePane",
-                8
-            ],
-            "cmd-b": "workspace::ToggleLeftSidebar",
-            "cmd-shift-f": "workspace::NewSearch",
-            "cmd-k cmd-t": "theme_selector::Toggle",
-            "cmd-k cmd-s": "zed::OpenKeymap",
-            "cmd-t": "project_symbols::Toggle",
-            "cmd-p": "file_finder::Toggle",
-            "cmd-shift-p": "command_palette::Toggle",
-            "cmd-shift-m": "diagnostics::Deploy",
-            "cmd-shift-e": "project_panel::ToggleFocus",
-            "cmd-alt-s": "workspace::SaveAll",
-            "cmd-k m": "language_selector::Toggle"
-        }
-    },
-    // Bindings from Sublime Text
-    {
-        "context": "Editor",
-        "bindings": {
-            "ctrl-shift-k": "editor::DeleteLine",
-            "cmd-shift-d": "editor::DuplicateLine",
-            "cmd-shift-l": "editor::SplitSelectionIntoLines",
-            "ctrl-cmd-up": "editor::MoveLineUp",
-            "ctrl-cmd-down": "editor::MoveLineDown",
-            "ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart",
-            "ctrl-alt-h": "editor::DeleteToPreviousSubwordStart",
-            "ctrl-alt-delete": "editor::DeleteToNextSubwordEnd",
-            "ctrl-alt-d": "editor::DeleteToNextSubwordEnd",
-            "ctrl-alt-left": "editor::MoveToPreviousSubwordStart",
-            "ctrl-alt-b": "editor::MoveToPreviousSubwordStart",
-            "ctrl-alt-right": "editor::MoveToNextSubwordEnd",
-            "ctrl-alt-f": "editor::MoveToNextSubwordEnd",
-            "ctrl-alt-shift-left": "editor::SelectToPreviousSubwordStart",
-            "ctrl-alt-shift-b": "editor::SelectToPreviousSubwordStart",
-            "ctrl-alt-shift-right": "editor::SelectToNextSubwordEnd",
-            "ctrl-alt-shift-f": "editor::SelectToNextSubwordEnd"
-        }
-    },
-    {
-        "bindings": {
-            "cmd-k cmd-left": "workspace::ActivatePreviousPane",
-            "cmd-k cmd-right": "workspace::ActivateNextPane"
-        }
-    },
-    // Bindings from Atom
-    {
-        "context": "Pane",
-        "bindings": {
-            "cmd-k up": "pane::SplitUp",
-            "cmd-k down": "pane::SplitDown",
-            "cmd-k left": "pane::SplitLeft",
-            "cmd-k right": "pane::SplitRight"
+  // Standard macOS bindings
+  {
+    "bindings": {
+      "up": "menu::SelectPrev",
+      "pageup": "menu::SelectFirst",
+      "shift-pageup": "menu::SelectFirst",
+      "ctrl-p": "menu::SelectPrev",
+      "down": "menu::SelectNext",
+      "pagedown": "menu::SelectLast",
+      "shift-pagedown": "menu::SelectFirst",
+      "ctrl-n": "menu::SelectNext",
+      "cmd-up": "menu::SelectFirst",
+      "cmd-down": "menu::SelectLast",
+      "enter": "menu::Confirm",
+      "escape": "menu::Cancel",
+      "ctrl-c": "menu::Cancel",
+      "cmd-{": "pane::ActivatePrevItem",
+      "cmd-}": "pane::ActivateNextItem",
+      "alt-cmd-left": "pane::ActivatePrevItem",
+      "alt-cmd-right": "pane::ActivateNextItem",
+      "cmd-w": "pane::CloseActiveItem",
+      "alt-cmd-t": "pane::CloseInactiveItems",
+      "cmd-k u": "pane::CloseCleanItems",
+      "cmd-k cmd-w": "pane::CloseAllItems",
+      "cmd-shift-w": "workspace::CloseWindow",
+      "cmd-s": "workspace::Save",
+      "cmd-shift-s": "workspace::SaveAs",
+      "cmd-=": "zed::IncreaseBufferFontSize",
+      "cmd--": "zed::DecreaseBufferFontSize",
+      "cmd-0": "zed::ResetBufferFontSize",
+      "cmd-,": "zed::OpenSettings",
+      "cmd-q": "zed::Quit",
+      "cmd-h": "zed::Hide",
+      "alt-cmd-h": "zed::HideOthers",
+      "cmd-m": "zed::Minimize",
+      "ctrl-cmd-f": "zed::ToggleFullScreen",
+      "cmd-n": "workspace::NewFile",
+      "cmd-shift-n": "workspace::NewWindow",
+      "cmd-o": "workspace::Open",
+      "alt-cmd-o": "projects::OpenRecent",
+      "ctrl-`": "workspace::NewTerminal"
+    }
+  },
+  {
+    "context": "Editor",
+    "bindings": {
+      "escape": "editor::Cancel",
+      "backspace": "editor::Backspace",
+      "shift-backspace": "editor::Backspace",
+      "ctrl-h": "editor::Backspace",
+      "delete": "editor::Delete",
+      "ctrl-d": "editor::Delete",
+      "tab": "editor::Tab",
+      "shift-tab": "editor::TabPrev",
+      "ctrl-k": "editor::CutToEndOfLine",
+      "ctrl-t": "editor::Transpose",
+      "cmd-backspace": "editor::DeleteToBeginningOfLine",
+      "cmd-delete": "editor::DeleteToEndOfLine",
+      "alt-backspace": "editor::DeleteToPreviousWordStart",
+      "alt-delete": "editor::DeleteToNextWordEnd",
+      "alt-h": "editor::DeleteToPreviousWordStart",
+      "alt-d": "editor::DeleteToNextWordEnd",
+      "cmd-x": "editor::Cut",
+      "cmd-c": "editor::Copy",
+      "cmd-v": "editor::Paste",
+      "cmd-z": "editor::Undo",
+      "cmd-shift-z": "editor::Redo",
+      "up": "editor::MoveUp",
+      "pageup": "editor::PageUp",
+      "shift-pageup": "editor::MovePageUp",
+      "home": "editor::MoveToBeginningOfLine",
+      "down": "editor::MoveDown",
+      "pagedown": "editor::PageDown",
+      "shift-pagedown": "editor::MovePageDown",
+      "end": "editor::MoveToEndOfLine",
+      "left": "editor::MoveLeft",
+      "right": "editor::MoveRight",
+      "ctrl-p": "editor::MoveUp",
+      "ctrl-n": "editor::MoveDown",
+      "ctrl-b": "editor::MoveLeft",
+      "ctrl-f": "editor::MoveRight",
+      "ctrl-l": "editor::NextScreen",
+      "alt-left": "editor::MoveToPreviousWordStart",
+      "alt-b": "editor::MoveToPreviousWordStart",
+      "alt-right": "editor::MoveToNextWordEnd",
+      "alt-f": "editor::MoveToNextWordEnd",
+      "cmd-left": "editor::MoveToBeginningOfLine",
+      "ctrl-a": "editor::MoveToBeginningOfLine",
+      "cmd-right": "editor::MoveToEndOfLine",
+      "ctrl-e": "editor::MoveToEndOfLine",
+      "cmd-up": "editor::MoveToBeginning",
+      "cmd-down": "editor::MoveToEnd",
+      "shift-up": "editor::SelectUp",
+      "ctrl-shift-p": "editor::SelectUp",
+      "shift-down": "editor::SelectDown",
+      "ctrl-shift-n": "editor::SelectDown",
+      "shift-left": "editor::SelectLeft",
+      "ctrl-shift-b": "editor::SelectLeft",
+      "shift-right": "editor::SelectRight",
+      "ctrl-shift-f": "editor::SelectRight",
+      "alt-shift-left": "editor::SelectToPreviousWordStart",
+      "alt-shift-b": "editor::SelectToPreviousWordStart",
+      "alt-shift-right": "editor::SelectToNextWordEnd",
+      "alt-shift-f": "editor::SelectToNextWordEnd",
+      "cmd-shift-up": "editor::SelectToBeginning",
+      "cmd-shift-down": "editor::SelectToEnd",
+      "cmd-a": "editor::SelectAll",
+      "cmd-l": "editor::SelectLine",
+      "cmd-shift-i": "editor::Format",
+      "cmd-shift-left": [
+        "editor::SelectToBeginningOfLine",
+        {
+          "stop_at_soft_wraps": true
         }
-    },
-    // Bindings that should be unified with bindings for more general actions
-    {
-        "context": "Editor && renaming",
-        "bindings": {
-            "enter": "editor::ConfirmRename"
+      ],
+      "shift-home": [
+        "editor::SelectToBeginningOfLine",
+        {
+          "stop_at_soft_wraps": true
         }
-    },
-    {
-        "context": "Editor && showing_completions",
-        "bindings": {
-            "enter": "editor::ConfirmCompletion",
-            "tab": "editor::ConfirmCompletion"
+      ],
+      "ctrl-shift-a": [
+        "editor::SelectToBeginningOfLine",
+        {
+          "stop_at_soft_wraps": true
         }
-    },
-    {
-        "context": "Editor && showing_code_actions",
-        "bindings": {
-            "enter": "editor::ConfirmCodeAction"
+      ],
+      "cmd-shift-right": [
+        "editor::SelectToEndOfLine",
+        {
+          "stop_at_soft_wraps": true
         }
-    },
-    // Custom bindings
-    {
-        "bindings": {
-            "ctrl-alt-cmd-f": "workspace::FollowNextCollaborator",
-            "cmd-shift-c": "collab::ToggleContactsMenu",
-            "cmd-alt-i": "zed::DebugElements"
+      ],
+      "shift-end": [
+        "editor::SelectToEndOfLine",
+        {
+          "stop_at_soft_wraps": true
         }
-    },
-    {
-        "context": "Editor",
-        "bindings": {
-            "alt-enter": "editor::OpenExcerpts",
-            "cmd-f8": "editor::GoToHunk",
-            "cmd-shift-f8": "editor::GoToPrevHunk"
+      ],
+      "ctrl-shift-e": [
+        "editor::SelectToEndOfLine",
+        {
+          "stop_at_soft_wraps": true
         }
-    },
-    {
-        "context": "ProjectSearchBar",
-        "bindings": {
-            "cmd-enter": "project_search::SearchInNew"
+      ],
+      "ctrl-v": [
+        "editor::MovePageDown",
+        {
+          "center_cursor": true
         }
-    },
-    {
-        "context": "Workspace",
-        "bindings": {
-            "shift-escape": "dock::FocusDock"
+      ],
+      "alt-v": [
+        "editor::MovePageUp",
+        {
+          "center_cursor": true
         }
-    },
-    {
-        "bindings": {
-            "cmd-shift-k cmd-shift-right": "dock::AnchorDockRight",
-            "cmd-shift-k cmd-shift-down": "dock::AnchorDockBottom",
-            "cmd-shift-k cmd-shift-up": "dock::ExpandDock"
+      ],
+      "ctrl-cmd-space": "editor::ShowCharacterPalette"
+    }
+  },
+  {
+    "context": "Editor && mode == full",
+    "bindings": {
+      "enter": "editor::Newline",
+      "cmd-shift-enter": "editor::NewlineAbove",
+      "cmd-enter": "editor::NewlineBelow",
+      "alt-z": "editor::ToggleSoftWrap",
+      "cmd-f": [
+        "buffer_search::Deploy",
+        {
+          "focus": true
         }
-    },
-    {
-        "context": "Pane",
-        "bindings": {
-            "cmd-escape": "dock::AddTabToDock"
+      ],
+      "cmd-e": [
+        "buffer_search::Deploy",
+        {
+          "focus": false
         }
-    },
-    {
-        "context": "Pane && docked",
-        "bindings": {
-            "shift-escape": "dock::HideDock",
-            "cmd-escape": "dock::RemoveTabFromDock"
+      ],
+      "alt-\\": "copilot::Suggest",
+      "alt-]": "copilot::NextSuggestion",
+      "alt-[": "copilot::PreviousSuggestion"
+    }
+  },
+  {
+    "context": "Editor && mode == auto_height",
+    "bindings": {
+      "alt-enter": "editor::Newline",
+      "cmd-alt-enter": "editor::NewlineBelow"
+    }
+  },
+  {
+    "context": "BufferSearchBar > Editor",
+    "bindings": {
+      "escape": "buffer_search::Dismiss",
+      "tab": "buffer_search::FocusEditor",
+      "enter": "search::SelectNextMatch",
+      "shift-enter": "search::SelectPrevMatch"
+    }
+  },
+  {
+    "context": "Pane",
+    "bindings": {
+      "cmd-f": "project_search::ToggleFocus",
+      "cmd-g": "search::SelectNextMatch",
+      "cmd-shift-g": "search::SelectPrevMatch",
+      "alt-cmd-c": "search::ToggleCaseSensitive",
+      "alt-cmd-w": "search::ToggleWholeWord",
+      "alt-cmd-r": "search::ToggleRegex"
+    }
+  },
+  // Bindings from VS Code
+  {
+    "context": "Editor",
+    "bindings": {
+      "cmd-[": "editor::Outdent",
+      "cmd-]": "editor::Indent",
+      "cmd-alt-up": "editor::AddSelectionAbove",
+      "cmd-ctrl-p": "editor::AddSelectionAbove",
+      "cmd-alt-down": "editor::AddSelectionBelow",
+      "cmd-ctrl-n": "editor::AddSelectionBelow",
+      "cmd-d": [
+        "editor::SelectNext",
+        {
+          "replace_newest": false
         }
-    },
-    {
-        "context": "ProjectPanel",
-        "bindings": {
-            "left": "project_panel::CollapseSelectedEntry",
-            "right": "project_panel::ExpandSelectedEntry",
-            "cmd-x": "project_panel::Cut",
-            "cmd-c": "project_panel::Copy",
-            "cmd-v": "project_panel::Paste",
-            "cmd-alt-c": "project_panel::CopyPath",
-            "alt-cmd-shift-c": "project_panel::CopyRelativePath",
-            "f2": "project_panel::Rename",
-            "backspace": "project_panel::Delete",
-            "alt-cmd-r": "project_panel::RevealInFinder"
+      ],
+      "cmd-k cmd-d": [
+        "editor::SelectNext",
+        {
+          "replace_newest": true
         }
-    },
-    {
-        "context": "Terminal",
-        "bindings": {
-            "ctrl-cmd-space": "terminal::ShowCharacterPalette",
-            "cmd-c": "terminal::Copy",
-            "cmd-v": "terminal::Paste",
-            "cmd-k": "terminal::Clear",
-            // Some nice conveniences
-            "cmd-backspace": [
-                "terminal::SendText",
-                "\u0015"
-            ],
-            "cmd-right": [
-                "terminal::SendText",
-                "\u0005"
-            ],
-            "cmd-left": [
-                "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": [
-                "terminal::SendKeystroke",
-                "up"
-            ],
-            "pageup": [
-                "terminal::SendKeystroke",
-                "pageup"
-            ],
-            "down": [
-                "terminal::SendKeystroke",
-                "down"
-            ],
-            "pagedown": [
-                "terminal::SendKeystroke",
-                "pagedown"
-            ],
-            "escape": [
-                "terminal::SendKeystroke",
-                "escape"
-            ],
-            "enter": [
-                "terminal::SendKeystroke",
-                "enter"
-            ],
-            "ctrl-c": [
-                "terminal::SendKeystroke",
-                "ctrl-c"
-            ]
+      ],
+      "cmd-k cmd-i": "editor::Hover",
+      "cmd-/": [
+        "editor::ToggleComments",
+        {
+          "advance_downwards": false
         }
+      ],
+      "alt-up": "editor::SelectLargerSyntaxNode",
+      "alt-down": "editor::SelectSmallerSyntaxNode",
+      "cmd-u": "editor::UndoSelection",
+      "cmd-shift-u": "editor::RedoSelection",
+      "f8": "editor::GoToDiagnostic",
+      "shift-f8": "editor::GoToPrevDiagnostic",
+      "f2": "editor::Rename",
+      "f12": "editor::GoToDefinition",
+      "cmd-f12": "editor::GoToTypeDefinition",
+      "alt-shift-f12": "editor::FindAllReferences",
+      "ctrl-m": "editor::MoveToEnclosingBracket",
+      "alt-cmd-[": "editor::Fold",
+      "alt-cmd-]": "editor::UnfoldLines",
+      "ctrl-space": "editor::ShowCompletions",
+      "cmd-.": "editor::ToggleCodeActions",
+      "alt-cmd-r": "editor::RevealInFinder"
+    }
+  },
+  {
+    "context": "Editor && mode == full",
+    "bindings": {
+      "cmd-shift-o": "outline::Toggle",
+      "ctrl-g": "go_to_line::Toggle"
+    }
+  },
+  {
+    "context": "Pane",
+    "bindings": {
+      "ctrl-1": [
+        "pane::ActivateItem",
+        0
+      ],
+      "ctrl-2": [
+        "pane::ActivateItem",
+        1
+      ],
+      "ctrl-3": [
+        "pane::ActivateItem",
+        2
+      ],
+      "ctrl-4": [
+        "pane::ActivateItem",
+        3
+      ],
+      "ctrl-5": [
+        "pane::ActivateItem",
+        4
+      ],
+      "ctrl-6": [
+        "pane::ActivateItem",
+        5
+      ],
+      "ctrl-7": [
+        "pane::ActivateItem",
+        6
+      ],
+      "ctrl-8": [
+        "pane::ActivateItem",
+        7
+      ],
+      "ctrl-9": [
+        "pane::ActivateItem",
+        8
+      ],
+      "ctrl-0": "pane::ActivateLastItem",
+      "ctrl--": "pane::GoBack",
+      "ctrl-_": "pane::GoForward",
+      "cmd-shift-t": "pane::ReopenClosedItem",
+      "cmd-shift-f": "project_search::ToggleFocus"
+    }
+  },
+  {
+    "context": "Workspace",
+    "bindings": {
+      "cmd-1": [
+        "workspace::ActivatePane",
+        0
+      ],
+      "cmd-2": [
+        "workspace::ActivatePane",
+        1
+      ],
+      "cmd-3": [
+        "workspace::ActivatePane",
+        2
+      ],
+      "cmd-4": [
+        "workspace::ActivatePane",
+        3
+      ],
+      "cmd-5": [
+        "workspace::ActivatePane",
+        4
+      ],
+      "cmd-6": [
+        "workspace::ActivatePane",
+        5
+      ],
+      "cmd-7": [
+        "workspace::ActivatePane",
+        6
+      ],
+      "cmd-8": [
+        "workspace::ActivatePane",
+        7
+      ],
+      "cmd-9": [
+        "workspace::ActivatePane",
+        8
+      ],
+      "cmd-b": "workspace::ToggleLeftSidebar",
+      "cmd-shift-f": "workspace::NewSearch",
+      "cmd-k cmd-t": "theme_selector::Toggle",
+      "cmd-k cmd-s": "zed::OpenKeymap",
+      "cmd-t": "project_symbols::Toggle",
+      "cmd-p": "file_finder::Toggle",
+      "cmd-shift-p": "command_palette::Toggle",
+      "cmd-shift-m": "diagnostics::Deploy",
+      "cmd-shift-e": "project_panel::ToggleFocus",
+      "cmd-alt-s": "workspace::SaveAll",
+      "cmd-k m": "language_selector::Toggle"
+    }
+  },
+  // Bindings from Sublime Text
+  {
+    "context": "Editor",
+    "bindings": {
+      "ctrl-shift-k": "editor::DeleteLine",
+      "cmd-shift-d": "editor::DuplicateLine",
+      "cmd-shift-l": "editor::SplitSelectionIntoLines",
+      "ctrl-cmd-up": "editor::MoveLineUp",
+      "ctrl-cmd-down": "editor::MoveLineDown",
+      "ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart",
+      "ctrl-alt-h": "editor::DeleteToPreviousSubwordStart",
+      "ctrl-alt-delete": "editor::DeleteToNextSubwordEnd",
+      "ctrl-alt-d": "editor::DeleteToNextSubwordEnd",
+      "ctrl-alt-left": "editor::MoveToPreviousSubwordStart",
+      "ctrl-alt-b": "editor::MoveToPreviousSubwordStart",
+      "ctrl-alt-right": "editor::MoveToNextSubwordEnd",
+      "ctrl-alt-f": "editor::MoveToNextSubwordEnd",
+      "ctrl-alt-shift-left": "editor::SelectToPreviousSubwordStart",
+      "ctrl-alt-shift-b": "editor::SelectToPreviousSubwordStart",
+      "ctrl-alt-shift-right": "editor::SelectToNextSubwordEnd",
+      "ctrl-alt-shift-f": "editor::SelectToNextSubwordEnd"
+    }
+  },
+  {
+    "bindings": {
+      "cmd-k cmd-left": "workspace::ActivatePreviousPane",
+      "cmd-k cmd-right": "workspace::ActivateNextPane"
+    }
+  },
+  // Bindings from Atom
+  {
+    "context": "Pane",
+    "bindings": {
+      "cmd-k up": "pane::SplitUp",
+      "cmd-k down": "pane::SplitDown",
+      "cmd-k left": "pane::SplitLeft",
+      "cmd-k right": "pane::SplitRight"
+    }
+  },
+  // Bindings that should be unified with bindings for more general actions
+  {
+    "context": "Editor && renaming",
+    "bindings": {
+      "enter": "editor::ConfirmRename"
+    }
+  },
+  {
+    "context": "Editor && showing_completions",
+    "bindings": {
+      "enter": "editor::ConfirmCompletion",
+      "tab": "editor::ConfirmCompletion"
+    }
+  },
+  {
+    "context": "Editor && showing_code_actions",
+    "bindings": {
+      "enter": "editor::ConfirmCodeAction"
+    }
+  },
+  // Custom bindings
+  {
+    "bindings": {
+      "ctrl-alt-cmd-f": "workspace::FollowNextCollaborator",
+      "cmd-shift-c": "collab::ToggleContactsMenu",
+      "cmd-alt-i": "zed::DebugElements"
+    }
+  },
+  {
+    "context": "Editor",
+    "bindings": {
+      "alt-enter": "editor::OpenExcerpts",
+      "cmd-f8": "editor::GoToHunk",
+      "cmd-shift-f8": "editor::GoToPrevHunk"
+    }
+  },
+  {
+    "context": "ProjectSearchBar",
+    "bindings": {
+      "cmd-enter": "project_search::SearchInNew"
+    }
+  },
+  {
+    "context": "Workspace",
+    "bindings": {
+      "shift-escape": "dock::FocusDock"
+    }
+  },
+  {
+    "bindings": {
+      "cmd-shift-k cmd-shift-right": "dock::AnchorDockRight",
+      "cmd-shift-k cmd-shift-down": "dock::AnchorDockBottom",
+      "cmd-shift-k cmd-shift-up": "dock::ExpandDock"
+    }
+  },
+  {
+    "context": "Pane",
+    "bindings": {
+      "cmd-escape": "dock::AddTabToDock"
+    }
+  },
+  {
+    "context": "Pane && docked",
+    "bindings": {
+      "shift-escape": "dock::HideDock",
+      "cmd-escape": "dock::RemoveTabFromDock"
+    }
+  },
+  {
+    "context": "ProjectPanel",
+    "bindings": {
+      "left": "project_panel::CollapseSelectedEntry",
+      "right": "project_panel::ExpandSelectedEntry",
+      "cmd-x": "project_panel::Cut",
+      "cmd-c": "project_panel::Copy",
+      "cmd-v": "project_panel::Paste",
+      "cmd-alt-c": "project_panel::CopyPath",
+      "alt-cmd-shift-c": "project_panel::CopyRelativePath",
+      "f2": "project_panel::Rename",
+      "backspace": "project_panel::Delete",
+      "alt-cmd-r": "project_panel::RevealInFinder"
+    }
+  },
+  {
+    "context": "Terminal",
+    "bindings": {
+      "ctrl-cmd-space": "terminal::ShowCharacterPalette",
+      "cmd-c": "terminal::Copy",
+      "cmd-v": "terminal::Paste",
+      "cmd-k": "terminal::Clear",
+      // Some nice conveniences
+      "cmd-backspace": [
+        "terminal::SendText",
+        "\u0015"
+      ],
+      "cmd-right": [
+        "terminal::SendText",
+        "\u0005"
+      ],
+      "cmd-left": [
+        "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": [
+        "terminal::SendKeystroke",
+        "up"
+      ],
+      "pageup": [
+        "terminal::SendKeystroke",
+        "pageup"
+      ],
+      "down": [
+        "terminal::SendKeystroke",
+        "down"
+      ],
+      "pagedown": [
+        "terminal::SendKeystroke",
+        "pagedown"
+      ],
+      "escape": [
+        "terminal::SendKeystroke",
+        "escape"
+      ],
+      "enter": [
+        "terminal::SendKeystroke",
+        "enter"
+      ],
+      "ctrl-c": [
+        "terminal::SendKeystroke",
+        "ctrl-c"
+      ]
     }
+  }
 ]

assets/keymaps/vim.json 🔗

@@ -1,325 +1,325 @@
 [
-    {
-        "context": "Editor && VimControl && !VimWaiting",
-        "bindings": {
-            "g": [
-                "vim::PushOperator",
-                {
-                    "Namespace": "G"
-                }
-            ],
-            "i": [
-                "vim::PushOperator",
-                {
-                    "Object": {
-                        "around": false
-                    }
-                }
-            ],
-            "a": [
-                "vim::PushOperator",
-                {
-                    "Object": {
-                        "around": true
-                    }
-                }
-            ],
-            "h": "vim::Left",
-            "backspace": "vim::Backspace",
-            "j": "vim::Down",
-            "enter": "vim::NextLineStart",
-            "k": "vim::Up",
-            "l": "vim::Right",
-            "$": "vim::EndOfLine",
-            "shift-g": "vim::EndOfDocument",
-            "w": "vim::NextWordStart",
-            "shift-w": [
-                "vim::NextWordStart",
-                {
-                    "ignorePunctuation": true
-                }
-            ],
-            "e": "vim::NextWordEnd",
-            "shift-e": [
-                "vim::NextWordEnd",
-                {
-                    "ignorePunctuation": true
-                }
-            ],
-            "b": "vim::PreviousWordStart",
-            "shift-b": [
-                "vim::PreviousWordStart",
-                {
-                    "ignorePunctuation": true
-                }
-            ],
-            "%": "vim::Matching",
-            "ctrl-y": [
-                "vim::Scroll",
-                "LineUp"
-            ],
-            "f": [
-                "vim::PushOperator",
-                {
-                    "FindForward": {
-                        "before": false
-                    }
-                }
-            ],
-            "t": [
-                "vim::PushOperator",
-                {
-                    "FindForward": {
-                        "before": true
-                    }
-                }
-            ],
-            "shift-f": [
-                "vim::PushOperator",
-                {
-                    "FindBackward": {
-                        "after": false
-                    }
-                }
-            ],
-            "shift-t": [
-                "vim::PushOperator",
-                {
-                    "FindBackward": {
-                        "after": true
-                    }
-                }
-            ],
-            "escape": "editor::Cancel",
-            "0": "vim::StartOfLine", // When no number operator present, use start of line motion
-            "1": [
-                "vim::Number",
-                1
-            ],
-            "2": [
-                "vim::Number",
-                2
-            ],
-            "3": [
-                "vim::Number",
-                3
-            ],
-            "4": [
-                "vim::Number",
-                4
-            ],
-            "5": [
-                "vim::Number",
-                5
-            ],
-            "6": [
-                "vim::Number",
-                6
-            ],
-            "7": [
-                "vim::Number",
-                7
-            ],
-            "8": [
-                "vim::Number",
-                8
-            ],
-            "9": [
-                "vim::Number",
-                9
-            ]
+  {
+    "context": "Editor && VimControl && !VimWaiting",
+    "bindings": {
+      "g": [
+        "vim::PushOperator",
+        {
+          "Namespace": "G"
         }
-    },
-    {
-        "context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting",
-        "bindings": {
-            "c": [
-                "vim::PushOperator",
-                "Change"
-            ],
-            "shift-c": "vim::ChangeToEndOfLine",
-            "d": [
-                "vim::PushOperator",
-                "Delete"
-            ],
-            "shift-d": "vim::DeleteToEndOfLine",
-            "y": [
-                "vim::PushOperator",
-                "Yank"
-            ],
-            "z": [
-                "vim::PushOperator",
-                {
-                    "Namespace": "Z"
-                }
-            ],
-            "i": [
-                "vim::SwitchMode",
-                "Insert"
-            ],
-            "shift-i": "vim::InsertFirstNonWhitespace",
-            "a": "vim::InsertAfter",
-            "shift-a": "vim::InsertEndOfLine",
-            "x": "vim::DeleteRight",
-            "shift-x": "vim::DeleteLeft",
-            "^": "vim::FirstNonWhitespace",
-            "o": "vim::InsertLineBelow",
-            "shift-o": "vim::InsertLineAbove",
-            "v": [
-                "vim::SwitchMode",
-                {
-                    "Visual": {
-                        "line": false
-                    }
-                }
-            ],
-            "shift-v": [
-                "vim::SwitchMode",
-                {
-                    "Visual": {
-                        "line": true
-                    }
-                }
-            ],
-            "p": "vim::Paste",
-            "u": "editor::Undo",
-            "ctrl-r": "editor::Redo",
-            "ctrl-o": "pane::GoBack",
-            "/": [
-                "buffer_search::Deploy",
-                {
-                    "focus": true
-                }
-            ],
-            "ctrl-f": [
-                "vim::Scroll",
-                "PageDown"
-            ],
-            "ctrl-b": [
-                "vim::Scroll",
-                "PageUp"
-            ],
-            "ctrl-d": [
-                "vim::Scroll",
-                "HalfPageDown"
-            ],
-            "ctrl-u": [
-                "vim::Scroll",
-                "HalfPageUp"
-            ],
-            "ctrl-e": [
-                "vim::Scroll",
-                "LineDown"
-            ],
-            "r": [
-                "vim::PushOperator",
-                "Replace"
-            ]
+      ],
+      "i": [
+        "vim::PushOperator",
+        {
+          "Object": {
+            "around": false
+          }
         }
-    },
-    {
-        "context": "Editor && vim_operator == n",
-        "bindings": {
-            "0": [
-                "vim::Number",
-                0
-            ]
+      ],
+      "a": [
+        "vim::PushOperator",
+        {
+          "Object": {
+            "around": true
+          }
         }
-    },
-    {
-        "context": "Editor && vim_operator == g",
-        "bindings": {
-            "g": "vim::StartOfDocument",
-            "h": "editor::Hover",
-            "escape": [
-                "vim::SwitchMode",
-                "Normal"
-            ],
-            "d": "editor::GoToDefinition"
+      ],
+      "h": "vim::Left",
+      "backspace": "vim::Backspace",
+      "j": "vim::Down",
+      "enter": "vim::NextLineStart",
+      "k": "vim::Up",
+      "l": "vim::Right",
+      "$": "vim::EndOfLine",
+      "shift-g": "vim::EndOfDocument",
+      "w": "vim::NextWordStart",
+      "shift-w": [
+        "vim::NextWordStart",
+        {
+          "ignorePunctuation": true
         }
-    },
-    {
-        "context": "Editor && vim_operator == c",
-        "bindings": {
-            "c": "vim::CurrentLine"
+      ],
+      "e": "vim::NextWordEnd",
+      "shift-e": [
+        "vim::NextWordEnd",
+        {
+          "ignorePunctuation": true
         }
-    },
-    {
-        "context": "Editor && vim_operator == d",
-        "bindings": {
-            "d": "vim::CurrentLine"
+      ],
+      "b": "vim::PreviousWordStart",
+      "shift-b": [
+        "vim::PreviousWordStart",
+        {
+          "ignorePunctuation": true
         }
-    },
-    {
-        "context": "Editor && vim_operator == y",
-        "bindings": {
-            "y": "vim::CurrentLine"
+      ],
+      "%": "vim::Matching",
+      "ctrl-y": [
+        "vim::Scroll",
+        "LineUp"
+      ],
+      "f": [
+        "vim::PushOperator",
+        {
+          "FindForward": {
+            "before": false
+          }
         }
-    },
-    {
-        "context": "Editor && vim_operator == z",
-        "bindings": {
-            "t": "editor::ScrollCursorTop",
-            "z": "editor::ScrollCursorCenter",
-            "b": "editor::ScrollCursorBottom",
-            "escape": [
-                "vim::SwitchMode",
-                "Normal"
-            ]
+      ],
+      "t": [
+        "vim::PushOperator",
+        {
+          "FindForward": {
+            "before": true
+          }
         }
-    },
-    {
-        "context": "Editor && VimObject",
-        "bindings": {
-            "w": "vim::Word",
-            "shift-w": [
-                "vim::Word",
-                {
-                    "ignorePunctuation": true
-                }
-            ],
-            "s": "vim::Sentence",
-            "'": "vim::Quotes",
-            "`": "vim::BackQuotes",
-            "\"": "vim::DoubleQuotes",
-            "(": "vim::Parentheses",
-            ")": "vim::Parentheses",
-            "[": "vim::SquareBrackets",
-            "]": "vim::SquareBrackets",
-            "{": "vim::CurlyBrackets",
-            "}": "vim::CurlyBrackets",
-            "<": "vim::AngleBrackets",
-            ">": "vim::AngleBrackets"
+      ],
+      "shift-f": [
+        "vim::PushOperator",
+        {
+          "FindBackward": {
+            "after": false
+          }
         }
-    },
-    {
-        "context": "Editor && vim_mode == visual && !VimWaiting",
-        "bindings": {
-            "u": "editor::Undo",
-            "c": "vim::VisualChange",
-            "d": "vim::VisualDelete",
-            "x": "vim::VisualDelete",
-            "y": "vim::VisualYank",
-            "p": "vim::VisualPaste",
-            "r": [
-                "vim::PushOperator",
-                "Replace"
-            ]
+      ],
+      "shift-t": [
+        "vim::PushOperator",
+        {
+          "FindBackward": {
+            "after": true
+          }
         }
-    },
-    {
-        "context": "Editor && vim_mode == insert",
-        "bindings": {
-            "escape": "vim::NormalBefore",
-            "ctrl-c": "vim::NormalBefore"
+      ],
+      "escape": "editor::Cancel",
+      "0": "vim::StartOfLine", // When no number operator present, use start of line motion
+      "1": [
+        "vim::Number",
+        1
+      ],
+      "2": [
+        "vim::Number",
+        2
+      ],
+      "3": [
+        "vim::Number",
+        3
+      ],
+      "4": [
+        "vim::Number",
+        4
+      ],
+      "5": [
+        "vim::Number",
+        5
+      ],
+      "6": [
+        "vim::Number",
+        6
+      ],
+      "7": [
+        "vim::Number",
+        7
+      ],
+      "8": [
+        "vim::Number",
+        8
+      ],
+      "9": [
+        "vim::Number",
+        9
+      ]
+    }
+  },
+  {
+    "context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting",
+    "bindings": {
+      "c": [
+        "vim::PushOperator",
+        "Change"
+      ],
+      "shift-c": "vim::ChangeToEndOfLine",
+      "d": [
+        "vim::PushOperator",
+        "Delete"
+      ],
+      "shift-d": "vim::DeleteToEndOfLine",
+      "y": [
+        "vim::PushOperator",
+        "Yank"
+      ],
+      "z": [
+        "vim::PushOperator",
+        {
+          "Namespace": "Z"
+        }
+      ],
+      "i": [
+        "vim::SwitchMode",
+        "Insert"
+      ],
+      "shift-i": "vim::InsertFirstNonWhitespace",
+      "a": "vim::InsertAfter",
+      "shift-a": "vim::InsertEndOfLine",
+      "x": "vim::DeleteRight",
+      "shift-x": "vim::DeleteLeft",
+      "^": "vim::FirstNonWhitespace",
+      "o": "vim::InsertLineBelow",
+      "shift-o": "vim::InsertLineAbove",
+      "v": [
+        "vim::SwitchMode",
+        {
+          "Visual": {
+            "line": false
+          }
+        }
+      ],
+      "shift-v": [
+        "vim::SwitchMode",
+        {
+          "Visual": {
+            "line": true
+          }
+        }
+      ],
+      "p": "vim::Paste",
+      "u": "editor::Undo",
+      "ctrl-r": "editor::Redo",
+      "ctrl-o": "pane::GoBack",
+      "/": [
+        "buffer_search::Deploy",
+        {
+          "focus": true
         }
-    },
-    {
-        "context": "Editor && VimWaiting",
-        "bindings": {
-            "tab": "vim::Tab",
-            "enter": "vim::Enter",
-            "escape": "editor::Cancel"
+      ],
+      "ctrl-f": [
+        "vim::Scroll",
+        "PageDown"
+      ],
+      "ctrl-b": [
+        "vim::Scroll",
+        "PageUp"
+      ],
+      "ctrl-d": [
+        "vim::Scroll",
+        "HalfPageDown"
+      ],
+      "ctrl-u": [
+        "vim::Scroll",
+        "HalfPageUp"
+      ],
+      "ctrl-e": [
+        "vim::Scroll",
+        "LineDown"
+      ],
+      "r": [
+        "vim::PushOperator",
+        "Replace"
+      ]
+    }
+  },
+  {
+    "context": "Editor && vim_operator == n",
+    "bindings": {
+      "0": [
+        "vim::Number",
+        0
+      ]
+    }
+  },
+  {
+    "context": "Editor && vim_operator == g",
+    "bindings": {
+      "g": "vim::StartOfDocument",
+      "h": "editor::Hover",
+      "escape": [
+        "vim::SwitchMode",
+        "Normal"
+      ],
+      "d": "editor::GoToDefinition"
+    }
+  },
+  {
+    "context": "Editor && vim_operator == c",
+    "bindings": {
+      "c": "vim::CurrentLine"
+    }
+  },
+  {
+    "context": "Editor && vim_operator == d",
+    "bindings": {
+      "d": "vim::CurrentLine"
+    }
+  },
+  {
+    "context": "Editor && vim_operator == y",
+    "bindings": {
+      "y": "vim::CurrentLine"
+    }
+  },
+  {
+    "context": "Editor && vim_operator == z",
+    "bindings": {
+      "t": "editor::ScrollCursorTop",
+      "z": "editor::ScrollCursorCenter",
+      "b": "editor::ScrollCursorBottom",
+      "escape": [
+        "vim::SwitchMode",
+        "Normal"
+      ]
+    }
+  },
+  {
+    "context": "Editor && VimObject",
+    "bindings": {
+      "w": "vim::Word",
+      "shift-w": [
+        "vim::Word",
+        {
+          "ignorePunctuation": true
         }
+      ],
+      "s": "vim::Sentence",
+      "'": "vim::Quotes",
+      "`": "vim::BackQuotes",
+      "\"": "vim::DoubleQuotes",
+      "(": "vim::Parentheses",
+      ")": "vim::Parentheses",
+      "[": "vim::SquareBrackets",
+      "]": "vim::SquareBrackets",
+      "{": "vim::CurlyBrackets",
+      "}": "vim::CurlyBrackets",
+      "<": "vim::AngleBrackets",
+      ">": "vim::AngleBrackets"
+    }
+  },
+  {
+    "context": "Editor && vim_mode == visual && !VimWaiting",
+    "bindings": {
+      "u": "editor::Undo",
+      "c": "vim::VisualChange",
+      "d": "vim::VisualDelete",
+      "x": "vim::VisualDelete",
+      "y": "vim::VisualYank",
+      "p": "vim::VisualPaste",
+      "r": [
+        "vim::PushOperator",
+        "Replace"
+      ]
+    }
+  },
+  {
+    "context": "Editor && vim_mode == insert",
+    "bindings": {
+      "escape": "vim::NormalBefore",
+      "ctrl-c": "vim::NormalBefore"
+    }
+  },
+  {
+    "context": "Editor && VimWaiting",
+    "bindings": {
+      "tab": "vim::Tab",
+      "enter": "vim::Enter",
+      "escape": "editor::Cancel"
     }
-]
+  }
+]

assets/settings/default.json 🔗

@@ -1,264 +1,267 @@
 {
-    // The name of the Zed theme to use for the UI
-    "theme": "One Dark",
-    // Features that can be globally enabled or disabled
-    "features": {
-        // Show Copilot icon in status bar
-        "copilot": true
-    },
-    // The name of a font to use for rendering text in the editor
-    "buffer_font_family": "Zed Mono",
-    // The OpenType features to enable for text in the editor.
-    "buffer_font_features": {
-        // Disable ligatures:
-        // "calt": false
-    },
-    // 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 to confirm before quitting Zed.
-    "confirm_quit": false,
-    // 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,
-    // Controls whether copilot provides suggestion immediately
-    // or waits for a `copilot::Toggle`
-    "show_copilot_suggestions": true,
-    // Whether the screen sharing icon is shown in the os status bar.
-    "show_call_status_icon": 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": "bottom",
-    // Whether or not to remove any trailing whitespace from lines of a buffer
-    // before saving it.
-    "remove_trailing_whitespace_on_save": true,
-    // Whether or not to ensure there's a single newline at the end of a buffer
-    // when saving it.
-    "ensure_final_newline_on_save": true,
-    // 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}"]
-    //       }
+  // The name of the Zed theme to use for the UI
+  "theme": "One Dark",
+  // Features that can be globally enabled or disabled
+  "features": {
+    // Show Copilot icon in status bar
+    "copilot": true
+  },
+  // The name of a font to use for rendering text in the editor
+  "buffer_font_family": "Zed Mono",
+  // The OpenType features to enable for text in the editor.
+  "buffer_font_features": {
+    // Disable ligatures:
+    // "calt": false
+  },
+  // 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 to confirm before quitting Zed.
+  "confirm_quit": false,
+  // 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,
+  // Controls whether copilot provides suggestion immediately
+  // or waits for a `copilot::Toggle`
+  "show_copilot_suggestions": true,
+  // Whether the screen sharing icon is shown in the os status bar.
+  "show_call_status_icon": 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": "bottom",
+  // Whether or not to remove any trailing whitespace from lines of a buffer
+  // before saving it.
+  "remove_trailing_whitespace_on_save": true,
+  // Whether or not to ensure there's a single newline at the end of a buffer
+  // when saving it.
+  "ensure_final_newline_on_save": true,
+  // 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,
+  // Control what info is collected by Zed.
+  "telemetry": {
+    // Send debug info like crash reports.
+    "diagnostics": true,
+    // Send anonymized usage data like what languages you're using Zed with.
+    "metrics": true
+  },
+  // Automatically update Zed
+  "auto_update": true,
+  // 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 in /etc/passwd
+    //      "shell": "system"
+    // 2. A program:
+    //      "shell": {
+    //        "program": "sh"
+    //      }
+    // 3. A program with arguments:
+    //     "shell": {
+    //         "with_arguments": {
+    //           "program": "/bin/bash",
+    //           "arguments": ["--login"]
+    //         }
     //     }
-    "formatter": "language_server",
-    // How to soft-wrap long lines of text. This setting can take
-    // three values:
+    "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/"
+    //        }
+    //      }
+    //
     //
-    // 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,
-    // Control what info is collected by Zed.
-    "telemetry": {
-        // Send debug info like crash reports.
-        "diagnostics": true,
-        // Send anonymized usage data like what languages you're using Zed with.
-        "metrics": true
+    "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"
     },
-    // Automatically update Zed
-    "auto_update": true,
-    // 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"
+    // Set the terminal's line height.
+    // May take 3 values:
+    //  1. Use a line height that's comfortable for reading, 1.618
+    //         "line_height": "comfortable"
+    //  2. Use a standard line height, 1.3. This option is useful for TUIs,
+    //      particularly if they use box characters
+    //         "line_height": "standard",
+    //  3. Use a custom line height.
+    //         "line_height": 1.2,
+    "line_height": "comfortable"
+    // 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"
     },
-    // 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"
+    "Elixir": {
+      "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 in /etc/passwd
-        //      "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 line height.
-        // May take 3 values:
-        //  1. Use a line height that's comfortable for reading, 1.618
-        //         "line_height": "comfortable"
-        //  2. Use a standard line height, 1.3. This option is useful for TUIs,
-        //      particularly if they use box characters
-        //         "line_height": "standard",
-        //  3. Use a custom line height.
-        //         "line_height": 1.2,
-        "line_height": "comfortable"
-        // 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"
+    "Go": {
+      "tab_size": 4,
+      "hard_tabs": true
     },
-    // Different settings for specific languages.
-    "languages": {
-        "Plain Text": {
-            "soft_wrap": "preferred_line_length"
-        },
-        "Elixir": {
-            "tab_size": 2
-        },
-        "Go": {
-            "tab_size": 4,
-            "hard_tabs": true
-        },
-        "Markdown": {
-            "soft_wrap": "preferred_line_length"
-        },
-        "JavaScript": {
-            "tab_size": 2
-        },
-        "TypeScript": {
-            "tab_size": 2
-        },
-        "TSX": {
-            "tab_size": 2
-        },
-        "YAML": {
-            "tab_size": 2
-        }
+    "Markdown": {
+      "soft_wrap": "preferred_line_length"
     },
-    // 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"
-        //         }
-        //     }
-        // }
+    "JavaScript": {
+      "tab_size": 2
+    },
+    "TypeScript": {
+      "tab_size": 2
+    },
+    "TSX": {
+      "tab_size": 2
+    },
+    "YAML": {
+      "tab_size": 2
+    },
+    "JSON": {
+      "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"
+    //         }
+    //     }
+    // }
+  }
 }

assets/settings/initial_user_settings.json 🔗

@@ -7,5 +7,5 @@
 // custom settings, run the `open default settings` command
 // from the command palette or from `Zed` application menu.
 {
-    "buffer_font_size": 15
+  "buffer_font_size": 15
 }

crates/activity_indicator/Cargo.toml 🔗

@@ -18,4 +18,4 @@ settings = { path = "../settings" }
 util = { path = "../util" }
 workspace = { path = "../workspace" }
 futures = "0.3"
-smallvec = { version = "1.6", features = ["union"] }
+smallvec = { workspace = true }

crates/clock/Cargo.toml 🔗

@@ -9,4 +9,4 @@ path = "src/clock.rs"
 doctest = false
 
 [dependencies]
-smallvec = { version = "1.6", features = ["union"] }
+smallvec = { workspace = true }

crates/collab/Cargo.toml 🔗

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

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

@@ -22,6 +22,7 @@ use language::{
     LanguageConfig, OffsetRangeExt, Point, Rope,
 };
 use live_kit_client::MacOSDisplay;
+use lsp::LanguageServerId;
 use project::{search::SearchQuery, DiagnosticSummary, Project, ProjectPath};
 use rand::prelude::*;
 use serde_json::json;
@@ -3477,6 +3478,7 @@ async fn test_collaborating_with_diagnostics(
                     worktree_id,
                     path: Arc::from(Path::new("a.rs")),
                 },
+                LanguageServerId(0),
                 DiagnosticSummary {
                     error_count: 1,
                     warning_count: 0,
@@ -3512,6 +3514,7 @@ async fn test_collaborating_with_diagnostics(
                 worktree_id,
                 path: Arc::from(Path::new("a.rs")),
             },
+            LanguageServerId(0),
             DiagnosticSummary {
                 error_count: 1,
                 warning_count: 0,
@@ -3552,10 +3555,10 @@ async fn test_collaborating_with_diagnostics(
                     worktree_id,
                     path: Arc::from(Path::new("a.rs")),
                 },
+                LanguageServerId(0),
                 DiagnosticSummary {
                     error_count: 1,
                     warning_count: 1,
-                    ..Default::default()
                 },
             )]
         );
@@ -3568,10 +3571,10 @@ async fn test_collaborating_with_diagnostics(
                     worktree_id,
                     path: Arc::from(Path::new("a.rs")),
                 },
+                LanguageServerId(0),
                 DiagnosticSummary {
                     error_count: 1,
                     warning_count: 1,
-                    ..Default::default()
                 },
             )]
         );

crates/context_menu/Cargo.toml 🔗

@@ -13,4 +13,4 @@ gpui = { path = "../gpui" }
 menu = { path = "../menu" }
 settings = { path = "../settings" }
 theme = { path = "../theme" }
-smallvec = "1.6"
+smallvec = { workspace = true }

crates/copilot/Cargo.toml 🔗

@@ -38,10 +38,13 @@ smol = "1.2.5"
 futures = "0.3"
 
 [dev-dependencies]
+clock = { path = "../clock" }
 collections = { path = "../collections", features = ["test-support"] }
+fs = { path = "../fs", features = ["test-support"] }
 gpui = { path = "../gpui", features = ["test-support"] }
 language = { path = "../language", features = ["test-support"] }
-settings = { path = "../settings", features = ["test-support"] }
 lsp = { path = "../lsp", features = ["test-support"] }
+rpc = { path = "../rpc", features = ["test-support"] }
+settings = { path = "../settings", features = ["test-support"] }
 util = { path = "../util", features = ["test-support"] }
 workspace = { path = "../workspace", features = ["test-support"] }

crates/copilot/src/copilot.rs 🔗

@@ -5,24 +5,29 @@ use anyhow::{anyhow, Context, Result};
 use async_compression::futures::bufread::GzipDecoder;
 use async_tar::Archive;
 use collections::HashMap;
-use futures::{future::Shared, Future, FutureExt, TryFutureExt};
-use gpui::{actions, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task};
-use language::{point_from_lsp, point_to_lsp, Anchor, Bias, Buffer, Language, ToPointUtf16};
+use futures::{channel::oneshot, future::Shared, Future, FutureExt, TryFutureExt};
+use gpui::{
+    actions, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle,
+};
+use language::{
+    point_from_lsp, point_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, Language, PointUtf16,
+    ToPointUtf16,
+};
 use log::{debug, error};
-use lsp::LanguageServer;
+use lsp::{LanguageServer, LanguageServerId};
 use node_runtime::NodeRuntime;
 use request::{LogMessage, StatusNotification};
 use settings::Settings;
 use smol::{fs, io::BufReader, stream::StreamExt};
 use std::{
     ffi::OsString,
+    mem,
     ops::Range,
     path::{Path, PathBuf},
     sync::Arc,
 };
 use util::{
-    channel::ReleaseChannel, fs::remove_matching, github::latest_github_release, http::HttpClient,
-    paths, ResultExt,
+    fs::remove_matching, github::latest_github_release, http::HttpClient, paths, ResultExt,
 };
 
 const COPILOT_AUTH_NAMESPACE: &'static str = "copilot_auth";
@@ -35,15 +40,6 @@ actions!(
 );
 
 pub fn init(http: Arc<dyn HttpClient>, node_runtime: Arc<NodeRuntime>, cx: &mut AppContext) {
-    // Disable Copilot for stable releases.
-    if *cx.global::<ReleaseChannel>() == ReleaseChannel::Stable {
-        cx.update_global::<collections::CommandPaletteFilter, _, _>(|filter, _cx| {
-            filter.filtered_namespaces.insert(COPILOT_NAMESPACE);
-            filter.filtered_namespaces.insert(COPILOT_AUTH_NAMESPACE);
-        });
-        return;
-    }
-
     let copilot = cx.add_model({
         let node_runtime = node_runtime.clone();
         move |cx| Copilot::start(http, node_runtime, cx)
@@ -98,15 +94,38 @@ pub fn init(http: Arc<dyn HttpClient>, node_runtime: Arc<NodeRuntime>, cx: &mut
 
 enum CopilotServer {
     Disabled,
-    Starting {
-        task: Shared<Task<()>>,
-    },
+    Starting { task: Shared<Task<()>> },
     Error(Arc<str>),
-    Started {
-        server: Arc<LanguageServer>,
-        status: SignInStatus,
-        subscriptions_by_buffer_id: HashMap<usize, gpui::Subscription>,
-    },
+    Running(RunningCopilotServer),
+}
+
+impl CopilotServer {
+    fn as_authenticated(&mut self) -> Result<&mut RunningCopilotServer> {
+        let server = self.as_running()?;
+        if matches!(server.sign_in_status, SignInStatus::Authorized { .. }) {
+            Ok(server)
+        } else {
+            Err(anyhow!("must sign in before using copilot"))
+        }
+    }
+
+    fn as_running(&mut self) -> Result<&mut RunningCopilotServer> {
+        match self {
+            CopilotServer::Starting { .. } => Err(anyhow!("copilot is still starting")),
+            CopilotServer::Disabled => Err(anyhow!("copilot is disabled")),
+            CopilotServer::Error(error) => Err(anyhow!(
+                "copilot was not started because of an error: {}",
+                error
+            )),
+            CopilotServer::Running(server) => Ok(server),
+        }
+    }
+}
+
+struct RunningCopilotServer {
+    lsp: Arc<LanguageServer>,
+    sign_in_status: SignInStatus,
+    registered_buffers: HashMap<usize, RegisteredBuffer>,
 }
 
 #[derive(Clone, Debug)]
@@ -141,8 +160,104 @@ impl Status {
     }
 }
 
-#[derive(Debug, PartialEq, Eq)]
+struct RegisteredBuffer {
+    id: usize,
+    uri: lsp::Url,
+    language_id: String,
+    snapshot: BufferSnapshot,
+    snapshot_version: i32,
+    _subscriptions: [gpui::Subscription; 2],
+    pending_buffer_change: Task<Option<()>>,
+}
+
+impl RegisteredBuffer {
+    fn report_changes(
+        &mut self,
+        buffer: &ModelHandle<Buffer>,
+        cx: &mut ModelContext<Copilot>,
+    ) -> oneshot::Receiver<(i32, BufferSnapshot)> {
+        let id = self.id;
+        let (done_tx, done_rx) = oneshot::channel();
+
+        if buffer.read(cx).version() == self.snapshot.version {
+            let _ = done_tx.send((self.snapshot_version, self.snapshot.clone()));
+        } else {
+            let buffer = buffer.downgrade();
+            let prev_pending_change =
+                mem::replace(&mut self.pending_buffer_change, Task::ready(None));
+            self.pending_buffer_change = cx.spawn_weak(|copilot, mut cx| async move {
+                prev_pending_change.await;
+
+                let old_version = copilot.upgrade(&cx)?.update(&mut cx, |copilot, _| {
+                    let server = copilot.server.as_authenticated().log_err()?;
+                    let buffer = server.registered_buffers.get_mut(&id)?;
+                    Some(buffer.snapshot.version.clone())
+                })?;
+                let new_snapshot = buffer
+                    .upgrade(&cx)?
+                    .read_with(&cx, |buffer, _| buffer.snapshot());
+
+                let content_changes = cx
+                    .background()
+                    .spawn({
+                        let new_snapshot = new_snapshot.clone();
+                        async move {
+                            new_snapshot
+                                .edits_since::<(PointUtf16, usize)>(&old_version)
+                                .map(|edit| {
+                                    let edit_start = edit.new.start.0;
+                                    let edit_end = edit_start + (edit.old.end.0 - edit.old.start.0);
+                                    let new_text = new_snapshot
+                                        .text_for_range(edit.new.start.1..edit.new.end.1)
+                                        .collect();
+                                    lsp::TextDocumentContentChangeEvent {
+                                        range: Some(lsp::Range::new(
+                                            point_to_lsp(edit_start),
+                                            point_to_lsp(edit_end),
+                                        )),
+                                        range_length: None,
+                                        text: new_text,
+                                    }
+                                })
+                                .collect::<Vec<_>>()
+                        }
+                    })
+                    .await;
+
+                copilot.upgrade(&cx)?.update(&mut cx, |copilot, _| {
+                    let server = copilot.server.as_authenticated().log_err()?;
+                    let buffer = server.registered_buffers.get_mut(&id)?;
+                    if !content_changes.is_empty() {
+                        buffer.snapshot_version += 1;
+                        buffer.snapshot = new_snapshot;
+                        server
+                            .lsp
+                            .notify::<lsp::notification::DidChangeTextDocument>(
+                                lsp::DidChangeTextDocumentParams {
+                                    text_document: lsp::VersionedTextDocumentIdentifier::new(
+                                        buffer.uri.clone(),
+                                        buffer.snapshot_version,
+                                    ),
+                                    content_changes,
+                                },
+                            )
+                            .log_err();
+                    }
+                    let _ = done_tx.send((buffer.snapshot_version, buffer.snapshot.clone()));
+                    Some(())
+                })?;
+
+                Some(())
+            });
+        }
+
+        done_rx
+    }
+}
+
+#[derive(Debug)]
 pub struct Completion {
+    uuid: String,
     pub range: Range<Anchor>,
     pub text: String,
 }
@@ -151,6 +266,7 @@ pub struct Copilot {
     http: Arc<dyn HttpClient>,
     node_runtime: Arc<NodeRuntime>,
     server: CopilotServer,
+    buffers: HashMap<usize, WeakModelHandle<Buffer>>,
 }
 
 impl Entity for Copilot {
@@ -212,12 +328,14 @@ impl Copilot {
                 http,
                 node_runtime,
                 server: CopilotServer::Starting { task: start_task },
+                buffers: Default::default(),
             }
         } else {
             Self {
                 http,
                 node_runtime,
                 server: CopilotServer::Disabled,
+                buffers: Default::default(),
             }
         }
     }
@@ -230,11 +348,12 @@ impl Copilot {
         let this = cx.add_model(|cx| Self {
             http: http.clone(),
             node_runtime: NodeRuntime::new(http, cx.background().clone()),
-            server: CopilotServer::Started {
-                server: Arc::new(server),
-                status: SignInStatus::Authorized,
-                subscriptions_by_buffer_id: Default::default(),
-            },
+            server: CopilotServer::Running(RunningCopilotServer {
+                lsp: Arc::new(server),
+                sign_in_status: SignInStatus::Authorized,
+                registered_buffers: Default::default(),
+            }),
+            buffers: Default::default(),
         });
         (this, fake_server)
     }
@@ -251,7 +370,7 @@ impl Copilot {
                 let node_path = node_runtime.binary_path().await?;
                 let arguments: &[OsString] = &[server_path.into(), "--stdio".into()];
                 let server = LanguageServer::new(
-                    0,
+                    LanguageServerId(0),
                     &node_path,
                     arguments,
                     Path::new("/"),
@@ -286,6 +405,19 @@ impl Copilot {
                     )
                     .detach();
 
+                server
+                    .request::<request::SetEditorInfo>(request::SetEditorInfoParams {
+                        editor_info: request::EditorInfo {
+                            name: "zed".into(),
+                            version: env!("CARGO_PKG_VERSION").into(),
+                        },
+                        editor_plugin_info: request::EditorPluginInfo {
+                            name: "zed-copilot".into(),
+                            version: "0.0.1".into(),
+                        },
+                    })
+                    .await?;
+
                 anyhow::Ok((server, status))
             };
 
@@ -294,11 +426,11 @@ impl Copilot {
                 cx.notify();
                 match server {
                     Ok((server, status)) => {
-                        this.server = CopilotServer::Started {
-                            server,
-                            status: SignInStatus::SignedOut,
-                            subscriptions_by_buffer_id: Default::default(),
-                        };
+                        this.server = CopilotServer::Running(RunningCopilotServer {
+                            lsp: server,
+                            sign_in_status: SignInStatus::SignedOut,
+                            registered_buffers: Default::default(),
+                        });
                         this.update_sign_in_status(status, cx);
                     }
                     Err(error) => {
@@ -311,8 +443,8 @@ impl Copilot {
     }
 
     fn sign_in(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
-        if let CopilotServer::Started { server, status, .. } = &mut self.server {
-            let task = match status {
+        if let CopilotServer::Running(server) = &mut self.server {
+            let task = match &server.sign_in_status {
                 SignInStatus::Authorized { .. } | SignInStatus::Unauthorized { .. } => {
                     Task::ready(Ok(())).shared()
                 }
@@ -321,11 +453,11 @@ impl Copilot {
                     task.clone()
                 }
                 SignInStatus::SignedOut => {
-                    let server = server.clone();
+                    let lsp = server.lsp.clone();
                     let task = cx
                         .spawn(|this, mut cx| async move {
                             let sign_in = async {
-                                let sign_in = server
+                                let sign_in = lsp
                                     .request::<request::SignInInitiate>(
                                         request::SignInInitiateParams {},
                                     )
@@ -336,8 +468,10 @@ impl Copilot {
                                     }
                                     request::SignInInitiateResult::PromptUserDeviceFlow(flow) => {
                                         this.update(&mut cx, |this, cx| {
-                                            if let CopilotServer::Started { status, .. } =
-                                                &mut this.server
+                                            if let CopilotServer::Running(RunningCopilotServer {
+                                                sign_in_status: status,
+                                                ..
+                                            }) = &mut this.server
                                             {
                                                 if let SignInStatus::SigningIn {
                                                     prompt: prompt_flow,
@@ -349,7 +483,7 @@ impl Copilot {
                                                 }
                                             }
                                         });
-                                        let response = server
+                                        let response = lsp
                                             .request::<request::SignInConfirm>(
                                                 request::SignInConfirmParams {
                                                     user_code: flow.user_code,
@@ -377,7 +511,7 @@ impl Copilot {
                             })
                         })
                         .shared();
-                    *status = SignInStatus::SigningIn {
+                    server.sign_in_status = SignInStatus::SigningIn {
                         prompt: None,
                         task: task.clone(),
                     };
@@ -396,10 +530,8 @@ impl Copilot {
     }
 
     fn sign_out(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
-        if let CopilotServer::Started { server, status, .. } = &mut self.server {
-            *status = SignInStatus::SignedOut;
-            cx.notify();
-
+        self.update_sign_in_status(request::SignInStatus::NotSignedIn, cx);
+        if let CopilotServer::Running(RunningCopilotServer { lsp: server, .. }) = &self.server {
             let server = server.clone();
             cx.background().spawn(async move {
                 server
@@ -433,6 +565,135 @@ impl Copilot {
         cx.foreground().spawn(start_task)
     }
 
+    pub fn register_buffer(&mut self, buffer: &ModelHandle<Buffer>, cx: &mut ModelContext<Self>) {
+        let buffer_id = buffer.id();
+        self.buffers.insert(buffer_id, buffer.downgrade());
+
+        if let CopilotServer::Running(RunningCopilotServer {
+            lsp: server,
+            sign_in_status: status,
+            registered_buffers,
+            ..
+        }) = &mut self.server
+        {
+            if !matches!(status, SignInStatus::Authorized { .. }) {
+                return;
+            }
+
+            registered_buffers.entry(buffer.id()).or_insert_with(|| {
+                let uri: lsp::Url = uri_for_buffer(buffer, cx);
+                let language_id = id_for_language(buffer.read(cx).language());
+                let snapshot = buffer.read(cx).snapshot();
+                server
+                    .notify::<lsp::notification::DidOpenTextDocument>(
+                        lsp::DidOpenTextDocumentParams {
+                            text_document: lsp::TextDocumentItem {
+                                uri: uri.clone(),
+                                language_id: language_id.clone(),
+                                version: 0,
+                                text: snapshot.text(),
+                            },
+                        },
+                    )
+                    .log_err();
+
+                RegisteredBuffer {
+                    id: buffer_id,
+                    uri,
+                    language_id,
+                    snapshot,
+                    snapshot_version: 0,
+                    pending_buffer_change: Task::ready(Some(())),
+                    _subscriptions: [
+                        cx.subscribe(buffer, |this, buffer, event, cx| {
+                            this.handle_buffer_event(buffer, event, cx).log_err();
+                        }),
+                        cx.observe_release(buffer, move |this, _buffer, _cx| {
+                            this.buffers.remove(&buffer_id);
+                            this.unregister_buffer(buffer_id);
+                        }),
+                    ],
+                }
+            });
+        }
+    }
+
+    fn handle_buffer_event(
+        &mut self,
+        buffer: ModelHandle<Buffer>,
+        event: &language::Event,
+        cx: &mut ModelContext<Self>,
+    ) -> Result<()> {
+        if let Ok(server) = self.server.as_running() {
+            if let Some(registered_buffer) = server.registered_buffers.get_mut(&buffer.id()) {
+                match event {
+                    language::Event::Edited => {
+                        let _ = registered_buffer.report_changes(&buffer, cx);
+                    }
+                    language::Event::Saved => {
+                        server
+                            .lsp
+                            .notify::<lsp::notification::DidSaveTextDocument>(
+                                lsp::DidSaveTextDocumentParams {
+                                    text_document: lsp::TextDocumentIdentifier::new(
+                                        registered_buffer.uri.clone(),
+                                    ),
+                                    text: None,
+                                },
+                            )?;
+                    }
+                    language::Event::FileHandleChanged | language::Event::LanguageChanged => {
+                        let new_language_id = id_for_language(buffer.read(cx).language());
+                        let new_uri = uri_for_buffer(&buffer, cx);
+                        if new_uri != registered_buffer.uri
+                            || new_language_id != registered_buffer.language_id
+                        {
+                            let old_uri = mem::replace(&mut registered_buffer.uri, new_uri);
+                            registered_buffer.language_id = new_language_id;
+                            server
+                                .lsp
+                                .notify::<lsp::notification::DidCloseTextDocument>(
+                                    lsp::DidCloseTextDocumentParams {
+                                        text_document: lsp::TextDocumentIdentifier::new(old_uri),
+                                    },
+                                )?;
+                            server
+                                .lsp
+                                .notify::<lsp::notification::DidOpenTextDocument>(
+                                    lsp::DidOpenTextDocumentParams {
+                                        text_document: lsp::TextDocumentItem::new(
+                                            registered_buffer.uri.clone(),
+                                            registered_buffer.language_id.clone(),
+                                            registered_buffer.snapshot_version,
+                                            registered_buffer.snapshot.text(),
+                                        ),
+                                    },
+                                )?;
+                        }
+                    }
+                    _ => {}
+                }
+            }
+        }
+
+        Ok(())
+    }
+
+    fn unregister_buffer(&mut self, buffer_id: usize) {
+        if let Ok(server) = self.server.as_running() {
+            if let Some(buffer) = server.registered_buffers.remove(&buffer_id) {
+                server
+                    .lsp
+                    .notify::<lsp::notification::DidCloseTextDocument>(
+                        lsp::DidCloseTextDocumentParams {
+                            text_document: lsp::TextDocumentIdentifier::new(buffer.uri),
+                        },
+                    )
+                    .log_err();
+            }
+        }
+    }
+
     pub fn completions<T>(
         &mut self,
         buffer: &ModelHandle<Buffer>,
@@ -457,6 +718,51 @@ impl Copilot {
         self.request_completions::<request::GetCompletionsCycling, _>(buffer, position, cx)
     }
 
+    pub fn accept_completion(
+        &mut self,
+        completion: &Completion,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<()>> {
+        let server = match self.server.as_authenticated() {
+            Ok(server) => server,
+            Err(error) => return Task::ready(Err(error)),
+        };
+        let request =
+            server
+                .lsp
+                .request::<request::NotifyAccepted>(request::NotifyAcceptedParams {
+                    uuid: completion.uuid.clone(),
+                });
+        cx.background().spawn(async move {
+            request.await?;
+            Ok(())
+        })
+    }
+
+    pub fn discard_completions(
+        &mut self,
+        completions: &[Completion],
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<()>> {
+        let server = match self.server.as_authenticated() {
+            Ok(server) => server,
+            Err(error) => return Task::ready(Err(error)),
+        };
+        let request =
+            server
+                .lsp
+                .request::<request::NotifyRejected>(request::NotifyRejectedParams {
+                    uuids: completions
+                        .iter()
+                        .map(|completion| completion.uuid.clone())
+                        .collect(),
+                });
+        cx.background().spawn(async move {
+            request.await?;
+            Ok(())
+        })
+    }
+
     fn request_completions<R, T>(
         &mut self,
         buffer: &ModelHandle<Buffer>,
@@ -464,116 +770,48 @@ impl Copilot {
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<Vec<Completion>>>
     where
-        R: lsp::request::Request<
-            Params = request::GetCompletionsParams,
-            Result = request::GetCompletionsResult,
-        >,
+        R: 'static
+            + lsp::request::Request<
+                Params = request::GetCompletionsParams,
+                Result = request::GetCompletionsResult,
+            >,
         T: ToPointUtf16,
     {
-        let buffer_id = buffer.id();
-        let uri: lsp::Url = format!("buffer://{}", buffer_id).parse().unwrap();
-        let snapshot = buffer.read(cx).snapshot();
-        let server = match &mut self.server {
-            CopilotServer::Starting { .. } => {
-                return Task::ready(Err(anyhow!("copilot is still starting")))
-            }
-            CopilotServer::Disabled => return Task::ready(Err(anyhow!("copilot is disabled"))),
-            CopilotServer::Error(error) => {
-                return Task::ready(Err(anyhow!(
-                    "copilot was not started because of an error: {}",
-                    error
-                )))
-            }
-            CopilotServer::Started {
-                server,
-                status,
-                subscriptions_by_buffer_id,
-            } => {
-                if matches!(status, SignInStatus::Authorized { .. }) {
-                    subscriptions_by_buffer_id
-                        .entry(buffer_id)
-                        .or_insert_with(|| {
-                            server
-                                .notify::<lsp::notification::DidOpenTextDocument>(
-                                    lsp::DidOpenTextDocumentParams {
-                                        text_document: lsp::TextDocumentItem {
-                                            uri: uri.clone(),
-                                            language_id: id_for_language(
-                                                buffer.read(cx).language(),
-                                            ),
-                                            version: 0,
-                                            text: snapshot.text(),
-                                        },
-                                    },
-                                )
-                                .log_err();
-
-                            let uri = uri.clone();
-                            cx.observe_release(buffer, move |this, _, _| {
-                                if let CopilotServer::Started {
-                                    server,
-                                    subscriptions_by_buffer_id,
-                                    ..
-                                } = &mut this.server
-                                {
-                                    server
-                                        .notify::<lsp::notification::DidCloseTextDocument>(
-                                            lsp::DidCloseTextDocumentParams {
-                                                text_document: lsp::TextDocumentIdentifier::new(
-                                                    uri.clone(),
-                                                ),
-                                            },
-                                        )
-                                        .log_err();
-                                    subscriptions_by_buffer_id.remove(&buffer_id);
-                                }
-                            })
-                        });
+        self.register_buffer(buffer, cx);
 
-                    server.clone()
-                } else {
-                    return Task::ready(Err(anyhow!("must sign in before using copilot")));
-                }
-            }
+        let server = match self.server.as_authenticated() {
+            Ok(server) => server,
+            Err(error) => return Task::ready(Err(error)),
         };
-
+        let lsp = server.lsp.clone();
+        let registered_buffer = server.registered_buffers.get_mut(&buffer.id()).unwrap();
+        let snapshot = registered_buffer.report_changes(buffer, cx);
+        let buffer = buffer.read(cx);
+        let uri = registered_buffer.uri.clone();
         let settings = cx.global::<Settings>();
-        let position = position.to_point_utf16(&snapshot);
-        let language = snapshot.language_at(position);
+        let position = position.to_point_utf16(buffer);
+        let language = buffer.language_at(position);
         let language_name = language.map(|language| language.name());
         let language_name = language_name.as_deref();
         let tab_size = settings.tab_size(language_name);
         let hard_tabs = settings.hard_tabs(language_name);
-        let language_id = id_for_language(language);
-
-        let path;
-        let relative_path;
-        if let Some(file) = snapshot.file() {
-            if let Some(file) = file.as_local() {
-                path = file.abs_path(cx);
-            } else {
-                path = file.full_path(cx);
-            }
-            relative_path = file.path().to_path_buf();
-        } else {
-            path = PathBuf::new();
-            relative_path = PathBuf::new();
-        }
+        let relative_path = buffer
+            .file()
+            .map(|file| file.path().to_path_buf())
+            .unwrap_or_default();
 
-        cx.background().spawn(async move {
-            let result = server
+        cx.foreground().spawn(async move {
+            let (version, snapshot) = snapshot.await?;
+            let result = lsp
                 .request::<R>(request::GetCompletionsParams {
                     doc: request::GetCompletionsDocument {
-                        source: snapshot.text(),
+                        uri,
                         tab_size: tab_size.into(),
                         indent_size: 1,
                         insert_spaces: !hard_tabs,
-                        uri,
-                        path: path.to_string_lossy().into(),
                         relative_path: relative_path.to_string_lossy().into(),
-                        language_id,
                         position: point_to_lsp(position),
-                        version: 0,
+                        version: version.try_into().unwrap(),
                     },
                 })
                 .await?;
@@ -586,6 +824,7 @@ impl Copilot {
                     let end =
                         snapshot.clip_point_utf16(point_from_lsp(completion.range.end), Bias::Left);
                     Completion {
+                        uuid: completion.uuid,
                         range: snapshot.anchor_before(start)..snapshot.anchor_after(end),
                         text: completion.text,
                     }
@@ -600,14 +839,16 @@ impl Copilot {
             CopilotServer::Starting { task } => Status::Starting { task: task.clone() },
             CopilotServer::Disabled => Status::Disabled,
             CopilotServer::Error(error) => Status::Error(error.clone()),
-            CopilotServer::Started { status, .. } => match status {
-                SignInStatus::Authorized { .. } => Status::Authorized,
-                SignInStatus::Unauthorized { .. } => Status::Unauthorized,
-                SignInStatus::SigningIn { prompt, .. } => Status::SigningIn {
-                    prompt: prompt.clone(),
-                },
-                SignInStatus::SignedOut => Status::SignedOut,
-            },
+            CopilotServer::Running(RunningCopilotServer { sign_in_status, .. }) => {
+                match sign_in_status {
+                    SignInStatus::Authorized { .. } => Status::Authorized,
+                    SignInStatus::Unauthorized { .. } => Status::Unauthorized,
+                    SignInStatus::SigningIn { prompt, .. } => Status::SigningIn {
+                        prompt: prompt.clone(),
+                    },
+                    SignInStatus::SignedOut => Status::SignedOut,
+                }
+            }
         }
     }
 
@@ -616,14 +857,34 @@ impl Copilot {
         lsp_status: request::SignInStatus,
         cx: &mut ModelContext<Self>,
     ) {
-        if let CopilotServer::Started { status, .. } = &mut self.server {
-            *status = match lsp_status {
+        self.buffers.retain(|_, buffer| buffer.is_upgradable(cx));
+
+        if let Ok(server) = self.server.as_running() {
+            match lsp_status {
                 request::SignInStatus::Ok { .. }
                 | request::SignInStatus::MaybeOk { .. }
-                | request::SignInStatus::AlreadySignedIn { .. } => SignInStatus::Authorized,
-                request::SignInStatus::NotAuthorized { .. } => SignInStatus::Unauthorized,
-                request::SignInStatus::NotSignedIn => SignInStatus::SignedOut,
-            };
+                | request::SignInStatus::AlreadySignedIn { .. } => {
+                    server.sign_in_status = SignInStatus::Authorized;
+                    for buffer in self.buffers.values().cloned().collect::<Vec<_>>() {
+                        if let Some(buffer) = buffer.upgrade(cx) {
+                            self.register_buffer(&buffer, cx);
+                        }
+                    }
+                }
+                request::SignInStatus::NotAuthorized { .. } => {
+                    server.sign_in_status = SignInStatus::Unauthorized;
+                    for buffer_id in self.buffers.keys().copied().collect::<Vec<_>>() {
+                        self.unregister_buffer(buffer_id);
+                    }
+                }
+                request::SignInStatus::NotSignedIn => {
+                    server.sign_in_status = SignInStatus::SignedOut;
+                    for buffer_id in self.buffers.keys().copied().collect::<Vec<_>>() {
+                        self.unregister_buffer(buffer_id);
+                    }
+                }
+            }
+
             cx.notify();
         }
     }
@@ -638,6 +899,14 @@ fn id_for_language(language: Option<&Arc<Language>>) -> String {
     }
 }
 
+fn uri_for_buffer(buffer: &ModelHandle<Buffer>, cx: &AppContext) -> lsp::Url {
+    if let Some(file) = buffer.read(cx).file().and_then(|file| file.as_local()) {
+        lsp::Url::from_file_path(file.abs_path(cx)).unwrap()
+    } else {
+        format!("buffer://{}", buffer.id()).parse().unwrap()
+    }
+}
+
 async fn clear_copilot_dir() {
     remove_matching(&paths::COPILOT_DIR, |_| true).await
 }
@@ -709,3 +978,226 @@ async fn get_copilot_lsp(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
         }
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use gpui::{executor::Deterministic, TestAppContext};
+
+    #[gpui::test(iterations = 10)]
+    async fn test_buffer_management(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
+        deterministic.forbid_parking();
+        let (copilot, mut lsp) = Copilot::fake(cx);
+
+        let buffer_1 = cx.add_model(|cx| Buffer::new(0, "Hello", cx));
+        let buffer_1_uri: lsp::Url = format!("buffer://{}", buffer_1.id()).parse().unwrap();
+        copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_1, cx));
+        assert_eq!(
+            lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
+                .await,
+            lsp::DidOpenTextDocumentParams {
+                text_document: lsp::TextDocumentItem::new(
+                    buffer_1_uri.clone(),
+                    "plaintext".into(),
+                    0,
+                    "Hello".into()
+                ),
+            }
+        );
+
+        let buffer_2 = cx.add_model(|cx| Buffer::new(0, "Goodbye", cx));
+        let buffer_2_uri: lsp::Url = format!("buffer://{}", buffer_2.id()).parse().unwrap();
+        copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_2, cx));
+        assert_eq!(
+            lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
+                .await,
+            lsp::DidOpenTextDocumentParams {
+                text_document: lsp::TextDocumentItem::new(
+                    buffer_2_uri.clone(),
+                    "plaintext".into(),
+                    0,
+                    "Goodbye".into()
+                ),
+            }
+        );
+
+        buffer_1.update(cx, |buffer, cx| buffer.edit([(5..5, " world")], None, cx));
+        assert_eq!(
+            lsp.receive_notification::<lsp::notification::DidChangeTextDocument>()
+                .await,
+            lsp::DidChangeTextDocumentParams {
+                text_document: lsp::VersionedTextDocumentIdentifier::new(buffer_1_uri.clone(), 1),
+                content_changes: vec![lsp::TextDocumentContentChangeEvent {
+                    range: Some(lsp::Range::new(
+                        lsp::Position::new(0, 5),
+                        lsp::Position::new(0, 5)
+                    )),
+                    range_length: None,
+                    text: " world".into(),
+                }],
+            }
+        );
+
+        // Ensure updates to the file are reflected in the LSP.
+        buffer_1
+            .update(cx, |buffer, cx| {
+                buffer.file_updated(
+                    Arc::new(File {
+                        abs_path: "/root/child/buffer-1".into(),
+                        path: Path::new("child/buffer-1").into(),
+                    }),
+                    cx,
+                )
+            })
+            .await;
+        assert_eq!(
+            lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
+                .await,
+            lsp::DidCloseTextDocumentParams {
+                text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri),
+            }
+        );
+        let buffer_1_uri = lsp::Url::from_file_path("/root/child/buffer-1").unwrap();
+        assert_eq!(
+            lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
+                .await,
+            lsp::DidOpenTextDocumentParams {
+                text_document: lsp::TextDocumentItem::new(
+                    buffer_1_uri.clone(),
+                    "plaintext".into(),
+                    1,
+                    "Hello world".into()
+                ),
+            }
+        );
+
+        // Ensure all previously-registered buffers are closed when signing out.
+        lsp.handle_request::<request::SignOut, _, _>(|_, _| async {
+            Ok(request::SignOutResult {})
+        });
+        copilot
+            .update(cx, |copilot, cx| copilot.sign_out(cx))
+            .await
+            .unwrap();
+        assert_eq!(
+            lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
+                .await,
+            lsp::DidCloseTextDocumentParams {
+                text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri.clone()),
+            }
+        );
+        assert_eq!(
+            lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
+                .await,
+            lsp::DidCloseTextDocumentParams {
+                text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri.clone()),
+            }
+        );
+
+        // Ensure all previously-registered buffers are re-opened when signing in.
+        lsp.handle_request::<request::SignInInitiate, _, _>(|_, _| async {
+            Ok(request::SignInInitiateResult::AlreadySignedIn {
+                user: "user-1".into(),
+            })
+        });
+        copilot
+            .update(cx, |copilot, cx| copilot.sign_in(cx))
+            .await
+            .unwrap();
+        assert_eq!(
+            lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
+                .await,
+            lsp::DidOpenTextDocumentParams {
+                text_document: lsp::TextDocumentItem::new(
+                    buffer_2_uri.clone(),
+                    "plaintext".into(),
+                    0,
+                    "Goodbye".into()
+                ),
+            }
+        );
+        assert_eq!(
+            lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
+                .await,
+            lsp::DidOpenTextDocumentParams {
+                text_document: lsp::TextDocumentItem::new(
+                    buffer_1_uri.clone(),
+                    "plaintext".into(),
+                    0,
+                    "Hello world".into()
+                ),
+            }
+        );
+
+        // Dropping a buffer causes it to be closed on the LSP side as well.
+        cx.update(|_| drop(buffer_2));
+        assert_eq!(
+            lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
+                .await,
+            lsp::DidCloseTextDocumentParams {
+                text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri),
+            }
+        );
+    }
+
+    struct File {
+        abs_path: PathBuf,
+        path: Arc<Path>,
+    }
+
+    impl language::File for File {
+        fn as_local(&self) -> Option<&dyn language::LocalFile> {
+            Some(self)
+        }
+
+        fn mtime(&self) -> std::time::SystemTime {
+            todo!()
+        }
+
+        fn path(&self) -> &Arc<Path> {
+            &self.path
+        }
+
+        fn full_path(&self, _: &AppContext) -> PathBuf {
+            todo!()
+        }
+
+        fn file_name<'a>(&'a self, _: &'a AppContext) -> &'a std::ffi::OsStr {
+            todo!()
+        }
+
+        fn is_deleted(&self) -> bool {
+            todo!()
+        }
+
+        fn as_any(&self) -> &dyn std::any::Any {
+            todo!()
+        }
+
+        fn to_proto(&self) -> rpc::proto::File {
+            todo!()
+        }
+    }
+
+    impl language::LocalFile for File {
+        fn abs_path(&self, _: &AppContext) -> PathBuf {
+            self.abs_path.clone()
+        }
+
+        fn load(&self, _: &AppContext) -> Task<Result<String>> {
+            todo!()
+        }
+
+        fn buffer_reloaded(
+            &self,
+            _: u64,
+            _: &clock::Global,
+            _: language::RopeFingerprint,
+            _: ::fs::LineEnding,
+            _: std::time::SystemTime,
+            _: &mut AppContext,
+        ) {
+            todo!()
+        }
+    }
+}

crates/copilot/src/request.rs 🔗

@@ -99,14 +99,11 @@ pub struct GetCompletionsParams {
 #[derive(Debug, Serialize, Deserialize)]
 #[serde(rename_all = "camelCase")]
 pub struct GetCompletionsDocument {
-    pub source: String,
     pub tab_size: u32,
     pub indent_size: u32,
     pub insert_spaces: bool,
     pub uri: lsp::Url,
-    pub path: String,
     pub relative_path: String,
-    pub language_id: String,
     pub position: lsp::Position,
     pub version: usize,
 }
@@ -169,3 +166,60 @@ impl lsp::notification::Notification for StatusNotification {
     type Params = StatusNotificationParams;
     const METHOD: &'static str = "statusNotification";
 }
+
+pub enum SetEditorInfo {}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct SetEditorInfoParams {
+    pub editor_info: EditorInfo,
+    pub editor_plugin_info: EditorPluginInfo,
+}
+
+impl lsp::request::Request for SetEditorInfo {
+    type Params = SetEditorInfoParams;
+    type Result = String;
+    const METHOD: &'static str = "setEditorInfo";
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct EditorInfo {
+    pub name: String,
+    pub version: String,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct EditorPluginInfo {
+    pub name: String,
+    pub version: String,
+}
+
+pub enum NotifyAccepted {}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct NotifyAcceptedParams {
+    pub uuid: String,
+}
+
+impl lsp::request::Request for NotifyAccepted {
+    type Params = NotifyAcceptedParams;
+    type Result = String;
+    const METHOD: &'static str = "notifyAccepted";
+}
+
+pub enum NotifyRejected {}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct NotifyRejectedParams {
+    pub uuids: Vec<String>,
+}
+
+impl lsp::request::Request for NotifyRejected {
+    type Params = NotifyRejectedParams;
+    type Result = String;
+    const METHOD: &'static str = "notifyRejected";
+}

crates/copilot/src/sign_in.rs 🔗

@@ -23,51 +23,51 @@ struct OpenGithub;
 const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot";
 
 pub fn init(cx: &mut AppContext) {
-    let copilot = Copilot::global(cx).unwrap();
+    if let Some(copilot) = Copilot::global(cx) {
+        let mut code_verification: Option<ViewHandle<CopilotCodeVerification>> = None;
+        cx.observe(&copilot, move |copilot, cx| {
+            let status = copilot.read(cx).status();
 
-    let mut code_verification: Option<ViewHandle<CopilotCodeVerification>> = None;
-    cx.observe(&copilot, move |copilot, cx| {
-        let status = copilot.read(cx).status();
-
-        match &status {
-            crate::Status::SigningIn { prompt } => {
-                if let Some(code_verification_handle) = code_verification.as_mut() {
-                    if cx.has_window(code_verification_handle.window_id()) {
-                        code_verification_handle.update(cx, |code_verification_view, cx| {
-                            code_verification_view.set_status(status, cx)
-                        });
-                        cx.activate_window(code_verification_handle.window_id());
-                    } else {
+            match &status {
+                crate::Status::SigningIn { prompt } => {
+                    if let Some(code_verification_handle) = code_verification.as_mut() {
+                        if cx.has_window(code_verification_handle.window_id()) {
+                            code_verification_handle.update(cx, |code_verification_view, cx| {
+                                code_verification_view.set_status(status, cx)
+                            });
+                            cx.activate_window(code_verification_handle.window_id());
+                        } else {
+                            create_copilot_auth_window(cx, &status, &mut code_verification);
+                        }
+                    } else if let Some(_prompt) = prompt {
                         create_copilot_auth_window(cx, &status, &mut code_verification);
                     }
-                } else if let Some(_prompt) = prompt {
-                    create_copilot_auth_window(cx, &status, &mut code_verification);
                 }
-            }
-            Status::Authorized | Status::Unauthorized => {
-                if let Some(code_verification) = code_verification.as_ref() {
-                    code_verification.update(cx, |code_verification, cx| {
-                        code_verification.set_status(status, cx)
-                    });
+                Status::Authorized | Status::Unauthorized => {
+                    if let Some(code_verification) = code_verification.as_ref() {
+                        code_verification.update(cx, |code_verification, cx| {
+                            code_verification.set_status(status, cx)
+                        });
 
-                    cx.platform().activate(true);
-                    cx.activate_window(code_verification.window_id());
+                        cx.platform().activate(true);
+                        cx.activate_window(code_verification.window_id());
+                    }
                 }
-            }
-            _ => {
-                if let Some(code_verification) = code_verification.take() {
-                    cx.remove_window(code_verification.window_id());
+                _ => {
+                    if let Some(code_verification) = code_verification.take() {
+                        cx.remove_window(code_verification.window_id());
+                    }
                 }
             }
-        }
-    })
-    .detach();
+        })
+        .detach();
 
-    cx.add_action(
-        |code_verification: &mut CopilotCodeVerification, _: &ClickedConnect, _| {
-            code_verification.connect_clicked = true;
-        },
-    );
+        cx.add_action(
+            |code_verification: &mut CopilotCodeVerification, _: &ClickedConnect, _| {
+                code_verification.connect_clicked = true;
+            },
+        );
+    }
 }
 
 fn create_copilot_auth_window(

crates/diagnostics/Cargo.toml 🔗

@@ -10,10 +10,11 @@ doctest = false
 
 [dependencies]
 anyhow = "1.0"
-smallvec = { version = "1.6", features = ["union"] }
+smallvec = { workspace = true }
 collections = { path = "../collections" }
 editor = { path = "../editor" }
 language = { path = "../language" }
+lsp = { path = "../lsp" }
 gpui = { path = "../gpui" }
 project = { path = "../project" }
 settings = { path = "../settings" }
@@ -27,6 +28,7 @@ unindent = "0.1"
 client = { path = "../client", features = ["test-support"] }
 editor = { path = "../editor", features = ["test-support"] }
 language = { path = "../language", features = ["test-support"] }
+lsp = { path = "../lsp", features = ["test-support"] }
 gpui = { path = "../gpui", features = ["test-support"] }
 workspace = { path = "../workspace", features = ["test-support"] }
 serde_json = { workspace = true }

crates/diagnostics/src/diagnostics.rs 🔗

@@ -1,7 +1,7 @@
 pub mod items;
 
 use anyhow::Result;
-use collections::{BTreeMap, HashSet};
+use collections::{BTreeSet, HashSet};
 use editor::{
     diagnostic_block_renderer,
     display_map::{BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock},
@@ -18,6 +18,7 @@ use language::{
     Anchor, Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection,
     SelectionGoal,
 };
+use lsp::LanguageServerId;
 use project::{DiagnosticSummary, Project, ProjectPath};
 use serde_json::json;
 use settings::Settings;
@@ -56,7 +57,7 @@ struct ProjectDiagnosticsEditor {
     summary: DiagnosticSummary,
     excerpts: ModelHandle<MultiBuffer>,
     path_states: Vec<PathState>,
-    paths_to_update: BTreeMap<ProjectPath, usize>,
+    paths_to_update: BTreeSet<(ProjectPath, LanguageServerId)>,
 }
 
 struct PathState {
@@ -72,6 +73,7 @@ struct Jump {
 }
 
 struct DiagnosticGroupState {
+    language_server_id: LanguageServerId,
     primary_diagnostic: DiagnosticEntry<language::Anchor>,
     primary_excerpt_ix: usize,
     excerpts: Vec<ExcerptId>,
@@ -116,7 +118,7 @@ impl View for ProjectDiagnosticsEditor {
             }),
             "summary": self.summary,
             "paths_to_update": self.paths_to_update.iter().map(|(path, server_id)|
-                (path.path.to_string_lossy(), server_id)
+                (path.path.to_string_lossy(), server_id.0)
             ).collect::<Vec<_>>(),
             "paths_states": self.path_states.iter().map(|state|
                 json!({
@@ -149,7 +151,7 @@ impl ProjectDiagnosticsEditor {
                 path,
             } => {
                 this.paths_to_update
-                    .insert(path.clone(), *language_server_id);
+                    .insert((path.clone(), *language_server_id));
             }
             _ => {}
         })
@@ -168,7 +170,7 @@ impl ProjectDiagnosticsEditor {
         let project = project_handle.read(cx);
         let paths_to_update = project
             .diagnostic_summaries(cx)
-            .map(|e| (e.0, e.1.language_server_id))
+            .map(|(path, server_id, _)| (path, server_id))
             .collect();
         let summary = project.diagnostic_summary(cx);
         let mut this = Self {
@@ -196,9 +198,13 @@ impl ProjectDiagnosticsEditor {
         }
     }
 
-    fn update_excerpts(&mut self, language_server_id: Option<usize>, cx: &mut ViewContext<Self>) {
+    fn update_excerpts(
+        &mut self,
+        language_server_id: Option<LanguageServerId>,
+        cx: &mut ViewContext<Self>,
+    ) {
         let mut paths = Vec::new();
-        self.paths_to_update.retain(|path, server_id| {
+        self.paths_to_update.retain(|(path, server_id)| {
             if language_server_id
                 .map_or(true, |language_server_id| language_server_id == *server_id)
             {
@@ -215,7 +221,9 @@ impl ProjectDiagnosticsEditor {
                     let buffer = project
                         .update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx))
                         .await?;
-                    this.update(&mut cx, |this, cx| this.populate_excerpts(path, buffer, cx))
+                    this.update(&mut cx, |this, cx| {
+                        this.populate_excerpts(path, language_server_id, buffer, cx)
+                    })
                 }
                 Result::<_, anyhow::Error>::Ok(())
             }
@@ -227,6 +235,7 @@ impl ProjectDiagnosticsEditor {
     fn populate_excerpts(
         &mut self,
         path: ProjectPath,
+        language_server_id: Option<LanguageServerId>,
         buffer: ModelHandle<Buffer>,
         cx: &mut ViewContext<Self>,
     ) {
@@ -265,9 +274,9 @@ impl ProjectDiagnosticsEditor {
         let excerpts_snapshot = self.excerpts.update(cx, |excerpts, excerpts_cx| {
             let mut old_groups = path_state.diagnostic_groups.iter().enumerate().peekable();
             let mut new_groups = snapshot
-                .diagnostic_groups()
+                .diagnostic_groups(language_server_id)
                 .into_iter()
-                .filter(|group| {
+                .filter(|(_, group)| {
                     group.entries[group.primary_ix].diagnostic.severity
                         <= DiagnosticSeverity::WARNING
                 })
@@ -279,12 +288,27 @@ impl ProjectDiagnosticsEditor {
                 match (old_groups.peek(), new_groups.peek()) {
                     (None, None) => break,
                     (None, Some(_)) => to_insert = new_groups.next(),
-                    (Some(_), None) => to_remove = old_groups.next(),
-                    (Some((_, old_group)), Some(new_group)) => {
+                    (Some((_, old_group)), None) => {
+                        if language_server_id.map_or(true, |id| id == old_group.language_server_id)
+                        {
+                            to_remove = old_groups.next();
+                        } else {
+                            to_keep = old_groups.next();
+                        }
+                    }
+                    (Some((_, old_group)), Some((_, new_group))) => {
                         let old_primary = &old_group.primary_diagnostic;
                         let new_primary = &new_group.entries[new_group.primary_ix];
                         match compare_diagnostics(old_primary, new_primary, &snapshot) {
-                            Ordering::Less => to_remove = old_groups.next(),
+                            Ordering::Less => {
+                                if language_server_id
+                                    .map_or(true, |id| id == old_group.language_server_id)
+                                {
+                                    to_remove = old_groups.next();
+                                } else {
+                                    to_keep = old_groups.next();
+                                }
+                            }
                             Ordering::Equal => {
                                 to_keep = old_groups.next();
                                 new_groups.next();
@@ -294,8 +318,9 @@ impl ProjectDiagnosticsEditor {
                     }
                 }
 
-                if let Some(group) = to_insert {
+                if let Some((language_server_id, group)) = to_insert {
                     let mut group_state = DiagnosticGroupState {
+                        language_server_id,
                         primary_diagnostic: group.entries[group.primary_ix].clone(),
                         primary_excerpt_ix: 0,
                         excerpts: Default::default(),
@@ -773,26 +798,24 @@ mod tests {
     };
     use gpui::TestAppContext;
     use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16, Unclipped};
+    use project::FakeFs;
     use serde_json::json;
     use unindent::Unindent as _;
-    use workspace::AppState;
 
     #[gpui::test]
     async fn test_diagnostics(cx: &mut TestAppContext) {
-        let app_state = cx.update(AppState::test);
-        app_state
-            .fs
-            .as_fake()
-            .insert_tree(
-                "/test",
-                json!({
-                    "consts.rs": "
+        Settings::test_async(cx);
+        let fs = FakeFs::new(cx.background());
+        fs.insert_tree(
+            "/test",
+            json!({
+                "consts.rs": "
                         const a: i32 = 'a';
                         const b: i32 = c;
                     "
-                    .unindent(),
+                .unindent(),
 
-                    "main.rs": "
+                "main.rs": "
                         fn main() {
                             let x = vec![];
                             let y = vec![];
@@ -804,19 +827,20 @@ mod tests {
                             d(x);
                         }
                     "
-                    .unindent(),
-                }),
-            )
-            .await;
+                .unindent(),
+            }),
+        )
+        .await;
 
-        let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
+        let language_server_id = LanguageServerId(0);
+        let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
         let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
 
         // Create some diagnostics
         project.update(cx, |project, cx| {
             project
                 .update_diagnostic_entries(
-                    0,
+                    language_server_id,
                     PathBuf::from("/test/main.rs"),
                     None,
                     vec![
@@ -965,10 +989,10 @@ mod tests {
 
         // Diagnostics are added for another earlier path.
         project.update(cx, |project, cx| {
-            project.disk_based_diagnostics_started(0, cx);
+            project.disk_based_diagnostics_started(language_server_id, cx);
             project
                 .update_diagnostic_entries(
-                    0,
+                    language_server_id,
                     PathBuf::from("/test/consts.rs"),
                     None,
                     vec![DiagnosticEntry {
@@ -985,7 +1009,7 @@ mod tests {
                     cx,
                 )
                 .unwrap();
-            project.disk_based_diagnostics_finished(0, cx);
+            project.disk_based_diagnostics_finished(language_server_id, cx);
         });
 
         view.next_notification(cx).await;
@@ -1065,10 +1089,10 @@ mod tests {
 
         // Diagnostics are added to the first path
         project.update(cx, |project, cx| {
-            project.disk_based_diagnostics_started(0, cx);
+            project.disk_based_diagnostics_started(language_server_id, cx);
             project
                 .update_diagnostic_entries(
-                    0,
+                    language_server_id,
                     PathBuf::from("/test/consts.rs"),
                     None,
                     vec![
@@ -1101,7 +1125,7 @@ mod tests {
                     cx,
                 )
                 .unwrap();
-            project.disk_based_diagnostics_finished(0, cx);
+            project.disk_based_diagnostics_finished(language_server_id, cx);
         });
 
         view.next_notification(cx).await;
@@ -1181,6 +1205,272 @@ mod tests {
         });
     }
 
+    #[gpui::test]
+    async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
+        Settings::test_async(cx);
+        let fs = FakeFs::new(cx.background());
+        fs.insert_tree(
+            "/test",
+            json!({
+                "main.js": "
+                    a();
+                    b();
+                    c();
+                    d();
+                    e();
+                ".unindent()
+            }),
+        )
+        .await;
+
+        let server_id_1 = LanguageServerId(100);
+        let server_id_2 = LanguageServerId(101);
+        let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+
+        let view = cx.add_view(&workspace, |cx| {
+            ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
+        });
+
+        // Two language servers start updating diagnostics
+        project.update(cx, |project, cx| {
+            project.disk_based_diagnostics_started(server_id_1, cx);
+            project.disk_based_diagnostics_started(server_id_2, cx);
+            project
+                .update_diagnostic_entries(
+                    server_id_1,
+                    PathBuf::from("/test/main.js"),
+                    None,
+                    vec![DiagnosticEntry {
+                        range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 1)),
+                        diagnostic: Diagnostic {
+                            message: "error 1".to_string(),
+                            severity: DiagnosticSeverity::WARNING,
+                            is_primary: true,
+                            is_disk_based: true,
+                            group_id: 1,
+                            ..Default::default()
+                        },
+                    }],
+                    cx,
+                )
+                .unwrap();
+            project
+                .update_diagnostic_entries(
+                    server_id_2,
+                    PathBuf::from("/test/main.js"),
+                    None,
+                    vec![DiagnosticEntry {
+                        range: Unclipped(PointUtf16::new(1, 0))..Unclipped(PointUtf16::new(1, 1)),
+                        diagnostic: Diagnostic {
+                            message: "warning 1".to_string(),
+                            severity: DiagnosticSeverity::ERROR,
+                            is_primary: true,
+                            is_disk_based: true,
+                            group_id: 2,
+                            ..Default::default()
+                        },
+                    }],
+                    cx,
+                )
+                .unwrap();
+        });
+
+        // The first language server finishes
+        project.update(cx, |project, cx| {
+            project.disk_based_diagnostics_finished(server_id_1, cx);
+        });
+
+        // Only the first language server's diagnostics are shown.
+        cx.foreground().run_until_parked();
+        view.update(cx, |view, cx| {
+            assert_eq!(
+                editor_blocks(&view.editor, cx),
+                [
+                    (0, "path header block".into()),
+                    (2, "diagnostic header".into()),
+                ]
+            );
+            assert_eq!(
+                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
+                concat!(
+                    "\n", // filename
+                    "\n", // padding
+                    // diagnostic group 1
+                    "\n",     // primary message
+                    "\n",     // padding
+                    "a();\n", //
+                    "b();",
+                )
+            );
+        });
+
+        // The second language server finishes
+        project.update(cx, |project, cx| {
+            project.disk_based_diagnostics_finished(server_id_2, cx);
+        });
+
+        // Both language server's diagnostics are shown.
+        cx.foreground().run_until_parked();
+        view.update(cx, |view, cx| {
+            assert_eq!(
+                editor_blocks(&view.editor, cx),
+                [
+                    (0, "path header block".into()),
+                    (2, "diagnostic header".into()),
+                    (6, "collapsed context".into()),
+                    (7, "diagnostic header".into()),
+                ]
+            );
+            assert_eq!(
+                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
+                concat!(
+                    "\n", // filename
+                    "\n", // padding
+                    // diagnostic group 1
+                    "\n",     // primary message
+                    "\n",     // padding
+                    "a();\n", // location
+                    "b();\n", //
+                    "\n",     // collapsed context
+                    // diagnostic group 2
+                    "\n",     // primary message
+                    "\n",     // padding
+                    "a();\n", // context
+                    "b();\n", //
+                    "c();",   // context
+                )
+            );
+        });
+
+        // Both language servers start updating diagnostics, and the first server finishes.
+        project.update(cx, |project, cx| {
+            project.disk_based_diagnostics_started(server_id_1, cx);
+            project.disk_based_diagnostics_started(server_id_2, cx);
+            project
+                .update_diagnostic_entries(
+                    server_id_1,
+                    PathBuf::from("/test/main.js"),
+                    None,
+                    vec![DiagnosticEntry {
+                        range: Unclipped(PointUtf16::new(2, 0))..Unclipped(PointUtf16::new(2, 1)),
+                        diagnostic: Diagnostic {
+                            message: "warning 2".to_string(),
+                            severity: DiagnosticSeverity::WARNING,
+                            is_primary: true,
+                            is_disk_based: true,
+                            group_id: 1,
+                            ..Default::default()
+                        },
+                    }],
+                    cx,
+                )
+                .unwrap();
+            project
+                .update_diagnostic_entries(
+                    server_id_2,
+                    PathBuf::from("/test/main.rs"),
+                    None,
+                    vec![],
+                    cx,
+                )
+                .unwrap();
+            project.disk_based_diagnostics_finished(server_id_1, cx);
+        });
+
+        // Only the first language server's diagnostics are updated.
+        cx.foreground().run_until_parked();
+        view.update(cx, |view, cx| {
+            assert_eq!(
+                editor_blocks(&view.editor, cx),
+                [
+                    (0, "path header block".into()),
+                    (2, "diagnostic header".into()),
+                    (7, "collapsed context".into()),
+                    (8, "diagnostic header".into()),
+                ]
+            );
+            assert_eq!(
+                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
+                concat!(
+                    "\n", // filename
+                    "\n", // padding
+                    // diagnostic group 1
+                    "\n",     // primary message
+                    "\n",     // padding
+                    "a();\n", // location
+                    "b();\n", //
+                    "c();\n", // context
+                    "\n",     // collapsed context
+                    // diagnostic group 2
+                    "\n",     // primary message
+                    "\n",     // padding
+                    "b();\n", // context
+                    "c();\n", //
+                    "d();",   // context
+                )
+            );
+        });
+
+        // The second language server finishes.
+        project.update(cx, |project, cx| {
+            project
+                .update_diagnostic_entries(
+                    server_id_2,
+                    PathBuf::from("/test/main.js"),
+                    None,
+                    vec![DiagnosticEntry {
+                        range: Unclipped(PointUtf16::new(3, 0))..Unclipped(PointUtf16::new(3, 1)),
+                        diagnostic: Diagnostic {
+                            message: "warning 2".to_string(),
+                            severity: DiagnosticSeverity::WARNING,
+                            is_primary: true,
+                            is_disk_based: true,
+                            group_id: 1,
+                            ..Default::default()
+                        },
+                    }],
+                    cx,
+                )
+                .unwrap();
+            project.disk_based_diagnostics_finished(server_id_2, cx);
+        });
+
+        // Both language servers' diagnostics are updated.
+        cx.foreground().run_until_parked();
+        view.update(cx, |view, cx| {
+            assert_eq!(
+                editor_blocks(&view.editor, cx),
+                [
+                    (0, "path header block".into()),
+                    (2, "diagnostic header".into()),
+                    (7, "collapsed context".into()),
+                    (8, "diagnostic header".into()),
+                ]
+            );
+            assert_eq!(
+                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
+                concat!(
+                    "\n", // filename
+                    "\n", // padding
+                    // diagnostic group 1
+                    "\n",     // primary message
+                    "\n",     // padding
+                    "b();\n", // location
+                    "c();\n", //
+                    "d();\n", // context
+                    "\n",     // collapsed context
+                    // diagnostic group 2
+                    "\n",     // primary message
+                    "\n",     // padding
+                    "c();\n", // context
+                    "d();\n", //
+                    "e();",   // context
+                )
+            );
+        });
+    }
+
     fn editor_blocks(editor: &ViewHandle<Editor>, cx: &mut AppContext) -> Vec<(u32, String)> {
         let mut presenter = cx.build_presenter(editor.id(), 0., Default::default());
         let mut cx = presenter.build_layout_context(Default::default(), false, cx);

crates/diagnostics/src/items.rs 🔗

@@ -7,6 +7,7 @@ use gpui::{
     ViewHandle, WeakViewHandle,
 };
 use language::Diagnostic;
+use lsp::LanguageServerId;
 use project::Project;
 use settings::Settings;
 use workspace::{item::ItemHandle, StatusItemView};
@@ -15,7 +16,7 @@ pub struct DiagnosticIndicator {
     summary: project::DiagnosticSummary,
     active_editor: Option<WeakViewHandle<Editor>>,
     current_diagnostic: Option<Diagnostic>,
-    in_progress_checks: HashSet<usize>,
+    in_progress_checks: HashSet<LanguageServerId>,
     _observe_active_editor: Option<Subscription>,
 }
 

crates/editor/Cargo.toml 🔗

@@ -58,7 +58,7 @@ postage = { workspace = true }
 rand = { version = "0.8.3", optional = true }
 serde = { workspace = true }
 serde_derive = { workspace = true }
-smallvec = { version = "1.6", features = ["union"] }
+smallvec = { workspace = true }
 smol = "1.2"
 tree-sitter-rust = { version = "*", optional = true }
 tree-sitter-html = { version = "*", optional = true }

crates/editor/src/editor.rs 🔗

@@ -52,7 +52,7 @@ pub use language::{char_kind, CharKind};
 use language::{
     AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, CursorShape,
     Diagnostic, DiagnosticSeverity, IndentKind, IndentSize, Language, OffsetRangeExt, OffsetUtf16,
-    Point, Rope, Selection, SelectionGoal, TransactionId,
+    Point, Selection, SelectionGoal, TransactionId,
 };
 use link_go_to_definition::{
     hide_link_definition, show_link_definition, LinkDefinitionKind, LinkGoToDefinitionState,
@@ -1037,6 +1037,10 @@ impl Default for CopilotState {
 }
 
 impl CopilotState {
+    fn active_completion(&self) -> Option<&copilot::Completion> {
+        self.completions.get(self.active_completion_index)
+    }
+
     fn text_for_active_completion(
         &self,
         cursor: Anchor,
@@ -1044,7 +1048,7 @@ impl CopilotState {
     ) -> Option<&str> {
         use language::ToOffset as _;
 
-        let completion = self.completions.get(self.active_completion_index)?;
+        let completion = self.active_completion()?;
         let excerpt_id = self.excerpt_id?;
         let completion_buffer = buffer.buffer_for_excerpt(excerpt_id)?;
         if excerpt_id != cursor.excerpt_id
@@ -1097,7 +1101,7 @@ impl CopilotState {
 
     fn push_completion(&mut self, new_completion: copilot::Completion) {
         for completion in &self.completions {
-            if *completion == new_completion {
+            if completion.text == new_completion.text && completion.range == new_completion.range {
                 return;
             }
         }
@@ -1496,7 +1500,7 @@ impl Editor {
             self.refresh_code_actions(cx);
             self.refresh_document_highlights(cx);
             refresh_matching_bracket_highlights(self, cx);
-            self.hide_copilot_suggestion(cx);
+            self.discard_copilot_suggestion(cx);
         }
 
         self.blink_manager.update(cx, BlinkManager::pause_blinking);
@@ -1870,7 +1874,7 @@ impl Editor {
             return;
         }
 
-        if self.hide_copilot_suggestion(cx).is_some() {
+        if self.discard_copilot_suggestion(cx) {
             return;
         }
 
@@ -2969,7 +2973,7 @@ impl Editor {
         Some(())
     }
 
-    fn cycle_suggestions(
+    fn cycle_copilot_suggestions(
         &mut self,
         direction: Direction,
         cx: &mut ViewContext<Self>,
@@ -3020,7 +3024,7 @@ impl Editor {
 
     fn next_copilot_suggestion(&mut self, _: &copilot::NextSuggestion, cx: &mut ViewContext<Self>) {
         if self.has_active_copilot_suggestion(cx) {
-            self.cycle_suggestions(Direction::Next, cx);
+            self.cycle_copilot_suggestions(Direction::Next, cx);
         } else {
             self.refresh_copilot_suggestions(false, cx);
         }
@@ -3032,37 +3036,55 @@ impl Editor {
         cx: &mut ViewContext<Self>,
     ) {
         if self.has_active_copilot_suggestion(cx) {
-            self.cycle_suggestions(Direction::Prev, cx);
+            self.cycle_copilot_suggestions(Direction::Prev, cx);
         } else {
             self.refresh_copilot_suggestions(false, cx);
         }
     }
 
     fn accept_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> bool {
-        if let Some(text) = self.hide_copilot_suggestion(cx) {
-            self.insert_with_autoindent_mode(&text.to_string(), None, cx);
+        if let Some(suggestion) = self
+            .display_map
+            .update(cx, |map, cx| map.replace_suggestion::<usize>(None, cx))
+        {
+            if let Some((copilot, completion)) =
+                Copilot::global(cx).zip(self.copilot_state.active_completion())
+            {
+                copilot
+                    .update(cx, |copilot, cx| copilot.accept_completion(completion, cx))
+                    .detach_and_log_err(cx);
+            }
+            self.insert_with_autoindent_mode(&suggestion.text.to_string(), None, cx);
+            cx.notify();
             true
         } else {
             false
         }
     }
 
-    fn has_active_copilot_suggestion(&self, cx: &AppContext) -> bool {
-        self.display_map.read(cx).has_suggestion()
-    }
-
-    fn hide_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> Option<Rope> {
+    fn discard_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> bool {
         if self.has_active_copilot_suggestion(cx) {
-            let old_suggestion = self
-                .display_map
+            if let Some(copilot) = Copilot::global(cx) {
+                copilot
+                    .update(cx, |copilot, cx| {
+                        copilot.discard_completions(&self.copilot_state.completions, cx)
+                    })
+                    .detach_and_log_err(cx);
+            }
+
+            self.display_map
                 .update(cx, |map, cx| map.replace_suggestion::<usize>(None, cx));
             cx.notify();
-            old_suggestion.map(|suggestion| suggestion.text)
+            true
         } else {
-            None
+            false
         }
     }
 
+    fn has_active_copilot_suggestion(&self, cx: &AppContext) -> bool {
+        self.display_map.read(cx).has_suggestion()
+    }
+
     fn update_visible_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) {
         let snapshot = self.buffer.read(cx).snapshot(cx);
         let selection = self.selections.newest_anchor();
@@ -3072,7 +3094,7 @@ impl Editor {
             || !self.completion_tasks.is_empty()
             || selection.start != selection.end
         {
-            self.hide_copilot_suggestion(cx);
+            self.discard_copilot_suggestion(cx);
         } else if let Some(text) = self
             .copilot_state
             .text_for_active_completion(cursor, &snapshot)
@@ -3088,13 +3110,13 @@ impl Editor {
             });
             cx.notify();
         } else {
-            self.hide_copilot_suggestion(cx);
+            self.discard_copilot_suggestion(cx);
         }
     }
 
     fn clear_copilot_suggestions(&mut self, cx: &mut ViewContext<Self>) {
         self.copilot_state = Default::default();
-        self.hide_copilot_suggestion(cx);
+        self.discard_copilot_suggestion(cx);
     }
 
     pub fn render_code_actions_indicator(
@@ -3212,7 +3234,7 @@ impl Editor {
             self.completion_tasks.clear();
         }
         self.context_menu = Some(menu);
-        self.hide_copilot_suggestion(cx);
+        self.discard_copilot_suggestion(cx);
         cx.notify();
     }
 
@@ -6643,6 +6665,7 @@ impl Editor {
             multi_buffer::Event::DiagnosticsUpdated => {
                 self.refresh_active_diagnostics(cx);
             }
+            multi_buffer::Event::LanguageChanged => {}
         }
     }
 

crates/editor/src/editor_tests.rs 🔗

@@ -4371,7 +4371,7 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext)
     cx.set_state(
         &[
             "one ",   //
-            "twoˇ",  //
+            "twoˇ",   //
             "three ", //
             "four",   //
         ]
@@ -4446,7 +4446,7 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext)
         &[
             "one",   //
             "",      //
-            "twoˇ", //
+            "twoˇ",  //
             "",      //
             "three", //
             "four",  //
@@ -4461,7 +4461,7 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext)
     cx.assert_editor_state(
         &[
             "one ",   //
-            "twoˇ",  //
+            "twoˇ",   //
             "three ", //
             "four",   //
         ]

crates/editor/src/hover_popover.rs 🔗

@@ -436,6 +436,7 @@ mod tests {
     use indoc::indoc;
 
     use language::{Diagnostic, DiagnosticSet};
+    use lsp::LanguageServerId;
     use project::HoverBlock;
     use smol::stream::StreamExt;
 
@@ -620,7 +621,7 @@ mod tests {
                 }],
                 &snapshot,
             );
-            buffer.update_diagnostics(set, cx);
+            buffer.update_diagnostics(LanguageServerId(0), set, cx);
         });
 
         // Hover pops diagnostic immediately

crates/editor/src/multi_buffer.rs 🔗

@@ -64,6 +64,7 @@ pub enum Event {
     },
     Edited,
     Reloaded,
+    LanguageChanged,
     Reparsed,
     Saved,
     FileHandleChanged,
@@ -1302,6 +1303,7 @@ impl MultiBuffer {
             language::Event::Saved => Event::Saved,
             language::Event::FileHandleChanged => Event::FileHandleChanged,
             language::Event::Reloaded => Event::Reloaded,
+            language::Event::LanguageChanged => Event::LanguageChanged,
             language::Event::Reparsed => Event::Reparsed,
             language::Event::DiagnosticsUpdated => Event::DiagnosticsUpdated,
             language::Event::Closed => Event::Closed,
@@ -2762,6 +2764,15 @@ impl MultiBufferSnapshot {
             .and_then(|(buffer, offset)| buffer.language_scope_at(offset))
     }
 
+    pub fn language_indent_size_at<T: ToOffset>(
+        &self,
+        position: T,
+        cx: &AppContext,
+    ) -> Option<IndentSize> {
+        let (buffer_snapshot, offset) = self.point_to_buffer_offset(position)?;
+        Some(buffer_snapshot.language_indent_size_at(offset, cx))
+    }
+
     pub fn is_dirty(&self) -> bool {
         self.is_dirty
     }
@@ -2789,7 +2800,7 @@ impl MultiBufferSnapshot {
     ) -> impl Iterator<Item = DiagnosticEntry<O>> + 'a
     where
         T: 'a + ToOffset,
-        O: 'a + text::FromAnchor,
+        O: 'a + text::FromAnchor + Ord,
     {
         self.as_singleton()
             .into_iter()

crates/gpui/Cargo.toml 🔗

@@ -44,7 +44,7 @@ seahash = "4.1"
 serde = { workspace = true }
 serde_derive = { workspace = true }
 serde_json = { workspace = true }
-smallvec = { version = "1.6", features = ["union"] }
+smallvec = { workspace = true }
 smol = "1.2"
 time = { version = "0.3", features = ["serde", "serde-well-known"] }
 tiny-skia = "0.5"

crates/language/Cargo.toml 🔗

@@ -50,7 +50,7 @@ serde = { workspace = true }
 serde_derive = { workspace = true }
 serde_json = { workspace = true }
 similar = "1.3"
-smallvec = { version = "1.6", features = ["union"] }
+smallvec = { workspace = true }
 smol = "1.2"
 tree-sitter = "0.20"
 tree-sitter-rust = { version = "*", optional = true }

crates/language/src/buffer.rs 🔗

@@ -16,9 +16,11 @@ use clock::ReplicaId;
 use fs::LineEnding;
 use futures::FutureExt as _;
 use gpui::{fonts::HighlightStyle, AppContext, Entity, ModelContext, Task};
+use lsp::LanguageServerId;
 use parking_lot::Mutex;
 use settings::Settings;
 use similar::{ChangeTag, TextDiff};
+use smallvec::SmallVec;
 use smol::future::yield_now;
 use std::{
     any::Any,
@@ -71,7 +73,7 @@ pub struct Buffer {
     syntax_map: Mutex<SyntaxMap>,
     parsing_in_background: bool,
     parse_count: usize,
-    diagnostics: DiagnosticSet,
+    diagnostics: SmallVec<[(LanguageServerId, DiagnosticSet); 2]>,
     remote_selections: TreeMap<ReplicaId, SelectionSet>,
     selections_update_count: usize,
     diagnostics_update_count: usize,
@@ -88,7 +90,7 @@ pub struct BufferSnapshot {
     pub git_diff: git::diff::BufferDiff,
     pub(crate) syntax: SyntaxSnapshot,
     file: Option<Arc<dyn File>>,
-    diagnostics: DiagnosticSet,
+    diagnostics: SmallVec<[(LanguageServerId, DiagnosticSet); 2]>,
     diagnostics_update_count: usize,
     file_update_count: usize,
     git_diff_update_count: usize,
@@ -156,6 +158,7 @@ pub struct Completion {
 
 #[derive(Clone, Debug)]
 pub struct CodeAction {
+    pub server_id: LanguageServerId,
     pub range: Range<Anchor>,
     pub lsp_action: lsp::CodeAction,
 }
@@ -163,16 +166,20 @@ pub struct CodeAction {
 #[derive(Clone, Debug, PartialEq, Eq)]
 pub enum Operation {
     Buffer(text::Operation),
+
     UpdateDiagnostics {
+        server_id: LanguageServerId,
         diagnostics: Arc<[DiagnosticEntry<Anchor>]>,
         lamport_timestamp: clock::Lamport,
     },
+
     UpdateSelections {
         selections: Arc<[Selection<Anchor>]>,
         lamport_timestamp: clock::Lamport,
         line_mode: bool,
         cursor_shape: CursorShape,
     },
+
     UpdateCompletionTriggers {
         triggers: Vec<String>,
         lamport_timestamp: clock::Lamport,
@@ -187,6 +194,7 @@ pub enum Event {
     Saved,
     FileHandleChanged,
     Reloaded,
+    LanguageChanged,
     Reparsed,
     DiagnosticsUpdated,
     Closed,
@@ -407,6 +415,7 @@ impl Buffer {
     ) -> Task<Vec<proto::Operation>> {
         let mut operations = Vec::new();
         operations.extend(self.deferred_ops.iter().map(proto::serialize_operation));
+
         operations.extend(self.remote_selections.iter().map(|(_, set)| {
             proto::serialize_operation(&Operation::UpdateSelections {
                 selections: set.selections.clone(),
@@ -415,10 +424,15 @@ impl Buffer {
                 cursor_shape: set.cursor_shape,
             })
         }));
-        operations.push(proto::serialize_operation(&Operation::UpdateDiagnostics {
-            diagnostics: self.diagnostics.iter().cloned().collect(),
-            lamport_timestamp: self.diagnostics_timestamp,
-        }));
+
+        for (server_id, diagnostics) in &self.diagnostics {
+            operations.push(proto::serialize_operation(&Operation::UpdateDiagnostics {
+                lamport_timestamp: self.diagnostics_timestamp,
+                server_id: *server_id,
+                diagnostics: diagnostics.iter().cloned().collect(),
+            }));
+        }
+
         operations.push(proto::serialize_operation(
             &Operation::UpdateCompletionTriggers {
                 triggers: self.completion_triggers.clone(),
@@ -536,6 +550,7 @@ impl Buffer {
         self.syntax_map.lock().clear();
         self.language = language;
         self.reparse(cx);
+        cx.emit(Event::LanguageChanged);
     }
 
     pub fn set_language_registry(&mut self, language_registry: Arc<LanguageRegistry>) {
@@ -863,13 +878,19 @@ impl Buffer {
         cx.notify();
     }
 
-    pub fn update_diagnostics(&mut self, diagnostics: DiagnosticSet, cx: &mut ModelContext<Self>) {
+    pub fn update_diagnostics(
+        &mut self,
+        server_id: LanguageServerId,
+        diagnostics: DiagnosticSet,
+        cx: &mut ModelContext<Self>,
+    ) {
         let lamport_timestamp = self.text.lamport_clock.tick();
         let op = Operation::UpdateDiagnostics {
+            server_id,
             diagnostics: diagnostics.iter().cloned().collect(),
             lamport_timestamp,
         };
-        self.apply_diagnostic_update(diagnostics, lamport_timestamp, cx);
+        self.apply_diagnostic_update(server_id, diagnostics, lamport_timestamp, cx);
         self.send_operation(op, cx);
     }
 
@@ -1577,11 +1598,13 @@ impl Buffer {
                 unreachable!("buffer operations should never be applied at this layer")
             }
             Operation::UpdateDiagnostics {
+                server_id,
                 diagnostics: diagnostic_set,
                 lamport_timestamp,
             } => {
                 let snapshot = self.snapshot();
                 self.apply_diagnostic_update(
+                    server_id,
                     DiagnosticSet::from_sorted_entries(diagnostic_set.iter().cloned(), &snapshot),
                     lamport_timestamp,
                     cx,
@@ -1623,12 +1646,16 @@ impl Buffer {
 
     fn apply_diagnostic_update(
         &mut self,
+        server_id: LanguageServerId,
         diagnostics: DiagnosticSet,
         lamport_timestamp: clock::Lamport,
         cx: &mut ModelContext<Self>,
     ) {
         if lamport_timestamp > self.diagnostics_timestamp {
-            self.diagnostics = diagnostics;
+            match self.diagnostics.binary_search_by_key(&server_id, |e| e.0) {
+                Err(ix) => self.diagnostics.insert(ix, (server_id, diagnostics)),
+                Ok(ix) => self.diagnostics[ix].1 = diagnostics,
+            };
             self.diagnostics_timestamp = lamport_timestamp;
             self.diagnostics_update_count += 1;
             self.text.lamport_clock.observe(lamport_timestamp);
@@ -2502,14 +2529,55 @@ impl BufferSnapshot {
     ) -> impl 'a + Iterator<Item = DiagnosticEntry<O>>
     where
         T: 'a + Clone + ToOffset,
-        O: 'a + FromAnchor,
+        O: 'a + FromAnchor + Ord,
     {
-        self.diagnostics.range(search_range, self, true, reversed)
+        let mut iterators: Vec<_> = self
+            .diagnostics
+            .iter()
+            .map(|(_, collection)| {
+                collection
+                    .range::<T, O>(search_range.clone(), self, true, reversed)
+                    .peekable()
+            })
+            .collect();
+
+        std::iter::from_fn(move || {
+            let (next_ix, _) = iterators
+                .iter_mut()
+                .enumerate()
+                .flat_map(|(ix, iter)| Some((ix, iter.peek()?)))
+                .min_by(|(_, a), (_, b)| a.range.start.cmp(&b.range.start))?;
+            iterators[next_ix].next()
+        })
     }
 
-    pub fn diagnostic_groups(&self) -> Vec<DiagnosticGroup<Anchor>> {
+    pub fn diagnostic_groups(
+        &self,
+        language_server_id: Option<LanguageServerId>,
+    ) -> Vec<(LanguageServerId, DiagnosticGroup<Anchor>)> {
         let mut groups = Vec::new();
-        self.diagnostics.groups(&mut groups, self);
+
+        if let Some(language_server_id) = language_server_id {
+            if let Ok(ix) = self
+                .diagnostics
+                .binary_search_by_key(&language_server_id, |e| e.0)
+            {
+                self.diagnostics[ix]
+                    .1
+                    .groups(language_server_id, &mut groups, self);
+            }
+        } else {
+            for (language_server_id, diagnostics) in self.diagnostics.iter() {
+                diagnostics.groups(*language_server_id, &mut groups, self);
+            }
+        }
+
+        groups.sort_by(|(id_a, group_a), (id_b, group_b)| {
+            let a_start = &group_a.entries[group_a.primary_ix].range.start;
+            let b_start = &group_b.entries[group_b.primary_ix].range.start;
+            a_start.cmp(b_start, self).then_with(|| id_a.cmp(&id_b))
+        });
+
         groups
     }
 
@@ -2520,7 +2588,9 @@ impl BufferSnapshot {
     where
         O: 'a + FromAnchor,
     {
-        self.diagnostics.group(group_id, self)
+        self.diagnostics
+            .iter()
+            .flat_map(move |(_, set)| set.group(group_id, self))
     }
 
     pub fn diagnostics_update_count(&self) -> usize {

crates/language/src/buffer_tests.rs 🔗

@@ -1866,7 +1866,7 @@ fn test_random_collaboration(cx: &mut AppContext, mut rng: StdRng) {
                         buffer,
                     );
                     log::info!("peer {} setting diagnostics: {:?}", replica_id, diagnostics);
-                    buffer.update_diagnostics(diagnostics, cx);
+                    buffer.update_diagnostics(LanguageServerId(0), diagnostics, cx);
                 });
                 mutation_count -= 1;
             }

crates/language/src/diagnostic_set.rs 🔗

@@ -1,5 +1,6 @@
 use crate::Diagnostic;
 use collections::HashMap;
+use lsp::LanguageServerId;
 use std::{
     cmp::{Ordering, Reverse},
     iter,
@@ -129,7 +130,12 @@ impl DiagnosticSet {
         })
     }
 
-    pub fn groups(&self, output: &mut Vec<DiagnosticGroup<Anchor>>, buffer: &text::BufferSnapshot) {
+    pub fn groups(
+        &self,
+        language_server_id: LanguageServerId,
+        output: &mut Vec<(LanguageServerId, DiagnosticGroup<Anchor>)>,
+        buffer: &text::BufferSnapshot,
+    ) {
         let mut groups = HashMap::default();
         for entry in self.diagnostics.iter() {
             groups
@@ -144,16 +150,22 @@ impl DiagnosticSet {
             entries
                 .iter()
                 .position(|entry| entry.diagnostic.is_primary)
-                .map(|primary_ix| DiagnosticGroup {
-                    entries,
-                    primary_ix,
+                .map(|primary_ix| {
+                    (
+                        language_server_id,
+                        DiagnosticGroup {
+                            entries,
+                            primary_ix,
+                        },
+                    )
                 })
         }));
-        output[start_ix..].sort_unstable_by(|a, b| {
-            a.entries[a.primary_ix]
+        output[start_ix..].sort_unstable_by(|(id_a, group_a), (id_b, group_b)| {
+            group_a.entries[group_a.primary_ix]
                 .range
                 .start
-                .cmp(&b.entries[b.primary_ix].range.start, buffer)
+                .cmp(&group_b.entries[group_b.primary_ix].range.start, buffer)
+                .then_with(|| id_a.cmp(&id_b))
         });
     }
 

crates/language/src/language.rs 🔗

@@ -54,6 +54,7 @@ use futures::channel::mpsc;
 pub use buffer::Operation;
 pub use buffer::*;
 pub use diagnostic_set::DiagnosticEntry;
+pub use lsp::LanguageServerId;
 pub use outline::{Outline, OutlineItem};
 pub use tree_sitter::{Parser, Tree};
 
@@ -414,7 +415,7 @@ pub struct BracketPair {
 pub struct Language {
     pub(crate) config: LanguageConfig,
     pub(crate) grammar: Option<Arc<Grammar>>,
-    pub(crate) adapter: Option<Arc<CachedLspAdapter>>,
+    pub(crate) adapters: Vec<Arc<CachedLspAdapter>>,
 
     #[cfg(any(test, feature = "test-support"))]
     fake_adapter: Option<(
@@ -492,7 +493,7 @@ struct AvailableLanguage {
     path: &'static str,
     config: LanguageConfig,
     grammar: tree_sitter::Language,
-    lsp_adapter: Option<Arc<dyn LspAdapter>>,
+    lsp_adapters: Vec<Arc<dyn LspAdapter>>,
     get_queries: fn(&str) -> LanguageQueries,
 }
 
@@ -513,6 +514,7 @@ pub struct LanguageRegistry {
 }
 
 struct LanguageRegistryState {
+    next_language_server_id: usize,
     languages: Vec<Arc<Language>>,
     available_languages: Vec<AvailableLanguage>,
     next_available_language_id: AvailableLanguageId,
@@ -522,11 +524,17 @@ struct LanguageRegistryState {
     version: usize,
 }
 
+pub struct PendingLanguageServer {
+    pub server_id: LanguageServerId,
+    pub task: Task<Result<lsp::LanguageServer>>,
+}
+
 impl LanguageRegistry {
     pub fn new(login_shell_env_loaded: Task<()>) -> Self {
         let (lsp_binary_statuses_tx, lsp_binary_statuses_rx) = async_broadcast::broadcast(16);
         Self {
             state: RwLock::new(LanguageRegistryState {
+                next_language_server_id: 0,
                 languages: vec![PLAIN_TEXT.clone()],
                 available_languages: Default::default(),
                 next_available_language_id: 0,
@@ -558,7 +566,7 @@ impl LanguageRegistry {
         path: &'static str,
         config: LanguageConfig,
         grammar: tree_sitter::Language,
-        lsp_adapter: Option<Arc<dyn LspAdapter>>,
+        lsp_adapters: Vec<Arc<dyn LspAdapter>>,
         get_queries: fn(&str) -> LanguageQueries,
     ) {
         let state = &mut *self.state.write();
@@ -567,7 +575,7 @@ impl LanguageRegistry {
             path,
             config,
             grammar,
-            lsp_adapter,
+            lsp_adapters,
             get_queries,
         });
     }
@@ -590,12 +598,13 @@ impl LanguageRegistry {
             state
                 .available_languages
                 .iter()
-                .filter_map(|l| l.lsp_adapter.clone())
+                .flat_map(|l| l.lsp_adapters.clone())
                 .chain(
                     state
                         .languages
                         .iter()
-                        .filter_map(|l| l.adapter.as_ref().map(|a| a.adapter.clone())),
+                        .flat_map(|language| &language.adapters)
+                        .map(|adapter| adapter.adapter.clone()),
                 )
                 .collect::<Vec<_>>()
         };
@@ -721,7 +730,7 @@ impl LanguageRegistry {
                                 let queries = (language.get_queries)(&language.path);
                                 let language =
                                     Language::new(language.config, Some(language.grammar))
-                                        .with_lsp_adapter(language.lsp_adapter)
+                                        .with_lsp_adapters(language.lsp_adapters)
                                         .await;
                                 let name = language.name();
                                 match language.with_queries(queries) {
@@ -776,16 +785,15 @@ impl LanguageRegistry {
 
     pub fn start_language_server(
         self: &Arc<Self>,
-        server_id: usize,
         language: Arc<Language>,
+        adapter: Arc<CachedLspAdapter>,
         root_path: Arc<Path>,
         http_client: Arc<dyn HttpClient>,
         cx: &mut AppContext,
-    ) -> Option<Task<Result<lsp::LanguageServer>>> {
+    ) -> Option<PendingLanguageServer> {
         #[cfg(any(test, feature = "test-support"))]
         if language.fake_adapter.is_some() {
-            let language = language;
-            return Some(cx.spawn(|cx| async move {
+            let task = cx.spawn(|cx| async move {
                 let (servers_tx, fake_adapter) = language.fake_adapter.as_ref().unwrap();
                 let (server, mut fake_server) = lsp::LanguageServer::fake(
                     fake_adapter.name.to_string(),
@@ -810,7 +818,10 @@ impl LanguageRegistry {
                     })
                     .detach();
                 Ok(server)
-            }));
+            });
+
+            let server_id = self.state.write().next_language_server_id();
+            return Some(PendingLanguageServer { server_id, task });
         }
 
         let download_dir = self
@@ -820,11 +831,16 @@ impl LanguageRegistry {
             .log_err()?;
 
         let this = self.clone();
-        let adapter = language.adapter.clone()?;
+        let language = language.clone();
+        let http_client = http_client.clone();
+        let download_dir = download_dir.clone();
+        let root_path = root_path.clone();
+        let adapter = adapter.clone();
         let lsp_binary_statuses = self.lsp_binary_statuses_tx.clone();
         let login_shell_env_loaded = self.login_shell_env_loaded.clone();
+        let server_id = self.state.write().next_language_server_id();
 
-        Some(cx.spawn(|cx| async move {
+        let task = cx.spawn(|cx| async move {
             login_shell_env_loaded.await;
 
             let mut lock = this.lsp_binary_paths.lock();
@@ -856,7 +872,9 @@ impl LanguageRegistry {
             )?;
 
             Ok(server)
-        }))
+        });
+
+        Some(PendingLanguageServer { server_id, task })
     }
 
     pub fn language_server_binary_statuses(
@@ -867,6 +885,10 @@ impl LanguageRegistry {
 }
 
 impl LanguageRegistryState {
+    fn next_language_server_id(&mut self) -> LanguageServerId {
+        LanguageServerId(post_inc(&mut self.next_language_server_id))
+    }
+
     fn add(&mut self, language: Arc<Language>) {
         if let Some(theme) = self.theme.as_ref() {
             language.set_theme(&theme.editor.syntax);
@@ -974,15 +996,15 @@ impl Language {
                     highlight_map: Default::default(),
                 })
             }),
-            adapter: None,
+            adapters: Vec::new(),
 
             #[cfg(any(test, feature = "test-support"))]
             fake_adapter: None,
         }
     }
 
-    pub fn lsp_adapter(&self) -> Option<Arc<CachedLspAdapter>> {
-        self.adapter.clone()
+    pub fn lsp_adapters(&self) -> &[Arc<CachedLspAdapter>] {
+        &self.adapters
     }
 
     pub fn id(&self) -> Option<usize> {
@@ -1209,9 +1231,9 @@ impl Language {
         Arc::get_mut(self.grammar.as_mut().unwrap()).unwrap()
     }
 
-    pub async fn with_lsp_adapter(mut self, lsp_adapter: Option<Arc<dyn LspAdapter>>) -> Self {
-        if let Some(adapter) = lsp_adapter {
-            self.adapter = Some(CachedLspAdapter::new(adapter).await);
+    pub async fn with_lsp_adapters(mut self, lsp_adapters: Vec<Arc<dyn LspAdapter>>) -> Self {
+        for adapter in lsp_adapters {
+            self.adapters.push(CachedLspAdapter::new(adapter).await);
         }
         self
     }
@@ -1224,7 +1246,7 @@ impl Language {
         let (servers_tx, servers_rx) = mpsc::unbounded();
         self.fake_adapter = Some((servers_tx, fake_lsp_adapter.clone()));
         let adapter = CachedLspAdapter::new(Arc::new(fake_lsp_adapter)).await;
-        self.adapter = Some(adapter);
+        self.adapters = vec![adapter];
         servers_rx
     }
 
@@ -1233,28 +1255,31 @@ impl Language {
     }
 
     pub async fn disk_based_diagnostic_sources(&self) -> &[String] {
-        match self.adapter.as_ref() {
+        match self.adapters.first().as_ref() {
             Some(adapter) => &adapter.disk_based_diagnostic_sources,
             None => &[],
         }
     }
 
     pub async fn disk_based_diagnostics_progress_token(&self) -> Option<&str> {
-        if let Some(adapter) = self.adapter.as_ref() {
-            adapter.disk_based_diagnostics_progress_token.as_deref()
-        } else {
-            None
+        for adapter in &self.adapters {
+            let token = adapter.disk_based_diagnostics_progress_token.as_deref();
+            if token.is_some() {
+                return token;
+            }
         }
+
+        None
     }
 
     pub async fn process_diagnostics(&self, diagnostics: &mut lsp::PublishDiagnosticsParams) {
-        if let Some(processor) = self.adapter.as_ref() {
-            processor.process_diagnostics(diagnostics).await;
+        for adapter in &self.adapters {
+            adapter.process_diagnostics(diagnostics).await;
         }
     }
 
     pub async fn process_completion(self: &Arc<Self>, completion: &mut lsp::CompletionItem) {
-        if let Some(adapter) = self.adapter.as_ref() {
+        for adapter in &self.adapters {
             adapter.process_completion(completion).await;
         }
     }
@@ -1263,7 +1288,8 @@ impl Language {
         self: &Arc<Self>,
         completion: &lsp::CompletionItem,
     ) -> Option<CodeLabel> {
-        self.adapter
+        self.adapters
+            .first()
             .as_ref()?
             .label_for_completion(completion, self)
             .await
@@ -1274,7 +1300,8 @@ impl Language {
         name: &str,
         kind: lsp::SymbolKind,
     ) -> Option<CodeLabel> {
-        self.adapter
+        self.adapters
+            .first()
             .as_ref()?
             .label_for_symbol(name, kind, self)
             .await
@@ -1559,7 +1586,7 @@ mod tests {
                 ..Default::default()
             },
             tree_sitter_javascript::language(),
-            None,
+            vec![],
             |_| Default::default(),
         );
 
@@ -1595,7 +1622,7 @@ mod tests {
                 ..Default::default()
             },
             tree_sitter_json::language(),
-            None,
+            vec![],
             |_| Default::default(),
         );
         languages.register(
@@ -1606,7 +1633,7 @@ mod tests {
                 ..Default::default()
             },
             tree_sitter_rust::language(),
-            None,
+            vec![],
             |_| Default::default(),
         );
         assert_eq!(

crates/language/src/proto.rs 🔗

@@ -4,7 +4,7 @@ use crate::{
 };
 use anyhow::{anyhow, Result};
 use clock::ReplicaId;
-use lsp::DiagnosticSeverity;
+use lsp::{DiagnosticSeverity, LanguageServerId};
 use rpc::proto;
 use std::{ops::Range, sync::Arc};
 use text::*;
@@ -40,6 +40,7 @@ pub fn serialize_operation(operation: &crate::Operation) -> proto::Operation {
             crate::Operation::Buffer(text::Operation::Edit(edit)) => {
                 proto::operation::Variant::Edit(serialize_edit_operation(edit))
             }
+
             crate::Operation::Buffer(text::Operation::Undo {
                 undo,
                 lamport_timestamp,
@@ -58,6 +59,7 @@ pub fn serialize_operation(operation: &crate::Operation) -> proto::Operation {
                     })
                     .collect(),
             }),
+
             crate::Operation::UpdateSelections {
                 selections,
                 line_mode,
@@ -70,14 +72,18 @@ pub fn serialize_operation(operation: &crate::Operation) -> proto::Operation {
                 line_mode: *line_mode,
                 cursor_shape: serialize_cursor_shape(cursor_shape) as i32,
             }),
+
             crate::Operation::UpdateDiagnostics {
-                diagnostics,
                 lamport_timestamp,
+                server_id,
+                diagnostics,
             } => proto::operation::Variant::UpdateDiagnostics(proto::UpdateDiagnostics {
                 replica_id: lamport_timestamp.replica_id as u32,
                 lamport_timestamp: lamport_timestamp.value,
+                server_id: server_id.0 as u64,
                 diagnostics: serialize_diagnostics(diagnostics.iter()),
             }),
+
             crate::Operation::UpdateCompletionTriggers {
                 triggers,
                 lamport_timestamp,
@@ -267,11 +273,12 @@ pub fn deserialize_operation(message: proto::Operation) -> Result<crate::Operati
             }
             proto::operation::Variant::UpdateDiagnostics(message) => {
                 crate::Operation::UpdateDiagnostics {
-                    diagnostics: deserialize_diagnostics(message.diagnostics),
                     lamport_timestamp: clock::Lamport {
                         replica_id: message.replica_id as ReplicaId,
                         value: message.lamport_timestamp,
                     },
+                    server_id: LanguageServerId(message.server_id as usize),
+                    diagnostics: deserialize_diagnostics(message.diagnostics),
                 }
             }
             proto::operation::Variant::UpdateCompletionTriggers(message) => {
@@ -462,6 +469,7 @@ pub async fn deserialize_completion(
 
 pub fn serialize_code_action(action: &CodeAction) -> proto::CodeAction {
     proto::CodeAction {
+        server_id: action.server_id.0 as u64,
         start: Some(serialize_anchor(&action.range.start)),
         end: Some(serialize_anchor(&action.range.end)),
         lsp_action: serde_json::to_vec(&action.lsp_action).unwrap(),
@@ -479,6 +487,7 @@ pub fn deserialize_code_action(action: proto::CodeAction) -> Result<CodeAction>
         .ok_or_else(|| anyhow!("invalid end"))?;
     let lsp_action = serde_json::from_slice(&action.lsp_action)?;
     Ok(CodeAction {
+        server_id: LanguageServerId(action.server_id as usize),
         range: start..end,
         lsp_action,
     })

crates/lsp/src/lsp.rs 🔗

@@ -16,6 +16,7 @@ use smol::{
     process::{self, Child},
 };
 use std::{
+    fmt,
     future::Future,
     io::Write,
     path::PathBuf,
@@ -35,7 +36,7 @@ type NotificationHandler = Box<dyn Send + FnMut(Option<usize>, &str, AsyncAppCon
 type ResponseHandler = Box<dyn Send + FnOnce(Result<&str, Error>)>;
 
 pub struct LanguageServer {
-    server_id: usize,
+    server_id: LanguageServerId,
     next_id: AtomicUsize,
     outbound_tx: channel::Sender<Vec<u8>>,
     name: String,
@@ -51,6 +52,10 @@ pub struct LanguageServer {
     _server: Option<Child>,
 }
 
+#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[repr(transparent)]
+pub struct LanguageServerId(pub usize);
+
 pub struct Subscription {
     method: &'static str,
     notification_handlers: Arc<Mutex<HashMap<&'static str, NotificationHandler>>>,
@@ -107,7 +112,7 @@ struct Error {
 
 impl LanguageServer {
     pub fn new<T: AsRef<std::ffi::OsStr>>(
-        server_id: usize,
+        server_id: LanguageServerId,
         binary_path: &Path,
         arguments: &[T],
         root_path: &Path,
@@ -158,7 +163,7 @@ impl LanguageServer {
     }
 
     fn new_internal<Stdin, Stdout, F>(
-        server_id: usize,
+        server_id: LanguageServerId,
         stdin: Stdin,
         stdout: Stdout,
         server: Option<Child>,
@@ -581,7 +586,7 @@ impl LanguageServer {
         &self.capabilities
     }
 
-    pub fn server_id(&self) -> usize {
+    pub fn server_id(&self) -> LanguageServerId {
         self.server_id
     }
 
@@ -685,6 +690,12 @@ impl Subscription {
     }
 }
 
+impl fmt::Display for LanguageServerId {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        self.0.fmt(f)
+    }
+}
+
 impl Drop for Subscription {
     fn drop(&mut self) {
         self.notification_handlers.lock().remove(self.method);
@@ -720,7 +731,7 @@ impl LanguageServer {
         let (notifications_tx, notifications_rx) = channel::unbounded();
 
         let server = Self::new_internal(
-            0,
+            LanguageServerId(0),
             stdin_writer,
             stdout_reader,
             None,
@@ -731,7 +742,7 @@ impl LanguageServer {
         );
         let fake = FakeLanguageServer {
             server: Arc::new(Self::new_internal(
-                0,
+                LanguageServerId(0),
                 stdout_writer,
                 stdin_reader,
                 None,

crates/project/Cargo.toml 🔗

@@ -19,6 +19,7 @@ test-support = [
 
 [dependencies]
 text = { path = "../text" }
+copilot = { path = "../copilot" }
 client = { path = "../client" }
 clock = { path = "../clock" }
 collections = { path = "../collections" }

crates/project/src/lsp_command.rs 🔗

@@ -12,7 +12,7 @@ use language::{
     range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, CachedLspAdapter, CharKind, CodeAction,
     Completion, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Unclipped,
 };
-use lsp::{DocumentHighlightKind, LanguageServer, ServerCapabilities};
+use lsp::{DocumentHighlightKind, LanguageServer, LanguageServerId, ServerCapabilities};
 use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag};
 use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc};
 
@@ -33,21 +33,25 @@ pub(crate) trait LspCommand: 'static + Sized {
         language_server: &Arc<LanguageServer>,
         cx: &AppContext,
     ) -> <Self::LspRequest as lsp::request::Request>::Params;
+
     async fn response_from_lsp(
         self,
         message: <Self::LspRequest as lsp::request::Request>::Result,
         project: ModelHandle<Project>,
         buffer: ModelHandle<Buffer>,
+        server_id: LanguageServerId,
         cx: AsyncAppContext,
     ) -> Result<Self::Response>;
 
     fn to_proto(&self, project_id: u64, buffer: &Buffer) -> Self::ProtoRequest;
+
     async fn from_proto(
         message: Self::ProtoRequest,
         project: ModelHandle<Project>,
         buffer: ModelHandle<Buffer>,
         cx: AsyncAppContext,
     ) -> Result<Self>;
+
     fn response_to_proto(
         response: Self::Response,
         project: &mut Project,
@@ -55,6 +59,7 @@ pub(crate) trait LspCommand: 'static + Sized {
         buffer_version: &clock::Global,
         cx: &mut AppContext,
     ) -> <Self::ProtoRequest as proto::RequestMessage>::Response;
+
     async fn response_from_proto(
         self,
         message: <Self::ProtoRequest as proto::RequestMessage>::Response,
@@ -62,6 +67,7 @@ pub(crate) trait LspCommand: 'static + Sized {
         buffer: ModelHandle<Buffer>,
         cx: AsyncAppContext,
     ) -> Result<Self::Response>;
+
     fn buffer_id_from_proto(message: &Self::ProtoRequest) -> u64;
 }
 
@@ -137,6 +143,7 @@ impl LspCommand for PrepareRename {
         message: Option<lsp::PrepareRenameResponse>,
         _: ModelHandle<Project>,
         buffer: ModelHandle<Buffer>,
+        _: LanguageServerId,
         cx: AsyncAppContext,
     ) -> Result<Option<Range<Anchor>>> {
         buffer.read_with(&cx, |buffer, _| {
@@ -263,10 +270,12 @@ impl LspCommand for PerformRename {
         message: Option<lsp::WorkspaceEdit>,
         project: ModelHandle<Project>,
         buffer: ModelHandle<Buffer>,
+        server_id: LanguageServerId,
         mut cx: AsyncAppContext,
     ) -> Result<ProjectTransaction> {
         if let Some(edit) = message {
-            let (lsp_adapter, lsp_server) = language_server_for_buffer(&project, &buffer, &mut cx)?;
+            let (lsp_adapter, lsp_server) =
+                language_server_for_buffer(&project, &buffer, server_id, &mut cx)?;
             Project::deserialize_workspace_edit(
                 project,
                 edit,
@@ -380,9 +389,10 @@ impl LspCommand for GetDefinition {
         message: Option<lsp::GotoDefinitionResponse>,
         project: ModelHandle<Project>,
         buffer: ModelHandle<Buffer>,
+        server_id: LanguageServerId,
         cx: AsyncAppContext,
     ) -> Result<Vec<LocationLink>> {
-        location_links_from_lsp(message, project, buffer, cx).await
+        location_links_from_lsp(message, project, buffer, server_id, cx).await
     }
 
     fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetDefinition {
@@ -472,9 +482,10 @@ impl LspCommand for GetTypeDefinition {
         message: Option<lsp::GotoTypeDefinitionResponse>,
         project: ModelHandle<Project>,
         buffer: ModelHandle<Buffer>,
+        server_id: LanguageServerId,
         cx: AsyncAppContext,
     ) -> Result<Vec<LocationLink>> {
-        location_links_from_lsp(message, project, buffer, cx).await
+        location_links_from_lsp(message, project, buffer, server_id, cx).await
     }
 
     fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetTypeDefinition {
@@ -537,12 +548,13 @@ impl LspCommand for GetTypeDefinition {
 fn language_server_for_buffer(
     project: &ModelHandle<Project>,
     buffer: &ModelHandle<Buffer>,
+    server_id: LanguageServerId,
     cx: &mut AsyncAppContext,
 ) -> Result<(Arc<CachedLspAdapter>, Arc<LanguageServer>)> {
     project
         .read_with(cx, |project, cx| {
             project
-                .language_server_for_buffer(buffer.read(cx), cx)
+                .language_server_for_buffer(buffer.read(cx), server_id, cx)
                 .map(|(adapter, server)| (adapter.clone(), server.clone()))
         })
         .ok_or_else(|| anyhow!("no language server found for buffer"))
@@ -614,6 +626,7 @@ async fn location_links_from_lsp(
     message: Option<lsp::GotoDefinitionResponse>,
     project: ModelHandle<Project>,
     buffer: ModelHandle<Buffer>,
+    server_id: LanguageServerId,
     mut cx: AsyncAppContext,
 ) -> Result<Vec<LocationLink>> {
     let message = match message {
@@ -642,7 +655,8 @@ async fn location_links_from_lsp(
         }
     }
 
-    let (lsp_adapter, language_server) = language_server_for_buffer(&project, &buffer, &mut cx)?;
+    let (lsp_adapter, language_server) =
+        language_server_for_buffer(&project, &buffer, server_id, &mut cx)?;
     let mut definitions = Vec::new();
     for (origin_range, target_uri, target_range) in unresolved_links {
         let target_buffer_handle = project
@@ -756,11 +770,12 @@ impl LspCommand for GetReferences {
         locations: Option<Vec<lsp::Location>>,
         project: ModelHandle<Project>,
         buffer: ModelHandle<Buffer>,
+        server_id: LanguageServerId,
         mut cx: AsyncAppContext,
     ) -> Result<Vec<Location>> {
         let mut references = Vec::new();
         let (lsp_adapter, language_server) =
-            language_server_for_buffer(&project, &buffer, &mut cx)?;
+            language_server_for_buffer(&project, &buffer, server_id, &mut cx)?;
 
         if let Some(locations) = locations {
             for lsp_location in locations {
@@ -917,6 +932,7 @@ impl LspCommand for GetDocumentHighlights {
         lsp_highlights: Option<Vec<lsp::DocumentHighlight>>,
         _: ModelHandle<Project>,
         buffer: ModelHandle<Buffer>,
+        _: LanguageServerId,
         cx: AsyncAppContext,
     ) -> Result<Vec<DocumentHighlight>> {
         buffer.read_with(&cx, |buffer, _| {
@@ -1062,6 +1078,7 @@ impl LspCommand for GetHover {
         message: Option<lsp::Hover>,
         _: ModelHandle<Project>,
         buffer: ModelHandle<Buffer>,
+        _: LanguageServerId,
         cx: AsyncAppContext,
     ) -> Result<Self::Response> {
         Ok(message.and_then(|hover| {
@@ -1283,6 +1300,7 @@ impl LspCommand for GetCompletions {
         completions: Option<lsp::CompletionResponse>,
         _: ModelHandle<Project>,
         buffer: ModelHandle<Buffer>,
+        _: LanguageServerId,
         cx: AsyncAppContext,
     ) -> Result<Vec<Completion>> {
         let completions = if let Some(completions) = completions {
@@ -1502,6 +1520,7 @@ impl LspCommand for GetCodeActions {
         actions: Option<lsp::CodeActionResponse>,
         _: ModelHandle<Project>,
         _: ModelHandle<Buffer>,
+        server_id: LanguageServerId,
         _: AsyncAppContext,
     ) -> Result<Vec<CodeAction>> {
         Ok(actions
@@ -1510,6 +1529,7 @@ impl LspCommand for GetCodeActions {
             .filter_map(|entry| {
                 if let lsp::CodeActionOrCommand::CodeAction(lsp_action) = entry {
                     Some(CodeAction {
+                        server_id,
                         range: self.range.clone(),
                         lsp_action,
                     })

crates/project/src/project.rs 🔗

@@ -12,6 +12,7 @@ use anyhow::{anyhow, Context, Result};
 use client::{proto, Client, TypedEnvelope, UserStore};
 use clock::ReplicaId;
 use collections::{hash_map, BTreeMap, HashMap, HashSet};
+use copilot::Copilot;
 use futures::{
     channel::mpsc::{self, UnboundedReceiver},
     future::{try_join_all, Shared},
@@ -30,12 +31,12 @@ use language::{
     range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, CachedLspAdapter, CodeAction, CodeLabel,
     Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Event as BufferEvent, File as _,
     Language, LanguageRegistry, LanguageServerName, LocalFile, OffsetRangeExt, Operation, Patch,
-    PointUtf16, RopeFingerprint, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction,
-    Unclipped,
+    PendingLanguageServer, PointUtf16, RopeFingerprint, TextBufferSnapshot, ToOffset, ToPointUtf16,
+    Transaction, Unclipped,
 };
 use lsp::{
     DiagnosticSeverity, DiagnosticTag, DidChangeWatchedFilesRegistrationOptions,
-    DocumentHighlightKind, LanguageServer, LanguageString, MarkedString,
+    DocumentHighlightKind, LanguageServer, LanguageServerId, LanguageString, MarkedString,
 };
 use lsp_command::*;
 use lsp_glob_set::LspGlobSet;
@@ -94,11 +95,10 @@ pub struct Project {
     active_entry: Option<ProjectEntryId>,
     buffer_changes_tx: mpsc::UnboundedSender<BufferMessage>,
     languages: Arc<LanguageRegistry>,
-    language_servers: HashMap<usize, LanguageServerState>,
-    language_server_ids: HashMap<(WorktreeId, LanguageServerName), usize>,
-    language_server_statuses: BTreeMap<usize, LanguageServerStatus>,
-    last_workspace_edits_by_language_server: HashMap<usize, ProjectTransaction>,
-    next_language_server_id: usize,
+    language_servers: HashMap<LanguageServerId, LanguageServerState>,
+    language_server_ids: HashMap<(WorktreeId, LanguageServerName), LanguageServerId>,
+    language_server_statuses: BTreeMap<LanguageServerId, LanguageServerStatus>,
+    last_workspace_edits_by_language_server: HashMap<LanguageServerId, ProjectTransaction>,
     client: Arc<client::Client>,
     next_entry_id: Arc<AtomicUsize>,
     join_project_response_message_id: u32,
@@ -123,12 +123,18 @@ pub struct Project {
     /// A mapping from a buffer ID to None means that we've started waiting for an ID but haven't finished loading it.
     /// Used for re-issuing buffer requests when peers temporarily disconnect
     incomplete_remote_buffers: HashMap<u64, Option<ModelHandle<Buffer>>>,
-    buffer_snapshots: HashMap<u64, Vec<(i32, TextBufferSnapshot)>>,
+    buffer_snapshots: HashMap<u64, HashMap<LanguageServerId, Vec<LspBufferSnapshot>>>, // buffer_id -> server_id -> vec of snapshots
     buffers_being_formatted: HashSet<usize>,
     nonce: u128,
     _maintain_buffer_languages: Task<()>,
     _maintain_workspace_config: Task<()>,
     terminals: Terminals,
+    copilot_enabled: bool,
+}
+
+struct LspBufferSnapshot {
+    version: i32,
+    snapshot: TextBufferSnapshot,
 }
 
 enum BufferMessage {
@@ -183,14 +189,14 @@ pub enum Event {
     WorktreeAdded,
     WorktreeRemoved(WorktreeId),
     DiskBasedDiagnosticsStarted {
-        language_server_id: usize,
+        language_server_id: LanguageServerId,
     },
     DiskBasedDiagnosticsFinished {
-        language_server_id: usize,
+        language_server_id: LanguageServerId,
     },
     DiagnosticsUpdated {
         path: ProjectPath,
-        language_server_id: usize,
+        language_server_id: LanguageServerId,
     },
     RemoteIdChanged(Option<u64>),
     DisconnectedFromHost,
@@ -237,7 +243,6 @@ pub struct ProjectPath {
 
 #[derive(Copy, Clone, Debug, Default, PartialEq, Serialize)]
 pub struct DiagnosticSummary {
-    pub language_server_id: usize,
     pub error_count: usize,
     pub warning_count: usize,
 }
@@ -308,12 +313,8 @@ pub struct Hover {
 pub struct ProjectTransaction(pub HashMap<ModelHandle<Buffer>, language::Transaction>);
 
 impl DiagnosticSummary {
-    fn new<'a, T: 'a>(
-        language_server_id: usize,
-        diagnostics: impl IntoIterator<Item = &'a DiagnosticEntry<T>>,
-    ) -> Self {
+    fn new<'a, T: 'a>(diagnostics: impl IntoIterator<Item = &'a DiagnosticEntry<T>>) -> Self {
         let mut this = Self {
-            language_server_id,
             error_count: 0,
             warning_count: 0,
         };
@@ -335,10 +336,14 @@ impl DiagnosticSummary {
         self.error_count == 0 && self.warning_count == 0
     }
 
-    pub fn to_proto(&self, path: &Path) -> proto::DiagnosticSummary {
+    pub fn to_proto(
+        &self,
+        language_server_id: LanguageServerId,
+        path: &Path,
+    ) -> proto::DiagnosticSummary {
         proto::DiagnosticSummary {
             path: path.to_string_lossy().to_string(),
-            language_server_id: self.language_server_id as u64,
+            language_server_id: language_server_id.0 as u64,
             error_count: self.error_count as u32,
             warning_count: self.warning_count as u32,
         }
@@ -467,11 +472,11 @@ impl Project {
                 language_server_statuses: Default::default(),
                 last_workspace_edits_by_language_server: Default::default(),
                 buffers_being_formatted: Default::default(),
-                next_language_server_id: 0,
                 nonce: StdRng::from_entropy().gen(),
                 terminals: Terminals {
                     local_handles: Vec::new(),
                 },
+                copilot_enabled: Copilot::global(cx).is_some(),
             }
         })
     }
@@ -540,7 +545,7 @@ impl Project {
                     .into_iter()
                     .map(|server| {
                         (
-                            server.id as usize,
+                            LanguageServerId(server.id as usize),
                             LanguageServerStatus {
                                 name: server.name,
                                 pending_work: Default::default(),
@@ -551,7 +556,6 @@ impl Project {
                     })
                     .collect(),
                 last_workspace_edits_by_language_server: Default::default(),
-                next_language_server_id: 0,
                 opened_buffers: Default::default(),
                 buffers_being_formatted: Default::default(),
                 buffer_snapshots: Default::default(),
@@ -559,6 +563,7 @@ impl Project {
                 terminals: Terminals {
                     local_handles: Vec::new(),
                 },
+                copilot_enabled: Copilot::global(cx).is_some(),
             };
             for worktree in worktrees {
                 let _ = this.add_worktree(&worktree, cx);
@@ -641,7 +646,7 @@ impl Project {
 
         let mut language_servers_to_stop = Vec::new();
         for language in self.languages.to_vec() {
-            if let Some(lsp_adapter) = language.lsp_adapter() {
+            for lsp_adapter in language.lsp_adapters() {
                 if !settings.enable_language_server(Some(&language.name())) {
                     let lsp_name = &lsp_adapter.name;
                     for (worktree_id, started_lsp_name) in self.language_server_ids.keys() {
@@ -661,7 +666,16 @@ impl Project {
 
         // Start all the newly-enabled language servers.
         for (worktree_id, worktree_path, language) in language_servers_to_start {
-            self.start_language_server(worktree_id, worktree_path, language, cx);
+            self.start_language_servers(worktree_id, worktree_path, language, cx);
+        }
+
+        if !self.copilot_enabled && Copilot::global(cx).is_some() {
+            self.copilot_enabled = true;
+            for buffer in self.opened_buffers.values() {
+                if let Some(buffer) = buffer.upgrade(cx) {
+                    self.register_buffer_with_copilot(&buffer, cx);
+                }
+            }
         }
 
         cx.notify();
@@ -1015,7 +1029,7 @@ impl Project {
                 .send(proto::StartLanguageServer {
                     project_id,
                     server: Some(proto::LanguageServer {
-                        id: *server_id as u64,
+                        id: server_id.0 as u64,
                         name: status.name.clone(),
                     }),
                 })
@@ -1142,7 +1156,7 @@ impl Project {
             .into_iter()
             .map(|server| {
                 (
-                    server.id as usize,
+                    LanguageServerId(server.id as usize),
                     LanguageServerStatus {
                         name: server.name,
                         pending_work: Default::default(),
@@ -1434,7 +1448,7 @@ impl Project {
     fn open_local_buffer_via_lsp(
         &mut self,
         abs_path: lsp::Url,
-        language_server_id: usize,
+        language_server_id: LanguageServerId,
         language_server_name: LanguageServerName,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<ModelHandle<Buffer>>> {
@@ -1532,12 +1546,13 @@ impl Project {
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<()>> {
         let worktree_task = self.find_or_create_local_worktree(&abs_path, true, cx);
-        let old_path =
-            File::from_dyn(buffer.read(cx).file()).and_then(|f| Some(f.as_local()?.abs_path(cx)));
+        let old_file = File::from_dyn(buffer.read(cx).file())
+            .filter(|f| f.is_local())
+            .cloned();
         cx.spawn(|this, mut cx| async move {
-            if let Some(old_path) = old_path {
+            if let Some(old_file) = &old_file {
                 this.update(&mut cx, |this, cx| {
-                    this.unregister_buffer_from_language_server(&buffer, old_path, cx);
+                    this.unregister_buffer_from_language_servers(&buffer, old_file, cx);
                 });
             }
             let (worktree, path) = worktree_task.await?;
@@ -1551,7 +1566,7 @@ impl Project {
                 .await?;
             this.update(&mut cx, |this, cx| {
                 this.detect_language_for_buffer(&buffer, cx);
-                this.register_buffer_with_language_server(&buffer, cx);
+                this.register_buffer_with_language_servers(&buffer, cx);
             });
             Ok(())
         })
@@ -1615,16 +1630,18 @@ impl Project {
         .detach();
 
         self.detect_language_for_buffer(buffer, cx);
-        self.register_buffer_with_language_server(buffer, cx);
+        self.register_buffer_with_language_servers(buffer, cx);
+        self.register_buffer_with_copilot(buffer, cx);
         cx.observe_release(buffer, |this, buffer, cx| {
             if let Some(file) = File::from_dyn(buffer.file()) {
                 if file.is_local() {
                     let uri = lsp::Url::from_file_path(file.abs_path(cx)).unwrap();
-                    if let Some((_, server)) = this.language_server_for_buffer(buffer, cx) {
+                    for server in this.language_servers_for_buffer(buffer, cx) {
                         server
+                            .1
                             .notify::<lsp::notification::DidCloseTextDocument>(
                                 lsp::DidCloseTextDocumentParams {
-                                    text_document: lsp::TextDocumentIdentifier::new(uri),
+                                    text_document: lsp::TextDocumentIdentifier::new(uri.clone()),
                                 },
                             )
                             .log_err();
@@ -1638,51 +1655,55 @@ impl Project {
         Ok(())
     }
 
-    fn register_buffer_with_language_server(
+    fn register_buffer_with_language_servers(
         &mut self,
         buffer_handle: &ModelHandle<Buffer>,
         cx: &mut ModelContext<Self>,
     ) {
         let buffer = buffer_handle.read(cx);
         let buffer_id = buffer.remote_id();
+
         if let Some(file) = File::from_dyn(buffer.file()) {
-            if file.is_local() {
-                let uri = lsp::Url::from_file_path(file.abs_path(cx)).unwrap();
-                let initial_snapshot = buffer.text_snapshot();
-
-                let mut language_server = None;
-                let mut language_id = None;
-                if let Some(language) = buffer.language() {
-                    let worktree_id = file.worktree_id(cx);
-                    if let Some(adapter) = language.lsp_adapter() {
-                        language_id = adapter.language_ids.get(language.name().as_ref()).cloned();
-                        language_server = self
-                            .language_server_ids
-                            .get(&(worktree_id, adapter.name.clone()))
-                            .and_then(|id| self.language_servers.get(id))
-                            .and_then(|server_state| {
-                                if let LanguageServerState::Running { server, .. } = server_state {
-                                    Some(server.clone())
-                                } else {
-                                    None
-                                }
-                            });
-                    }
-                }
+            if !file.is_local() {
+                return;
+            }
 
-                if let Some(local_worktree) = file.worktree.read(cx).as_local() {
-                    if let Some(diagnostics) = local_worktree.diagnostics_for_path(file.path()) {
-                        self.update_buffer_diagnostics(buffer_handle, diagnostics, None, cx)
-                            .log_err();
-                    }
+            let uri = lsp::Url::from_file_path(file.abs_path(cx)).unwrap();
+            let initial_snapshot = buffer.text_snapshot();
+            let language = buffer.language().cloned();
+            let worktree_id = file.worktree_id(cx);
+
+            if let Some(local_worktree) = file.worktree.read(cx).as_local() {
+                for (server_id, diagnostics) in local_worktree.diagnostics_for_path(file.path()) {
+                    self.update_buffer_diagnostics(buffer_handle, server_id, None, diagnostics, cx)
+                        .log_err();
                 }
+            }
+
+            if let Some(language) = language {
+                for adapter in language.lsp_adapters() {
+                    let language_id = adapter.language_ids.get(language.name().as_ref()).cloned();
+                    let server = self
+                        .language_server_ids
+                        .get(&(worktree_id, adapter.name.clone()))
+                        .and_then(|id| self.language_servers.get(id))
+                        .and_then(|server_state| {
+                            if let LanguageServerState::Running { server, .. } = server_state {
+                                Some(server.clone())
+                            } else {
+                                None
+                            }
+                        });
+                    let server = match server {
+                        Some(server) => server,
+                        None => continue,
+                    };
 
-                if let Some(server) = language_server {
                     server
                         .notify::<lsp::notification::DidOpenTextDocument>(
                             lsp::DidOpenTextDocumentParams {
                                 text_document: lsp::TextDocumentItem::new(
-                                    uri,
+                                    uri.clone(),
                                     language_id.unwrap_or_default(),
                                     0,
                                     initial_snapshot.text(),
@@ -1690,6 +1711,7 @@ impl Project {
                             },
                         )
                         .log_err();
+
                     buffer_handle.update(cx, |buffer, cx| {
                         buffer.set_completion_triggers(
                             server
@@ -1699,31 +1721,50 @@ impl Project {
                                 .and_then(|provider| provider.trigger_characters.clone())
                                 .unwrap_or_default(),
                             cx,
-                        )
+                        );
                     });
+
+                    let snapshot = LspBufferSnapshot {
+                        version: 0,
+                        snapshot: initial_snapshot.clone(),
+                    };
                     self.buffer_snapshots
-                        .insert(buffer_id, vec![(0, initial_snapshot)]);
+                        .entry(buffer_id)
+                        .or_default()
+                        .insert(server.server_id(), vec![snapshot]);
                 }
             }
         }
     }
 
-    fn unregister_buffer_from_language_server(
+    fn unregister_buffer_from_language_servers(
         &mut self,
         buffer: &ModelHandle<Buffer>,
-        old_path: PathBuf,
+        old_file: &File,
         cx: &mut ModelContext<Self>,
     ) {
+        let old_path = match old_file.as_local() {
+            Some(local) => local.abs_path(cx),
+            None => return,
+        };
+
         buffer.update(cx, |buffer, cx| {
-            buffer.update_diagnostics(Default::default(), cx);
+            let worktree_id = old_file.worktree_id(cx);
+            let ids = &self.language_server_ids;
+
+            let language = buffer.language().cloned();
+            let adapters = language.iter().flat_map(|language| language.lsp_adapters());
+            for &server_id in adapters.flat_map(|a| ids.get(&(worktree_id, a.name.clone()))) {
+                buffer.update_diagnostics(server_id, Default::default(), cx);
+            }
+
             self.buffer_snapshots.remove(&buffer.remote_id());
-            if let Some((_, language_server)) = self.language_server_for_buffer(buffer, cx) {
+            let file_url = lsp::Url::from_file_path(old_path).unwrap();
+            for (_, language_server) in self.language_servers_for_buffer(buffer, cx) {
                 language_server
                     .notify::<lsp::notification::DidCloseTextDocument>(
                         lsp::DidCloseTextDocumentParams {
-                            text_document: lsp::TextDocumentIdentifier::new(
-                                lsp::Url::from_file_path(old_path).unwrap(),
-                            ),
+                            text_document: lsp::TextDocumentIdentifier::new(file_url.clone()),
                         },
                     )
                     .log_err();
@@ -1731,6 +1772,16 @@ impl Project {
         });
     }
 
+    fn register_buffer_with_copilot(
+        &self,
+        buffer_handle: &ModelHandle<Buffer>,
+        cx: &mut ModelContext<Self>,
+    ) {
+        if let Some(copilot) = Copilot::global(cx) {
+            copilot.update(cx, |copilot, cx| copilot.register_buffer(buffer_handle, cx));
+        }
+    }
+
     async fn send_buffer_messages(
         this: WeakModelHandle<Self>,
         rx: UnboundedReceiver<BufferMessage>,
@@ -1809,52 +1860,67 @@ impl Project {
                     })
                     .ok();
             }
+
             BufferEvent::Edited { .. } => {
-                let language_server = self
-                    .language_server_for_buffer(buffer.read(cx), cx)
-                    .map(|(_, server)| server.clone())?;
                 let buffer = buffer.read(cx);
                 let file = File::from_dyn(buffer.file())?;
                 let abs_path = file.as_local()?.abs_path(cx);
                 let uri = lsp::Url::from_file_path(abs_path).unwrap();
-                let buffer_snapshots = self.buffer_snapshots.get_mut(&buffer.remote_id())?;
-                let (version, prev_snapshot) = buffer_snapshots.last()?;
                 let next_snapshot = buffer.text_snapshot();
-                let next_version = version + 1;
-
-                let content_changes = buffer
-                    .edits_since::<(PointUtf16, usize)>(prev_snapshot.version())
-                    .map(|edit| {
-                        let edit_start = edit.new.start.0;
-                        let edit_end = edit_start + (edit.old.end.0 - edit.old.start.0);
-                        let new_text = next_snapshot
-                            .text_for_range(edit.new.start.1..edit.new.end.1)
-                            .collect();
-                        lsp::TextDocumentContentChangeEvent {
-                            range: Some(lsp::Range::new(
-                                point_to_lsp(edit_start),
-                                point_to_lsp(edit_end),
-                            )),
-                            range_length: None,
-                            text: new_text,
-                        }
-                    })
+
+                let language_servers: Vec<_> = self
+                    .language_servers_iter_for_buffer(buffer, cx)
+                    .map(|i| i.1.clone())
                     .collect();
 
-                buffer_snapshots.push((next_version, next_snapshot));
+                for language_server in language_servers {
+                    let language_server = language_server.clone();
+
+                    let buffer_snapshots = self
+                        .buffer_snapshots
+                        .get_mut(&buffer.remote_id())
+                        .and_then(|m| m.get_mut(&language_server.server_id()))?;
+                    let previous_snapshot = buffer_snapshots.last()?;
+                    let next_version = previous_snapshot.version + 1;
+
+                    let content_changes = buffer
+                        .edits_since::<(PointUtf16, usize)>(previous_snapshot.snapshot.version())
+                        .map(|edit| {
+                            let edit_start = edit.new.start.0;
+                            let edit_end = edit_start + (edit.old.end.0 - edit.old.start.0);
+                            let new_text = next_snapshot
+                                .text_for_range(edit.new.start.1..edit.new.end.1)
+                                .collect();
+                            lsp::TextDocumentContentChangeEvent {
+                                range: Some(lsp::Range::new(
+                                    point_to_lsp(edit_start),
+                                    point_to_lsp(edit_end),
+                                )),
+                                range_length: None,
+                                text: new_text,
+                            }
+                        })
+                        .collect();
 
-                language_server
-                    .notify::<lsp::notification::DidChangeTextDocument>(
-                        lsp::DidChangeTextDocumentParams {
-                            text_document: lsp::VersionedTextDocumentIdentifier::new(
-                                uri,
-                                next_version,
-                            ),
-                            content_changes,
-                        },
-                    )
-                    .log_err();
+                    buffer_snapshots.push(LspBufferSnapshot {
+                        version: next_version,
+                        snapshot: next_snapshot.clone(),
+                    });
+
+                    language_server
+                        .notify::<lsp::notification::DidChangeTextDocument>(
+                            lsp::DidChangeTextDocumentParams {
+                                text_document: lsp::VersionedTextDocumentIdentifier::new(
+                                    uri.clone(),
+                                    next_version,
+                                ),
+                                content_changes,
+                            },
+                        )
+                        .log_err();
+                }
             }
+
             BufferEvent::Saved => {
                 let file = File::from_dyn(buffer.read(cx).file())?;
                 let worktree_id = file.worktree_id(cx);
@@ -1874,24 +1940,26 @@ impl Project {
                         .log_err();
                 }
 
-                let language_server_id = self.language_server_id_for_buffer(buffer.read(cx), cx)?;
-                if let Some(LanguageServerState::Running {
-                    adapter,
-                    simulate_disk_based_diagnostics_completion,
-                    ..
-                }) = self.language_servers.get_mut(&language_server_id)
-                {
-                    // After saving a buffer using a language server that doesn't provide
-                    // a disk-based progress token, kick off a timer that will reset every
-                    // time the buffer is saved. If the timer eventually fires, simulate
-                    // disk-based diagnostics being finished so that other pieces of UI
-                    // (e.g., project diagnostics view, diagnostic status bar) can update.
-                    // We don't emit an event right away because the language server might take
-                    // some time to publish diagnostics.
-                    if adapter.disk_based_diagnostics_progress_token.is_none() {
-                        const DISK_BASED_DIAGNOSTICS_DEBOUNCE: Duration = Duration::from_secs(1);
-
-                        let task = cx.spawn_weak(|this, mut cx| async move {
+                let language_server_ids = self.language_server_ids_for_buffer(buffer.read(cx), cx);
+                for language_server_id in language_server_ids {
+                    if let Some(LanguageServerState::Running {
+                        adapter,
+                        simulate_disk_based_diagnostics_completion,
+                        ..
+                    }) = self.language_servers.get_mut(&language_server_id)
+                    {
+                        // After saving a buffer using a language server that doesn't provide
+                        // a disk-based progress token, kick off a timer that will reset every
+                        // time the buffer is saved. If the timer eventually fires, simulate
+                        // disk-based diagnostics being finished so that other pieces of UI
+                        // (e.g., project diagnostics view, diagnostic status bar) can update.
+                        // We don't emit an event right away because the language server might take
+                        // some time to publish diagnostics.
+                        if adapter.disk_based_diagnostics_progress_token.is_none() {
+                            const DISK_BASED_DIAGNOSTICS_DEBOUNCE: Duration =
+                                Duration::from_secs(1);
+
+                            let task = cx.spawn_weak(|this, mut cx| async move {
                             cx.background().timer(DISK_BASED_DIAGNOSTICS_DEBOUNCE).await;
                             if let Some(this) = this.upgrade(&cx) {
                                 this.update(&mut cx, |this, cx | {
@@ -1905,10 +1973,12 @@ impl Project {
                                 });
                             }
                         });
-                        *simulate_disk_based_diagnostics_completion = Some(task);
+                            *simulate_disk_based_diagnostics_completion = Some(task);
+                        }
                     }
                 }
             }
+
             _ => {}
         }
 
@@ -1963,7 +2033,7 @@ impl Project {
 
                         for buffer in plain_text_buffers {
                             project.detect_language_for_buffer(&buffer, cx);
-                            project.register_buffer_with_language_server(&buffer, cx);
+                            project.register_buffer_with_language_servers(&buffer, cx);
                         }
 
                         for buffer in buffers_with_unknown_injections {
@@ -2047,12 +2117,12 @@ impl Project {
             if let Some(worktree) = file.worktree.read(cx).as_local() {
                 let worktree_id = worktree.id();
                 let worktree_abs_path = worktree.abs_path().clone();
-                self.start_language_server(worktree_id, worktree_abs_path, new_language, cx);
+                self.start_language_servers(worktree_id, worktree_abs_path, new_language, cx);
             }
         }
     }
 
-    fn start_language_server(
+    fn start_language_servers(
         &mut self,
         worktree_id: WorktreeId,
         worktree_path: Arc<Path>,
@@ -2066,313 +2136,332 @@ impl Project {
             return;
         }
 
-        let adapter = if let Some(adapter) = language.lsp_adapter() {
-            adapter
-        } else {
-            return;
-        };
-        let key = (worktree_id, adapter.name.clone());
+        for adapter in language.lsp_adapters() {
+            let key = (worktree_id, adapter.name.clone());
+            if self.language_server_ids.contains_key(&key) {
+                continue;
+            }
 
-        let mut initialization_options = adapter.initialization_options.clone();
+            let pending_server = match self.languages.start_language_server(
+                language.clone(),
+                adapter.clone(),
+                worktree_path.clone(),
+                self.client.http_client(),
+                cx,
+            ) {
+                Some(pending_server) => pending_server,
+                None => continue,
+            };
 
-        let lsp = &cx.global::<Settings>().lsp.get(&adapter.name.0);
-        let override_options = lsp.map(|s| s.initialization_options.clone()).flatten();
-        match (&mut initialization_options, override_options) {
-            (Some(initialization_options), Some(override_options)) => {
-                merge_json_value_into(override_options, initialization_options);
-            }
-            (None, override_options) => initialization_options = override_options,
-            _ => {}
-        }
+            let lsp = &cx.global::<Settings>().lsp.get(&adapter.name.0);
+            let override_options = lsp.map(|s| s.initialization_options.clone()).flatten();
 
-        self.language_server_ids
-            .entry(key.clone())
-            .or_insert_with(|| {
-                let languages = self.languages.clone();
-                let server_id = post_inc(&mut self.next_language_server_id);
-                let language_server = self.languages.start_language_server(
-                    server_id,
-                    language.clone(),
-                    worktree_path,
-                    self.client.http_client(),
-                    cx,
-                );
-                self.language_servers.insert(
-                    server_id,
-                    LanguageServerState::Starting(cx.spawn_weak(|this, mut cx| async move {
-                        let workspace_config =
-                            cx.update(|cx| languages.workspace_configuration(cx)).await;
-                        let language_server = language_server?.await.log_err()?;
-                        let language_server = language_server
-                            .initialize(initialization_options)
-                            .await
-                            .log_err()?;
-                        let this = this.upgrade(&cx)?;
+            let mut initialization_options = adapter.initialization_options.clone();
+            match (&mut initialization_options, override_options) {
+                (Some(initialization_options), Some(override_options)) => {
+                    merge_json_value_into(override_options, initialization_options);
+                }
+                (None, override_options) => initialization_options = override_options,
+                _ => {}
+            }
 
-                        language_server
-                            .on_notification::<lsp::notification::PublishDiagnostics, _>({
-                                let this = this.downgrade();
-                                let adapter = adapter.clone();
-                                move |mut params, cx| {
-                                    let this = this;
-                                    let adapter = adapter.clone();
-                                    cx.spawn(|mut cx| async move {
-                                        adapter.process_diagnostics(&mut params).await;
-                                        if let Some(this) = this.upgrade(&cx) {
-                                            this.update(&mut cx, |this, cx| {
-                                                this.update_diagnostics(
-                                                    server_id,
-                                                    params,
-                                                    &adapter.disk_based_diagnostic_sources,
-                                                    cx,
-                                                )
-                                                .log_err();
-                                            });
-                                        }
-                                    })
-                                    .detach();
-                                }
-                            })
-                            .detach();
+            let server_id = pending_server.server_id;
+            let state = self.setup_pending_language_server(
+                initialization_options,
+                pending_server,
+                adapter.clone(),
+                language.clone(),
+                key.clone(),
+                cx,
+            );
+            self.language_servers.insert(server_id, state);
+            self.language_server_ids.insert(key.clone(), server_id);
+        }
+    }
 
-                        language_server
-                            .on_request::<lsp::request::WorkspaceConfiguration, _, _>({
-                                let languages = languages.clone();
-                                move |params, mut cx| {
-                                    let languages = languages.clone();
-                                    async move {
-                                        let workspace_config = cx
-                                            .update(|cx| languages.workspace_configuration(cx))
-                                            .await;
-                                        Ok(params
-                                            .items
-                                            .into_iter()
-                                            .map(|item| {
-                                                if let Some(section) = &item.section {
-                                                    workspace_config
-                                                        .get(section)
-                                                        .cloned()
-                                                        .unwrap_or(serde_json::Value::Null)
-                                                } else {
-                                                    workspace_config.clone()
-                                                }
-                                            })
-                                            .collect())
-                                    }
-                                }
-                            })
-                            .detach();
+    fn setup_pending_language_server(
+        &mut self,
+        initialization_options: Option<serde_json::Value>,
+        pending_server: PendingLanguageServer,
+        adapter: Arc<CachedLspAdapter>,
+        language: Arc<Language>,
+        key: (WorktreeId, LanguageServerName),
+        cx: &mut ModelContext<Project>,
+    ) -> LanguageServerState {
+        let server_id = pending_server.server_id;
+        let languages = self.languages.clone();
 
-                        // Even though we don't have handling for these requests, respond to them to
-                        // avoid stalling any language server like `gopls` which waits for a response
-                        // to these requests when initializing.
-                        language_server
-                            .on_request::<lsp::request::WorkDoneProgressCreate, _, _>({
-                                let this = this.downgrade();
-                                move |params, mut cx| async move {
-                                    if let Some(this) = this.upgrade(&cx) {
-                                        this.update(&mut cx, |this, _| {
-                                            if let Some(status) =
-                                                this.language_server_statuses.get_mut(&server_id)
-                                            {
-                                                if let lsp::NumberOrString::String(token) =
-                                                    params.token
-                                                {
-                                                    status.progress_tokens.insert(token);
-                                                }
-                                            }
-                                        });
-                                    }
-                                    Ok(())
-                                }
-                            })
-                            .detach();
-                        language_server
-                            .on_request::<lsp::request::RegisterCapability, _, _>({
-                                let this = this.downgrade();
-                                move |params, mut cx| async move {
-                                    let this = this
-                                        .upgrade(&cx)
-                                        .ok_or_else(|| anyhow!("project dropped"))?;
-                                    for reg in params.registrations {
-                                        if reg.method == "workspace/didChangeWatchedFiles" {
-                                            if let Some(options) = reg.register_options {
-                                                let options = serde_json::from_value(options)?;
-                                                this.update(&mut cx, |this, cx| {
-                                                    this.on_lsp_did_change_watched_files(
-                                                        server_id, options, cx,
-                                                    );
-                                                });
-                                            }
-                                        }
-                                    }
-                                    Ok(())
-                                }
-                            })
-                            .detach();
+        LanguageServerState::Starting(cx.spawn_weak(|this, mut cx| async move {
+            let workspace_config = cx.update(|cx| languages.workspace_configuration(cx)).await;
+            let language_server = pending_server.task.await.log_err()?;
+            let language_server = language_server
+                .initialize(initialization_options)
+                .await
+                .log_err()?;
+            let this = this.upgrade(&cx)?;
 
-                        language_server
-                            .on_request::<lsp::request::ApplyWorkspaceEdit, _, _>({
-                                let this = this.downgrade();
-                                let adapter = adapter.clone();
-                                let language_server = language_server.clone();
-                                move |params, cx| {
-                                    Self::on_lsp_workspace_edit(
-                                        this,
-                                        params,
+            language_server
+                .on_notification::<lsp::notification::PublishDiagnostics, _>({
+                    let this = this.downgrade();
+                    let adapter = adapter.clone();
+                    move |mut params, cx| {
+                        let this = this;
+                        let adapter = adapter.clone();
+                        cx.spawn(|mut cx| async move {
+                            adapter.process_diagnostics(&mut params).await;
+                            if let Some(this) = this.upgrade(&cx) {
+                                this.update(&mut cx, |this, cx| {
+                                    this.update_diagnostics(
                                         server_id,
-                                        adapter.clone(),
-                                        language_server.clone(),
+                                        params,
+                                        &adapter.disk_based_diagnostic_sources,
                                         cx,
                                     )
-                                }
-                            })
-                            .detach();
-
-                        let disk_based_diagnostics_progress_token =
-                            adapter.disk_based_diagnostics_progress_token.clone();
-
-                        language_server
-                            .on_notification::<lsp::notification::Progress, _>({
-                                let this = this.downgrade();
-                                move |params, mut cx| {
-                                    if let Some(this) = this.upgrade(&cx) {
-                                        this.update(&mut cx, |this, cx| {
-                                            this.on_lsp_progress(
-                                                params,
-                                                server_id,
-                                                disk_based_diagnostics_progress_token.clone(),
-                                                cx,
-                                            );
-                                        });
-                                    }
-                                }
-                            })
-                            .detach();
-
-                        language_server
-                            .notify::<lsp::notification::DidChangeConfiguration>(
-                                lsp::DidChangeConfigurationParams {
-                                    settings: workspace_config,
-                                },
-                            )
-                            .ok();
-
-                        this.update(&mut cx, |this, cx| {
-                            // If the language server for this key doesn't match the server id, don't store the
-                            // server. Which will cause it to be dropped, killing the process
-                            if this
-                                .language_server_ids
-                                .get(&key)
-                                .map(|id| id != &server_id)
-                                .unwrap_or(false)
-                            {
-                                return None;
-                            }
-
-                            // Update language_servers collection with Running variant of LanguageServerState
-                            // indicating that the server is up and running and ready
-                            this.language_servers.insert(
-                                server_id,
-                                LanguageServerState::Running {
-                                    adapter: adapter.clone(),
-                                    language,
-                                    watched_paths: Default::default(),
-                                    server: language_server.clone(),
-                                    simulate_disk_based_diagnostics_completion: None,
-                                },
-                            );
-                            this.language_server_statuses.insert(
-                                server_id,
-                                LanguageServerStatus {
-                                    name: language_server.name().to_string(),
-                                    pending_work: Default::default(),
-                                    has_pending_diagnostic_updates: false,
-                                    progress_tokens: Default::default(),
-                                },
-                            );
-
-                            if let Some(project_id) = this.remote_id() {
-                                this.client
-                                    .send(proto::StartLanguageServer {
-                                        project_id,
-                                        server: Some(proto::LanguageServer {
-                                            id: server_id as u64,
-                                            name: language_server.name().to_string(),
-                                        }),
-                                    })
                                     .log_err();
+                                });
                             }
+                        })
+                        .detach();
+                    }
+                })
+                .detach();
 
-                            // Tell the language server about every open buffer in the worktree that matches the language.
-                            for buffer in this.opened_buffers.values() {
-                                if let Some(buffer_handle) = buffer.upgrade(cx) {
-                                    let buffer = buffer_handle.read(cx);
-                                    let file = if let Some(file) = File::from_dyn(buffer.file()) {
-                                        file
-                                    } else {
-                                        continue;
-                                    };
-                                    let language = if let Some(language) = buffer.language() {
-                                        language
+            language_server
+                .on_request::<lsp::request::WorkspaceConfiguration, _, _>({
+                    let languages = languages.clone();
+                    move |params, mut cx| {
+                        let languages = languages.clone();
+                        async move {
+                            let workspace_config =
+                                cx.update(|cx| languages.workspace_configuration(cx)).await;
+                            Ok(params
+                                .items
+                                .into_iter()
+                                .map(|item| {
+                                    if let Some(section) = &item.section {
+                                        workspace_config
+                                            .get(section)
+                                            .cloned()
+                                            .unwrap_or(serde_json::Value::Null)
                                     } else {
-                                        continue;
-                                    };
-                                    if file.worktree.read(cx).id() != key.0
-                                        || language.lsp_adapter().map(|a| a.name.clone())
-                                            != Some(key.1.clone())
-                                    {
-                                        continue;
+                                        workspace_config.clone()
                                     }
+                                })
+                                .collect())
+                        }
+                    }
+                })
+                .detach();
 
-                                    let file = file.as_local()?;
-                                    let versions = this
-                                        .buffer_snapshots
-                                        .entry(buffer.remote_id())
-                                        .or_insert_with(|| vec![(0, buffer.text_snapshot())]);
-
-                                    let (version, initial_snapshot) = versions.last().unwrap();
-                                    let uri = lsp::Url::from_file_path(file.abs_path(cx)).unwrap();
-                                    language_server
-                                        .notify::<lsp::notification::DidOpenTextDocument>(
-                                            lsp::DidOpenTextDocumentParams {
-                                                text_document: lsp::TextDocumentItem::new(
-                                                    uri,
-                                                    adapter
-                                                        .language_ids
-                                                        .get(language.name().as_ref())
-                                                        .cloned()
-                                                        .unwrap_or_default(),
-                                                    *version,
-                                                    initial_snapshot.text(),
-                                                ),
-                                            },
-                                        )
-                                        .log_err()?;
-                                    buffer_handle.update(cx, |buffer, cx| {
-                                        buffer.set_completion_triggers(
-                                            language_server
-                                                .capabilities()
-                                                .completion_provider
-                                                .as_ref()
-                                                .and_then(|provider| {
-                                                    provider.trigger_characters.clone()
-                                                })
-                                                .unwrap_or_default(),
-                                            cx,
-                                        )
+            // Even though we don't have handling for these requests, respond to them to
+            // avoid stalling any language server like `gopls` which waits for a response
+            // to these requests when initializing.
+            language_server
+                .on_request::<lsp::request::WorkDoneProgressCreate, _, _>({
+                    let this = this.downgrade();
+                    move |params, mut cx| async move {
+                        if let Some(this) = this.upgrade(&cx) {
+                            this.update(&mut cx, |this, _| {
+                                if let Some(status) =
+                                    this.language_server_statuses.get_mut(&server_id)
+                                {
+                                    if let lsp::NumberOrString::String(token) = params.token {
+                                        status.progress_tokens.insert(token);
+                                    }
+                                }
+                            });
+                        }
+                        Ok(())
+                    }
+                })
+                .detach();
+            language_server
+                .on_request::<lsp::request::RegisterCapability, _, _>({
+                    let this = this.downgrade();
+                    move |params, mut cx| async move {
+                        let this = this
+                            .upgrade(&cx)
+                            .ok_or_else(|| anyhow!("project dropped"))?;
+                        for reg in params.registrations {
+                            if reg.method == "workspace/didChangeWatchedFiles" {
+                                if let Some(options) = reg.register_options {
+                                    let options = serde_json::from_value(options)?;
+                                    this.update(&mut cx, |this, cx| {
+                                        this.on_lsp_did_change_watched_files(
+                                            server_id, options, cx,
+                                        );
                                     });
                                 }
                             }
+                        }
+                        Ok(())
+                    }
+                })
+                .detach();
 
-                            cx.notify();
-                            Some(language_server)
-                        })
-                    })),
+            language_server
+                .on_request::<lsp::request::ApplyWorkspaceEdit, _, _>({
+                    let this = this.downgrade();
+                    let adapter = adapter.clone();
+                    let language_server = language_server.clone();
+                    move |params, cx| {
+                        Self::on_lsp_workspace_edit(
+                            this,
+                            params,
+                            server_id,
+                            adapter.clone(),
+                            language_server.clone(),
+                            cx,
+                        )
+                    }
+                })
+                .detach();
+
+            let disk_based_diagnostics_progress_token =
+                adapter.disk_based_diagnostics_progress_token.clone();
+
+            language_server
+                .on_notification::<lsp::notification::Progress, _>({
+                    let this = this.downgrade();
+                    move |params, mut cx| {
+                        if let Some(this) = this.upgrade(&cx) {
+                            this.update(&mut cx, |this, cx| {
+                                this.on_lsp_progress(
+                                    params,
+                                    server_id,
+                                    disk_based_diagnostics_progress_token.clone(),
+                                    cx,
+                                );
+                            });
+                        }
+                    }
+                })
+                .detach();
+
+            language_server
+                .notify::<lsp::notification::DidChangeConfiguration>(
+                    lsp::DidChangeConfigurationParams {
+                        settings: workspace_config,
+                    },
+                )
+                .ok();
+
+            this.update(&mut cx, |this, cx| {
+                // If the language server for this key doesn't match the server id, don't store the
+                // server. Which will cause it to be dropped, killing the process
+                if this
+                    .language_server_ids
+                    .get(&key)
+                    .map(|id| id != &server_id)
+                    .unwrap_or(false)
+                {
+                    return None;
+                }
+
+                // Update language_servers collection with Running variant of LanguageServerState
+                // indicating that the server is up and running and ready
+                this.language_servers.insert(
+                    server_id,
+                    LanguageServerState::Running {
+                        adapter: adapter.clone(),
+                        language: language.clone(),
+                        watched_paths: Default::default(),
+                        server: language_server.clone(),
+                        simulate_disk_based_diagnostics_completion: None,
+                    },
+                );
+                this.language_server_statuses.insert(
+                    server_id,
+                    LanguageServerStatus {
+                        name: language_server.name().to_string(),
+                        pending_work: Default::default(),
+                        has_pending_diagnostic_updates: false,
+                        progress_tokens: Default::default(),
+                    },
                 );
 
-                server_id
-            });
+                if let Some(project_id) = this.remote_id() {
+                    this.client
+                        .send(proto::StartLanguageServer {
+                            project_id,
+                            server: Some(proto::LanguageServer {
+                                id: server_id.0 as u64,
+                                name: language_server.name().to_string(),
+                            }),
+                        })
+                        .log_err();
+                }
+
+                // Tell the language server about every open buffer in the worktree that matches the language.
+                for buffer in this.opened_buffers.values() {
+                    if let Some(buffer_handle) = buffer.upgrade(cx) {
+                        let buffer = buffer_handle.read(cx);
+                        let file = match File::from_dyn(buffer.file()) {
+                            Some(file) => file,
+                            None => continue,
+                        };
+                        let language = match buffer.language() {
+                            Some(language) => language,
+                            None => continue,
+                        };
+
+                        if file.worktree.read(cx).id() != key.0
+                            || !language.lsp_adapters().iter().any(|a| a.name == key.1)
+                        {
+                            continue;
+                        }
+
+                        let file = file.as_local()?;
+                        let versions = this
+                            .buffer_snapshots
+                            .entry(buffer.remote_id())
+                            .or_default()
+                            .entry(server_id)
+                            .or_insert_with(|| {
+                                vec![LspBufferSnapshot {
+                                    version: 0,
+                                    snapshot: buffer.text_snapshot(),
+                                }]
+                            });
+
+                        let snapshot = versions.last().unwrap();
+                        let version = snapshot.version;
+                        let initial_snapshot = &snapshot.snapshot;
+                        let uri = lsp::Url::from_file_path(file.abs_path(cx)).unwrap();
+                        language_server
+                            .notify::<lsp::notification::DidOpenTextDocument>(
+                                lsp::DidOpenTextDocumentParams {
+                                    text_document: lsp::TextDocumentItem::new(
+                                        uri,
+                                        adapter
+                                            .language_ids
+                                            .get(language.name().as_ref())
+                                            .cloned()
+                                            .unwrap_or_default(),
+                                        version,
+                                        initial_snapshot.text(),
+                                    ),
+                                },
+                            )
+                            .log_err()?;
+                        buffer_handle.update(cx, |buffer, cx| {
+                            buffer.set_completion_triggers(
+                                language_server
+                                    .capabilities()
+                                    .completion_provider
+                                    .as_ref()
+                                    .and_then(|provider| provider.trigger_characters.clone())
+                                    .unwrap_or_default(),
+                                cx,
+                            )
+                        });
+                    }
+                }
+
+                cx.notify();
+                Some(language_server)
+            })
+        }))
     }
 
     // Returns a list of all of the worktrees which no longer have a language server and the root path

crates/project/src/project_tests.rs 🔗

@@ -303,6 +303,7 @@ async fn test_managing_language_servers(
 
     rust_buffer2.update(cx, |buffer, cx| {
         buffer.update_diagnostics(
+            LanguageServerId(0),
             DiagnosticSet::from_sorted_entries(
                 vec![DiagnosticEntry {
                     diagnostic: Default::default(),
@@ -399,7 +400,7 @@ async fn test_managing_language_servers(
             .text_document,
         lsp::TextDocumentItem {
             uri: lsp::Url::from_file_path("/the-root/test.rs").unwrap(),
-            version: 1,
+            version: 0,
             text: rust_buffer.read_with(cx, |buffer, _| buffer.text()),
             language_id: Default::default()
         }
@@ -426,7 +427,7 @@ async fn test_managing_language_servers(
             },
             lsp::TextDocumentItem {
                 uri: lsp::Url::from_file_path("/the-root/test3.json").unwrap(),
-                version: 1,
+                version: 0,
                 text: rust_buffer2.read_with(cx, |buffer, _| buffer.text()),
                 language_id: Default::default()
             }
@@ -581,7 +582,7 @@ async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
     project.update(cx, |project, cx| {
         project
             .update_diagnostics(
-                0,
+                LanguageServerId(0),
                 lsp::PublishDiagnosticsParams {
                     uri: Url::from_file_path("/dir/a.rs").unwrap(),
                     version: None,
@@ -598,7 +599,7 @@ async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
             .unwrap();
         project
             .update_diagnostics(
-                0,
+                LanguageServerId(0),
                 lsp::PublishDiagnosticsParams {
                     uri: Url::from_file_path("/dir/b.rs").unwrap(),
                     version: None,
@@ -674,7 +675,7 @@ async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
     project.update(cx, |project, cx| {
         project
             .update_diagnostics(
-                0,
+                LanguageServerId(0),
                 lsp::PublishDiagnosticsParams {
                     uri: Url::from_file_path("/root/other.rs").unwrap(),
                     version: None,
@@ -766,7 +767,7 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
     assert_eq!(
         events.next().await.unwrap(),
         Event::DiskBasedDiagnosticsStarted {
-            language_server_id: 0,
+            language_server_id: LanguageServerId(0),
         }
     );
 
@@ -783,7 +784,7 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
     assert_eq!(
         events.next().await.unwrap(),
         Event::DiagnosticsUpdated {
-            language_server_id: 0,
+            language_server_id: LanguageServerId(0),
             path: (worktree_id, Path::new("a.rs")).into()
         }
     );
@@ -792,7 +793,7 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
     assert_eq!(
         events.next().await.unwrap(),
         Event::DiskBasedDiagnosticsFinished {
-            language_server_id: 0
+            language_server_id: LanguageServerId(0)
         }
     );
 
@@ -830,7 +831,7 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
     assert_eq!(
         events.next().await.unwrap(),
         Event::DiagnosticsUpdated {
-            language_server_id: 0,
+            language_server_id: LanguageServerId(0),
             path: (worktree_id, Path::new("a.rs")).into()
         }
     );
@@ -891,7 +892,7 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC
     assert_eq!(
         events.next().await.unwrap(),
         Event::DiskBasedDiagnosticsStarted {
-            language_server_id: 1
+            language_server_id: LanguageServerId(1)
         }
     );
     project.read_with(cx, |project, _| {
@@ -899,7 +900,7 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC
             project
                 .language_servers_running_disk_based_diagnostics()
                 .collect::<Vec<_>>(),
-            [1]
+            [LanguageServerId(1)]
         );
     });
 
@@ -909,7 +910,7 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC
     assert_eq!(
         events.next().await.unwrap(),
         Event::DiskBasedDiagnosticsFinished {
-            language_server_id: 1
+            language_server_id: LanguageServerId(1)
         }
     );
     project.read_with(cx, |project, _| {
@@ -917,7 +918,7 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC
             project
                 .language_servers_running_disk_based_diagnostics()
                 .collect::<Vec<_>>(),
-            [0; 0]
+            [LanguageServerId(0); 0]
         );
     });
 }
@@ -1402,6 +1403,8 @@ async fn test_empty_diagnostic_ranges(cx: &mut gpui::TestAppContext) {
         project
             .update_buffer_diagnostics(
                 &buffer,
+                LanguageServerId(0),
+                None,
                 vec![
                     DiagnosticEntry {
                         range: Unclipped(PointUtf16::new(0, 10))..Unclipped(PointUtf16::new(0, 10)),
@@ -1420,7 +1423,6 @@ async fn test_empty_diagnostic_ranges(cx: &mut gpui::TestAppContext) {
                         },
                     },
                 ],
-                None,
                 cx,
             )
             .unwrap();
@@ -1447,6 +1449,64 @@ async fn test_empty_diagnostic_ranges(cx: &mut gpui::TestAppContext) {
     });
 }
 
+#[gpui::test]
+async fn test_diagnostics_from_multiple_language_servers(cx: &mut gpui::TestAppContext) {
+    println!("hello from stdout");
+    eprintln!("hello from stderr");
+    cx.foreground().forbid_parking();
+
+    let fs = FakeFs::new(cx.background());
+    fs.insert_tree("/dir", json!({ "a.rs": "one two three" }))
+        .await;
+
+    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
+
+    project.update(cx, |project, cx| {
+        project
+            .update_diagnostic_entries(
+                LanguageServerId(0),
+                Path::new("/dir/a.rs").to_owned(),
+                None,
+                vec![DiagnosticEntry {
+                    range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 3)),
+                    diagnostic: Diagnostic {
+                        severity: DiagnosticSeverity::ERROR,
+                        is_primary: true,
+                        message: "syntax error a1".to_string(),
+                        ..Default::default()
+                    },
+                }],
+                cx,
+            )
+            .unwrap();
+        project
+            .update_diagnostic_entries(
+                LanguageServerId(1),
+                Path::new("/dir/a.rs").to_owned(),
+                None,
+                vec![DiagnosticEntry {
+                    range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 3)),
+                    diagnostic: Diagnostic {
+                        severity: DiagnosticSeverity::ERROR,
+                        is_primary: true,
+                        message: "syntax error b1".to_string(),
+                        ..Default::default()
+                    },
+                }],
+                cx,
+            )
+            .unwrap();
+
+        assert_eq!(
+            project.diagnostic_summary(cx),
+            DiagnosticSummary {
+                error_count: 2,
+                warning_count: 0,
+            }
+        );
+    });
+}
+
 #[gpui::test]
 async fn test_edits_from_lsp_with_past_version(cx: &mut gpui::TestAppContext) {
     cx.foreground().forbid_parking();
@@ -1573,6 +1633,7 @@ async fn test_edits_from_lsp_with_past_version(cx: &mut gpui::TestAppContext) {
                         new_text: "".into(),
                     },
                 ],
+                LanguageServerId(0),
                 Some(lsp_document_version),
                 cx,
             )
@@ -1667,6 +1728,7 @@ async fn test_edits_from_lsp_with_edits_on_adjacent_lines(cx: &mut gpui::TestApp
                         new_text: "".into(),
                     },
                 ],
+                LanguageServerId(0),
                 None,
                 cx,
             )
@@ -1770,6 +1832,7 @@ async fn test_invalid_edits_from_lsp(cx: &mut gpui::TestAppContext) {
                         .unindent(),
                     },
                 ],
+                LanguageServerId(0),
                 None,
                 cx,
             )
@@ -2258,7 +2321,7 @@ async fn test_save_as(cx: &mut gpui::TestAppContext) {
             ..Default::default()
         },
         tree_sitter_rust::language(),
-        None,
+        vec![],
         |_| Default::default(),
     );
 
@@ -2948,7 +3011,9 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
     };
 
     project
-        .update(cx, |p, cx| p.update_diagnostics(0, message, &[], cx))
+        .update(cx, |p, cx| {
+            p.update_diagnostics(LanguageServerId(0), message, &[], cx)
+        })
         .unwrap();
     let buffer = buffer.read_with(cx, |buffer, _| buffer.snapshot());
 

crates/project/src/worktree.rs 🔗

@@ -26,6 +26,7 @@ use language::{
     },
     Buffer, DiagnosticEntry, File as _, PointUtf16, Rope, RopeFingerprint, Unclipped,
 };
+use lsp::LanguageServerId;
 use parking_lot::Mutex;
 use postage::{
     barrier,
@@ -50,7 +51,7 @@ use std::{
     },
     time::{Duration, SystemTime},
 };
-use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet};
+use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeSet};
 use util::{paths::HOME, ResultExt, TryFutureExt};
 
 #[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)]
@@ -67,8 +68,14 @@ pub struct LocalWorktree {
     is_scanning: (watch::Sender<bool>, watch::Receiver<bool>),
     _background_scanner_task: Task<()>,
     share: Option<ShareState>,
-    diagnostics: HashMap<Arc<Path>, Vec<DiagnosticEntry<Unclipped<PointUtf16>>>>,
-    diagnostic_summaries: TreeMap<PathKey, DiagnosticSummary>,
+    diagnostics: HashMap<
+        Arc<Path>,
+        Vec<(
+            LanguageServerId,
+            Vec<DiagnosticEntry<Unclipped<PointUtf16>>>,
+        )>,
+    >,
+    diagnostic_summaries: HashMap<Arc<Path>, HashMap<LanguageServerId, DiagnosticSummary>>,
     client: Arc<Client>,
     fs: Arc<dyn Fs>,
     visible: bool,
@@ -82,7 +89,7 @@ pub struct RemoteWorktree {
     updates_tx: Option<UnboundedSender<proto::UpdateWorktree>>,
     snapshot_subscriptions: VecDeque<(usize, oneshot::Sender<()>)>,
     replica_id: ReplicaId,
-    diagnostic_summaries: TreeMap<PathKey, DiagnosticSummary>,
+    diagnostic_summaries: HashMap<Arc<Path>, HashMap<LanguageServerId, DiagnosticSummary>>,
     visible: bool,
     disconnected: bool,
 }
@@ -463,13 +470,17 @@ impl Worktree {
 
     pub fn diagnostic_summaries(
         &self,
-    ) -> impl Iterator<Item = (Arc<Path>, DiagnosticSummary)> + '_ {
+    ) -> impl Iterator<Item = (Arc<Path>, LanguageServerId, DiagnosticSummary)> + '_ {
         match self {
             Worktree::Local(worktree) => &worktree.diagnostic_summaries,
             Worktree::Remote(worktree) => &worktree.diagnostic_summaries,
         }
         .iter()
-        .map(|(path, summary)| (path.0.clone(), *summary))
+        .flat_map(|(path, summaries)| {
+            summaries
+                .iter()
+                .map(move |(&server_id, &summary)| (path.clone(), server_id, summary))
+        })
     }
 
     pub fn abs_path(&self) -> Arc<Path> {
@@ -514,31 +525,54 @@ impl LocalWorktree {
     pub fn diagnostics_for_path(
         &self,
         path: &Path,
-    ) -> Option<Vec<DiagnosticEntry<Unclipped<PointUtf16>>>> {
-        self.diagnostics.get(path).cloned()
+    ) -> Vec<(
+        LanguageServerId,
+        Vec<DiagnosticEntry<Unclipped<PointUtf16>>>,
+    )> {
+        self.diagnostics.get(path).cloned().unwrap_or_default()
     }
 
     pub fn update_diagnostics(
         &mut self,
-        language_server_id: usize,
+        server_id: LanguageServerId,
         worktree_path: Arc<Path>,
         diagnostics: Vec<DiagnosticEntry<Unclipped<PointUtf16>>>,
         _: &mut ModelContext<Worktree>,
     ) -> Result<bool> {
-        self.diagnostics.remove(&worktree_path);
-        let old_summary = self
+        let summaries_by_server_id = self
             .diagnostic_summaries
-            .remove(&PathKey(worktree_path.clone()))
+            .entry(worktree_path.clone())
+            .or_default();
+
+        let old_summary = summaries_by_server_id
+            .remove(&server_id)
             .unwrap_or_default();
-        let new_summary = DiagnosticSummary::new(language_server_id, &diagnostics);
-        if !new_summary.is_empty() {
-            self.diagnostic_summaries
-                .insert(PathKey(worktree_path.clone()), new_summary);
-            self.diagnostics.insert(worktree_path.clone(), diagnostics);
+
+        let new_summary = DiagnosticSummary::new(&diagnostics);
+        if new_summary.is_empty() {
+            if let Some(diagnostics_by_server_id) = self.diagnostics.get_mut(&worktree_path) {
+                if let Ok(ix) = diagnostics_by_server_id.binary_search_by_key(&server_id, |e| e.0) {
+                    diagnostics_by_server_id.remove(ix);
+                }
+                if diagnostics_by_server_id.is_empty() {
+                    self.diagnostics.remove(&worktree_path);
+                }
+            }
+        } else {
+            summaries_by_server_id.insert(server_id, new_summary);
+            let diagnostics_by_server_id =
+                self.diagnostics.entry(worktree_path.clone()).or_default();
+            match diagnostics_by_server_id.binary_search_by_key(&server_id, |e| e.0) {
+                Ok(ix) => {
+                    diagnostics_by_server_id[ix] = (server_id, diagnostics);
+                }
+                Err(ix) => {
+                    diagnostics_by_server_id.insert(ix, (server_id, diagnostics));
+                }
+            }
         }
 
-        let updated = !old_summary.is_empty() || !new_summary.is_empty();
-        if updated {
+        if !old_summary.is_empty() || !new_summary.is_empty() {
             if let Some(share) = self.share.as_ref() {
                 self.client
                     .send(proto::UpdateDiagnosticSummary {
@@ -546,7 +580,7 @@ impl LocalWorktree {
                         worktree_id: self.id().to_proto(),
                         summary: Some(proto::DiagnosticSummary {
                             path: worktree_path.to_string_lossy().to_string(),
-                            language_server_id: language_server_id as u64,
+                            language_server_id: server_id.0 as u64,
                             error_count: new_summary.error_count as u32,
                             warning_count: new_summary.warning_count as u32,
                         }),
@@ -555,7 +589,7 @@ impl LocalWorktree {
             }
         }
 
-        Ok(updated)
+        Ok(!old_summary.is_empty() || !new_summary.is_empty())
     }
 
     fn set_snapshot(&mut self, new_snapshot: LocalSnapshot, cx: &mut ModelContext<Worktree>) {
@@ -945,13 +979,15 @@ impl LocalWorktree {
             let (resume_updates_tx, mut resume_updates_rx) = watch::channel();
             let worktree_id = cx.model_id() as u64;
 
-            for (path, summary) in self.diagnostic_summaries.iter() {
-                if let Err(e) = self.client.send(proto::UpdateDiagnosticSummary {
-                    project_id,
-                    worktree_id,
-                    summary: Some(summary.to_proto(&path.0)),
-                }) {
-                    return Task::ready(Err(e));
+            for (path, summaries) in &self.diagnostic_summaries {
+                for (&server_id, summary) in summaries {
+                    if let Err(e) = self.client.send(proto::UpdateDiagnosticSummary {
+                        project_id,
+                        worktree_id,
+                        summary: Some(summary.to_proto(server_id, &path)),
+                    }) {
+                        return Task::ready(Err(e));
+                    }
                 }
             }
 
@@ -1109,15 +1145,24 @@ impl RemoteWorktree {
         path: Arc<Path>,
         summary: &proto::DiagnosticSummary,
     ) {
+        let server_id = LanguageServerId(summary.language_server_id as usize);
         let summary = DiagnosticSummary {
-            language_server_id: summary.language_server_id as usize,
             error_count: summary.error_count as usize,
             warning_count: summary.warning_count as usize,
         };
+
         if summary.is_empty() {
-            self.diagnostic_summaries.remove(&PathKey(path));
+            if let Some(summaries) = self.diagnostic_summaries.get_mut(&path) {
+                summaries.remove(&server_id);
+                if summaries.is_empty() {
+                    self.diagnostic_summaries.remove(&path);
+                }
+            }
         } else {
-            self.diagnostic_summaries.insert(PathKey(path), summary);
+            self.diagnostic_summaries
+                .entry(path)
+                .or_default()
+                .insert(server_id, summary);
         }
     }
 

crates/rope/Cargo.toml 🔗

@@ -9,7 +9,7 @@ path = "src/rope.rs"
 
 [dependencies]
 bromberg_sl2 = { git = "https://github.com/zed-industries/bromberg_sl2", rev = "950bc5482c216c395049ae33ae4501e08975f17f" }
-smallvec = { version = "1.6", features = ["union"] }
+smallvec = { workspace = true }
 sum_tree = { path = "../sum_tree" }
 arrayvec = "0.7.1"
 log = { version = "0.4.16", features = ["kv_unstable_serde"] }

crates/rpc/proto/zed.proto 🔗

@@ -684,9 +684,10 @@ message SearchProjectResponse {
 }
 
 message CodeAction {
-    Anchor start = 1;
-    Anchor end = 2;
-    bytes lsp_action = 3;
+    uint64 server_id = 1;
+    Anchor start = 2;
+    Anchor end = 3;
+    bytes lsp_action = 4;
 }
 
 message ProjectTransaction {
@@ -860,7 +861,8 @@ message IncomingContactRequest {
 message UpdateDiagnostics {
     uint32 replica_id = 1;
     uint32 lamport_timestamp = 2;
-    repeated Diagnostic diagnostics = 3;
+    uint64 server_id = 3;
+    repeated Diagnostic diagnostics = 4;
 }
 
 message Follow {

crates/rpc/src/rpc.rs 🔗

@@ -6,4 +6,4 @@ pub use conn::Connection;
 pub use peer::*;
 mod macros;
 
-pub const PROTOCOL_VERSION: u32 = 51;
+pub const PROTOCOL_VERSION: u32 = 52;

crates/search/Cargo.toml 🔗

@@ -25,7 +25,7 @@ log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 postage = { workspace = true }
 serde = { workspace = true }
 serde_derive = { workspace = true }
-smallvec = { version = "1.6", features = ["union"] }
+smallvec = { workspace = true }
 smol = "1.2"
 
 [dev-dependencies]

crates/snippet/Cargo.toml 🔗

@@ -10,4 +10,4 @@ doctest = false
 
 [dependencies]
 anyhow = "1.0"
-smallvec = { version = "1.6", features = ["union"] }
+smallvec = { workspace = true }

crates/terminal/Cargo.toml 🔗

@@ -17,7 +17,7 @@ theme = { path = "../theme" }
 util = { path = "../util" }
 alacritty_terminal = { git = "https://github.com/zed-industries/alacritty", rev = "a51dbe25d67e84d6ed4261e640d3954fbdd9be45" }
 procinfo = { git = "https://github.com/zed-industries/wezterm", rev = "5cd757e5f2eb039ed0c6bb6512223e69d5efc64d", default-features = false }
-smallvec = { version = "1.6", features = ["union"] }
+smallvec = { workspace = true }
 smol = "1.2.5"
 mio-extras = "2.0.6"
 futures = "0.3"

crates/terminal_view/Cargo.toml 🔗

@@ -21,7 +21,7 @@ workspace = { path = "../workspace" }
 db = { path = "../db" }
 procinfo = { git = "https://github.com/zed-industries/wezterm", rev = "5cd757e5f2eb039ed0c6bb6512223e69d5efc64d", default-features = false }
 terminal = { path = "../terminal" }
-smallvec = { version = "1.6", features = ["union"] }
+smallvec = { workspace = true }
 smol = "1.2.5"
 mio-extras = "2.0.6"
 futures = "0.3"

crates/text/Cargo.toml 🔗

@@ -24,7 +24,7 @@ log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 parking_lot = "0.11"
 postage = { workspace = true }
 rand = { version = "0.8.3", optional = true }
-smallvec = { version = "1.6", features = ["union"] }
+smallvec = { workspace = true }
 util = { path = "../util" }
 regex = "1.5"
 

crates/theme_testbench/Cargo.toml 🔗

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

crates/workspace/Cargo.toml 🔗

@@ -47,7 +47,7 @@ postage = { workspace = true }
 serde = { workspace = true }
 serde_derive = { workspace = true }
 serde_json = { workspace = true }
-smallvec = { version = "1.6", features = ["union"] }
+smallvec = { workspace = true }
 indoc = "1.0.4"
 uuid = { version = "1.1.2", features = ["v4"] }
 

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.83.0"
+version = "0.84.0"
 publish = false
 
 [lib]
@@ -96,7 +96,7 @@ serde_derive = { workspace = true }
 serde_json = { workspace = true }
 serde_path_to_error = "0.1.4"
 simplelog = "0.9"
-smallvec = { version = "1.6", features = ["union"] }
+smallvec = { workspace = true }
 smol = "1.2.5"
 tempdir = { version = "0.3.7" }
 thiserror = "1.0.29"

crates/zed/src/languages.rs 🔗

@@ -37,121 +37,107 @@ pub fn init(
     themes: Arc<ThemeRegistry>,
     node_runtime: Arc<NodeRuntime>,
 ) {
-    for (name, grammar, lsp_adapter) in [
+    fn adapter_arc(adapter: impl LspAdapter) -> Arc<dyn LspAdapter> {
+        Arc::new(adapter)
+    }
+
+    let languages_list = [
         (
             "c",
             tree_sitter_c::language(),
-            Some(Arc::new(c::CLspAdapter) as Arc<dyn LspAdapter>),
+            vec![adapter_arc(c::CLspAdapter)],
         ),
         (
             "cpp",
             tree_sitter_cpp::language(),
-            Some(Arc::new(c::CLspAdapter)),
-        ),
-        (
-            "css",
-            tree_sitter_css::language(),
-            None, //
+            vec![adapter_arc(c::CLspAdapter)],
         ),
+        ("css", tree_sitter_css::language(), vec![]),
         (
             "elixir",
             tree_sitter_elixir::language(),
-            Some(Arc::new(elixir::ElixirLspAdapter)),
+            vec![adapter_arc(elixir::ElixirLspAdapter)],
         ),
         (
             "go",
             tree_sitter_go::language(),
-            Some(Arc::new(go::GoLspAdapter)),
+            vec![adapter_arc(go::GoLspAdapter)],
         ),
         (
             "json",
             tree_sitter_json::language(),
-            Some(Arc::new(json::JsonLspAdapter::new(
+            vec![adapter_arc(json::JsonLspAdapter::new(
                 node_runtime.clone(),
                 languages.clone(),
                 themes.clone(),
-            ))),
-        ),
-        (
-            "markdown",
-            tree_sitter_markdown::language(),
-            None, //
+            ))],
         ),
+        ("markdown", tree_sitter_markdown::language(), vec![]),
         (
             "python",
             tree_sitter_python::language(),
-            Some(Arc::new(python::PythonLspAdapter::new(
+            vec![adapter_arc(python::PythonLspAdapter::new(
                 node_runtime.clone(),
-            ))),
+            ))],
         ),
         (
             "rust",
             tree_sitter_rust::language(),
-            Some(Arc::new(rust::RustLspAdapter)),
-        ),
-        (
-            "toml",
-            tree_sitter_toml::language(),
-            None, //
+            vec![adapter_arc(rust::RustLspAdapter)],
         ),
+        ("toml", tree_sitter_toml::language(), vec![]),
         (
             "tsx",
             tree_sitter_typescript::language_tsx(),
-            Some(Arc::new(typescript::TypeScriptLspAdapter::new(
+            vec![adapter_arc(typescript::TypeScriptLspAdapter::new(
                 node_runtime.clone(),
-            ))),
+            ))],
         ),
         (
             "typescript",
             tree_sitter_typescript::language_typescript(),
-            Some(Arc::new(typescript::TypeScriptLspAdapter::new(
+            vec![adapter_arc(typescript::TypeScriptLspAdapter::new(
                 node_runtime.clone(),
-            ))),
+            ))],
         ),
         (
             "javascript",
             tree_sitter_typescript::language_tsx(),
-            Some(Arc::new(typescript::TypeScriptLspAdapter::new(
+            vec![adapter_arc(typescript::TypeScriptLspAdapter::new(
                 node_runtime.clone(),
-            ))),
+            ))],
         ),
         (
             "html",
             tree_sitter_html::language(),
-            Some(Arc::new(html::HtmlLspAdapter::new(node_runtime.clone()))),
+            vec![adapter_arc(html::HtmlLspAdapter::new(node_runtime.clone()))],
         ),
         (
             "ruby",
             tree_sitter_ruby::language(),
-            Some(Arc::new(ruby::RubyLanguageServer)),
+            vec![adapter_arc(ruby::RubyLanguageServer)],
         ),
         (
             "erb",
             tree_sitter_embedded_template::language(),
-            Some(Arc::new(ruby::RubyLanguageServer)),
-        ),
-        (
-            "scheme",
-            tree_sitter_scheme::language(),
-            None, //
-        ),
-        (
-            "racket",
-            tree_sitter_racket::language(),
-            None, //
+            vec![adapter_arc(ruby::RubyLanguageServer)],
         ),
+        ("scheme", tree_sitter_scheme::language(), vec![]),
+        ("racket", tree_sitter_racket::language(), vec![]),
         (
             "lua",
             tree_sitter_lua::language(),
-            Some(Arc::new(lua::LuaLspAdapter)),
+            vec![adapter_arc(lua::LuaLspAdapter)],
         ),
         (
             "yaml",
             tree_sitter_yaml::language(),
-            Some(Arc::new(yaml::YamlLspAdapter::new(node_runtime.clone()))),
+            vec![adapter_arc(yaml::YamlLspAdapter::new(node_runtime.clone()))],
         ),
-    ] {
-        languages.register(name, load_config(name), grammar, lsp_adapter, load_queries);
+    ];
+
+    for (name, grammar, lsp_adapters) in languages_list {
+        languages.register(name, load_config(name), grammar, lsp_adapters, load_queries);
     }
 }
 
@@ -163,7 +149,7 @@ pub async fn language(
 ) -> Arc<Language> {
     Arc::new(
         Language::new(load_config(name), Some(grammar))
-            .with_lsp_adapter(lsp_adapter)
+            .with_lsp_adapters(lsp_adapter.into_iter().collect())
             .await
             .with_queries(load_queries(name))
             .unwrap(),

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

@@ -1,21 +1,23 @@
 use anyhow::{anyhow, Result};
 use async_trait::async_trait;
-use futures::StreamExt;
+use futures::{future::BoxFuture, FutureExt};
+use gpui::AppContext;
 use language::{LanguageServerBinary, LanguageServerName, LspAdapter};
 use lsp::CodeActionKind;
 use node_runtime::NodeRuntime;
-use serde_json::json;
+use serde_json::{json, Value};
 use smol::fs;
 use std::{
     any::Any,
     ffi::OsString,
+    future,
     path::{Path, PathBuf},
     sync::Arc,
 };
 use util::http::HttpClient;
 use util::ResultExt;
 
-fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
+fn typescript_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
     vec![
         server_path.into(),
         "--stdio".into(),
@@ -24,6 +26,10 @@ fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
     ]
 }
 
+fn eslint_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
+    vec![server_path.into(), "--stdio".into()]
+}
+
 pub struct TypeScriptLspAdapter {
     node: Arc<NodeRuntime>,
 }
@@ -37,7 +43,7 @@ impl TypeScriptLspAdapter {
     }
 }
 
-struct Versions {
+struct TypeScriptVersions {
     typescript_version: String,
     server_version: String,
 }
@@ -52,7 +58,7 @@ impl LspAdapter for TypeScriptLspAdapter {
         &self,
         _: Arc<dyn HttpClient>,
     ) -> Result<Box<dyn 'static + Send + Any>> {
-        Ok(Box::new(Versions {
+        Ok(Box::new(TypeScriptVersions {
             typescript_version: self.node.npm_package_latest_version("typescript").await?,
             server_version: self
                 .node
@@ -67,7 +73,7 @@ impl LspAdapter for TypeScriptLspAdapter {
         _: Arc<dyn HttpClient>,
         container_dir: PathBuf,
     ) -> Result<LanguageServerBinary> {
-        let versions = versions.downcast::<Versions>().unwrap();
+        let versions = versions.downcast::<TypeScriptVersions>().unwrap();
         let server_path = container_dir.join(Self::NEW_SERVER_PATH);
 
         if fs::metadata(&server_path).await.is_err() {
@@ -87,37 +93,28 @@ impl LspAdapter for TypeScriptLspAdapter {
 
         Ok(LanguageServerBinary {
             path: self.node.binary_path().await?,
-            arguments: server_binary_arguments(&server_path),
+            arguments: typescript_server_binary_arguments(&server_path),
         })
     }
 
     async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
         (|| async move {
-            let mut last_version_dir = None;
-            let mut entries = fs::read_dir(&container_dir).await?;
-            while let Some(entry) = entries.next().await {
-                let entry = entry?;
-                if entry.file_type().await?.is_dir() {
-                    last_version_dir = Some(entry.path());
-                }
-            }
-            let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
-            let old_server_path = last_version_dir.join(Self::OLD_SERVER_PATH);
-            let new_server_path = last_version_dir.join(Self::NEW_SERVER_PATH);
+            let old_server_path = container_dir.join(Self::OLD_SERVER_PATH);
+            let new_server_path = container_dir.join(Self::NEW_SERVER_PATH);
             if new_server_path.exists() {
                 Ok(LanguageServerBinary {
                     path: self.node.binary_path().await?,
-                    arguments: server_binary_arguments(&new_server_path),
+                    arguments: typescript_server_binary_arguments(&new_server_path),
                 })
             } else if old_server_path.exists() {
                 Ok(LanguageServerBinary {
                     path: self.node.binary_path().await?,
-                    arguments: server_binary_arguments(&old_server_path),
+                    arguments: typescript_server_binary_arguments(&old_server_path),
                 })
             } else {
                 Err(anyhow!(
                     "missing executable in directory {:?}",
-                    last_version_dir
+                    container_dir
                 ))
             }
         })()
@@ -170,6 +167,136 @@ impl LspAdapter for TypeScriptLspAdapter {
     }
 }
 
+pub struct EsLintLspAdapter {
+    node: Arc<NodeRuntime>,
+}
+
+impl EsLintLspAdapter {
+    const SERVER_PATH: &'static str =
+        "node_modules/vscode-langservers-extracted/lib/eslint-language-server/eslintServer.js";
+
+    #[allow(unused)]
+    pub fn new(node: Arc<NodeRuntime>) -> Self {
+        EsLintLspAdapter { node }
+    }
+}
+
+#[async_trait]
+impl LspAdapter for EsLintLspAdapter {
+    fn workspace_configuration(&self, _: &mut AppContext) -> Option<BoxFuture<'static, Value>> {
+        Some(
+            future::ready(json!({
+                "": {
+                      "validate": "on",
+                      "packageManager": "npm",
+                      "useESLintClass": false,
+                      "experimental": {
+                        "useFlatConfig": false
+                      },
+                      "codeActionOnSave": {
+                        "mode": "all"
+                      },
+                      "format": false,
+                      "quiet": false,
+                      "onIgnoredFiles": "off",
+                      "options": {},
+                      "rulesCustomizations": [],
+                      "run": "onType",
+                      "problems": {
+                        "shortenToSingleLine": false
+                      },
+                      "nodePath": null,
+                      "workspaceFolder": {
+                        "name": "testing_ts",
+                        "uri": "file:///Users/julia/Stuff/testing_ts"
+                      },
+                      "codeAction": {
+                        "disableRuleComment": {
+                          "enable": true,
+                          "location": "separateLine",
+                          "commentStyle": "line"
+                        },
+                        "showDocumentation": {
+                          "enable": true
+                        }
+                      }
+                }
+            }))
+            .boxed(),
+        )
+    }
+
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("eslint".into())
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        _: Arc<dyn HttpClient>,
+    ) -> Result<Box<dyn 'static + Send + Any>> {
+        Ok(Box::new(
+            self.node
+                .npm_package_latest_version("vscode-langservers-extracted")
+                .await?,
+        ))
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        versions: Box<dyn 'static + Send + Any>,
+        _: Arc<dyn HttpClient>,
+        container_dir: PathBuf,
+    ) -> Result<LanguageServerBinary> {
+        let version = versions.downcast::<String>().unwrap();
+        let server_path = container_dir.join(Self::SERVER_PATH);
+
+        if fs::metadata(&server_path).await.is_err() {
+            self.node
+                .npm_install_packages(
+                    [("vscode-langservers-extracted", version.as_str())],
+                    &container_dir,
+                )
+                .await?;
+        }
+
+        Ok(LanguageServerBinary {
+            path: self.node.binary_path().await?,
+            arguments: eslint_server_binary_arguments(&server_path),
+        })
+    }
+
+    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
+        (|| async move {
+            let server_path = container_dir.join(Self::SERVER_PATH);
+            if server_path.exists() {
+                Ok(LanguageServerBinary {
+                    path: self.node.binary_path().await?,
+                    arguments: eslint_server_binary_arguments(&server_path),
+                })
+            } else {
+                Err(anyhow!(
+                    "missing executable in directory {:?}",
+                    container_dir
+                ))
+            }
+        })()
+        .await
+        .log_err()
+    }
+
+    async fn label_for_completion(
+        &self,
+        _item: &lsp::CompletionItem,
+        _language: &Arc<language::Language>,
+    ) -> Option<language::CodeLabel> {
+        None
+    }
+
+    async fn initialization_options(&self) -> Option<serde_json::Value> {
+        None
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use gpui::TestAppContext;

crates/zed/src/main.rs 🔗

@@ -21,7 +21,7 @@ use log::LevelFilter;
 use node_runtime::NodeRuntime;
 use parking_lot::Mutex;
 use project::Fs;
-use serde_json::json;
+use serde::{Deserialize, Serialize};
 use settings::{
     self, settings_file::SettingsFile, KeymapFileContent, Settings, SettingsFileContent,
     WorkingDirectory,
@@ -317,6 +317,30 @@ fn init_logger() {
     }
 }
 
+#[derive(Serialize, Deserialize)]
+struct LocationData {
+    file: String,
+    line: u32,
+}
+
+#[derive(Serialize, Deserialize)]
+struct Panic {
+    thread: String,
+    payload: String,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    location_data: Option<LocationData>,
+    backtrace: Vec<String>,
+    // TODO
+    // stripped_backtrace: String,
+}
+
+#[derive(Serialize)]
+struct PanicRequest {
+    panic: Panic,
+    version: String,
+    token: String,
+}
+
 fn init_panic_hook(app_version: String) {
     let is_pty = stdout_is_a_pty();
     panic::set_hook(Box::new(move |info| {
@@ -333,39 +357,38 @@ fn init_panic_hook(app_version: String) {
             },
         };
 
-        let message = match info.location() {
-            Some(location) => {
-                format!(
-                    "thread '{}' panicked at '{}'\n{}:{}\n{:?}",
-                    thread,
-                    payload,
-                    location.file(),
-                    location.line(),
-                    backtrace
-                )
-            }
-            None => format!(
-                "thread '{}' panicked at '{}'\n{:?}",
-                thread, payload, backtrace
-            ),
+        let panic_data = Panic {
+            thread: thread.into(),
+            payload: payload.into(),
+            location_data: info.location().map(|location| LocationData {
+                file: location.file().into(),
+                line: location.line(),
+            }),
+            backtrace: format!("{:?}", backtrace)
+                .split("\n")
+                .map(|line| line.to_string())
+                .collect(),
+            // modified_backtrace: None,
         };
 
-        if is_pty {
-            eprintln!("{}", message);
-            return;
-        }
+        if let Some(panic_data_json) = serde_json::to_string_pretty(&panic_data).log_err() {
+            if is_pty {
+                eprintln!("{}", panic_data_json);
+                return;
+            }
 
-        let timestamp = chrono::Utc::now().format("%Y_%m_%d %H_%M_%S").to_string();
-        let panic_file_path =
-            paths::LOGS_DIR.join(format!("zed-{}-{}.panic", app_version, timestamp));
-        let panic_file = std::fs::OpenOptions::new()
-            .append(true)
-            .create(true)
-            .open(&panic_file_path)
-            .log_err();
-        if let Some(mut panic_file) = panic_file {
-            write!(&mut panic_file, "{}", message).log_err();
-            panic_file.flush().log_err();
+            let timestamp = chrono::Utc::now().format("%Y_%m_%d %H_%M_%S").to_string();
+            let panic_file_path =
+                paths::LOGS_DIR.join(format!("zed-{}-{}.panic", app_version, timestamp));
+            let panic_file = std::fs::OpenOptions::new()
+                .append(true)
+                .create(true)
+                .open(&panic_file_path)
+                .log_err();
+            if let Some(mut panic_file) = panic_file {
+                write!(&mut panic_file, "{}", panic_data_json).log_err();
+                panic_file.flush().log_err();
+            }
         }
     }));
 }
@@ -402,15 +425,17 @@ fn upload_previous_panics(http: Arc<dyn HttpClient>, cx: &mut AppContext) {
                     };
 
                     if diagnostics_telemetry {
-                        let text = smol::fs::read_to_string(&child_path)
+                        let panic_data_text = smol::fs::read_to_string(&child_path)
                             .await
                             .context("error reading panic file")?;
-                        let body = serde_json::to_string(&json!({
-                            "text": text,
-                            "version": version,
-                            "token": ZED_SECRET_CLIENT_TOKEN,
-                        }))
+
+                        let body = serde_json::to_string(&PanicRequest {
+                            panic: serde_json::from_str(&panic_data_text)?,
+                            version: version.to_string(),
+                            token: ZED_SECRET_CLIENT_TOKEN.into(),
+                        })
                         .unwrap();
+
                         let request = Request::post(&panic_report_url)
                             .redirect_policy(isahc::config::RedirectPolicy::Follow)
                             .header("Content-Type", "application/json")