Add initial support for defining language server adapters in WebAssembly-based extensions (#8645)

Max Brunsfeld , Marshall , Nathan , and Marshall Bowers created

This PR adds **internal** ability to run arbitrary language servers via
WebAssembly extensions. The functionality isn't exposed yet - we're just
landing this in this early state because there have been a lot of
changes to the `LspAdapter` trait, and other language server logic.

## Next steps

* Currently, wasm extensions can only define how to *install* and run a
language server, they can't yet implement the other LSP adapter methods,
such as formatting completion labels and workspace symbols.
* We don't have an automatic way to install or develop these types of
extensions
* We don't have a way to package these types of extensions in our
extensions repo, to make them available via our extensions API.
* The Rust extension API crate, `zed-extension-api` has not yet been
published to crates.io, because we still consider the API a work in
progress.

Release Notes:

- N/A

---------

Co-authored-by: Marshall <marshall@zed.dev>
Co-authored-by: Nathan <nathan@zed.dev>
Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>

Change summary

.github/workflows/ci.yml                                      |   6 
.gitignore                                                    |   1 
Cargo.lock                                                    | 681 ++++
Cargo.toml                                                    |   4 
crates/activity_indicator/src/activity_indicator.rs           |  48 
crates/collab/src/tests.rs                                    |  17 
crates/collab/src/tests/editor_tests.rs                       | 200 -
crates/collab/src/tests/integration_tests.rs                  | 180 
crates/collab/src/tests/random_project_collaboration_tests.rs |  14 
crates/copilot/src/copilot.rs                                 |  12 
crates/editor/src/editor_tests.rs                             | 211 
crates/editor/src/inlay_hint_cache.rs                         | 207 
crates/editor/src/test/editor_lsp_test_context.rs             |  19 
crates/extension/Cargo.toml                                   |   9 
crates/extension/src/extension_lsp_adapter.rs                 |  90 
crates/extension/src/extension_store.rs                       | 623 ++-
crates/extension/src/extension_store_test.rs                  | 365 ++
crates/extension/src/wasm_host.rs                             | 405 ++
crates/extension_api/Cargo.toml                               |  14 
crates/extension_api/LICENSE-APACHE                           |   1 
crates/extension_api/build.rs                                 |  15 
crates/extension_api/src/extension_api.rs                     |  62 
crates/extension_api/wit/extension.wit                        |  80 
crates/extensions_ui/src/extensions_ui.rs                     |   8 
crates/fs/Cargo.toml                                          |   1 
crates/fs/src/fs.rs                                           | 131 
crates/language/src/language.rs                               | 292 +-
crates/language/src/language_registry.rs                      | 392 +-
crates/language/src/proto.rs                                  |   9 
crates/language_tools/src/lsp_log_tests.rs                    |  40 
crates/languages/src/astro.rs                                 |   4 
crates/languages/src/c.rs                                     |   6 
crates/languages/src/clojure.rs                               |   4 
crates/languages/src/csharp.rs                                |   4 
crates/languages/src/css.rs                                   |   4 
crates/languages/src/dart.rs                                  |   4 
crates/languages/src/deno.rs                                  |   4 
crates/languages/src/dockerfile.rs                            |   4 
crates/languages/src/elixir.rs                                |  12 
crates/languages/src/elm.rs                                   |   4 
crates/languages/src/erlang.rs                                |   4 
crates/languages/src/gleam.rs                                 |   4 
crates/languages/src/go.rs                                    |  88 
crates/languages/src/haskell.rs                               |   4 
crates/languages/src/html.rs                                  |   4 
crates/languages/src/json.rs                                  |   4 
crates/languages/src/lib.rs                                   |  16 
crates/languages/src/lua.rs                                   |   4 
crates/languages/src/nu.rs                                    |   4 
crates/languages/src/ocaml.rs                                 |   4 
crates/languages/src/php.rs                                   |   4 
crates/languages/src/prisma.rs                                |   4 
crates/languages/src/purescript.rs                            |   4 
crates/languages/src/python.rs                                |   6 
crates/languages/src/ruby.rs                                  |   4 
crates/languages/src/rust.rs                                  |  98 
crates/languages/src/svelte.rs                                |   4 
crates/languages/src/tailwind.rs                              |   4 
crates/languages/src/terraform.rs                             |   6 
crates/languages/src/toml.rs                                  |   4 
crates/languages/src/typescript.rs                            |  15 
crates/languages/src/uiua.rs                                  |   4 
crates/languages/src/vue.rs                                   |   4 
crates/languages/src/yaml.rs                                  |   4 
crates/languages/src/zig.rs                                   |  31 
crates/lsp/src/lsp.rs                                         |  25 
crates/prettier/src/prettier.rs                               |  14 
crates/project/src/lsp_command.rs                             |  26 
crates/project/src/prettier_support.rs                        |  32 
crates/project/src/project.rs                                 | 170 
crates/project/src/project_tests.rs                           | 548 +--
crates/project/src/task_inventory.rs                          |   2 
crates/project_symbols/src/project_symbols.rs                 |  21 
crates/vim/src/command.rs                                     |   4 
crates/zed/src/main.rs                                        |   1 
crates/zed/src/zed.rs                                         |   2 
extensions/gleam/Cargo.toml                                   |  13 
extensions/gleam/extension.toml                               |  13 
extensions/gleam/languages/gleam/config.toml                  |  11 
extensions/gleam/languages/gleam/highlights.scm               | 130 
extensions/gleam/languages/gleam/indents.scm                  |   3 
extensions/gleam/languages/gleam/outline.scm                  |  31 
extensions/gleam/src/bindings.rs                              |  11 
extensions/gleam/src/gleam.rs                                 |  91 
84 files changed, 3,704 insertions(+), 1,963 deletions(-)

Detailed changes

.github/workflows/ci.yml 🔗

@@ -86,6 +86,12 @@ jobs:
           clean: false
           submodules: "recursive"
 
+      - name: Install cargo-component
+        run: |
+          if ! which cargo-component > /dev/null; then
+            cargo install cargo-component
+          fi
+
       - name: cargo clippy
         shell: bash -euxo pipefail {0}
         run: script/clippy

.gitignore 🔗

@@ -10,6 +10,7 @@
 /assets/*licenses.md
 **/venv
 .build
+*.wasm
 Packages
 *.xcodeproj
 xcuserdata/

Cargo.lock 🔗

@@ -157,6 +157,12 @@ dependencies = [
  "pkg-config",
 ]
 
+[[package]]
+name = "ambient-authority"
+version = "0.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e9d4ee0d472d1cd2e28c97dfa124b3d8d992e10eb0a035f33f5d12e3a177ba3b"
+
 [[package]]
 name = "android-tzdata"
 version = "0.1.1"
@@ -1657,6 +1663,83 @@ dependencies = [
  "wayland-client",
 ]
 
+[[package]]
+name = "cap-fs-ext"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88e341d15ac1029aadce600be764a1a1edafe40e03cde23285bc1d261b3a4866"
+dependencies = [
+ "cap-primitives",
+ "cap-std",
+ "io-lifetimes 2.0.3",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "cap-net-ext"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "434168fe6533055f0f4204039abe3ff6d7db338ef46872a5fa39e9d5ad5ab7a9"
+dependencies = [
+ "cap-primitives",
+ "cap-std",
+ "rustix 0.38.30",
+ "smallvec",
+]
+
+[[package]]
+name = "cap-primitives"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe16767ed8eee6d3f1f00d6a7576b81c226ab917eb54b96e5f77a5216ef67abb"
+dependencies = [
+ "ambient-authority",
+ "fs-set-times",
+ "io-extras",
+ "io-lifetimes 2.0.3",
+ "ipnet",
+ "maybe-owned",
+ "rustix 0.38.30",
+ "windows-sys 0.52.0",
+ "winx",
+]
+
+[[package]]
+name = "cap-rand"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20e5695565f0cd7106bc3c7170323597540e772bb73e0be2cd2c662a0f8fa4ca"
+dependencies = [
+ "ambient-authority",
+ "rand 0.8.5",
+]
+
+[[package]]
+name = "cap-std"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "593db20e4c51f62d3284bae7ee718849c3214f93a3b94ea1899ad85ba119d330"
+dependencies = [
+ "cap-primitives",
+ "io-extras",
+ "io-lifetimes 2.0.3",
+ "rustix 0.38.30",
+]
+
+[[package]]
+name = "cap-time-ext"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03261630f291f425430a36f38c847828265bc928f517cdd2004c56f4b02f002b"
+dependencies = [
+ "ambient-authority",
+ "cap-primitives",
+ "iana-time-zone",
+ "once_cell",
+ "rustix 0.38.30",
+ "winx",
+]
+
 [[package]]
 name = "castaway"
 version = "0.1.2"
@@ -2437,6 +2520,15 @@ dependencies = [
  "windows 0.46.0",
 ]
 
+[[package]]
+name = "cpp_demangle"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eeaa953eaad386a53111e47172c2fedba671e5684c8dd601a5f474f4f118710f"
+dependencies = [
+ "cfg-if 1.0.0",
+]
+
 [[package]]
 name = "cpufeatures"
 version = "0.2.9"
@@ -2784,6 +2876,15 @@ dependencies = [
  "util",
 ]
 
+[[package]]
+name = "debugid"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d"
+dependencies = [
+ "uuid",
+]
+
 [[package]]
 name = "deflate"
 version = "0.8.6"
@@ -2923,6 +3024,16 @@ dependencies = [
  "subtle",
 ]
 
+[[package]]
+name = "directories-next"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc"
+dependencies = [
+ "cfg-if 1.0.0",
+ "dirs-sys-next",
+]
+
 [[package]]
 name = "dirs"
 version = "3.0.2"
@@ -3252,13 +3363,16 @@ dependencies = [
  "anyhow",
  "async-compression",
  "async-tar",
+ "async-trait",
  "collections",
  "fs",
  "futures 0.3.28",
  "gpui",
  "language",
  "log",
- "parking_lot 0.11.2",
+ "lsp",
+ "node_runtime",
+ "project",
  "schemars",
  "serde",
  "serde_json",
@@ -3266,6 +3380,9 @@ dependencies = [
  "theme",
  "toml 0.8.10",
  "util",
+ "wasmparser",
+ "wasmtime",
+ "wasmtime-wasi",
 ]
 
 [[package]]
@@ -3331,6 +3448,17 @@ version = "2.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764"
 
+[[package]]
+name = "fd-lock"
+version = "4.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e5768da2206272c81ef0b5e951a41862938a6070da63bcea197899942d3b947"
+dependencies = [
+ "cfg-if 1.0.0",
+ "rustix 0.38.30",
+ "windows-sys 0.52.0",
+]
+
 [[package]]
 name = "feature_flags"
 version = "0.1.0"
@@ -3607,6 +3735,7 @@ name = "fs"
 version = "0.1.0"
 dependencies = [
  "anyhow",
+ "async-tar",
  "async-trait",
  "collections",
  "fsevent",
@@ -3631,6 +3760,17 @@ dependencies = [
  "windows-sys 0.52.0",
 ]
 
+[[package]]
+name = "fs-set-times"
+version = "0.20.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "033b337d725b97690d86893f9de22b67b80dcc4e9ad815f348254c38119db8fb"
+dependencies = [
+ "io-lifetimes 2.0.3",
+ "rustix 0.38.30",
+ "windows-sys 0.52.0",
+]
+
 [[package]]
 name = "fsevent"
 version = "2.0.2"
@@ -3846,6 +3986,28 @@ dependencies = [
  "thread_local",
 ]
 
+[[package]]
+name = "fxhash"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
+dependencies = [
+ "byteorder",
+]
+
+[[package]]
+name = "fxprof-processed-profile"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27d12c0aed7f1e24276a241aadc4cb8ea9f83000f34bc062b7cc2d51e3b0fabd"
+dependencies = [
+ "bitflags 2.4.1",
+ "debugid",
+ "fxhash",
+ "serde",
+ "serde_json",
+]
+
 [[package]]
 name = "generic-array"
 version = "0.14.7"
@@ -4435,6 +4597,12 @@ dependencies = [
  "cc",
 ]
 
+[[package]]
+name = "id-arena"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005"
+
 [[package]]
 name = "idna"
 version = "0.4.0"
@@ -4558,6 +4726,16 @@ dependencies = [
  "cfg-if 1.0.0",
 ]
 
+[[package]]
+name = "io-extras"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c301e73fb90e8a29e600a9f402d095765f74310d582916a952f618836a1bd1ed"
+dependencies = [
+ "io-lifetimes 2.0.3",
+ "windows-sys 0.52.0",
+]
+
 [[package]]
 name = "io-lifetimes"
 version = "1.0.11"
@@ -4569,6 +4747,12 @@ dependencies = [
  "windows-sys 0.48.0",
 ]
 
+[[package]]
+name = "io-lifetimes"
+version = "2.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a611371471e98973dbcab4e0ec66c31a10bc356eeb4d54a0e05eac8158fe38c"
+
 [[package]]
 name = "iovec"
 version = "0.1.4"
@@ -4673,6 +4857,26 @@ version = "1.0.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
 
+[[package]]
+name = "ittapi"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b996fe614c41395cdaedf3cf408a9534851090959d90d54a535f675550b64b1"
+dependencies = [
+ "anyhow",
+ "ittapi-sys",
+ "log",
+]
+
+[[package]]
+name = "ittapi-sys"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52f5385394064fa2c886205dba02598013ce83d3e92d33dbdc0c52fe0e7bf4fc"
+dependencies = [
+ "cc",
+]
+
 [[package]]
 name = "jni"
 version = "0.19.0"
@@ -5327,6 +5531,12 @@ dependencies = [
  "rawpointer",
 ]
 
+[[package]]
+name = "maybe-owned"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4"
+
 [[package]]
 name = "md-5"
 version = "0.10.5"
@@ -7683,7 +7893,7 @@ checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06"
 dependencies = [
  "bitflags 1.3.2",
  "errno",
- "io-lifetimes",
+ "io-lifetimes 1.0.11",
  "libc",
  "linux-raw-sys 0.3.8",
  "windows-sys 0.48.0",
@@ -7700,6 +7910,7 @@ dependencies = [
  "itoa",
  "libc",
  "linux-raw-sys 0.4.12",
+ "once_cell",
  "windows-sys 0.52.0",
 ]
 
@@ -8531,6 +8742,15 @@ dependencies = [
  "windows-sys 0.48.0",
 ]
 
+[[package]]
+name = "spdx"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29ef1a0fa1e39ac22972c8db23ff89aea700ab96aa87114e1fb55937a631a0c9"
+dependencies = [
+ "smallvec",
+]
+
 [[package]]
 name = "spin"
 version = "0.5.2"
@@ -9098,6 +9318,22 @@ dependencies = [
  "winapi 0.3.9",
 ]
 
+[[package]]
+name = "system-interface"
+version = "0.26.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0682e006dd35771e392a6623ac180999a9a854b1d4a6c12fb2e804941c2b1f58"
+dependencies = [
+ "bitflags 2.4.1",
+ "cap-fs-ext",
+ "cap-std",
+ "fd-lock",
+ "io-lifetimes 2.0.3",
+ "rustix 0.38.30",
+ "windows-sys 0.52.0",
+ "winx",
+]
+
 [[package]]
 name = "taffy"
 version = "0.3.11"
@@ -10715,6 +10951,32 @@ version = "0.11.0+wasi-snapshot-preview1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
 
+[[package]]
+name = "wasi-common"
+version = "18.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "082a661fe31df4dbb34409f4835ad3d8ba65036bf74aaec9b21fde779978aba7"
+dependencies = [
+ "anyhow",
+ "bitflags 2.4.1",
+ "cap-fs-ext",
+ "cap-rand",
+ "cap-std",
+ "cap-time-ext",
+ "fs-set-times",
+ "io-extras",
+ "io-lifetimes 2.0.3",
+ "log",
+ "once_cell",
+ "rustix 0.38.30",
+ "system-interface",
+ "thiserror",
+ "tracing",
+ "wasmtime",
+ "wiggle",
+ "windows-sys 0.52.0",
+]
+
 [[package]]
 name = "wasm-bindgen"
 version = "0.2.87"
@@ -10790,6 +11052,31 @@ dependencies = [
  "leb128",
 ]
 
+[[package]]
+name = "wasm-encoder"
+version = "0.200.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e3fb0c8fbddd78aa6095b850dfeedbc7506cf5f81e633f69cf8f2333ab84b9"
+dependencies = [
+ "leb128",
+]
+
+[[package]]
+name = "wasm-metadata"
+version = "0.10.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18ebaa7bd0f9e7a5e5dd29b9a998acf21c4abed74265524dd7e85934597bfb10"
+dependencies = [
+ "anyhow",
+ "indexmap 2.0.0",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "spdx",
+ "wasm-encoder 0.41.2",
+ "wasmparser",
+]
+
 [[package]]
 name = "wasmparser"
 version = "0.121.2"
@@ -10801,33 +11088,57 @@ dependencies = [
  "semver",
 ]
 
+[[package]]
+name = "wasmprinter"
+version = "0.2.80"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60e73986a6b7fdfedb7c5bf9e7eb71135486507c8fbc4c0c42cffcb6532988b7"
+dependencies = [
+ "anyhow",
+ "wasmparser",
+]
+
 [[package]]
 name = "wasmtime"
 version = "18.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b06f80b13fdeba0ea5267813d0f06af822309f7125fc8db6094bcd485f0a4ae7"
 dependencies = [
+ "addr2line",
  "anyhow",
+ "async-trait",
  "bincode",
  "bumpalo",
  "cfg-if 1.0.0",
+ "encoding_rs",
+ "fxprof-processed-profile",
  "gimli",
  "indexmap 2.0.0",
+ "ittapi",
  "libc",
  "log",
  "object",
  "once_cell",
  "paste",
+ "rayon",
  "rustix 0.38.30",
  "serde",
  "serde_derive",
  "serde_json",
  "target-lexicon",
+ "wasm-encoder 0.41.2",
  "wasmparser",
+ "wasmtime-cache",
+ "wasmtime-component-macro",
+ "wasmtime-component-util",
  "wasmtime-cranelift",
  "wasmtime-environ",
+ "wasmtime-fiber",
+ "wasmtime-jit-debug",
  "wasmtime-jit-icache-coherence",
  "wasmtime-runtime",
+ "wasmtime-winch",
+ "wat",
  "windows-sys 0.52.0",
 ]
 
@@ -10864,6 +11175,47 @@ dependencies = [
  "quote",
 ]
 
+[[package]]
+name = "wasmtime-cache"
+version = "18.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0a78f86b27f099bea3aaa0894464e22e84a08cadf3d8cd353378d3d15385535"
+dependencies = [
+ "anyhow",
+ "base64 0.21.4",
+ "bincode",
+ "directories-next",
+ "log",
+ "rustix 0.38.30",
+ "serde",
+ "serde_derive",
+ "sha2 0.10.7",
+ "toml 0.5.11",
+ "windows-sys 0.52.0",
+ "zstd",
+]
+
+[[package]]
+name = "wasmtime-component-macro"
+version = "18.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93e54483c542e304e17fa73d3f9263bf071e21915c8f048c7d42916da5b4bfd6"
+dependencies = [
+ "anyhow",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.48",
+ "wasmtime-component-util",
+ "wasmtime-wit-bindgen",
+ "wit-parser 0.13.2",
+]
+
+[[package]]
+name = "wasmtime-component-util"
+version = "18.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c9f72619f484df95fc03162cdef9cb98778abc4103811849501bb34e79a3aac"
+
 [[package]]
 name = "wasmtime-cranelift"
 version = "18.0.1"
@@ -10913,19 +11265,51 @@ checksum = "e8da991421528c2767053cb0cfa70b5d28279100dbcf70ed7f74b51abe1656ef"
 dependencies = [
  "anyhow",
  "bincode",
+ "cpp_demangle",
  "cranelift-entity",
  "gimli",
  "indexmap 2.0.0",
  "log",
  "object",
+ "rustc-demangle",
  "serde",
  "serde_derive",
  "target-lexicon",
  "thiserror",
+ "wasm-encoder 0.41.2",
  "wasmparser",
+ "wasmprinter",
+ "wasmtime-component-util",
  "wasmtime-types",
 ]
 
+[[package]]
+name = "wasmtime-fiber"
+version = "18.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6fdd780272515bfcdf316e2efe20231719ec40223d67fcdd7d17068a16d39384"
+dependencies = [
+ "anyhow",
+ "cc",
+ "cfg-if 1.0.0",
+ "rustix 0.38.30",
+ "wasmtime-asm-macros",
+ "wasmtime-versioned-export-macros",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "wasmtime-jit-debug"
+version = "18.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87be9ed561dbe2aca3bde30d442c292fda53748343d0220873d1df65270c8fcf"
+dependencies = [
+ "object",
+ "once_cell",
+ "rustix 0.38.30",
+ "wasmtime-versioned-export-macros",
+]
+
 [[package]]
 name = "wasmtime-jit-icache-coherence"
 version = "18.0.1"
@@ -10946,6 +11330,7 @@ dependencies = [
  "anyhow",
  "cc",
  "cfg-if 1.0.0",
+ "encoding_rs",
  "indexmap 2.0.0",
  "libc",
  "log",
@@ -10956,9 +11341,11 @@ dependencies = [
  "psm",
  "rustix 0.38.30",
  "sptr",
- "wasm-encoder",
+ "wasm-encoder 0.41.2",
  "wasmtime-asm-macros",
  "wasmtime-environ",
+ "wasmtime-fiber",
+ "wasmtime-jit-debug",
  "wasmtime-versioned-export-macros",
  "wasmtime-wmemcheck",
  "windows-sys 0.52.0",
@@ -10988,12 +11375,105 @@ dependencies = [
  "syn 2.0.48",
 ]
 
+[[package]]
+name = "wasmtime-wasi"
+version = "18.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f7d9cfaf9f70e83a164f5d772e376fafa2d7b7b0ca2ef88f9bcaf8b2363a38b"
+dependencies = [
+ "anyhow",
+ "async-trait",
+ "bitflags 2.4.1",
+ "bytes 1.5.0",
+ "cap-fs-ext",
+ "cap-net-ext",
+ "cap-rand",
+ "cap-std",
+ "cap-time-ext",
+ "fs-set-times",
+ "futures 0.3.28",
+ "io-extras",
+ "io-lifetimes 2.0.3",
+ "log",
+ "once_cell",
+ "rustix 0.38.30",
+ "system-interface",
+ "thiserror",
+ "tokio",
+ "tracing",
+ "url",
+ "wasi-common",
+ "wasmtime",
+ "wiggle",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "wasmtime-winch"
+version = "18.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f773a904d2bd5ecd8ad095f4c965ad56a836929d8c26368621f75328d500649"
+dependencies = [
+ "anyhow",
+ "cranelift-codegen",
+ "gimli",
+ "object",
+ "target-lexicon",
+ "wasmparser",
+ "wasmtime-cranelift-shared",
+ "wasmtime-environ",
+ "winch-codegen",
+]
+
+[[package]]
+name = "wasmtime-wit-bindgen"
+version = "18.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff6e9754e0a526238ea66da9ba21965a54846a2b22d9de89a298fb8998389507"
+dependencies = [
+ "anyhow",
+ "heck 0.4.1",
+ "indexmap 2.0.0",
+ "wit-parser 0.13.2",
+]
+
 [[package]]
 name = "wasmtime-wmemcheck"
 version = "18.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "acdf5b8da6ebf7549dad0cd32ca4a3a0461449ef4feec9d0d8450d8da9f51f9b"
 
+[[package]]
+name = "wast"
+version = "35.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ef140f1b49946586078353a453a1d28ba90adfc54dde75710bc1931de204d68"
+dependencies = [
+ "leb128",
+]
+
+[[package]]
+name = "wast"
+version = "200.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1810d14e6b03ebb8fb05eef4009ad5749c989b65197d83bce7de7172ed91366"
+dependencies = [
+ "bumpalo",
+ "leb128",
+ "memchr",
+ "unicode-width",
+ "wasm-encoder 0.200.0",
+]
+
+[[package]]
+name = "wat"
+version = "1.200.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "776cbd10e217f83869beaa3f40e312bb9e91d5eee29bbf6f560db1261b6a4c3d"
+dependencies = [
+ "wast 200.0.0",
+]
+
 [[package]]
 name = "wayland-backend"
 version = "0.3.3"
@@ -11133,6 +11613,48 @@ version = "1.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50"
 
+[[package]]
+name = "wiggle"
+version = "18.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "454570f4fecadb881f0ba157e98b575a2850607a9eac79d8868f3ab70633f632"
+dependencies = [
+ "anyhow",
+ "async-trait",
+ "bitflags 2.4.1",
+ "thiserror",
+ "tracing",
+ "wasmtime",
+ "wiggle-macro",
+]
+
+[[package]]
+name = "wiggle-generate"
+version = "18.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "443ac1ebb753ca22bca98d01742762de1243ff722839907c35ea683a8264c74e"
+dependencies = [
+ "anyhow",
+ "heck 0.4.1",
+ "proc-macro2",
+ "quote",
+ "shellexpand",
+ "syn 2.0.48",
+ "witx",
+]
+
+[[package]]
+name = "wiggle-macro"
+version = "18.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e9e2f1f06ae07bac15273774782c04ab14e9adfbf414762fc84dbbfcf7fb1ac"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.48",
+ "wiggle-generate",
+]
+
 [[package]]
 name = "winapi"
 version = "0.2.8"
@@ -11176,6 +11698,22 @@ version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
 
+[[package]]
+name = "winch-codegen"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52f7eaac56988f986181099c15860946fea93ed826322a1f92c4ff04541b7744"
+dependencies = [
+ "anyhow",
+ "cranelift-codegen",
+ "gimli",
+ "regalloc2",
+ "smallvec",
+ "target-lexicon",
+ "wasmparser",
+ "wasmtime-environ",
+]
+
 [[package]]
 name = "windows"
 version = "0.46.0"
@@ -11420,6 +11958,16 @@ dependencies = [
  "windows-sys 0.48.0",
 ]
 
+[[package]]
+name = "winx"
+version = "0.36.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9643b83820c0cd246ecabe5fa454dd04ba4fa67996369466d0747472d337346"
+dependencies = [
+ "bitflags 2.4.1",
+ "windows-sys 0.52.0",
+]
+
 [[package]]
 name = "wio"
 version = "0.2.2"
@@ -11429,6 +11977,119 @@ dependencies = [
  "winapi 0.3.9",
 ]
 
+[[package]]
+name = "wit-bindgen"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5408d742fcdf418b766f23b2393f0f4d9b10b72b7cd96d9525626943593e8cc0"
+dependencies = [
+ "bitflags 2.4.1",
+ "wit-bindgen-rust-macro",
+]
+
+[[package]]
+name = "wit-bindgen-core"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7146725463d08ccf9c6c5357a7a6c1fff96185d95d6e84e7c75c92e5b1273c93"
+dependencies = [
+ "anyhow",
+ "wit-parser 0.14.0",
+]
+
+[[package]]
+name = "wit-bindgen-rust"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb5fefcf93ff2ea03c8fe9b9db2caee3096103c0e3cd62ed54f6f9493aa6b405"
+dependencies = [
+ "anyhow",
+ "heck 0.4.1",
+ "wasm-metadata",
+ "wit-bindgen-core",
+ "wit-component",
+]
+
+[[package]]
+name = "wit-bindgen-rust-macro"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce4059a1adc671e4457f457cb638ed2f766a1a462bb7daa3b638c6fb1fda156e"
+dependencies = [
+ "anyhow",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.48",
+ "wit-bindgen-core",
+ "wit-bindgen-rust",
+]
+
+[[package]]
+name = "wit-component"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be60cd1b2ff7919305301d0c27528d4867bd793afe890ba3837743da9655d91b"
+dependencies = [
+ "anyhow",
+ "bitflags 2.4.1",
+ "indexmap 2.0.0",
+ "log",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "wasm-encoder 0.41.2",
+ "wasm-metadata",
+ "wasmparser",
+ "wit-parser 0.14.0",
+]
+
+[[package]]
+name = "wit-parser"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "316b36a9f0005f5aa4b03c39bc3728d045df136f8c13a73b7db4510dec725e08"
+dependencies = [
+ "anyhow",
+ "id-arena",
+ "indexmap 2.0.0",
+ "log",
+ "semver",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "unicode-xid",
+]
+
+[[package]]
+name = "wit-parser"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ee4ad7310367bf272507c0c8e0c74a80b4ed586b833f7c7ca0b7588f686f11a"
+dependencies = [
+ "anyhow",
+ "id-arena",
+ "indexmap 2.0.0",
+ "log",
+ "semver",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "unicode-xid",
+ "wasmparser",
+]
+
+[[package]]
+name = "witx"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e366f27a5cabcddb2706a78296a40b8fcc451e1a6aba2fc1d94b4a01bdaaef4b"
+dependencies = [
+ "anyhow",
+ "log",
+ "thiserror",
+ "wast 35.0.2",
+]
+
 [[package]]
 name = "workspace"
 version = "0.1.0"
@@ -11727,6 +12388,20 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "zed_extension_api"
+version = "0.1.0"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
+name = "zed_gleam"
+version = "0.0.1"
+dependencies = [
+ "zed_extension_api",
+]
+
 [[package]]
 name = "zeno"
 version = "0.2.3"

Cargo.toml 🔗

@@ -23,6 +23,7 @@ members = [
     "crates/diagnostics",
     "crates/editor",
     "crates/extension",
+    "crates/extension_api",
     "crates/extensions_ui",
     "crates/feature_flags",
     "crates/feedback",
@@ -91,6 +92,7 @@ members = [
     "crates/workspace",
     "crates/zed",
     "crates/zed_actions",
+    "extensions/gleam",
 ]
 default-members = ["crates/zed"]
 resolver = "2"
@@ -298,7 +300,9 @@ unindent = "0.1.7"
 unicase = "2.6"
 url = "2.2"
 uuid = { version = "1.1.2", features = ["v4"] }
+wasmparser = "0.121"
 wasmtime = "18.0"
+wasmtime-wasi = "18.0"
 which = "6.0.0"
 sys-locale = "0.3.1"
 

crates/activity_indicator/src/activity_indicator.rs 🔗

@@ -6,7 +6,7 @@ use gpui::{
     ParentElement as _, Render, SharedString, StatefulInteractiveElement, Styled, View,
     ViewContext, VisualContext as _,
 };
-use language::{LanguageRegistry, LanguageServerBinaryStatus};
+use language::{LanguageRegistry, LanguageServerBinaryStatus, LanguageServerName};
 use project::{LanguageServerProgress, Project};
 use smallvec::SmallVec;
 use std::{cmp::Reverse, fmt::Write, sync::Arc};
@@ -30,7 +30,7 @@ pub struct ActivityIndicator {
 }
 
 struct LspStatus {
-    name: Arc<str>,
+    name: LanguageServerName,
     status: LanguageServerBinaryStatus,
 }
 
@@ -58,13 +58,10 @@ impl ActivityIndicator {
         let this = cx.new_view(|cx: &mut ViewContext<Self>| {
             let mut status_events = languages.language_server_binary_statuses();
             cx.spawn(|this, mut cx| async move {
-                while let Some((language, event)) = status_events.next().await {
+                while let Some((name, status)) = status_events.next().await {
                     this.update(&mut cx, |this, cx| {
-                        this.statuses.retain(|s| s.name != language.name());
-                        this.statuses.push(LspStatus {
-                            name: language.name(),
-                            status: event,
-                        });
+                        this.statuses.retain(|s| s.name != name);
+                        this.statuses.push(LspStatus { name, status });
                         cx.notify();
                     })?;
                 }
@@ -114,7 +111,7 @@ impl ActivityIndicator {
         self.statuses.retain(|status| {
             if let LanguageServerBinaryStatus::Failed { error } = &status.status {
                 cx.emit(Event::ShowError {
-                    lsp_name: status.name.clone(),
+                    lsp_name: status.name.0.clone(),
                     error: error.clone(),
                 });
                 false
@@ -202,11 +199,12 @@ impl ActivityIndicator {
         let mut checking_for_update = SmallVec::<[_; 3]>::new();
         let mut failed = SmallVec::<[_; 3]>::new();
         for status in &self.statuses {
-            let name = status.name.clone();
             match status.status {
-                LanguageServerBinaryStatus::CheckingForUpdate => checking_for_update.push(name),
-                LanguageServerBinaryStatus::Downloading => downloading.push(name),
-                LanguageServerBinaryStatus::Failed { .. } => failed.push(name),
+                LanguageServerBinaryStatus::CheckingForUpdate => {
+                    checking_for_update.push(status.name.0.as_ref())
+                }
+                LanguageServerBinaryStatus::Downloading => downloading.push(status.name.0.as_ref()),
+                LanguageServerBinaryStatus::Failed { .. } => failed.push(status.name.0.as_ref()),
                 LanguageServerBinaryStatus::Downloaded | LanguageServerBinaryStatus::Cached => {}
             }
         }
@@ -214,34 +212,28 @@ impl ActivityIndicator {
         if !downloading.is_empty() {
             return Content {
                 icon: Some(DOWNLOAD_ICON),
-                message: format!(
-                    "Downloading {} language server{}...",
-                    downloading.join(", "),
-                    if downloading.len() > 1 { "s" } else { "" }
-                ),
+                message: format!("Downloading {}...", downloading.join(", "),),
                 on_click: None,
             };
-        } else if !checking_for_update.is_empty() {
+        }
+
+        if !checking_for_update.is_empty() {
             return Content {
                 icon: Some(DOWNLOAD_ICON),
                 message: format!(
-                    "Checking for updates to {} language server{}...",
+                    "Checking for updates to {}...",
                     checking_for_update.join(", "),
-                    if checking_for_update.len() > 1 {
-                        "s"
-                    } else {
-                        ""
-                    }
                 ),
                 on_click: None,
             };
-        } else if !failed.is_empty() {
+        }
+
+        if !failed.is_empty() {
             return Content {
                 icon: Some(WARNING_ICON),
                 message: format!(
-                    "Failed to download {} language server{}. Click to show error.",
+                    "Failed to download {}. Click to show error.",
                     failed.join(", "),
-                    if failed.len() > 1 { "s" } else { "" }
                 ),
                 on_click: Some(Arc::new(|this, cx| {
                     this.show_error_message(&Default::default(), cx)

crates/collab/src/tests.rs 🔗

@@ -1,3 +1,5 @@
+use std::sync::Arc;
+
 use call::Room;
 use client::ChannelId;
 use gpui::{Model, TestAppContext};
@@ -15,6 +17,7 @@ mod random_project_collaboration_tests;
 mod randomized_test_helpers;
 mod test_server;
 
+use language::{tree_sitter_rust, Language, LanguageConfig, LanguageMatcher};
 pub use randomized_test_helpers::{
     run_randomized_test, save_randomized_test_plan, RandomizedTest, TestError, UserTestPlan,
 };
@@ -47,3 +50,17 @@ fn room_participants(room: &Model<Room>, cx: &mut TestAppContext) -> RoomPartici
 fn channel_id(room: &Model<Room>, cx: &mut TestAppContext) -> Option<ChannelId> {
     cx.read(|cx| room.read(cx).channel_id())
 }
+
+fn rust_lang() -> Arc<Language> {
+    Arc::new(Language::new(
+        LanguageConfig {
+            name: "Rust".into(),
+            matcher: LanguageMatcher {
+                path_suffixes: vec!["rs".to_string()],
+                ..Default::default()
+            },
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::language()),
+    ))
+}

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

@@ -1,11 +1,7 @@
-use std::{
-    path::Path,
-    sync::{
-        atomic::{self, AtomicBool, AtomicUsize},
-        Arc,
-    },
+use crate::{
+    rpc::RECONNECT_TIMEOUT,
+    tests::{rust_lang, TestServer},
 };
-
 use call::ActiveCall;
 use editor::{
     actions::{
@@ -19,16 +15,21 @@ use gpui::{TestAppContext, VisualContext, VisualTestContext};
 use indoc::indoc;
 use language::{
     language_settings::{AllLanguageSettings, InlayHintSettings},
-    tree_sitter_rust, FakeLspAdapter, Language, LanguageConfig, LanguageMatcher,
+    FakeLspAdapter,
 };
 use rpc::RECEIVE_TIMEOUT;
 use serde_json::json;
 use settings::SettingsStore;
+use std::{
+    path::Path,
+    sync::{
+        atomic::{self, AtomicBool, AtomicUsize},
+        Arc,
+    },
+};
 use text::Point;
 use workspace::Workspace;
 
-use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer};
-
 #[gpui::test(iterations = 10)]
 async fn test_host_disconnect(
     cx_a: &mut TestAppContext,
@@ -265,20 +266,10 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
         .await;
     let active_call_a = cx_a.read(ActiveCall::global);
 
-    // Set up a fake language server.
-    let mut language = Language::new(
-        LanguageConfig {
-            name: "Rust".into(),
-            matcher: LanguageMatcher {
-                path_suffixes: vec!["rs".to_string()],
-                ..Default::default()
-            },
-            ..Default::default()
-        },
-        Some(tree_sitter_rust::language()),
-    );
-    let mut fake_language_servers = language
-        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+    client_a.language_registry().add(rust_lang());
+    let mut fake_language_servers = client_a.language_registry().register_fake_lsp_adapter(
+        "Rust",
+        FakeLspAdapter {
             capabilities: lsp::ServerCapabilities {
                 completion_provider: Some(lsp::CompletionOptions {
                     trigger_characters: Some(vec![".".to_string()]),
@@ -288,9 +279,8 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
                 ..Default::default()
             },
             ..Default::default()
-        }))
-        .await;
-    client_a.language_registry().add(Arc::new(language));
+        },
+    );
 
     client_a
         .fs()
@@ -455,19 +445,10 @@ async fn test_collaborating_with_code_actions(
     cx_b.update(editor::init);
 
     // Set up a fake language server.
-    let mut language = Language::new(
-        LanguageConfig {
-            name: "Rust".into(),
-            matcher: LanguageMatcher {
-                path_suffixes: vec!["rs".to_string()],
-                ..Default::default()
-            },
-            ..Default::default()
-        },
-        Some(tree_sitter_rust::language()),
-    );
-    let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
-    client_a.language_registry().add(Arc::new(language));
+    client_a.language_registry().add(rust_lang());
+    let mut fake_language_servers = client_a
+        .language_registry()
+        .register_fake_lsp_adapter("Rust", FakeLspAdapter::default());
 
     client_a
         .fs()
@@ -671,19 +652,10 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
     cx_b.update(editor::init);
 
     // Set up a fake language server.
-    let mut language = Language::new(
-        LanguageConfig {
-            name: "Rust".into(),
-            matcher: LanguageMatcher {
-                path_suffixes: vec!["rs".to_string()],
-                ..Default::default()
-            },
-            ..Default::default()
-        },
-        Some(tree_sitter_rust::language()),
-    );
-    let mut fake_language_servers = language
-        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+    client_a.language_registry().add(rust_lang());
+    let mut fake_language_servers = client_a.language_registry().register_fake_lsp_adapter(
+        "Rust",
+        FakeLspAdapter {
             capabilities: lsp::ServerCapabilities {
                 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
                     prepare_provider: Some(true),
@@ -692,9 +664,8 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
                 ..Default::default()
             },
             ..Default::default()
-        }))
-        .await;
-    client_a.language_registry().add(Arc::new(language));
+        },
+    );
 
     client_a
         .fs()
@@ -858,25 +829,14 @@ async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut Tes
 
     cx_b.update(editor::init);
 
-    // Set up a fake language server.
-    let mut language = Language::new(
-        LanguageConfig {
-            name: "Rust".into(),
-            matcher: LanguageMatcher {
-                path_suffixes: vec!["rs".to_string()],
-                ..Default::default()
-            },
+    client_a.language_registry().add(rust_lang());
+    let mut fake_language_servers = client_a.language_registry().register_fake_lsp_adapter(
+        "Rust",
+        FakeLspAdapter {
+            name: "the-language-server".into(),
             ..Default::default()
         },
-        Some(tree_sitter_rust::language()),
     );
-    let mut fake_language_servers = language
-        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
-            name: "the-language-server",
-            ..Default::default()
-        }))
-        .await;
-    client_a.language_registry().add(Arc::new(language));
 
     client_a
         .fs()
@@ -1152,20 +1112,10 @@ async fn test_on_input_format_from_host_to_guest(
         .await;
     let active_call_a = cx_a.read(ActiveCall::global);
 
-    // Set up a fake language server.
-    let mut language = Language::new(
-        LanguageConfig {
-            name: "Rust".into(),
-            matcher: LanguageMatcher {
-                path_suffixes: vec!["rs".to_string()],
-                ..Default::default()
-            },
-            ..Default::default()
-        },
-        Some(tree_sitter_rust::language()),
-    );
-    let mut fake_language_servers = language
-        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+    client_a.language_registry().add(rust_lang());
+    let mut fake_language_servers = client_a.language_registry().register_fake_lsp_adapter(
+        "Rust",
+        FakeLspAdapter {
             capabilities: lsp::ServerCapabilities {
                 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
                     first_trigger_character: ":".to_string(),
@@ -1174,9 +1124,8 @@ async fn test_on_input_format_from_host_to_guest(
                 ..Default::default()
             },
             ..Default::default()
-        }))
-        .await;
-    client_a.language_registry().add(Arc::new(language));
+        },
+    );
 
     client_a
         .fs()
@@ -1283,20 +1232,10 @@ async fn test_on_input_format_from_guest_to_host(
         .await;
     let active_call_a = cx_a.read(ActiveCall::global);
 
-    // Set up a fake language server.
-    let mut language = Language::new(
-        LanguageConfig {
-            name: "Rust".into(),
-            matcher: LanguageMatcher {
-                path_suffixes: vec!["rs".to_string()],
-                ..Default::default()
-            },
-            ..Default::default()
-        },
-        Some(tree_sitter_rust::language()),
-    );
-    let mut fake_language_servers = language
-        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+    client_a.language_registry().add(rust_lang());
+    let mut fake_language_servers = client_a.language_registry().register_fake_lsp_adapter(
+        "Rust",
+        FakeLspAdapter {
             capabilities: lsp::ServerCapabilities {
                 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
                     first_trigger_character: ":".to_string(),
@@ -1305,9 +1244,8 @@ async fn test_on_input_format_from_guest_to_host(
                 ..Default::default()
             },
             ..Default::default()
-        }))
-        .await;
-    client_a.language_registry().add(Arc::new(language));
+        },
+    );
 
     client_a
         .fs()
@@ -1450,29 +1388,18 @@ async fn test_mutual_editor_inlay_hint_cache_update(
         });
     });
 
-    let mut language = Language::new(
-        LanguageConfig {
-            name: "Rust".into(),
-            matcher: LanguageMatcher {
-                path_suffixes: vec!["rs".to_string()],
-                ..Default::default()
-            },
-            ..Default::default()
-        },
-        Some(tree_sitter_rust::language()),
-    );
-    let mut fake_language_servers = language
-        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+    client_a.language_registry().add(rust_lang());
+    client_b.language_registry().add(rust_lang());
+    let mut fake_language_servers = client_a.language_registry().register_fake_lsp_adapter(
+        "Rust",
+        FakeLspAdapter {
             capabilities: lsp::ServerCapabilities {
                 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
                 ..Default::default()
             },
             ..Default::default()
-        }))
-        .await;
-    let language = Arc::new(language);
-    client_a.language_registry().add(Arc::clone(&language));
-    client_b.language_registry().add(language);
+        },
+    );
 
     // Client A opens a project.
     client_a
@@ -1723,29 +1650,18 @@ async fn test_inlay_hint_refresh_is_forwarded(
         });
     });
 
-    let mut language = Language::new(
-        LanguageConfig {
-            name: "Rust".into(),
-            matcher: LanguageMatcher {
-                path_suffixes: vec!["rs".to_string()],
-                ..Default::default()
-            },
-            ..Default::default()
-        },
-        Some(tree_sitter_rust::language()),
-    );
-    let mut fake_language_servers = language
-        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+    client_a.language_registry().add(rust_lang());
+    client_b.language_registry().add(rust_lang());
+    let mut fake_language_servers = client_a.language_registry().register_fake_lsp_adapter(
+        "Rust",
+        FakeLspAdapter {
             capabilities: lsp::ServerCapabilities {
                 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
                 ..Default::default()
             },
             ..Default::default()
-        }))
-        .await;
-    let language = Arc::new(language);
-    client_a.language_registry().add(Arc::clone(&language));
-    client_b.language_registry().add(language);
+        },
+    );
 
     client_a
         .fs()

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

@@ -1,6 +1,6 @@
 use crate::{
     rpc::{CLEANUP_TIMEOUT, RECONNECT_TIMEOUT},
-    tests::{channel_id, room_participants, RoomParticipants, TestClient, TestServer},
+    tests::{channel_id, room_participants, rust_lang, RoomParticipants, TestClient, TestServer},
 };
 use call::{room, ActiveCall, ParticipantLocation, Room};
 use client::{User, RECEIVE_TIMEOUT};
@@ -3785,8 +3785,7 @@ async fn test_collaborating_with_diagnostics(
         .await;
     let active_call_a = cx_a.read(ActiveCall::global);
 
-    // Set up a fake language server.
-    let mut language = Language::new(
+    client_a.language_registry().add(Arc::new(Language::new(
         LanguageConfig {
             name: "Rust".into(),
             matcher: LanguageMatcher {
@@ -3796,9 +3795,10 @@ async fn test_collaborating_with_diagnostics(
             ..Default::default()
         },
         Some(tree_sitter_rust::language()),
-    );
-    let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
-    client_a.language_registry().add(Arc::new(language));
+    )));
+    let mut fake_language_servers = client_a
+        .language_registry()
+        .register_fake_lsp_adapter("Rust", Default::default());
 
     // Share a project as client A
     client_a
@@ -4066,26 +4066,15 @@ async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering(
         .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
         .await;
 
-    // Set up a fake language server.
-    let mut language = Language::new(
-        LanguageConfig {
-            name: "Rust".into(),
-            matcher: LanguageMatcher {
-                path_suffixes: vec!["rs".to_string()],
-                ..Default::default()
-            },
-            ..Default::default()
-        },
-        Some(tree_sitter_rust::language()),
-    );
-    let mut fake_language_servers = language
-        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+    client_a.language_registry().add(rust_lang());
+    let mut fake_language_servers = client_a.language_registry().register_fake_lsp_adapter(
+        "Rust",
+        FakeLspAdapter {
             disk_based_diagnostics_progress_token: Some("the-disk-based-token".into()),
             disk_based_diagnostics_sources: vec!["the-disk-based-diagnostics-source".into()],
             ..Default::default()
-        }))
-        .await;
-    client_a.language_registry().add(Arc::new(language));
+        },
+    );
 
     let file_names = &["one.rs", "two.rs", "three.rs", "four.rs", "five.rs"];
     client_a
@@ -4298,20 +4287,10 @@ async fn test_formatting_buffer(
         .await;
     let active_call_a = cx_a.read(ActiveCall::global);
 
-    // Set up a fake language server.
-    let mut language = Language::new(
-        LanguageConfig {
-            name: "Rust".into(),
-            matcher: LanguageMatcher {
-                path_suffixes: vec!["rs".to_string()],
-                ..Default::default()
-            },
-            ..Default::default()
-        },
-        Some(tree_sitter_rust::language()),
-    );
-    let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
-    client_a.language_registry().add(Arc::new(language));
+    client_a.language_registry().add(rust_lang());
+    let mut fake_language_servers = client_a
+        .language_registry()
+        .register_fake_lsp_adapter("Rust", FakeLspAdapter::default());
 
     // Here we insert a fake tree with a directory that exists on disk. This is needed
     // because later we'll invoke a command, which requires passing a working directory
@@ -4406,8 +4385,9 @@ async fn test_prettier_formatting_buffer(
         .await;
     let active_call_a = cx_a.read(ActiveCall::global);
 
-    // Set up a fake language server.
-    let mut language = Language::new(
+    let test_plugin = "test_plugin";
+
+    client_a.language_registry().add(Arc::new(Language::new(
         LanguageConfig {
             name: "Rust".into(),
             matcher: LanguageMatcher {
@@ -4418,16 +4398,14 @@ async fn test_prettier_formatting_buffer(
             ..Default::default()
         },
         Some(tree_sitter_rust::language()),
-    );
-    let test_plugin = "test_plugin";
-    let mut fake_language_servers = language
-        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+    )));
+    let mut fake_language_servers = client_a.language_registry().register_fake_lsp_adapter(
+        "Rust",
+        FakeLspAdapter {
             prettier_plugins: vec![test_plugin],
             ..Default::default()
-        }))
-        .await;
-    let language = Arc::new(language);
-    client_a.language_registry().add(Arc::clone(&language));
+        },
+    );
 
     // Here we insert a fake tree with a directory that exists on disk. This is needed
     // because later we'll invoke a command, which requires passing a working directory
@@ -4525,20 +4503,10 @@ async fn test_definition(
         .await;
     let active_call_a = cx_a.read(ActiveCall::global);
 
-    // Set up a fake language server.
-    let mut language = Language::new(
-        LanguageConfig {
-            name: "Rust".into(),
-            matcher: LanguageMatcher {
-                path_suffixes: vec!["rs".to_string()],
-                ..Default::default()
-            },
-            ..Default::default()
-        },
-        Some(tree_sitter_rust::language()),
-    );
-    let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
-    client_a.language_registry().add(Arc::new(language));
+    let mut fake_language_servers = client_a
+        .language_registry()
+        .register_fake_lsp_adapter("Rust", Default::default());
+    client_a.language_registry().add(rust_lang());
 
     client_a
         .fs()
@@ -4672,20 +4640,10 @@ async fn test_references(
         .await;
     let active_call_a = cx_a.read(ActiveCall::global);
 
-    // Set up a fake language server.
-    let mut language = Language::new(
-        LanguageConfig {
-            name: "Rust".into(),
-            matcher: LanguageMatcher {
-                path_suffixes: vec!["rs".to_string()],
-                ..Default::default()
-            },
-            ..Default::default()
-        },
-        Some(tree_sitter_rust::language()),
-    );
-    let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
-    client_a.language_registry().add(Arc::new(language));
+    client_a.language_registry().add(rust_lang());
+    let mut fake_language_servers = client_a
+        .language_registry()
+        .register_fake_lsp_adapter("Rust", Default::default());
 
     client_a
         .fs()
@@ -4872,20 +4830,10 @@ async fn test_document_highlights(
         )
         .await;
 
-    // Set up a fake language server.
-    let mut language = Language::new(
-        LanguageConfig {
-            name: "Rust".into(),
-            matcher: LanguageMatcher {
-                path_suffixes: vec!["rs".to_string()],
-                ..Default::default()
-            },
-            ..Default::default()
-        },
-        Some(tree_sitter_rust::language()),
-    );
-    let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
-    client_a.language_registry().add(Arc::new(language));
+    let mut fake_language_servers = client_a
+        .language_registry()
+        .register_fake_lsp_adapter("Rust", Default::default());
+    client_a.language_registry().add(rust_lang());
 
     let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
     let project_id = active_call_a
@@ -4978,20 +4926,10 @@ async fn test_lsp_hover(
         )
         .await;
 
-    // Set up a fake language server.
-    let mut language = Language::new(
-        LanguageConfig {
-            name: "Rust".into(),
-            matcher: LanguageMatcher {
-                path_suffixes: vec!["rs".to_string()],
-                ..Default::default()
-            },
-            ..Default::default()
-        },
-        Some(tree_sitter_rust::language()),
-    );
-    let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
-    client_a.language_registry().add(Arc::new(language));
+    client_a.language_registry().add(rust_lang());
+    let mut fake_language_servers = client_a
+        .language_registry()
+        .register_fake_lsp_adapter("Rust", Default::default());
 
     let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
     let project_id = active_call_a
@@ -5077,20 +5015,10 @@ async fn test_project_symbols(
         .await;
     let active_call_a = cx_a.read(ActiveCall::global);
 
-    // Set up a fake language server.
-    let mut language = Language::new(
-        LanguageConfig {
-            name: "Rust".into(),
-            matcher: LanguageMatcher {
-                path_suffixes: vec!["rs".to_string()],
-                ..Default::default()
-            },
-            ..Default::default()
-        },
-        Some(tree_sitter_rust::language()),
-    );
-    let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
-    client_a.language_registry().add(Arc::new(language));
+    client_a.language_registry().add(rust_lang());
+    let mut fake_language_servers = client_a
+        .language_registry()
+        .register_fake_lsp_adapter("Rust", Default::default());
 
     client_a
         .fs()
@@ -5189,20 +5117,10 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it(
         .await;
     let active_call_a = cx_a.read(ActiveCall::global);
 
-    // Set up a fake language server.
-    let mut language = Language::new(
-        LanguageConfig {
-            name: "Rust".into(),
-            matcher: LanguageMatcher {
-                path_suffixes: vec!["rs".to_string()],
-                ..Default::default()
-            },
-            ..Default::default()
-        },
-        Some(tree_sitter_rust::language()),
-    );
-    let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
-    client_a.language_registry().add(Arc::new(language));
+    client_a.language_registry().add(rust_lang());
+    let mut fake_language_servers = client_a
+        .language_registry()
+        .register_fake_lsp_adapter("Rust", Default::default());
 
     client_a
         .fs()

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

@@ -1021,7 +1021,7 @@ impl RandomizedTest for ProjectCollaborationTest {
     }
 
     async fn on_client_added(client: &Rc<TestClient>, _: &mut TestAppContext) {
-        let mut language = Language::new(
+        client.language_registry().add(Arc::new(Language::new(
             LanguageConfig {
                 name: "Rust".into(),
                 matcher: LanguageMatcher {
@@ -1031,9 +1031,10 @@ impl RandomizedTest for ProjectCollaborationTest {
                 ..Default::default()
             },
             None,
-        );
-        language
-            .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+        )));
+        client.language_registry().register_fake_lsp_adapter(
+            "Rust",
+            FakeLspAdapter {
                 name: "the-fake-language-server",
                 capabilities: lsp::LanguageServer::full_capabilities(),
                 initializer: Some(Box::new({
@@ -1132,9 +1133,8 @@ impl RandomizedTest for ProjectCollaborationTest {
                     }
                 })),
                 ..Default::default()
-            }))
-            .await;
-        client.app_state.languages.add(Arc::new(language));
+            },
+        );
     }
 
     async fn on_quiesce(_: &mut TestServer, clients: &mut [(Rc<TestClient>, TestAppContext)]) {

crates/copilot/src/copilot.rs 🔗

@@ -383,8 +383,16 @@ impl Copilot {
         use lsp::FakeLanguageServer;
         use node_runtime::FakeNodeRuntime;
 
-        let (server, fake_server) =
-            FakeLanguageServer::new("copilot".into(), Default::default(), cx.to_async());
+        let (server, fake_server) = FakeLanguageServer::new(
+            LanguageServerBinary {
+                path: "path/to/copilot".into(),
+                arguments: vec![],
+                env: None,
+            },
+            "copilot".into(),
+            Default::default(),
+            cx.to_async(),
+        );
         let http = util::http::FakeHttpClient::create(|_| async { unreachable!() });
         let node_runtime = FakeNodeRuntime::new();
         let this = cx.new_model(|cx| Self {

crates/editor/src/editor_tests.rs 🔗

@@ -5233,32 +5233,24 @@ async fn test_snippets(cx: &mut gpui::TestAppContext) {
 async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
     init_test(cx, |_| {});
 
-    let mut language = Language::new(
-        LanguageConfig {
-            name: "Rust".into(),
-            matcher: LanguageMatcher {
-                path_suffixes: vec!["rs".to_string()],
-                ..Default::default()
-            },
-            ..Default::default()
-        },
-        Some(tree_sitter_rust::language()),
-    );
-    let mut fake_servers = language
-        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+    let fs = FakeFs::new(cx.executor());
+    fs.insert_file("/file.rs", Default::default()).await;
+
+    let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
+
+    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+    language_registry.add(rust_lang());
+    let mut fake_servers = language_registry.register_fake_lsp_adapter(
+        "Rust",
+        FakeLspAdapter {
             capabilities: lsp::ServerCapabilities {
                 document_formatting_provider: Some(lsp::OneOf::Left(true)),
                 ..Default::default()
             },
             ..Default::default()
-        }))
-        .await;
-
-    let fs = FakeFs::new(cx.executor());
-    fs.insert_file("/file.rs", Default::default()).await;
+        },
+    );
 
-    let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
-    _ = project.update(cx, |project, _| project.languages().add(Arc::new(language)));
     let buffer = project
         .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
         .await
@@ -5355,32 +5347,24 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
 async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
     init_test(cx, |_| {});
 
-    let mut language = Language::new(
-        LanguageConfig {
-            name: "Rust".into(),
-            matcher: LanguageMatcher {
-                path_suffixes: vec!["rs".to_string()],
-                ..Default::default()
-            },
-            ..Default::default()
-        },
-        Some(tree_sitter_rust::language()),
-    );
-    let mut fake_servers = language
-        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+    let fs = FakeFs::new(cx.executor());
+    fs.insert_file("/file.rs", Default::default()).await;
+
+    let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
+
+    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+    language_registry.add(rust_lang());
+    let mut fake_servers = language_registry.register_fake_lsp_adapter(
+        "Rust",
+        FakeLspAdapter {
             capabilities: lsp::ServerCapabilities {
                 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
                 ..Default::default()
             },
             ..Default::default()
-        }))
-        .await;
-
-    let fs = FakeFs::new(cx.executor());
-    fs.insert_file("/file.rs", Default::default()).await;
+        },
+    );
 
-    let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
-    _ = project.update(cx, |project, _| project.languages().add(Arc::new(language)));
     let buffer = project
         .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
         .await
@@ -5480,7 +5464,13 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
         settings.defaults.formatter = Some(language_settings::Formatter::LanguageServer)
     });
 
-    let mut language = Language::new(
+    let fs = FakeFs::new(cx.executor());
+    fs.insert_file("/file.rs", Default::default()).await;
+
+    let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
+
+    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+    language_registry.add(Arc::new(Language::new(
         LanguageConfig {
             name: "Rust".into(),
             matcher: LanguageMatcher {
@@ -5493,24 +5483,18 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
             ..Default::default()
         },
         Some(tree_sitter_rust::language()),
-    );
-    let mut fake_servers = language
-        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+    )));
+    let mut fake_servers = language_registry.register_fake_lsp_adapter(
+        "Rust",
+        FakeLspAdapter {
             capabilities: lsp::ServerCapabilities {
                 document_formatting_provider: Some(lsp::OneOf::Left(true)),
                 ..Default::default()
             },
             ..Default::default()
-        }))
-        .await;
-
-    let fs = FakeFs::new(cx.executor());
-    fs.insert_file("/file.rs", Default::default()).await;
+        },
+    );
 
-    let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
-    _ = project.update(cx, |project, _| {
-        project.languages().add(Arc::new(language));
-    });
     let buffer = project
         .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
         .await
@@ -7912,7 +7896,19 @@ async fn test_copilot_disabled_globs(executor: BackgroundExecutor, cx: &mut gpui
 async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) {
     init_test(cx, |_| {});
 
-    let mut language = Language::new(
+    let fs = FakeFs::new(cx.executor());
+    fs.insert_tree(
+        "/a",
+        json!({
+            "main.rs": "fn main() { let a = 5; }",
+            "other.rs": "// Test file",
+        }),
+    )
+    .await;
+    let project = Project::test(fs, ["/a".as_ref()], cx).await;
+
+    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+    language_registry.add(Arc::new(Language::new(
         LanguageConfig {
             name: "Rust".into(),
             matcher: LanguageMatcher {
@@ -7931,9 +7927,10 @@ async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) {
             ..Default::default()
         },
         Some(tree_sitter_rust::language()),
-    );
-    let mut fake_servers = language
-        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+    )));
+    let mut fake_servers = language_registry.register_fake_lsp_adapter(
+        "Rust",
+        FakeLspAdapter {
             capabilities: lsp::ServerCapabilities {
                 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
                     first_trigger_character: "{".to_string(),
@@ -7942,20 +7939,9 @@ async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) {
                 ..Default::default()
             },
             ..Default::default()
-        }))
-        .await;
+        },
+    );
 
-    let fs = FakeFs::new(cx.executor());
-    fs.insert_tree(
-        "/a",
-        json!({
-            "main.rs": "fn main() { let a = 5; }",
-            "other.rs": "// Test file",
-        }),
-    )
-    .await;
-    let project = Project::test(fs, ["/a".as_ref()], cx).await;
-    _ = project.update(cx, |project, _| project.languages().add(Arc::new(language)));
     let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
 
     let cx = &mut VisualTestContext::from_window(*workspace, cx);
@@ -8026,8 +8012,25 @@ async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) {
 async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::TestAppContext) {
     init_test(cx, |_| {});
 
+    let fs = FakeFs::new(cx.executor());
+    fs.insert_tree(
+        "/a",
+        json!({
+            "main.rs": "fn main() { let a = 5; }",
+            "other.rs": "// Test file",
+        }),
+    )
+    .await;
+
+    let project = Project::test(fs, ["/a".as_ref()], cx).await;
+
+    let server_restarts = Arc::new(AtomicUsize::new(0));
+    let closure_restarts = Arc::clone(&server_restarts);
+    let language_server_name = "test language server";
     let language_name: Arc<str> = "Rust".into();
-    let mut language = Language::new(
+
+    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+    language_registry.add(Arc::new(Language::new(
         LanguageConfig {
             name: Arc::clone(&language_name),
             matcher: LanguageMatcher {
@@ -8037,13 +8040,10 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::Test
             ..Default::default()
         },
         Some(tree_sitter_rust::language()),
-    );
-
-    let server_restarts = Arc::new(AtomicUsize::new(0));
-    let closure_restarts = Arc::clone(&server_restarts);
-    let language_server_name = "test language server";
-    let mut fake_servers = language
-        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+    )));
+    let mut fake_servers = language_registry.register_fake_lsp_adapter(
+        "Rust",
+        FakeLspAdapter {
             name: language_server_name,
             initialization_options: Some(json!({
                 "testOptionValue": true
@@ -8056,20 +8056,9 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::Test
                 });
             })),
             ..Default::default()
-        }))
-        .await;
+        },
+    );
 
-    let fs = FakeFs::new(cx.executor());
-    fs.insert_tree(
-        "/a",
-        json!({
-            "main.rs": "fn main() { let a = 5; }",
-            "other.rs": "// Test file",
-        }),
-    )
-    .await;
-    let project = Project::test(fs, ["/a".as_ref()], cx).await;
-    _ = project.update(cx, |project, _| project.languages().add(Arc::new(language)));
     let _window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
     let _buffer = project
         .update(cx, |project, cx| {
@@ -8365,7 +8354,13 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
         settings.defaults.formatter = Some(language_settings::Formatter::Prettier)
     });
 
-    let mut language = Language::new(
+    let fs = FakeFs::new(cx.executor());
+    fs.insert_file("/file.rs", Default::default()).await;
+
+    let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
+    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+
+    language_registry.add(Arc::new(Language::new(
         LanguageConfig {
             name: "Rust".into(),
             matcher: LanguageMatcher {
@@ -8376,24 +8371,18 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
             ..Default::default()
         },
         Some(tree_sitter_rust::language()),
-    );
+    )));
 
     let test_plugin = "test_plugin";
-    let _ = language
-        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+    let _ = language_registry.register_fake_lsp_adapter(
+        "Rust",
+        FakeLspAdapter {
             prettier_plugins: vec![test_plugin],
             ..Default::default()
-        }))
-        .await;
-
-    let fs = FakeFs::new(cx.executor());
-    fs.insert_file("/file.rs", Default::default()).await;
+        },
+    );
 
-    let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
     let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
-    _ = project.update(cx, |project, _| {
-        project.languages().add(Arc::new(language));
-    });
     let buffer = project
         .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
         .await
@@ -8685,3 +8674,17 @@ pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsC
 
     update_test_language_settings(cx, f);
 }
+
+pub(crate) fn rust_lang() -> Arc<Language> {
+    Arc::new(Language::new(
+        LanguageConfig {
+            name: "Rust".into(),
+            matcher: LanguageMatcher {
+                path_suffixes: vec!["rs".to_string()],
+                ..Default::default()
+            },
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::language()),
+    ))
+}

crates/editor/src/inlay_hint_cache.rs 🔗

@@ -1553,12 +1553,14 @@ pub mod tests {
                     }),
                 )
                 .await;
+
         let project = Project::test(fs, ["/a".as_ref()], cx).await;
 
+        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
         let mut rs_fake_servers = None;
         let mut md_fake_servers = None;
         for (name, path_suffix) in [("Rust", "rs"), ("Markdown", "md")] {
-            let mut language = Language::new(
+            language_registry.add(Arc::new(Language::new(
                 LanguageConfig {
                     name: name.into(),
                     matcher: LanguageMatcher {
@@ -1568,25 +1570,23 @@ pub mod tests {
                     ..Default::default()
                 },
                 Some(tree_sitter_rust::language()),
-            );
-            let fake_servers = language
-                .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+            )));
+            let fake_servers = language_registry.register_fake_lsp_adapter(
+                name,
+                FakeLspAdapter {
                     name,
                     capabilities: lsp::ServerCapabilities {
                         inlay_hint_provider: Some(lsp::OneOf::Left(true)),
                         ..Default::default()
                     },
                     ..Default::default()
-                }))
-                .await;
+                },
+            );
             match name {
                 "Rust" => rs_fake_servers = Some(fake_servers),
                 "Markdown" => md_fake_servers = Some(fake_servers),
                 _ => unreachable!(),
             }
-            project.update(cx, |project, _| {
-                project.languages().add(Arc::new(language));
-            });
         }
 
         let rs_buffer = project
@@ -2253,26 +2253,6 @@ pub mod tests {
             })
         });
 
-        let mut language = Language::new(
-            LanguageConfig {
-                name: "Rust".into(),
-                matcher: LanguageMatcher {
-                    path_suffixes: vec!["rs".to_string()],
-                    ..Default::default()
-                },
-                ..Default::default()
-            },
-            Some(tree_sitter_rust::language()),
-        );
-        let mut fake_servers = language
-            .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
-                capabilities: lsp::ServerCapabilities {
-                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
-                    ..Default::default()
-                },
-                ..Default::default()
-            }))
-            .await;
         let fs = FakeFs::new(cx.background_executor.clone());
         fs.insert_tree(
             "/a",
@@ -2282,8 +2262,22 @@ pub mod tests {
             }),
         )
         .await;
+
         let project = Project::test(fs, ["/a".as_ref()], cx).await;
-        project.update(cx, |project, _| project.languages().add(Arc::new(language)));
+
+        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+        language_registry.add(crate::editor_tests::rust_lang());
+        let mut fake_servers = language_registry.register_fake_lsp_adapter(
+            "Rust",
+            FakeLspAdapter {
+                capabilities: lsp::ServerCapabilities {
+                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+                    ..Default::default()
+                },
+                ..Default::default()
+            },
+        );
+
         let buffer = project
             .update(cx, |project, cx| {
                 project.open_local_buffer("/a/main.rs", cx)
@@ -2554,27 +2548,6 @@ pub mod tests {
             })
         });
 
-        let mut language = Language::new(
-            LanguageConfig {
-                name: "Rust".into(),
-                matcher: LanguageMatcher {
-                    path_suffixes: vec!["rs".to_string()],
-                    ..Default::default()
-                },
-                ..Default::default()
-            },
-            Some(tree_sitter_rust::language()),
-        );
-        let mut fake_servers = language
-            .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
-                capabilities: lsp::ServerCapabilities {
-                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
-                    ..Default::default()
-                },
-                ..Default::default()
-            }))
-            .await;
-        let language = Arc::new(language);
         let fs = FakeFs::new(cx.background_executor.clone());
         fs.insert_tree(
                 "/a",
@@ -2584,10 +2557,23 @@ pub mod tests {
                 }),
             )
             .await;
+
         let project = Project::test(fs, ["/a".as_ref()], cx).await;
-        project.update(cx, |project, _| {
-            project.languages().add(Arc::clone(&language))
-        });
+
+        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+        let language = crate::editor_tests::rust_lang();
+        language_registry.add(language);
+        let mut fake_servers = language_registry.register_fake_lsp_adapter(
+            "Rust",
+            FakeLspAdapter {
+                capabilities: lsp::ServerCapabilities {
+                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+                    ..Default::default()
+                },
+                ..Default::default()
+            },
+        );
+
         let worktree_id = project.update(cx, |project, cx| {
             project.worktrees().next().unwrap().read(cx).id()
         });
@@ -2911,27 +2897,6 @@ pub mod tests {
             })
         });
 
-        let mut language = Language::new(
-            LanguageConfig {
-                name: "Rust".into(),
-                matcher: LanguageMatcher {
-                    path_suffixes: vec!["rs".to_string()],
-                    ..Default::default()
-                },
-                ..Default::default()
-            },
-            Some(tree_sitter_rust::language()),
-        );
-        let mut fake_servers = language
-            .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
-                capabilities: lsp::ServerCapabilities {
-                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
-                    ..Default::default()
-                },
-                ..Default::default()
-            }))
-            .await;
-        let language = Arc::new(language);
         let fs = FakeFs::new(cx.background_executor.clone());
         fs.insert_tree(
             "/a",
@@ -2941,10 +2906,22 @@ pub mod tests {
             }),
         )
         .await;
+
         let project = Project::test(fs, ["/a".as_ref()], cx).await;
-        project.update(cx, |project, _| {
-            project.languages().add(Arc::clone(&language))
-        });
+
+        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+        language_registry.add(crate::editor_tests::rust_lang());
+        let mut fake_servers = language_registry.register_fake_lsp_adapter(
+            "Rust",
+            FakeLspAdapter {
+                capabilities: lsp::ServerCapabilities {
+                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+                    ..Default::default()
+                },
+                ..Default::default()
+            },
+        );
+
         let worktree_id = project.update(cx, |project, cx| {
             project.worktrees().next().unwrap().read(cx).id()
         });
@@ -3149,26 +3126,6 @@ pub mod tests {
             })
         });
 
-        let mut language = Language::new(
-            LanguageConfig {
-                name: "Rust".into(),
-                matcher: LanguageMatcher {
-                    path_suffixes: vec!["rs".to_string()],
-                    ..Default::default()
-                },
-                ..Default::default()
-            },
-            Some(tree_sitter_rust::language()),
-        );
-        let mut fake_servers = language
-            .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
-                capabilities: lsp::ServerCapabilities {
-                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
-                    ..Default::default()
-                },
-                ..Default::default()
-            }))
-            .await;
         let fs = FakeFs::new(cx.background_executor.clone());
         fs.insert_tree(
             "/a",
@@ -3178,8 +3135,22 @@ pub mod tests {
             }),
         )
         .await;
+
         let project = Project::test(fs, ["/a".as_ref()], cx).await;
-        project.update(cx, |project, _| project.languages().add(Arc::new(language)));
+
+        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+        language_registry.add(crate::editor_tests::rust_lang());
+        let mut fake_servers = language_registry.register_fake_lsp_adapter(
+            "Rust",
+            FakeLspAdapter {
+                capabilities: lsp::ServerCapabilities {
+                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+                    ..Default::default()
+                },
+                ..Default::default()
+            },
+        );
+
         let buffer = project
             .update(cx, |project, cx| {
                 project.open_local_buffer("/a/main.rs", cx)
@@ -3396,7 +3367,20 @@ pub mod tests {
     async fn prepare_test_objects(
         cx: &mut TestAppContext,
     ) -> (&'static str, WindowHandle<Editor>, FakeLanguageServer) {
-        let mut language = Language::new(
+        let fs = FakeFs::new(cx.background_executor.clone());
+        fs.insert_tree(
+            "/a",
+            json!({
+                "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
+                "other.rs": "// Test file",
+            }),
+        )
+        .await;
+
+        let project = Project::test(fs, ["/a".as_ref()], cx).await;
+
+        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+        language_registry.add(Arc::new(Language::new(
             LanguageConfig {
                 name: "Rust".into(),
                 matcher: LanguageMatcher {
@@ -3406,29 +3390,18 @@ pub mod tests {
                 ..Default::default()
             },
             Some(tree_sitter_rust::language()),
-        );
-        let mut fake_servers = language
-            .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+        )));
+        let mut fake_servers = language_registry.register_fake_lsp_adapter(
+            "Rust",
+            FakeLspAdapter {
                 capabilities: lsp::ServerCapabilities {
                     inlay_hint_provider: Some(lsp::OneOf::Left(true)),
                     ..Default::default()
                 },
                 ..Default::default()
-            }))
-            .await;
-
-        let fs = FakeFs::new(cx.background_executor.clone());
-        fs.insert_tree(
-            "/a",
-            json!({
-                "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
-                "other.rs": "// Test file",
-            }),
-        )
-        .await;
+            },
+        );
 
-        let project = Project::test(fs, ["/a".as_ref()], cx).await;
-        project.update(cx, |project, _| project.languages().add(Arc::new(language)));
         let buffer = project
             .update(cx, |project, cx| {
                 project.open_local_buffer("/a/main.rs", cx)

crates/editor/src/test/editor_lsp_test_context.rs 🔗

@@ -32,7 +32,7 @@ pub struct EditorLspTestContext {
 
 impl EditorLspTestContext {
     pub async fn new(
-        mut language: Language,
+        language: Language,
         capabilities: lsp::ServerCapabilities,
         cx: &mut gpui::TestAppContext,
     ) -> EditorLspTestContext {
@@ -53,16 +53,17 @@ impl EditorLspTestContext {
                 .expect("language must have a path suffix for EditorLspTestContext")
         );
 
-        let mut fake_servers = language
-            .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
-                capabilities,
-                ..Default::default()
-            }))
-            .await;
-
         let project = Project::test(app_state.fs.clone(), [], cx).await;
 
-        project.update(cx, |project, _| project.languages().add(Arc::new(language)));
+        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+        let mut fake_servers = language_registry.register_fake_lsp_adapter(
+            language.name().as_ref(),
+            FakeLspAdapter {
+                capabilities,
+                ..Default::default()
+            },
+        );
+        language_registry.add(Arc::new(language));
 
         app_state
             .fs

crates/extension/Cargo.toml 🔗

@@ -16,13 +16,16 @@ path = "src/extension_json_schemas.rs"
 anyhow.workspace = true
 async-compression.workspace = true
 async-tar.workspace = true
+async-trait.workspace = true
 collections.workspace = true
 fs.workspace = true
 futures.workspace = true
 gpui.workspace = true
 language.workspace = true
 log.workspace = true
-parking_lot.workspace = true
+lsp.workspace = true
+node_runtime.workspace = true
+project.workspace = true
 schemars.workspace = true
 serde.workspace = true
 serde_json.workspace = true
@@ -30,8 +33,12 @@ settings.workspace = true
 theme.workspace = true
 toml.workspace = true
 util.workspace = true
+wasmtime = { workspace = true, features = ["async"] }
+wasmtime-wasi.workspace = true
+wasmparser.workspace = true
 
 [dev-dependencies]
 fs = { workspace = true, features = ["test-support"] }
 gpui = { workspace = true, features = ["test-support"] }
 language = { workspace = true, features = ["test-support"] }
+project = { workspace = true, features = ["test-support"] }

crates/extension/src/extension_lsp_adapter.rs 🔗

@@ -0,0 +1,90 @@
+use crate::wasm_host::{wit::LanguageServerConfig, WasmExtension};
+use anyhow::{anyhow, Result};
+use async_trait::async_trait;
+use futures::{Future, FutureExt};
+use gpui::AsyncAppContext;
+use language::{Language, LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp::LanguageServerBinary;
+use std::{
+    any::Any,
+    path::{Path, PathBuf},
+    pin::Pin,
+    sync::Arc,
+};
+use wasmtime_wasi::preview2::WasiView as _;
+
+pub struct ExtensionLspAdapter {
+    pub(crate) extension: WasmExtension,
+    pub(crate) config: LanguageServerConfig,
+    pub(crate) work_dir: PathBuf,
+}
+
+#[async_trait]
+impl LspAdapter for ExtensionLspAdapter {
+    fn name(&self) -> LanguageServerName {
+        LanguageServerName(self.config.name.clone().into())
+    }
+
+    fn get_language_server_command<'a>(
+        self: Arc<Self>,
+        _: Arc<Language>,
+        _: Arc<Path>,
+        delegate: Arc<dyn LspAdapterDelegate>,
+        _: futures::lock::MutexGuard<'a, Option<LanguageServerBinary>>,
+        _: &'a mut AsyncAppContext,
+    ) -> Pin<Box<dyn 'a + Future<Output = Result<LanguageServerBinary>>>> {
+        async move {
+            let command = self
+                .extension
+                .call({
+                    let this = self.clone();
+                    |extension, store| {
+                        async move {
+                            let resource = store.data_mut().table().push(delegate)?;
+                            extension
+                                .call_language_server_command(store, &this.config, resource)
+                                .await
+                        }
+                        .boxed()
+                    }
+                })
+                .await?
+                .map_err(|e| anyhow!("{}", e))?;
+
+            Ok(LanguageServerBinary {
+                path: self.work_dir.join(&command.command).into(),
+                arguments: command.args.into_iter().map(|arg| arg.into()).collect(),
+                env: Some(command.env.into_iter().collect()),
+            })
+        }
+        .boxed_local()
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Send + Any>> {
+        unreachable!("get_language_server_command is overridden")
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        _: Box<dyn 'static + Send + Any>,
+        _: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        unreachable!("get_language_server_command is overridden")
+    }
+
+    async fn cached_server_binary(
+        &self,
+        _: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        unreachable!("get_language_server_command is overridden")
+    }
+
+    async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
+        None
+    }
+}

crates/extension/src/extension_store.rs 🔗

@@ -1,48 +1,118 @@
+mod extension_lsp_adapter;
+mod wasm_host;
+
+#[cfg(test)]
+mod extension_store_test;
+
 use anyhow::{anyhow, bail, Context as _, Result};
 use async_compression::futures::bufread::GzipDecoder;
 use async_tar::Archive;
 use collections::{BTreeMap, HashSet};
 use fs::{Fs, RemoveOptions};
-use futures::channel::mpsc::unbounded;
-use futures::StreamExt as _;
-use futures::{io::BufReader, AsyncReadExt as _};
+use futures::{channel::mpsc::unbounded, io::BufReader, AsyncReadExt as _, StreamExt as _};
 use gpui::{actions, AppContext, Context, Global, Model, ModelContext, Task};
 use language::{
-    LanguageConfig, LanguageMatcher, LanguageQueries, LanguageRegistry, QUERY_FILENAME_PREFIXES,
+    LanguageConfig, LanguageMatcher, LanguageQueries, LanguageRegistry, LanguageServerName,
+    QUERY_FILENAME_PREFIXES,
 };
-use parking_lot::RwLock;
+use node_runtime::NodeRuntime;
 use serde::{Deserialize, Serialize};
-use std::cmp::Ordering;
 use std::{
+    cmp::Ordering,
     ffi::OsStr,
-    path::{Path, PathBuf},
+    path::{self, Path, PathBuf},
     sync::Arc,
     time::Duration,
 };
 use theme::{ThemeRegistry, ThemeSettings};
-use util::http::{AsyncBody, HttpClientWithUrl};
-use util::TryFutureExt;
-use util::{http::HttpClient, paths::EXTENSIONS_DIR, ResultExt};
+use util::{
+    http::{AsyncBody, HttpClient, HttpClientWithUrl},
+    paths::EXTENSIONS_DIR,
+    ResultExt, TryFutureExt,
+};
+use wasm_host::{WasmExtension, WasmHost};
 
-#[cfg(test)]
-mod extension_store_test;
+use crate::{extension_lsp_adapter::ExtensionLspAdapter, wasm_host::wit};
 
 #[derive(Deserialize)]
 pub struct ExtensionsApiResponse {
-    pub data: Vec<Extension>,
+    pub data: Vec<ExtensionApiResponse>,
 }
 
 #[derive(Clone, Deserialize)]
-pub struct Extension {
+pub struct ExtensionApiResponse {
     pub id: Arc<str>,
-    pub version: Arc<str>,
     pub name: String,
+    pub version: Arc<str>,
     pub description: Option<String>,
     pub authors: Vec<String>,
     pub repository: String,
     pub download_count: usize,
 }
 
+/// This is the old version of the extension manifest, from when it was `extension.json`.
+#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
+pub struct OldExtensionManifest {
+    pub name: String,
+    pub version: Arc<str>,
+
+    #[serde(default)]
+    pub description: Option<String>,
+    #[serde(default)]
+    pub repository: Option<String>,
+    #[serde(default)]
+    pub authors: Vec<String>,
+
+    #[serde(default)]
+    pub themes: BTreeMap<Arc<str>, PathBuf>,
+    #[serde(default)]
+    pub languages: BTreeMap<Arc<str>, PathBuf>,
+    #[serde(default)]
+    pub grammars: BTreeMap<Arc<str>, PathBuf>,
+}
+
+#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
+pub struct ExtensionManifest {
+    pub id: Arc<str>,
+    pub name: String,
+    pub version: Arc<str>,
+
+    #[serde(default)]
+    pub description: Option<String>,
+    #[serde(default)]
+    pub repository: Option<String>,
+    #[serde(default)]
+    pub authors: Vec<String>,
+    #[serde(default)]
+    pub lib: LibManifestEntry,
+
+    #[serde(default)]
+    pub themes: Vec<PathBuf>,
+    #[serde(default)]
+    pub languages: Vec<PathBuf>,
+    #[serde(default)]
+    pub grammars: BTreeMap<Arc<str>, GrammarManifestEntry>,
+    #[serde(default)]
+    pub language_servers: BTreeMap<LanguageServerName, LanguageServerManifestEntry>,
+}
+
+#[derive(Clone, Default, PartialEq, Eq, Debug, Deserialize, Serialize)]
+pub struct LibManifestEntry {
+    path: Option<PathBuf>,
+}
+
+#[derive(Clone, Default, PartialEq, Eq, Debug, Deserialize, Serialize)]
+pub struct GrammarManifestEntry {
+    repository: String,
+    #[serde(alias = "commit")]
+    rev: String,
+}
+
+#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
+pub struct LanguageServerManifestEntry {
+    language: Arc<str>,
+}
+
 #[derive(Clone)]
 pub enum ExtensionStatus {
     NotInstalled,
@@ -67,7 +137,7 @@ impl ExtensionStatus {
 }
 
 pub struct ExtensionStore {
-    manifest: Arc<RwLock<Manifest>>,
+    extension_index: ExtensionIndex,
     fs: Arc<dyn Fs>,
     http_client: Arc<HttpClientWithUrl>,
     extensions_dir: PathBuf,
@@ -76,7 +146,9 @@ pub struct ExtensionStore {
     manifest_path: PathBuf,
     language_registry: Arc<LanguageRegistry>,
     theme_registry: Arc<ThemeRegistry>,
-    extension_changes: ExtensionChanges,
+    modified_extensions: HashSet<Arc<str>>,
+    wasm_host: Arc<WasmHost>,
+    wasm_extensions: Vec<(Arc<ExtensionManifest>, WasmExtension)>,
     reload_task: Option<Task<Option<()>>>,
     needs_reload: bool,
     _watch_extensions_dir: [Task<()>; 2],
@@ -86,56 +158,44 @@ struct GlobalExtensionStore(Model<ExtensionStore>);
 
 impl Global for GlobalExtensionStore {}
 
-#[derive(Debug, Deserialize, Serialize, Default)]
-pub struct Manifest {
-    pub extensions: BTreeMap<Arc<str>, Arc<str>>,
-    pub grammars: BTreeMap<Arc<str>, GrammarManifestEntry>,
-    pub languages: BTreeMap<Arc<str>, LanguageManifestEntry>,
-    pub themes: BTreeMap<Arc<str>, ThemeManifestEntry>,
+#[derive(Debug, Deserialize, Serialize, Default, PartialEq, Eq)]
+pub struct ExtensionIndex {
+    pub extensions: BTreeMap<Arc<str>, Arc<ExtensionManifest>>,
+    pub themes: BTreeMap<Arc<str>, ExtensionIndexEntry>,
+    pub languages: BTreeMap<Arc<str>, ExtensionIndexLanguageEntry>,
 }
 
-#[derive(PartialEq, Eq, Debug, PartialOrd, Ord, Deserialize, Serialize)]
-pub struct GrammarManifestEntry {
-    extension: String,
+#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Deserialize, Serialize)]
+pub struct ExtensionIndexEntry {
+    extension: Arc<str>,
     path: PathBuf,
 }
 
 #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Deserialize, Serialize)]
-pub struct LanguageManifestEntry {
-    extension: String,
+pub struct ExtensionIndexLanguageEntry {
+    extension: Arc<str>,
     path: PathBuf,
     matcher: LanguageMatcher,
     grammar: Option<Arc<str>>,
 }
 
-#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)]
-pub struct ThemeManifestEntry {
-    extension: String,
-    path: PathBuf,
-}
-
-#[derive(Default)]
-struct ExtensionChanges {
-    languages: HashSet<Arc<str>>,
-    grammars: HashSet<Arc<str>>,
-    themes: HashSet<Arc<str>>,
-}
-
 actions!(zed, [ReloadExtensions]);
 
 pub fn init(
     fs: Arc<fs::RealFs>,
     http_client: Arc<HttpClientWithUrl>,
+    node_runtime: Arc<dyn NodeRuntime>,
     language_registry: Arc<LanguageRegistry>,
     theme_registry: Arc<ThemeRegistry>,
     cx: &mut AppContext,
 ) {
-    let store = cx.new_model(|cx| {
+    let store = cx.new_model(move |cx| {
         ExtensionStore::new(
             EXTENSIONS_DIR.clone(),
-            fs.clone(),
-            http_client.clone(),
-            language_registry.clone(),
+            fs,
+            http_client,
+            node_runtime,
+            language_registry,
             theme_registry,
             cx,
         )
@@ -158,19 +218,28 @@ impl ExtensionStore {
         extensions_dir: PathBuf,
         fs: Arc<dyn Fs>,
         http_client: Arc<HttpClientWithUrl>,
+        node_runtime: Arc<dyn NodeRuntime>,
         language_registry: Arc<LanguageRegistry>,
         theme_registry: Arc<ThemeRegistry>,
         cx: &mut ModelContext<Self>,
     ) -> Self {
         let mut this = Self {
-            manifest: Default::default(),
+            extension_index: Default::default(),
             extensions_dir: extensions_dir.join("installed"),
             manifest_path: extensions_dir.join("manifest.json"),
             extensions_being_installed: Default::default(),
             extensions_being_uninstalled: Default::default(),
             reload_task: None,
+            wasm_host: WasmHost::new(
+                fs.clone(),
+                http_client.clone(),
+                node_runtime,
+                language_registry.clone(),
+                extensions_dir.join("work"),
+            ),
+            wasm_extensions: Vec::new(),
             needs_reload: false,
-            extension_changes: ExtensionChanges::default(),
+            modified_extensions: Default::default(),
             fs,
             http_client,
             language_registry,
@@ -194,7 +263,8 @@ impl ExtensionStore {
 
         if let Some(manifest_content) = manifest_content.log_err() {
             if let Some(manifest) = serde_json::from_str(&manifest_content).log_err() {
-                self.manifest_updated(manifest, cx);
+                // TODO: don't detach
+                self.extensions_updated(manifest, cx).detach();
             }
         }
 
@@ -221,11 +291,15 @@ impl ExtensionStore {
             return ExtensionStatus::Removing;
         }
 
-        let installed_version = self.manifest.read().extensions.get(extension_id).cloned();
+        let installed_version = self
+            .extension_index
+            .extensions
+            .get(extension_id)
+            .map(|manifest| manifest.version.clone());
         let is_installing = self.extensions_being_installed.contains(extension_id);
         match (installed_version, is_installing) {
             (Some(_), true) => ExtensionStatus::Upgrading,
-            (Some(version), false) => ExtensionStatus::Installed(version.clone()),
+            (Some(version), false) => ExtensionStatus::Installed(version),
             (None, true) => ExtensionStatus::Installing,
             (None, false) => ExtensionStatus::NotInstalled,
         }
@@ -235,7 +309,7 @@ impl ExtensionStore {
         &self,
         search: Option<&str>,
         cx: &mut ModelContext<Self>,
-    ) -> Task<Result<Vec<Extension>>> {
+    ) -> Task<Result<Vec<ExtensionApiResponse>>> {
         let url = self.http_client.build_zed_api_url(&format!(
             "/extensions{query}",
             query = search
@@ -335,7 +409,11 @@ impl ExtensionStore {
     /// no longer in the manifest, or whose files have changed on disk.
     /// Then it loads any themes, languages, or grammars that are newly
     /// added to the manifest, or whose files have changed on disk.
-    fn manifest_updated(&mut self, manifest: Manifest, cx: &mut ModelContext<Self>) {
+    fn extensions_updated(
+        &mut self,
+        new_index: ExtensionIndex,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<()>> {
         fn diff<'a, T, I1, I2>(
             old_keys: I1,
             new_keys: I2,
@@ -379,54 +457,104 @@ impl ExtensionStore {
             }
         }
 
-        let old_manifest = self.manifest.read();
-        let (languages_to_remove, languages_to_add) = diff(
-            old_manifest.languages.iter(),
-            manifest.languages.iter(),
-            &self.extension_changes.languages,
-        );
-        let (grammars_to_remove, grammars_to_add) = diff(
-            old_manifest.grammars.iter(),
-            manifest.grammars.iter(),
-            &self.extension_changes.grammars,
-        );
-        let (themes_to_remove, themes_to_add) = diff(
-            old_manifest.themes.iter(),
-            manifest.themes.iter(),
-            &self.extension_changes.themes,
+        let old_index = &self.extension_index;
+        let (extensions_to_unload, extensions_to_load) = diff(
+            old_index.extensions.iter(),
+            new_index.extensions.iter(),
+            &self.modified_extensions,
         );
-        self.extension_changes.clear();
-        drop(old_manifest);
+        self.modified_extensions.clear();
 
-        let themes_to_remove = &themes_to_remove
-            .into_iter()
-            .map(|theme| theme.into())
+        let themes_to_remove = old_index
+            .themes
+            .iter()
+            .filter_map(|(name, entry)| {
+                if extensions_to_unload.contains(&entry.extension) {
+                    Some(name.clone().into())
+                } else {
+                    None
+                }
+            })
+            .collect::<Vec<_>>();
+        let languages_to_remove = old_index
+            .languages
+            .iter()
+            .filter_map(|(name, entry)| {
+                if extensions_to_unload.contains(&entry.extension) {
+                    Some(name.clone())
+                } else {
+                    None
+                }
+            })
+            .collect::<Vec<_>>();
+        let empty = Default::default();
+        let grammars_to_remove = extensions_to_unload
+            .iter()
+            .flat_map(|extension_id| {
+                old_index
+                    .extensions
+                    .get(extension_id)
+                    .map_or(&empty, |extension| &extension.grammars)
+                    .keys()
+                    .cloned()
+            })
             .collect::<Vec<_>>();
+
+        self.wasm_extensions
+            .retain(|(extension, _)| !extensions_to_unload.contains(&extension.id));
+
+        for extension_id in &extensions_to_unload {
+            if let Some(extension) = old_index.extensions.get(extension_id) {
+                for (language_server_name, config) in extension.language_servers.iter() {
+                    self.language_registry
+                        .remove_lsp_adapter(config.language.as_ref(), language_server_name);
+                }
+            }
+        }
+
         self.theme_registry.remove_user_themes(&themes_to_remove);
         self.language_registry
             .remove_languages(&languages_to_remove, &grammars_to_remove);
 
-        self.language_registry
-            .register_wasm_grammars(grammars_to_add.iter().map(|grammar_name| {
-                let grammar = manifest.grammars.get(grammar_name).unwrap();
+        let languages_to_add = new_index
+            .languages
+            .iter()
+            .filter(|(_, entry)| extensions_to_load.contains(&entry.extension))
+            .collect::<Vec<_>>();
+        let mut grammars_to_add = Vec::new();
+        let mut themes_to_add = Vec::new();
+        for extension_id in &extensions_to_load {
+            let Some(extension) = new_index.extensions.get(extension_id) else {
+                continue;
+            };
+
+            grammars_to_add.extend(extension.grammars.keys().map(|grammar_name| {
                 let mut grammar_path = self.extensions_dir.clone();
-                grammar_path.extend([grammar.extension.as_ref(), grammar.path.as_path()]);
+                grammar_path.extend([extension_id.as_ref(), "grammars"]);
+                grammar_path.push(grammar_name.as_ref());
+                grammar_path.set_extension("wasm");
                 (grammar_name.clone(), grammar_path)
             }));
+            themes_to_add.extend(extension.themes.iter().map(|theme_path| {
+                let mut path = self.extensions_dir.clone();
+                path.extend([Path::new(extension_id.as_ref()), theme_path.as_path()]);
+                path
+            }));
+        }
 
-        for language_name in &languages_to_add {
-            if language_name.as_ref() == "Swift" {
-                continue;
-            }
+        self.language_registry
+            .register_wasm_grammars(grammars_to_add);
 
-            let language = manifest.languages.get(language_name.as_ref()).unwrap();
+        for (language_name, language) in languages_to_add {
             let mut language_path = self.extensions_dir.clone();
-            language_path.extend([language.extension.as_ref(), language.path.as_path()]);
+            language_path.extend([
+                Path::new(language.extension.as_ref()),
+                language.path.as_path(),
+            ]);
             self.language_registry.register_language(
                 language_name.clone(),
                 language.grammar.clone(),
                 language.matcher.clone(),
-                vec![],
                 move || {
                     let config = std::fs::read_to_string(language_path.join("config.toml"))?;
                     let config: LanguageConfig = ::toml::from_str(&config)?;
@@ -436,107 +564,119 @@ impl ExtensionStore {
             );
         }
 
-        let (reload_theme_tx, mut reload_theme_rx) = unbounded();
         let fs = self.fs.clone();
+        let wasm_host = self.wasm_host.clone();
         let root_dir = self.extensions_dir.clone();
         let theme_registry = self.theme_registry.clone();
-        let themes = themes_to_add
+        let extension_manifests = extensions_to_load
             .iter()
-            .filter_map(|name| manifest.themes.get(name).cloned())
+            .filter_map(|name| new_index.extensions.get(name).cloned())
             .collect::<Vec<_>>();
-        cx.background_executor()
-            .spawn(async move {
-                for theme in &themes {
-                    let mut theme_path = root_dir.clone();
-                    theme_path.extend([theme.extension.as_ref(), theme.path.as_path()]);
-
-                    theme_registry
-                        .load_user_theme(&theme_path, fs.clone())
-                        .await
-                        .log_err();
-                }
 
-                reload_theme_tx.unbounded_send(()).ok();
-            })
-            .detach();
+        self.extension_index = new_index;
+        cx.notify();
 
-        cx.spawn(|_, cx| async move {
-            while let Some(_) = reload_theme_rx.next().await {
-                if cx
-                    .update(|cx| ThemeSettings::reload_current_theme(cx))
-                    .is_err()
-                {
-                    break;
-                }
+        cx.spawn(|this, mut cx| async move {
+            cx.background_executor()
+                .spawn({
+                    let fs = fs.clone();
+                    async move {
+                        for theme_path in &themes_to_add {
+                            theme_registry
+                                .load_user_theme(&theme_path, fs.clone())
+                                .await
+                                .log_err();
+                        }
+                    }
+                })
+                .await;
+
+            let mut wasm_extensions = Vec::new();
+            for extension_manifest in extension_manifests {
+                let Some(wasm_path) = &extension_manifest.lib.path else {
+                    continue;
+                };
+
+                let mut path = root_dir.clone();
+                path.extend([
+                    Path::new(extension_manifest.id.as_ref()),
+                    wasm_path.as_path(),
+                ]);
+                let mut wasm_file = fs
+                    .open_sync(&path)
+                    .await
+                    .context("failed to open wasm file")?;
+                let mut wasm_bytes = Vec::new();
+                wasm_file
+                    .read_to_end(&mut wasm_bytes)
+                    .context("failed to read wasm")?;
+                let wasm_extension = wasm_host
+                    .load_extension(
+                        wasm_bytes,
+                        extension_manifest.clone(),
+                        cx.background_executor().clone(),
+                    )
+                    .await
+                    .context("failed to load wasm extension")?;
+                wasm_extensions.push((extension_manifest.clone(), wasm_extension));
             }
-        })
-        .detach();
 
-        *self.manifest.write() = manifest;
-        cx.notify();
+            this.update(&mut cx, |this, cx| {
+                for (manifest, wasm_extension) in &wasm_extensions {
+                    for (language_server_name, language_server_config) in &manifest.language_servers
+                    {
+                        this.language_registry.register_lsp_adapter(
+                            language_server_config.language.clone(),
+                            Arc::new(ExtensionLspAdapter {
+                                extension: wasm_extension.clone(),
+                                work_dir: this.wasm_host.work_dir.join(manifest.id.as_ref()),
+                                config: wit::LanguageServerConfig {
+                                    name: language_server_name.0.to_string(),
+                                    language_name: language_server_config.language.to_string(),
+                                },
+                            }),
+                        );
+                    }
+                }
+                this.wasm_extensions.extend(wasm_extensions);
+                ThemeSettings::reload_current_theme(cx)
+            })
+            .ok();
+            Ok(())
+        })
     }
 
     fn watch_extensions_dir(&self, cx: &mut ModelContext<Self>) -> [Task<()>; 2] {
-        let manifest = self.manifest.clone();
         let fs = self.fs.clone();
         let extensions_dir = self.extensions_dir.clone();
-
-        let (changes_tx, mut changes_rx) = unbounded();
+        let (changed_extensions_tx, mut changed_extensions_rx) = unbounded();
 
         let events_task = cx.background_executor().spawn(async move {
             let mut events = fs.watch(&extensions_dir, Duration::from_millis(250)).await;
             while let Some(events) = events.next().await {
-                let mut changed_grammars = HashSet::default();
-                let mut changed_languages = HashSet::default();
-                let mut changed_themes = HashSet::default();
-
-                {
-                    let manifest = manifest.read();
-                    for event in events {
-                        for (grammar_name, grammar) in &manifest.grammars {
-                            let mut grammar_path = extensions_dir.clone();
-                            grammar_path
-                                .extend([grammar.extension.as_ref(), grammar.path.as_path()]);
-                            if event.path.starts_with(&grammar_path) || event.path == grammar_path {
-                                changed_grammars.insert(grammar_name.clone());
-                            }
-                        }
-
-                        for (language_name, language) in &manifest.languages {
-                            let mut language_path = extensions_dir.clone();
-                            language_path
-                                .extend([language.extension.as_ref(), language.path.as_path()]);
-                            if event.path.starts_with(&language_path) || event.path == language_path
-                            {
-                                changed_languages.insert(language_name.clone());
-                            }
-                        }
+                for event in events {
+                    let Ok(event_path) = event.path.strip_prefix(&extensions_dir) else {
+                        continue;
+                    };
 
-                        for (theme_name, theme) in &manifest.themes {
-                            let mut theme_path = extensions_dir.clone();
-                            theme_path.extend([theme.extension.as_ref(), theme.path.as_path()]);
-                            if event.path.starts_with(&theme_path) || event.path == theme_path {
-                                changed_themes.insert(theme_name.clone());
-                            }
+                    if let Some(path::Component::Normal(extension_dir_name)) =
+                        event_path.components().next()
+                    {
+                        if let Some(extension_id) = extension_dir_name.to_str() {
+                            changed_extensions_tx
+                                .unbounded_send(Arc::from(extension_id))
+                                .ok();
                         }
                     }
                 }
-
-                changes_tx
-                    .unbounded_send(ExtensionChanges {
-                        languages: changed_languages,
-                        grammars: changed_grammars,
-                        themes: changed_themes,
-                    })
-                    .ok();
             }
         });
 
         let reload_task = cx.spawn(|this, mut cx| async move {
-            while let Some(changes) = changes_rx.next().await {
+            while let Some(changed_extension_id) = changed_extensions_rx.next().await {
                 if this
                     .update(&mut cx, |this, cx| {
-                        this.extension_changes.merge(changes);
+                        this.modified_extensions.insert(changed_extension_id);
                         this.reload(cx);
                     })
                     .is_err()
@@ -556,16 +696,18 @@ impl ExtensionStore {
         }
 
         let fs = self.fs.clone();
+        let work_dir = self.wasm_host.work_dir.clone();
         let extensions_dir = self.extensions_dir.clone();
         let manifest_path = self.manifest_path.clone();
         self.needs_reload = false;
         self.reload_task = Some(cx.spawn(|this, mut cx| {
             async move {
-                let manifest = cx
+                let extension_index = cx
                     .background_executor()
                     .spawn(async move {
-                        let mut manifest = Manifest::default();
+                        let mut index = ExtensionIndex::default();
 
+                        fs.create_dir(&work_dir).await.log_err();
                         fs.create_dir(&extensions_dir).await.log_err();
 
                         let extension_paths = fs.read_dir(&extensions_dir).await;
@@ -574,20 +716,16 @@ impl ExtensionStore {
                                 let Ok(extension_dir) = extension_dir else {
                                     continue;
                                 };
-                                Self::add_extension_to_manifest(
-                                    fs.clone(),
-                                    extension_dir,
-                                    &mut manifest,
-                                )
-                                .await
-                                .log_err();
+                                Self::add_extension_to_index(fs.clone(), extension_dir, &mut index)
+                                    .await
+                                    .log_err();
                             }
                         }
 
-                        if let Ok(manifest_json) = serde_json::to_string_pretty(&manifest) {
+                        if let Ok(index_json) = serde_json::to_string_pretty(&index) {
                             fs.save(
                                 &manifest_path,
-                                &manifest_json.as_str().into(),
+                                &index_json.as_str().into(),
                                 Default::default(),
                             )
                             .await
@@ -595,12 +733,17 @@ impl ExtensionStore {
                             .log_err();
                         }
 
-                        manifest
+                        index
                     })
                     .await;
 
+                if let Ok(task) = this.update(&mut cx, |this, cx| {
+                    this.extensions_updated(extension_index, cx)
+                }) {
+                    task.await.log_err();
+                }
+
                 this.update(&mut cx, |this, cx| {
-                    this.manifest_updated(manifest, cx);
                     this.reload_task.take();
                     if this.needs_reload {
                         this.reload(cx);
@@ -611,52 +754,65 @@ impl ExtensionStore {
         }));
     }
 
-    async fn add_extension_to_manifest(
+    async fn add_extension_to_index(
         fs: Arc<dyn Fs>,
         extension_dir: PathBuf,
-        manifest: &mut Manifest,
+        index: &mut ExtensionIndex,
     ) -> Result<()> {
         let extension_name = extension_dir
             .file_name()
             .and_then(OsStr::to_str)
             .ok_or_else(|| anyhow!("invalid extension name"))?;
 
-        #[derive(Deserialize)]
-        struct ExtensionJson {
-            pub version: String,
-        }
-
-        let extension_json_path = extension_dir.join("extension.json");
-        let extension_json = fs
-            .load(&extension_json_path)
-            .await
-            .context("failed to load extension.json")?;
-        let extension_json: ExtensionJson =
-            serde_json::from_str(&extension_json).context("invalid extension.json")?;
-
-        manifest
-            .extensions
-            .insert(extension_name.into(), extension_json.version.into());
-
-        if let Ok(mut grammar_paths) = fs.read_dir(&extension_dir.join("grammars")).await {
-            while let Some(grammar_path) = grammar_paths.next().await {
-                let grammar_path = grammar_path?;
-                let Ok(relative_path) = grammar_path.strip_prefix(&extension_dir) else {
-                    continue;
-                };
-                let Some(grammar_name) = grammar_path.file_stem().and_then(OsStr::to_str) else {
-                    continue;
-                };
-
-                manifest.grammars.insert(
-                    grammar_name.into(),
-                    GrammarManifestEntry {
-                        extension: extension_name.into(),
-                        path: relative_path.into(),
-                    },
-                );
-            }
-        }
+        let mut extension_manifest_path = extension_dir.join("extension.json");
+        let mut extension_manifest;
+        if fs.is_file(&extension_manifest_path).await {
+            let manifest_content = fs
+                .load(&extension_manifest_path)
+                .await
+                .with_context(|| format!("failed to load {extension_name} extension.json"))?;
+            let manifest_json = serde_json::from_str::<OldExtensionManifest>(&manifest_content)
+                .with_context(|| {
+                    format!("invalid extension.json for extension {extension_name}")
+                })?;
+
+            extension_manifest = ExtensionManifest {
+                id: extension_name.into(),
+                name: manifest_json.name,
+                version: manifest_json.version,
+                description: manifest_json.description,
+                repository: manifest_json.repository,
+                authors: manifest_json.authors,
+                lib: Default::default(),
+                themes: {
+                    let mut themes = manifest_json.themes.into_values().collect::<Vec<_>>();
+                    themes.sort();
+                    themes.dedup();
+                    themes
+                },
+                languages: {
+                    let mut languages = manifest_json.languages.into_values().collect::<Vec<_>>();
+                    languages.sort();
+                    languages.dedup();
+                    languages
+                },
+                grammars: manifest_json
+                    .grammars
+                    .into_iter()
+                    .map(|(grammar_name, _)| (grammar_name, Default::default()))
+                    .collect(),
+                language_servers: Default::default(),
+            };
+        } else {
+            extension_manifest_path.set_extension("toml");
+            let manifest_content = fs
+                .load(&extension_manifest_path)
+                .await
+                .with_context(|| format!("failed to load {extension_name} extension.toml"))?;
+            extension_manifest = ::toml::from_str(&manifest_content).with_context(|| {
+                format!("invalid extension.json for extension {extension_name}")
+            })?;
+        };
 
         if let Ok(mut language_paths) = fs.read_dir(&extension_dir.join("languages")).await {
             while let Some(language_path) = language_paths.next().await {
@@ -673,11 +829,16 @@ impl ExtensionStore {
                 let config = fs.load(&language_path.join("config.toml")).await?;
                 let config = ::toml::from_str::<LanguageConfig>(&config)?;
 
-                manifest.languages.insert(
+                let relative_path = relative_path.to_path_buf();
+                if !extension_manifest.languages.contains(&relative_path) {
+                    extension_manifest.languages.push(relative_path.clone());
+                }
+
+                index.languages.insert(
                     config.name.clone(),
-                    LanguageManifestEntry {
+                    ExtensionIndexLanguageEntry {
                         extension: extension_name.into(),
-                        path: relative_path.into(),
+                        path: relative_path,
                         matcher: config.matcher,
                         grammar: config.grammar,
                     },
@@ -699,32 +860,36 @@ impl ExtensionStore {
                     continue;
                 };
 
-                for theme in theme_family.themes {
-                    let location = ThemeManifestEntry {
-                        extension: extension_name.into(),
-                        path: relative_path.into(),
-                    };
+                let relative_path = relative_path.to_path_buf();
+                if !extension_manifest.themes.contains(&relative_path) {
+                    extension_manifest.themes.push(relative_path.clone());
+                }
 
-                    manifest.themes.insert(theme.name.into(), location);
+                for theme in theme_family.themes {
+                    index.themes.insert(
+                        theme.name.into(),
+                        ExtensionIndexEntry {
+                            extension: extension_name.into(),
+                            path: relative_path.clone(),
+                        },
+                    );
                 }
             }
         }
 
-        Ok(())
-    }
-}
+        let default_extension_wasm_path = extension_dir.join("extension.wasm");
+        if fs.is_file(&default_extension_wasm_path).await {
+            extension_manifest
+                .lib
+                .path
+                .get_or_insert(default_extension_wasm_path);
+        }
 
-impl ExtensionChanges {
-    fn clear(&mut self) {
-        self.grammars.clear();
-        self.languages.clear();
-        self.themes.clear();
-    }
+        index
+            .extensions
+            .insert(extension_name.into(), Arc::new(extension_manifest));
 
-    fn merge(&mut self, other: Self) {
-        self.grammars.extend(other.grammars);
-        self.languages.extend(other.languages);
-        self.themes.extend(other.themes);
+        Ok(())
     }
 }
 

crates/extension/src/extension_store_test.rs 🔗

@@ -1,14 +1,27 @@
 use crate::{
-    ExtensionStore, GrammarManifestEntry, LanguageManifestEntry, Manifest, ThemeManifestEntry,
+    ExtensionIndex, ExtensionIndexEntry, ExtensionIndexLanguageEntry, ExtensionManifest,
+    ExtensionStore, GrammarManifestEntry,
 };
-use fs::FakeFs;
+use async_compression::futures::bufread::GzipEncoder;
+use collections::BTreeMap;
+use fs::{FakeFs, Fs};
+use futures::{io::BufReader, AsyncReadExt, StreamExt};
 use gpui::{Context, TestAppContext};
-use language::{LanguageMatcher, LanguageRegistry};
+use language::{
+    Language, LanguageConfig, LanguageMatcher, LanguageRegistry, LanguageServerBinaryStatus,
+    LanguageServerName,
+};
+use node_runtime::FakeNodeRuntime;
+use project::Project;
 use serde_json::json;
 use settings::SettingsStore;
-use std::{path::PathBuf, sync::Arc};
+use std::{
+    ffi::OsString,
+    path::{Path, PathBuf},
+    sync::Arc,
+};
 use theme::ThemeRegistry;
-use util::http::FakeHttpClient;
+use util::http::{FakeHttpClient, Response};
 
 #[gpui::test]
 async fn test_extension_store(cx: &mut TestAppContext) {
@@ -29,7 +42,13 @@ async fn test_extension_store(cx: &mut TestAppContext) {
                     "extension.json": r#"{
                         "id": "zed-monokai",
                         "name": "Zed Monokai",
-                        "version": "2.0.0"
+                        "version": "2.0.0",
+                        "themes": {
+                            "Monokai Dark": "themes/monokai.json",
+                            "Monokai Light": "themes/monokai.json",
+                            "Monokai Pro Dark": "themes/monokai-pro.json",
+                            "Monokai Pro Light": "themes/monokai-pro.json"
+                        }
                     }"#,
                     "themes": {
                         "monokai.json": r#"{
@@ -70,7 +89,15 @@ async fn test_extension_store(cx: &mut TestAppContext) {
                     "extension.json": r#"{
                         "id": "zed-ruby",
                         "name": "Zed Ruby",
-                        "version": "1.0.0"
+                        "version": "1.0.0",
+                        "grammars": {
+                            "ruby": "grammars/ruby.wasm",
+                            "embedded_template": "grammars/embedded_template.wasm"
+                        },
+                        "languages": {
+                            "ruby": "languages/ruby",
+                            "erb": "languages/erb"
+                        }
                     }"#,
                     "grammars": {
                         "ruby.wasm": "",
@@ -100,27 +127,49 @@ async fn test_extension_store(cx: &mut TestAppContext) {
     )
     .await;
 
-    let mut expected_manifest = Manifest {
+    let mut expected_index = ExtensionIndex {
         extensions: [
-            ("zed-ruby".into(), "1.0.0".into()),
-            ("zed-monokai".into(), "2.0.0".into()),
-        ]
-        .into_iter()
-        .collect(),
-        grammars: [
             (
-                "embedded_template".into(),
-                GrammarManifestEntry {
-                    extension: "zed-ruby".into(),
-                    path: "grammars/embedded_template.wasm".into(),
-                },
+                "zed-ruby".into(),
+                ExtensionManifest {
+                    id: "zed-ruby".into(),
+                    name: "Zed Ruby".into(),
+                    version: "1.0.0".into(),
+                    description: None,
+                    authors: Vec::new(),
+                    repository: None,
+                    themes: Default::default(),
+                    lib: Default::default(),
+                    languages: vec!["languages/erb".into(), "languages/ruby".into()],
+                    grammars: [
+                        ("embedded_template".into(), GrammarManifestEntry::default()),
+                        ("ruby".into(), GrammarManifestEntry::default()),
+                    ]
+                    .into_iter()
+                    .collect(),
+                    language_servers: BTreeMap::default(),
+                }
+                .into(),
             ),
             (
-                "ruby".into(),
-                GrammarManifestEntry {
-                    extension: "zed-ruby".into(),
-                    path: "grammars/ruby.wasm".into(),
-                },
+                "zed-monokai".into(),
+                ExtensionManifest {
+                    id: "zed-monokai".into(),
+                    name: "Zed Monokai".into(),
+                    version: "2.0.0".into(),
+                    description: None,
+                    authors: vec![],
+                    repository: None,
+                    themes: vec![
+                        "themes/monokai-pro.json".into(),
+                        "themes/monokai.json".into(),
+                    ],
+                    lib: Default::default(),
+                    languages: Default::default(),
+                    grammars: BTreeMap::default(),
+                    language_servers: BTreeMap::default(),
+                }
+                .into(),
             ),
         ]
         .into_iter()
@@ -128,7 +177,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
         languages: [
             (
                 "ERB".into(),
-                LanguageManifestEntry {
+                ExtensionIndexLanguageEntry {
                     extension: "zed-ruby".into(),
                     path: "languages/erb".into(),
                     grammar: Some("embedded_template".into()),
@@ -140,7 +189,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
             ),
             (
                 "Ruby".into(),
-                LanguageManifestEntry {
+                ExtensionIndexLanguageEntry {
                     extension: "zed-ruby".into(),
                     path: "languages/ruby".into(),
                     grammar: Some("ruby".into()),
@@ -156,28 +205,28 @@ async fn test_extension_store(cx: &mut TestAppContext) {
         themes: [
             (
                 "Monokai Dark".into(),
-                ThemeManifestEntry {
+                ExtensionIndexEntry {
                     extension: "zed-monokai".into(),
                     path: "themes/monokai.json".into(),
                 },
             ),
             (
                 "Monokai Light".into(),
-                ThemeManifestEntry {
+                ExtensionIndexEntry {
                     extension: "zed-monokai".into(),
                     path: "themes/monokai.json".into(),
                 },
             ),
             (
                 "Monokai Pro Dark".into(),
-                ThemeManifestEntry {
+                ExtensionIndexEntry {
                     extension: "zed-monokai".into(),
                     path: "themes/monokai-pro.json".into(),
                 },
             ),
             (
                 "Monokai Pro Light".into(),
-                ThemeManifestEntry {
+                ExtensionIndexEntry {
                     extension: "zed-monokai".into(),
                     path: "themes/monokai-pro.json".into(),
                 },
@@ -189,12 +238,14 @@ async fn test_extension_store(cx: &mut TestAppContext) {
 
     let language_registry = Arc::new(LanguageRegistry::test());
     let theme_registry = Arc::new(ThemeRegistry::new(Box::new(())));
+    let node_runtime = FakeNodeRuntime::new();
 
     let store = cx.new_model(|cx| {
         ExtensionStore::new(
             PathBuf::from("/the-extension-dir"),
             fs.clone(),
             http_client.clone(),
+            node_runtime.clone(),
             language_registry.clone(),
             theme_registry.clone(),
             cx,
@@ -203,10 +254,10 @@ async fn test_extension_store(cx: &mut TestAppContext) {
 
     cx.executor().run_until_parked();
     store.read_with(cx, |store, _| {
-        let manifest = store.manifest.read();
-        assert_eq!(manifest.grammars, expected_manifest.grammars);
-        assert_eq!(manifest.languages, expected_manifest.languages);
-        assert_eq!(manifest.themes, expected_manifest.themes);
+        let index = &store.extension_index;
+        assert_eq!(index.extensions, expected_index.extensions);
+        assert_eq!(index.languages, expected_index.languages);
+        assert_eq!(index.themes, expected_index.themes);
 
         assert_eq!(
             language_registry.language_names(),
@@ -230,7 +281,10 @@ async fn test_extension_store(cx: &mut TestAppContext) {
             "extension.json": r#"{
                 "id": "zed-gruvbox",
                 "name": "Zed Gruvbox",
-                "version": "1.0.0"
+                "version": "1.0.0",
+                "themes": {
+                    "Gruvbox": "themes/gruvbox.json"
+                }
             }"#,
             "themes": {
                 "gruvbox.json": r#"{
@@ -249,9 +303,26 @@ async fn test_extension_store(cx: &mut TestAppContext) {
     )
     .await;
 
-    expected_manifest.themes.insert(
+    expected_index.extensions.insert(
+        "zed-gruvbox".into(),
+        ExtensionManifest {
+            id: "zed-gruvbox".into(),
+            name: "Zed Gruvbox".into(),
+            version: "1.0.0".into(),
+            description: None,
+            authors: vec![],
+            repository: None,
+            themes: vec!["themes/gruvbox.json".into()],
+            lib: Default::default(),
+            languages: Default::default(),
+            grammars: BTreeMap::default(),
+            language_servers: BTreeMap::default(),
+        }
+        .into(),
+    );
+    expected_index.themes.insert(
         "Gruvbox".into(),
-        ThemeManifestEntry {
+        ExtensionIndexEntry {
             extension: "zed-gruvbox".into(),
             path: "themes/gruvbox.json".into(),
         },
@@ -261,10 +332,10 @@ async fn test_extension_store(cx: &mut TestAppContext) {
 
     cx.executor().run_until_parked();
     store.read_with(cx, |store, _| {
-        let manifest = store.manifest.read();
-        assert_eq!(manifest.grammars, expected_manifest.grammars);
-        assert_eq!(manifest.languages, expected_manifest.languages);
-        assert_eq!(manifest.themes, expected_manifest.themes);
+        let index = &store.extension_index;
+        assert_eq!(index.extensions, expected_index.extensions);
+        assert_eq!(index.languages, expected_index.languages);
+        assert_eq!(index.themes, expected_index.themes);
 
         assert_eq!(
             theme_registry.list_names(false),
@@ -289,6 +360,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
             PathBuf::from("/the-extension-dir"),
             fs.clone(),
             http_client.clone(),
+            node_runtime.clone(),
             language_registry.clone(),
             theme_registry.clone(),
             cx,
@@ -297,11 +369,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
 
     cx.executor().run_until_parked();
     store.read_with(cx, |store, _| {
-        let manifest = store.manifest.read();
-        assert_eq!(manifest.grammars, expected_manifest.grammars);
-        assert_eq!(manifest.languages, expected_manifest.languages);
-        assert_eq!(manifest.themes, expected_manifest.themes);
-
+        assert_eq!(store.extension_index, expected_index);
         assert_eq!(
             language_registry.language_names(),
             ["ERB", "Plain Text", "Ruby"]
@@ -333,19 +401,204 @@ async fn test_extension_store(cx: &mut TestAppContext) {
     });
 
     cx.executor().run_until_parked();
-    expected_manifest.extensions.remove("zed-ruby");
-    expected_manifest.languages.remove("Ruby");
-    expected_manifest.languages.remove("ERB");
-    expected_manifest.grammars.remove("ruby");
-    expected_manifest.grammars.remove("embedded_template");
+    expected_index.extensions.remove("zed-ruby");
+    expected_index.languages.remove("Ruby");
+    expected_index.languages.remove("ERB");
 
     store.read_with(cx, |store, _| {
-        let manifest = store.manifest.read();
-        assert_eq!(manifest.grammars, expected_manifest.grammars);
-        assert_eq!(manifest.languages, expected_manifest.languages);
-        assert_eq!(manifest.themes, expected_manifest.themes);
-
+        assert_eq!(store.extension_index, expected_index);
         assert_eq!(language_registry.language_names(), ["Plain Text"]);
         assert_eq!(language_registry.grammar_names(), []);
     });
 }
+
+#[gpui::test]
+async fn test_extension_store_with_gleam_extension(cx: &mut TestAppContext) {
+    init_test(cx);
+
+    let gleam_extension_dir = PathBuf::from_iter([
+        env!("CARGO_MANIFEST_DIR"),
+        "..",
+        "..",
+        "extensions",
+        "gleam",
+    ])
+    .canonicalize()
+    .unwrap();
+
+    compile_extension("zed_gleam", &gleam_extension_dir);
+
+    let fs = FakeFs::new(cx.executor());
+    fs.insert_tree("/the-extension-dir", json!({ "installed": {} }))
+        .await;
+    fs.insert_tree_from_real_fs("/the-extension-dir/installed/gleam", gleam_extension_dir)
+        .await;
+
+    fs.insert_tree(
+        "/the-project-dir",
+        json!({
+            ".tool-versions": "rust 1.73.0",
+            "test.gleam": ""
+        }),
+    )
+    .await;
+
+    let project = Project::test(fs.clone(), ["/the-project-dir".as_ref()], cx).await;
+
+    let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
+    let theme_registry = Arc::new(ThemeRegistry::new(Box::new(())));
+    let node_runtime = FakeNodeRuntime::new();
+
+    let mut status_updates = language_registry.language_server_binary_statuses();
+
+    let http_client = FakeHttpClient::create({
+        move |request| async move {
+            match request.uri().to_string().as_str() {
+                "https://api.github.com/repos/gleam-lang/gleam/releases" => Ok(Response::new(
+                    json!([
+                        {
+                            "tag_name": "v1.2.3",
+                            "prerelease": false,
+                            "tarball_url": "",
+                            "zipball_url": "",
+                            "assets": [
+                                {
+                                    "name": "gleam-v1.2.3-aarch64-apple-darwin.tar.gz",
+                                    "browser_download_url": "http://example.com/the-download"
+                                }
+                            ]
+                        }
+                    ])
+                    .to_string()
+                    .into(),
+                )),
+
+                "http://example.com/the-download" => {
+                    let mut bytes = Vec::<u8>::new();
+                    let mut archive = async_tar::Builder::new(&mut bytes);
+                    let mut header = async_tar::Header::new_gnu();
+                    let content = "the-gleam-binary-contents".as_bytes();
+                    header.set_size(content.len() as u64);
+                    archive
+                        .append_data(&mut header, "gleam", content)
+                        .await
+                        .unwrap();
+                    archive.into_inner().await.unwrap();
+
+                    let mut gzipped_bytes = Vec::new();
+                    let mut encoder = GzipEncoder::new(BufReader::new(bytes.as_slice()));
+                    encoder.read_to_end(&mut gzipped_bytes).await.unwrap();
+
+                    Ok(Response::new(gzipped_bytes.into()))
+                }
+
+                _ => Ok(Response::builder().status(404).body("not found".into())?),
+            }
+        }
+    });
+
+    let _store = cx.new_model(|cx| {
+        ExtensionStore::new(
+            PathBuf::from("/the-extension-dir"),
+            fs.clone(),
+            http_client.clone(),
+            node_runtime,
+            language_registry.clone(),
+            theme_registry.clone(),
+            cx,
+        )
+    });
+
+    cx.executor().run_until_parked();
+
+    let mut fake_servers = language_registry.fake_language_servers("Gleam");
+
+    let buffer = project
+        .update(cx, |project, cx| {
+            project.open_local_buffer("/the-project-dir/test.gleam", cx)
+        })
+        .await
+        .unwrap();
+
+    project.update(cx, |project, cx| {
+        project.set_language_for_buffer(
+            &buffer,
+            Arc::new(Language::new(
+                LanguageConfig {
+                    name: "Gleam".into(),
+                    ..Default::default()
+                },
+                None,
+            )),
+            cx,
+        )
+    });
+
+    let fake_server = fake_servers.next().await.unwrap();
+
+    assert_eq!(
+        fs.load("/the-extension-dir/work/gleam/gleam-v1.2.3/gleam".as_ref())
+            .await
+            .unwrap(),
+        "the-gleam-binary-contents"
+    );
+
+    assert_eq!(
+        fake_server.binary.path,
+        PathBuf::from("/the-extension-dir/work/gleam/gleam-v1.2.3/gleam")
+    );
+    assert_eq!(fake_server.binary.arguments, [OsString::from("lsp")]);
+
+    assert_eq!(
+        [
+            status_updates.next().await.unwrap(),
+            status_updates.next().await.unwrap(),
+            status_updates.next().await.unwrap(),
+        ],
+        [
+            (
+                LanguageServerName("gleam".into()),
+                LanguageServerBinaryStatus::CheckingForUpdate
+            ),
+            (
+                LanguageServerName("gleam".into()),
+                LanguageServerBinaryStatus::Downloading
+            ),
+            (
+                LanguageServerName("gleam".into()),
+                LanguageServerBinaryStatus::Downloaded
+            )
+        ]
+    );
+}
+
+fn compile_extension(name: &str, extension_dir_path: &Path) {
+    let output = std::process::Command::new("cargo")
+        .args(["component", "build", "--target-dir"])
+        .arg(extension_dir_path.join("target"))
+        .current_dir(&extension_dir_path)
+        .output()
+        .unwrap();
+
+    assert!(
+        output.status.success(),
+        "failed to build component {}",
+        String::from_utf8_lossy(&output.stderr)
+    );
+
+    let mut wasm_path = PathBuf::from(extension_dir_path);
+    wasm_path.extend(["target", "wasm32-wasi", "debug", name]);
+    wasm_path.set_extension("wasm");
+
+    std::fs::rename(wasm_path, extension_dir_path.join("extension.wasm")).unwrap();
+}
+
+fn init_test(cx: &mut TestAppContext) {
+    cx.update(|cx| {
+        let store = SettingsStore::test(cx);
+        cx.set_global(store);
+        theme::init(theme::LoadThemes::JustBase, cx);
+        Project::init_settings(cx);
+        language::init(cx);
+    });
+}

crates/extension/src/wasm_host.rs 🔗

@@ -0,0 +1,405 @@
+use crate::ExtensionManifest;
+use anyhow::{anyhow, bail, Context as _, Result};
+use async_compression::futures::bufread::GzipDecoder;
+use async_tar::Archive;
+use async_trait::async_trait;
+use fs::Fs;
+use futures::{
+    channel::{mpsc::UnboundedSender, oneshot},
+    future::BoxFuture,
+    io::BufReader,
+    Future, FutureExt, StreamExt as _,
+};
+use gpui::BackgroundExecutor;
+use language::{LanguageRegistry, LanguageServerBinaryStatus, LspAdapterDelegate};
+use node_runtime::NodeRuntime;
+use std::{
+    path::PathBuf,
+    sync::{Arc, OnceLock},
+};
+use util::{http::HttpClient, SemanticVersion};
+use wasmtime::{
+    component::{Component, Linker, Resource, ResourceTable},
+    Engine, Store,
+};
+use wasmtime_wasi::preview2::{command as wasi_command, WasiCtx, WasiCtxBuilder, WasiView};
+
+pub mod wit {
+    wasmtime::component::bindgen!({
+        async: true,
+        path: "../extension_api/wit",
+        with: {
+             "worktree": super::ExtensionWorktree,
+        },
+    });
+}
+
+pub type ExtensionWorktree = Arc<dyn LspAdapterDelegate>;
+
+pub(crate) struct WasmHost {
+    engine: Engine,
+    linker: Arc<wasmtime::component::Linker<WasmState>>,
+    http_client: Arc<dyn HttpClient>,
+    node_runtime: Arc<dyn NodeRuntime>,
+    language_registry: Arc<LanguageRegistry>,
+    fs: Arc<dyn Fs>,
+    pub(crate) work_dir: PathBuf,
+}
+
+#[derive(Clone)]
+pub struct WasmExtension {
+    tx: UnboundedSender<ExtensionCall>,
+    #[allow(unused)]
+    zed_api_version: SemanticVersion,
+}
+
+pub(crate) struct WasmState {
+    manifest: Arc<ExtensionManifest>,
+    table: ResourceTable,
+    ctx: WasiCtx,
+    host: Arc<WasmHost>,
+}
+
+type ExtensionCall = Box<
+    dyn Send
+        + for<'a> FnOnce(&'a mut wit::Extension, &'a mut Store<WasmState>) -> BoxFuture<'a, ()>,
+>;
+
+static WASM_ENGINE: OnceLock<wasmtime::Engine> = OnceLock::new();
+
+impl WasmHost {
+    pub fn new(
+        fs: Arc<dyn Fs>,
+        http_client: Arc<dyn HttpClient>,
+        node_runtime: Arc<dyn NodeRuntime>,
+        language_registry: Arc<LanguageRegistry>,
+        work_dir: PathBuf,
+    ) -> Arc<Self> {
+        let engine = WASM_ENGINE
+            .get_or_init(|| {
+                let mut config = wasmtime::Config::new();
+                config.wasm_component_model(true);
+                config.async_support(true);
+                wasmtime::Engine::new(&config).unwrap()
+            })
+            .clone();
+        let mut linker = Linker::new(&engine);
+        wasi_command::add_to_linker(&mut linker).unwrap();
+        wit::Extension::add_to_linker(&mut linker, |state: &mut WasmState| state).unwrap();
+        Arc::new(Self {
+            engine,
+            linker: Arc::new(linker),
+            fs,
+            work_dir,
+            http_client,
+            node_runtime,
+            language_registry,
+        })
+    }
+
+    pub fn load_extension(
+        self: &Arc<Self>,
+        wasm_bytes: Vec<u8>,
+        manifest: Arc<ExtensionManifest>,
+        executor: BackgroundExecutor,
+    ) -> impl 'static + Future<Output = Result<WasmExtension>> {
+        let this = self.clone();
+        async move {
+            let component = Component::from_binary(&this.engine, &wasm_bytes)
+                .context("failed to compile wasm component")?;
+
+            let mut zed_api_version = None;
+            for part in wasmparser::Parser::new(0).parse_all(&wasm_bytes) {
+                if let wasmparser::Payload::CustomSection(s) = part? {
+                    if s.name() == "zed:api-version" {
+                        if s.data().len() != 6 {
+                            bail!(
+                                "extension {} has invalid zed:api-version section: {:?}",
+                                manifest.id,
+                                s.data()
+                            );
+                        }
+
+                        let major = u16::from_be_bytes(s.data()[0..2].try_into().unwrap()) as _;
+                        let minor = u16::from_be_bytes(s.data()[2..4].try_into().unwrap()) as _;
+                        let patch = u16::from_be_bytes(s.data()[4..6].try_into().unwrap()) as _;
+                        zed_api_version = Some(SemanticVersion {
+                            major,
+                            minor,
+                            patch,
+                        })
+                    }
+                }
+            }
+
+            let Some(zed_api_version) = zed_api_version else {
+                bail!("extension {} has no zed:api-version section", manifest.id);
+            };
+
+            let mut store = wasmtime::Store::new(
+                &this.engine,
+                WasmState {
+                    manifest,
+                    table: ResourceTable::new(),
+                    ctx: WasiCtxBuilder::new()
+                        .inherit_stdio()
+                        .env("RUST_BACKTRACE", "1")
+                        .build(),
+                    host: this.clone(),
+                },
+            );
+            let (mut extension, instance) =
+                wit::Extension::instantiate_async(&mut store, &component, &this.linker)
+                    .await
+                    .context("failed to instantiate wasm component")?;
+            let (tx, mut rx) = futures::channel::mpsc::unbounded::<ExtensionCall>();
+            executor
+                .spawn(async move {
+                    extension.call_init_extension(&mut store).await.unwrap();
+
+                    let _instance = instance;
+                    while let Some(call) = rx.next().await {
+                        (call)(&mut extension, &mut store).await;
+                    }
+                })
+                .detach();
+            Ok(WasmExtension {
+                tx,
+                zed_api_version,
+            })
+        }
+    }
+}
+
+impl WasmExtension {
+    pub async fn call<T, Fn>(&self, f: Fn) -> T
+    where
+        T: 'static + Send,
+        Fn: 'static
+            + Send
+            + for<'a> FnOnce(&'a mut wit::Extension, &'a mut Store<WasmState>) -> BoxFuture<'a, T>,
+    {
+        let (return_tx, return_rx) = oneshot::channel();
+        self.tx
+            .clone()
+            .unbounded_send(Box::new(move |extension, store| {
+                async {
+                    let result = f(extension, store).await;
+                    return_tx.send(result).ok();
+                }
+                .boxed()
+            }))
+            .expect("wasm extension channel should not be closed yet");
+        return_rx.await.expect("wasm extension channel")
+    }
+}
+
+#[async_trait]
+impl wit::HostWorktree for WasmState {
+    async fn read_text_file(
+        &mut self,
+        delegate: Resource<Arc<dyn LspAdapterDelegate>>,
+        path: String,
+    ) -> wasmtime::Result<Result<String, String>> {
+        let delegate = self.table().get(&delegate)?;
+        Ok(delegate
+            .read_text_file(path.into())
+            .await
+            .map_err(|error| error.to_string()))
+    }
+
+    fn drop(&mut self, _worktree: Resource<wit::Worktree>) -> Result<()> {
+        // we only ever hand out borrows of worktrees
+        Ok(())
+    }
+}
+
+#[async_trait]
+impl wit::ExtensionImports for WasmState {
+    async fn npm_package_latest_version(
+        &mut self,
+        package_name: String,
+    ) -> wasmtime::Result<Result<String, String>> {
+        async fn inner(this: &mut WasmState, package_name: String) -> anyhow::Result<String> {
+            this.host
+                .node_runtime
+                .npm_package_latest_version(&package_name)
+                .await
+        }
+
+        Ok(inner(self, package_name)
+            .await
+            .map_err(|err| err.to_string()))
+    }
+
+    async fn latest_github_release(
+        &mut self,
+        repo: String,
+        options: wit::GithubReleaseOptions,
+    ) -> wasmtime::Result<Result<wit::GithubRelease, String>> {
+        async fn inner(
+            this: &mut WasmState,
+            repo: String,
+            options: wit::GithubReleaseOptions,
+        ) -> anyhow::Result<wit::GithubRelease> {
+            let release = util::github::latest_github_release(
+                &repo,
+                options.require_assets,
+                options.pre_release,
+                this.host.http_client.clone(),
+            )
+            .await?;
+            Ok(wit::GithubRelease {
+                version: release.tag_name,
+                assets: release
+                    .assets
+                    .into_iter()
+                    .map(|asset| wit::GithubReleaseAsset {
+                        name: asset.name,
+                        download_url: asset.browser_download_url,
+                    })
+                    .collect(),
+            })
+        }
+
+        Ok(inner(self, repo, options)
+            .await
+            .map_err(|err| err.to_string()))
+    }
+
+    async fn current_platform(&mut self) -> Result<(wit::Os, wit::Architecture)> {
+        Ok((
+            match std::env::consts::OS {
+                "macos" => wit::Os::Mac,
+                "linux" => wit::Os::Linux,
+                "windows" => wit::Os::Windows,
+                _ => panic!("unsupported os"),
+            },
+            match std::env::consts::ARCH {
+                "aarch64" => wit::Architecture::Aarch64,
+                "x86" => wit::Architecture::X86,
+                "x86_64" => wit::Architecture::X8664,
+                _ => panic!("unsupported architecture"),
+            },
+        ))
+    }
+
+    async fn set_language_server_installation_status(
+        &mut self,
+        server_name: String,
+        status: wit::LanguageServerInstallationStatus,
+    ) -> wasmtime::Result<()> {
+        let status = match status {
+            wit::LanguageServerInstallationStatus::CheckingForUpdate => {
+                LanguageServerBinaryStatus::CheckingForUpdate
+            }
+            wit::LanguageServerInstallationStatus::Downloading => {
+                LanguageServerBinaryStatus::Downloading
+            }
+            wit::LanguageServerInstallationStatus::Downloaded => {
+                LanguageServerBinaryStatus::Downloaded
+            }
+            wit::LanguageServerInstallationStatus::Cached => LanguageServerBinaryStatus::Cached,
+            wit::LanguageServerInstallationStatus::Failed(error) => {
+                LanguageServerBinaryStatus::Failed { error }
+            }
+        };
+
+        self.host
+            .language_registry
+            .update_lsp_status(language::LanguageServerName(server_name.into()), status);
+        Ok(())
+    }
+
+    async fn download_file(
+        &mut self,
+        url: String,
+        filename: String,
+        file_type: wit::DownloadedFileType,
+    ) -> wasmtime::Result<Result<(), String>> {
+        async fn inner(
+            this: &mut WasmState,
+            url: String,
+            filename: String,
+            file_type: wit::DownloadedFileType,
+        ) -> anyhow::Result<()> {
+            this.host.fs.create_dir(&this.host.work_dir).await?;
+            let container_dir = this.host.work_dir.join(this.manifest.id.as_ref());
+            let destination_path = container_dir.join(&filename);
+
+            let mut response = this
+                .host
+                .http_client
+                .get(&url, Default::default(), true)
+                .await
+                .map_err(|err| anyhow!("error downloading release: {}", err))?;
+
+            if !response.status().is_success() {
+                Err(anyhow!(
+                    "download failed with status {}",
+                    response.status().to_string()
+                ))?;
+            }
+            let body = BufReader::new(response.body_mut());
+
+            match file_type {
+                wit::DownloadedFileType::Uncompressed => {
+                    futures::pin_mut!(body);
+                    this.host
+                        .fs
+                        .create_file_with(&destination_path, body)
+                        .await?;
+                }
+                wit::DownloadedFileType::Gzip => {
+                    let body = GzipDecoder::new(body);
+                    futures::pin_mut!(body);
+                    this.host
+                        .fs
+                        .create_file_with(&destination_path, body)
+                        .await?;
+                }
+                wit::DownloadedFileType::GzipTar => {
+                    let body = GzipDecoder::new(body);
+                    futures::pin_mut!(body);
+                    this.host
+                        .fs
+                        .extract_tar_file(&destination_path, Archive::new(body))
+                        .await?;
+                }
+                wit::DownloadedFileType::Zip => {
+                    let zip_filename = format!("{filename}.zip");
+                    let mut zip_path = destination_path.clone();
+                    zip_path.set_file_name(zip_filename);
+                    futures::pin_mut!(body);
+                    this.host.fs.create_file_with(&zip_path, body).await?;
+
+                    let unzip_status = std::process::Command::new("unzip")
+                        .current_dir(&container_dir)
+                        .arg(&zip_path)
+                        .output()?
+                        .status;
+                    if !unzip_status.success() {
+                        Err(anyhow!("failed to unzip {filename} archive"))?;
+                    }
+                }
+            }
+
+            Ok(())
+        }
+
+        Ok(inner(self, url, filename, file_type)
+            .await
+            .map(|_| ())
+            .map_err(|err| err.to_string()))
+    }
+}
+
+impl WasiView for WasmState {
+    fn table(&mut self) -> &mut ResourceTable {
+        &mut self.table
+    }
+
+    fn ctx(&mut self) -> &mut WasiCtx {
+        &mut self.ctx
+    }
+}

crates/extension_api/Cargo.toml 🔗

@@ -0,0 +1,14 @@
+[package]
+name = "zed_extension_api"
+version = "0.1.0"
+edition = "2021"
+license = "Apache-2.0"
+
+[lib]
+path = "src/extension_api.rs"
+
+[dependencies]
+wit-bindgen = "0.18"
+
+[package.metadata.component]
+target = { path = "wit" }

crates/extension_api/build.rs 🔗

@@ -0,0 +1,15 @@
+fn main() {
+    let version = std::env::var("CARGO_PKG_VERSION").unwrap();
+    let out_dir = std::env::var("OUT_DIR").unwrap();
+
+    let mut parts = version.split(|c: char| !c.is_digit(10));
+    let major = parts.next().unwrap().parse::<u16>().unwrap().to_be_bytes();
+    let minor = parts.next().unwrap().parse::<u16>().unwrap().to_be_bytes();
+    let patch = parts.next().unwrap().parse::<u16>().unwrap().to_be_bytes();
+
+    std::fs::write(
+        std::path::Path::new(&out_dir).join("version_bytes"),
+        [major[0], major[1], minor[0], minor[1], patch[0], patch[1]],
+    )
+    .unwrap();
+}

crates/extension_api/src/extension_api.rs 🔗

@@ -0,0 +1,62 @@
+pub struct Guest;
+pub use wit::*;
+
+pub type Result<T, E = String> = core::result::Result<T, E>;
+
+pub trait Extension: Send + Sync {
+    fn new() -> Self
+    where
+        Self: Sized;
+
+    fn language_server_command(
+        &mut self,
+        config: wit::LanguageServerConfig,
+        worktree: &wit::Worktree,
+    ) -> Result<Command>;
+}
+
+#[macro_export]
+macro_rules! register_extension {
+    ($extension_type:ty) => {
+        #[export_name = "init-extension"]
+        pub extern "C" fn __init_extension() {
+            zed_extension_api::register_extension(|| {
+                Box::new(<$extension_type as zed_extension_api::Extension>::new())
+            });
+        }
+    };
+}
+
+#[doc(hidden)]
+pub fn register_extension(build_extension: fn() -> Box<dyn Extension>) {
+    unsafe { EXTENSION = Some((build_extension)()) }
+}
+
+fn extension() -> &'static mut dyn Extension {
+    unsafe { EXTENSION.as_deref_mut().unwrap() }
+}
+
+static mut EXTENSION: Option<Box<dyn Extension>> = None;
+
+#[cfg(target_arch = "wasm32")]
+#[link_section = "zed:api-version"]
+#[doc(hidden)]
+pub static ZED_API_VERSION: [u8; 6] = *include_bytes!(concat!(env!("OUT_DIR"), "/version_bytes"));
+
+mod wit {
+    wit_bindgen::generate!({
+        exports: { world: super::Component },
+        skip: ["init-extension"]
+    });
+}
+
+struct Component;
+
+impl wit::Guest for Component {
+    fn language_server_command(
+        config: wit::LanguageServerConfig,
+        worktree: &wit::Worktree,
+    ) -> Result<wit::Command> {
+        extension().language_server_command(config, worktree)
+    }
+}

crates/extension_api/wit/extension.wit 🔗

@@ -0,0 +1,80 @@
+package zed:extension;
+
+world extension {
+    export init-extension: func();
+
+    record github-release {
+        version: string,
+        assets: list<github-release-asset>,
+    }
+
+    record github-release-asset {
+        name: string,
+        download-url: string,
+    }
+
+    record github-release-options {
+        require-assets: bool,
+        pre-release: bool,
+    }
+
+    enum os {
+        mac,
+        linux,
+        windows,
+    }
+
+    enum architecture {
+        aarch64,
+        x86,
+        x8664,
+    }
+
+    enum downloaded-file-type {
+        gzip,
+        gzip-tar,
+        zip,
+        uncompressed,
+    }
+
+    variant language-server-installation-status {
+        checking-for-update,
+        downloaded,
+        downloading,
+        cached,
+        failed(string),
+    }
+
+    /// Gets the current operating system and architecture
+    import current-platform: func() -> tuple<os, architecture>;
+
+    /// Gets the latest version of the given NPM package.
+    import npm-package-latest-version: func(package-name: string) -> result<string, string>;
+
+    /// Gets the latest release for the given GitHub repository.
+    import latest-github-release: func(repo: string, options: github-release-options) -> result<github-release, string>;
+
+    /// Downloads a file from the given url, and saves it to the given filename within the extension's
+    /// working directory. Extracts the file according to the given file type.
+    import download-file: func(url: string, output-filename: string, file-type: downloaded-file-type) -> result<_, string>;
+
+    /// Updates the installation status for the given language server.
+    import set-language-server-installation-status: func(language-server-name: string, status: language-server-installation-status);
+
+    record command {
+        command: string,
+        args: list<string>,
+        env: list<tuple<string, string>>,
+    }
+
+    resource worktree {
+        read-text-file: func(path: string) -> result<string, string>;
+    }
+
+    record language-server-config {
+        name: string,
+        language-name: string,
+    }
+
+    export language-server-command: func(config: language-server-config, worktree: borrow<worktree>) -> result<command, string>;
+}

crates/extensions_ui/src/extensions_ui.rs 🔗

@@ -1,6 +1,6 @@
 use client::telemetry::Telemetry;
 use editor::{Editor, EditorElement, EditorStyle};
-use extension::{Extension, ExtensionStatus, ExtensionStore};
+use extension::{ExtensionApiResponse, ExtensionStatus, ExtensionStore};
 use gpui::{
     actions, canvas, uniform_list, AnyElement, AppContext, AvailableSpace, EventEmitter,
     FocusableView, FontStyle, FontWeight, InteractiveElement, KeyContext, ParentElement, Render,
@@ -42,7 +42,7 @@ pub struct ExtensionsPage {
     telemetry: Arc<Telemetry>,
     is_fetching_extensions: bool,
     filter: ExtensionFilter,
-    extension_entries: Vec<Extension>,
+    extension_entries: Vec<ExtensionApiResponse>,
     query_editor: View<Editor>,
     query_contains_error: bool,
     _subscription: gpui::Subscription,
@@ -78,7 +78,7 @@ impl ExtensionsPage {
         })
     }
 
-    fn filtered_extension_entries(&self, cx: &mut ViewContext<Self>) -> Vec<Extension> {
+    fn filtered_extension_entries(&self, cx: &mut ViewContext<Self>) -> Vec<ExtensionApiResponse> {
         let extension_store = ExtensionStore::global(cx).read(cx);
 
         self.extension_entries
@@ -154,7 +154,7 @@ impl ExtensionsPage {
             .collect()
     }
 
-    fn render_entry(&self, extension: &Extension, cx: &mut ViewContext<Self>) -> Div {
+    fn render_entry(&self, extension: &ExtensionApiResponse, cx: &mut ViewContext<Self>) -> Div {
         let status = ExtensionStore::global(cx)
             .read(cx)
             .extension_status(&extension.id);

crates/fs/Cargo.toml 🔗

@@ -17,6 +17,7 @@ util.workspace = true
 sum_tree.workspace = true
 
 anyhow.workspace = true
+async-tar.workspace = true
 async-trait.workspace = true
 futures.workspace = true
 tempfile.workspace = true

crates/fs/src/fs.rs 🔗

@@ -14,7 +14,8 @@ use notify::{Config, EventKind, Watcher};
 #[cfg(unix)]
 use std::os::unix::fs::MetadataExt;
 
-use futures::{future::BoxFuture, Stream, StreamExt};
+use async_tar::Archive;
+use futures::{future::BoxFuture, AsyncRead, Stream, StreamExt};
 use git2::Repository as LibGitRepository;
 use parking_lot::Mutex;
 use repository::GitRepository;
@@ -43,6 +44,16 @@ use std::ffi::OsStr;
 pub trait Fs: Send + Sync {
     async fn create_dir(&self, path: &Path) -> Result<()>;
     async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()>;
+    async fn create_file_with(
+        &self,
+        path: &Path,
+        content: Pin<&mut (dyn AsyncRead + Send)>,
+    ) -> Result<()>;
+    async fn extract_tar_file(
+        &self,
+        path: &Path,
+        content: Archive<Pin<&mut (dyn AsyncRead + Send)>>,
+    ) -> Result<()>;
     async fn copy_file(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()>;
     async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()>;
     async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()>;
@@ -125,6 +136,25 @@ impl Fs for RealFs {
         Ok(())
     }
 
+    async fn create_file_with(
+        &self,
+        path: &Path,
+        content: Pin<&mut (dyn AsyncRead + Send)>,
+    ) -> Result<()> {
+        let mut file = smol::fs::File::create(&path).await?;
+        futures::io::copy(content, &mut file).await?;
+        Ok(())
+    }
+
+    async fn extract_tar_file(
+        &self,
+        path: &Path,
+        content: Archive<Pin<&mut (dyn AsyncRead + Send)>>,
+    ) -> Result<()> {
+        content.unpack(path).await?;
+        Ok(())
+    }
+
     async fn copy_file(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()> {
         if !options.overwrite && smol::fs::metadata(target).await.is_ok() {
             if options.ignore_if_exists {
@@ -429,7 +459,7 @@ enum FakeFsEntry {
     File {
         inode: u64,
         mtime: SystemTime,
-        content: String,
+        content: Vec<u8>,
     },
     Dir {
         inode: u64,
@@ -575,7 +605,7 @@ impl FakeFs {
         })
     }
 
-    pub async fn insert_file(&self, path: impl AsRef<Path>, content: String) {
+    pub async fn insert_file(&self, path: impl AsRef<Path>, content: Vec<u8>) {
         self.write_file_internal(path, content).unwrap()
     }
 
@@ -598,7 +628,7 @@ impl FakeFs {
         state.emit_event(&[path]);
     }
 
-    pub fn write_file_internal(&self, path: impl AsRef<Path>, content: String) -> Result<()> {
+    fn write_file_internal(&self, path: impl AsRef<Path>, content: Vec<u8>) -> Result<()> {
         let mut state = self.state.lock();
         let path = path.as_ref();
         let inode = state.next_inode;
@@ -625,6 +655,16 @@ impl FakeFs {
         Ok(())
     }
 
+    async fn load_internal(&self, path: impl AsRef<Path>) -> Result<Vec<u8>> {
+        let path = path.as_ref();
+        let path = normalize_path(path);
+        self.simulate_random_delay().await;
+        let state = self.state.lock();
+        let entry = state.read_path(&path)?;
+        let entry = entry.lock();
+        entry.file_content(&path).cloned()
+    }
+
     pub fn pause_events(&self) {
         self.state.lock().events_paused = true;
     }
@@ -662,7 +702,7 @@ impl FakeFs {
                     self.create_dir(path).await.unwrap();
                 }
                 String(contents) => {
-                    self.insert_file(&path, contents).await;
+                    self.insert_file(&path, contents.into_bytes()).await;
                 }
                 _ => {
                     panic!("JSON object must contain only objects, strings, or null");
@@ -672,6 +712,30 @@ impl FakeFs {
         .boxed()
     }
 
+    pub fn insert_tree_from_real_fs<'a>(
+        &'a self,
+        path: impl 'a + AsRef<Path> + Send,
+        src_path: impl 'a + AsRef<Path> + Send,
+    ) -> futures::future::BoxFuture<'a, ()> {
+        use futures::FutureExt as _;
+
+        async move {
+            let path = path.as_ref();
+            if std::fs::metadata(&src_path).unwrap().is_file() {
+                let contents = std::fs::read(src_path).unwrap();
+                self.insert_file(path, contents).await;
+            } else {
+                self.create_dir(path).await.unwrap();
+                for entry in std::fs::read_dir(&src_path).unwrap() {
+                    let entry = entry.unwrap();
+                    self.insert_tree_from_real_fs(&path.join(entry.file_name()), &entry.path())
+                        .await;
+                }
+            }
+        }
+        .boxed()
+    }
+
     pub fn with_git_state<F>(&self, dot_git: &Path, emit_git_event: bool, f: F)
     where
         F: FnOnce(&mut FakeGitRepositoryState),
@@ -832,7 +896,7 @@ impl FakeFsEntry {
         matches!(self, Self::Symlink { .. })
     }
 
-    fn file_content(&self, path: &Path) -> Result<&String> {
+    fn file_content(&self, path: &Path) -> Result<&Vec<u8>> {
         if let Self::File { content, .. } = self {
             Ok(content)
         } else {
@@ -840,7 +904,7 @@ impl FakeFsEntry {
         }
     }
 
-    fn set_file_content(&mut self, path: &Path, new_content: String) -> Result<()> {
+    fn set_file_content(&mut self, path: &Path, new_content: Vec<u8>) -> Result<()> {
         if let Self::File { content, mtime, .. } = self {
             *mtime = SystemTime::now();
             *content = new_content;
@@ -909,7 +973,7 @@ impl Fs for FakeFs {
         let file = Arc::new(Mutex::new(FakeFsEntry::File {
             inode,
             mtime,
-            content: String::new(),
+            content: Vec::new(),
         }));
         state.write_path(path, |entry| {
             match entry {
@@ -930,6 +994,36 @@ impl Fs for FakeFs {
         Ok(())
     }
 
+    async fn create_file_with(
+        &self,
+        path: &Path,
+        mut content: Pin<&mut (dyn AsyncRead + Send)>,
+    ) -> Result<()> {
+        let mut bytes = Vec::new();
+        content.read_to_end(&mut bytes).await?;
+        self.write_file_internal(path, bytes)?;
+        Ok(())
+    }
+
+    async fn extract_tar_file(
+        &self,
+        path: &Path,
+        content: Archive<Pin<&mut (dyn AsyncRead + Send)>>,
+    ) -> Result<()> {
+        let mut entries = content.entries()?;
+        while let Some(entry) = entries.next().await {
+            let mut entry = entry?;
+            if entry.header().entry_type().is_file() {
+                let path = path.join(entry.path()?.as_ref());
+                let mut bytes = Vec::new();
+                entry.read_to_end(&mut bytes).await?;
+                self.create_dir(path.parent().unwrap()).await?;
+                self.write_file_internal(&path, bytes)?;
+            }
+        }
+        Ok(())
+    }
+
     async fn rename(&self, old_path: &Path, new_path: &Path, options: RenameOptions) -> Result<()> {
         self.simulate_random_delay().await;
 
@@ -1000,7 +1094,7 @@ impl Fs for FakeFs {
                 e.insert(Arc::new(Mutex::new(FakeFsEntry::File {
                     inode,
                     mtime,
-                    content: String::new(),
+                    content: Vec::new(),
                 })))
                 .clone(),
             )),
@@ -1079,35 +1173,30 @@ impl Fs for FakeFs {
     }
 
     async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read>> {
-        let text = self.load(path).await?;
-        Ok(Box::new(io::Cursor::new(text)))
+        let bytes = self.load_internal(path).await?;
+        Ok(Box::new(io::Cursor::new(bytes)))
     }
 
     async fn load(&self, path: &Path) -> Result<String> {
-        let path = normalize_path(path);
-        self.simulate_random_delay().await;
-        let state = self.state.lock();
-        let entry = state.read_path(&path)?;
-        let entry = entry.lock();
-        entry.file_content(&path).cloned()
+        let content = self.load_internal(path).await?;
+        Ok(String::from_utf8(content.clone())?)
     }
 
     async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> {
         self.simulate_random_delay().await;
         let path = normalize_path(path.as_path());
-        self.write_file_internal(path, data.to_string())?;
-
+        self.write_file_internal(path, data.into_bytes())?;
         Ok(())
     }
 
     async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()> {
         self.simulate_random_delay().await;
         let path = normalize_path(path);
-        let content = chunks(text, line_ending).collect();
+        let content = chunks(text, line_ending).collect::<String>();
         if let Some(path) = path.parent() {
             self.create_dir(path).await?;
         }
-        self.write_file_internal(path, content)?;
+        self.write_file_internal(path, content.into_bytes())?;
         Ok(())
     }
 

crates/language/src/language.rs 🔗

@@ -22,6 +22,7 @@ pub mod markdown;
 use anyhow::{anyhow, Context, Result};
 use async_trait::async_trait;
 use collections::{HashMap, HashSet};
+use futures::Future;
 use gpui::{AppContext, AsyncAppContext, Model, Task};
 pub use highlight_map::HighlightMap;
 use lazy_static::lazy_static;
@@ -35,6 +36,7 @@ use schemars::{
 };
 use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
 use serde_json::Value;
+use smol::future::FutureExt as _;
 use std::{
     any::Any,
     cell::RefCell,
@@ -44,6 +46,7 @@ use std::{
     mem,
     ops::Range,
     path::{Path, PathBuf},
+    pin::Pin,
     str,
     sync::{
         atomic::{AtomicU64, AtomicUsize, Ordering::SeqCst},
@@ -86,7 +89,9 @@ thread_local! {
 lazy_static! {
     static ref NEXT_LANGUAGE_ID: AtomicUsize = Default::default();
     static ref NEXT_GRAMMAR_ID: AtomicUsize = Default::default();
-    static ref WASM_ENGINE: wasmtime::Engine = wasmtime::Engine::default();
+    static ref WASM_ENGINE: wasmtime::Engine = {
+        wasmtime::Engine::new(&wasmtime::Config::new()).unwrap()
+    };
 
     /// A shared grammar for plain text, exposed for reuse by downstream crates.
     pub static ref PLAIN_TEXT: Arc<Language> = Arc::new(Language::new(
@@ -106,10 +111,10 @@ pub trait ToLspPosition {
 }
 
 /// A name of a language server.
-#[derive(Clone, Debug, PartialEq, Eq, Hash)]
+#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
 pub struct LanguageServerName(pub Arc<str>);
-#[derive(Debug, Clone, PartialEq, Eq, Hash)]
 
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
 pub struct Location {
     pub buffer: Model<Buffer>,
     pub range: Range<Anchor>,
@@ -120,54 +125,44 @@ pub struct Location {
 /// once at startup, and caches the results.
 pub struct CachedLspAdapter {
     pub name: LanguageServerName,
-    pub short_name: &'static str,
     pub disk_based_diagnostic_sources: Vec<String>,
     pub disk_based_diagnostics_progress_token: Option<String>,
     pub language_ids: HashMap<String, String>,
     pub adapter: Arc<dyn LspAdapter>,
     pub reinstall_attempt_count: AtomicU64,
+    cached_binary: futures::lock::Mutex<Option<LanguageServerBinary>>,
 }
 
 impl CachedLspAdapter {
-    pub async fn new(adapter: Arc<dyn LspAdapter>) -> Arc<Self> {
+    pub fn new(adapter: Arc<dyn LspAdapter>) -> Arc<Self> {
         let name = adapter.name();
-        let short_name = adapter.short_name();
         let disk_based_diagnostic_sources = adapter.disk_based_diagnostic_sources();
         let disk_based_diagnostics_progress_token = adapter.disk_based_diagnostics_progress_token();
         let language_ids = adapter.language_ids();
 
         Arc::new(CachedLspAdapter {
             name,
-            short_name,
             disk_based_diagnostic_sources,
             disk_based_diagnostics_progress_token,
             language_ids,
             adapter,
+            cached_binary: Default::default(),
             reinstall_attempt_count: AtomicU64::new(0),
         })
     }
 
-    pub fn check_if_user_installed(
-        &self,
-        delegate: &Arc<dyn LspAdapterDelegate>,
+    pub async fn get_language_server_command(
+        self: Arc<Self>,
+        language: Arc<Language>,
+        container_dir: Arc<Path>,
+        delegate: Arc<dyn LspAdapterDelegate>,
         cx: &mut AsyncAppContext,
-    ) -> Option<Task<Option<LanguageServerBinary>>> {
-        self.adapter.check_if_user_installed(delegate, cx)
-    }
-
-    pub async fn fetch_latest_server_version(
-        &self,
-        delegate: &dyn LspAdapterDelegate,
-    ) -> Result<Box<dyn 'static + Send + Any>> {
-        self.adapter.fetch_latest_server_version(delegate).await
-    }
-
-    pub fn will_fetch_server(
-        &self,
-        delegate: &Arc<dyn LspAdapterDelegate>,
-        cx: &mut AsyncAppContext,
-    ) -> Option<Task<Result<()>>> {
-        self.adapter.will_fetch_server(delegate, cx)
+    ) -> Result<LanguageServerBinary> {
+        let cached_binary = self.cached_binary.lock().await;
+        self.adapter
+            .clone()
+            .get_language_server_command(language, container_dir, delegate, cached_binary, cx)
+            .await
     }
 
     pub fn will_start_server(
@@ -178,27 +173,6 @@ impl CachedLspAdapter {
         self.adapter.will_start_server(delegate, cx)
     }
 
-    pub async fn fetch_server_binary(
-        &self,
-        version: Box<dyn 'static + Send + Any>,
-        container_dir: PathBuf,
-        delegate: &dyn LspAdapterDelegate,
-    ) -> Result<LanguageServerBinary> {
-        self.adapter
-            .fetch_server_binary(version, container_dir, delegate)
-            .await
-    }
-
-    pub async fn cached_server_binary(
-        &self,
-        container_dir: PathBuf,
-        delegate: &dyn LspAdapterDelegate,
-    ) -> Option<LanguageServerBinary> {
-        self.adapter
-            .cached_server_binary(container_dir, delegate)
-            .await
-    }
-
     pub fn can_be_reinstalled(&self) -> bool {
         self.adapter.can_be_reinstalled()
     }
@@ -248,31 +222,124 @@ impl CachedLspAdapter {
     pub fn prettier_plugins(&self) -> &[&'static str] {
         self.adapter.prettier_plugins()
     }
+
+    #[cfg(any(test, feature = "test-support"))]
+    fn as_fake(&self) -> Option<&FakeLspAdapter> {
+        self.adapter.as_fake()
+    }
 }
 
 /// [`LspAdapterDelegate`] allows [`LspAdapter]` implementations to interface with the application
 // e.g. to display a notification or fetch data from the web.
+#[async_trait]
 pub trait LspAdapterDelegate: Send + Sync {
     fn show_notification(&self, message: &str, cx: &mut AppContext);
     fn http_client(&self) -> Arc<dyn HttpClient>;
-    fn which_command(
-        &self,
-        command: OsString,
-        cx: &AppContext,
-    ) -> Task<Option<(PathBuf, HashMap<String, String>)>>;
+    fn update_status(&self, language: LanguageServerName, status: LanguageServerBinaryStatus);
+
+    async fn which_command(&self, command: OsString) -> Option<(PathBuf, HashMap<String, String>)>;
+    async fn read_text_file(&self, path: PathBuf) -> Result<String>;
 }
 
 #[async_trait]
 pub trait LspAdapter: 'static + Send + Sync {
     fn name(&self) -> LanguageServerName;
 
-    fn short_name(&self) -> &'static str;
+    fn get_language_server_command<'a>(
+        self: Arc<Self>,
+        language: Arc<Language>,
+        container_dir: Arc<Path>,
+        delegate: Arc<dyn LspAdapterDelegate>,
+        mut cached_binary: futures::lock::MutexGuard<'a, Option<LanguageServerBinary>>,
+        cx: &'a mut AsyncAppContext,
+    ) -> Pin<Box<dyn 'a + Future<Output = Result<LanguageServerBinary>>>> {
+        async move {
+            // First we check whether the adapter can give us a user-installed binary.
+            // If so, we do *not* want to cache that, because each worktree might give us a different
+            // binary:
+            //
+            //      worktree 1: user-installed at `.bin/gopls`
+            //      worktree 2: user-installed at `~/bin/gopls`
+            //      worktree 3: no gopls found in PATH -> fallback to Zed installation
+            //
+            // We only want to cache when we fall back to the global one,
+            // because we don't want to download and overwrite our global one
+            // for each worktree we might have open.
+            if let Some(binary) = self.check_if_user_installed(delegate.as_ref()).await {
+                log::info!(
+                    "found user-installed language server for {}. path: {:?}, arguments: {:?}",
+                    language.name(),
+                    binary.path,
+                    binary.arguments
+                );
+                return Ok(binary);
+            }
+
+            if let Some(cached_binary) = cached_binary.as_ref() {
+                return Ok(cached_binary.clone());
+            }
+
+            if !container_dir.exists() {
+                smol::fs::create_dir_all(&container_dir)
+                    .await
+                    .context("failed to create container directory")?;
+            }
 
-    fn check_if_user_installed(
+            if let Some(task) = self.will_fetch_server(&delegate, cx) {
+                task.await?;
+            }
+
+            let name = self.name();
+            log::info!("fetching latest version of language server {:?}", name.0);
+            delegate.update_status(
+                name.clone(),
+                LanguageServerBinaryStatus::CheckingForUpdate,
+            );
+            let version_info = self.fetch_latest_server_version(delegate.as_ref()).await?;
+
+            log::info!("downloading language server {:?}", name.0);
+            delegate.update_status(self.name(), LanguageServerBinaryStatus::Downloading);
+            let mut binary = self
+                .fetch_server_binary(version_info, container_dir.to_path_buf(), delegate.as_ref())
+                .await;
+
+            delegate.update_status(name.clone(), LanguageServerBinaryStatus::Downloaded);
+
+            if let Err(error) = binary.as_ref() {
+                if let Some(prev_downloaded_binary) = self
+                    .cached_server_binary(container_dir.to_path_buf(), delegate.as_ref())
+                    .await
+                {
+                    delegate.update_status(name.clone(), LanguageServerBinaryStatus::Cached);
+                    log::info!(
+                        "failed to fetch newest version of language server {:?}. falling back to using {:?}",
+                        name.clone(),
+                        prev_downloaded_binary.path.display()
+                    );
+                    binary = Ok(prev_downloaded_binary);
+                } else {
+                    delegate.update_status(
+                        name.clone(),
+                        LanguageServerBinaryStatus::Failed {
+                            error: format!("{:?}", error),
+                        },
+                    );
+                }
+            }
+
+            if let Ok(binary) = &binary {
+                *cached_binary = Some(binary.clone());
+            }
+
+            binary
+        }
+        .boxed_local()
+    }
+
+    async fn check_if_user_installed(
         &self,
-        _: &Arc<dyn LspAdapterDelegate>,
-        _: &mut AsyncAppContext,
-    ) -> Option<Task<Option<LanguageServerBinary>>> {
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
         None
     }
 
@@ -384,6 +451,11 @@ pub trait LspAdapter: 'static + Send + Sync {
     fn prettier_plugins(&self) -> &[&'static str] {
         &[]
     }
+
+    #[cfg(any(test, feature = "test-support"))]
+    fn as_fake(&self) -> Option<&FakeLspAdapter> {
+        None
+    }
 }
 
 #[derive(Clone, Debug, PartialEq, Eq)]
@@ -578,6 +650,7 @@ pub struct FakeLspAdapter {
     pub disk_based_diagnostics_progress_token: Option<String>,
     pub disk_based_diagnostics_sources: Vec<String>,
     pub prettier_plugins: Vec<&'static str>,
+    pub language_server_binary: LanguageServerBinary,
 }
 
 /// Configuration of handling bracket pairs for a given language.
@@ -654,13 +727,6 @@ pub struct Language {
     pub(crate) id: LanguageId,
     pub(crate) config: LanguageConfig,
     pub(crate) grammar: Option<Arc<Grammar>>,
-    pub(crate) adapters: Vec<Arc<CachedLspAdapter>>,
-
-    #[cfg(any(test, feature = "test-support"))]
-    fake_adapter: Option<(
-        futures::channel::mpsc::UnboundedSender<lsp::FakeLanguageServer>,
-        Arc<FakeLspAdapter>,
-    )>,
 }
 
 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
@@ -775,17 +841,9 @@ impl Language {
                     highlight_map: Default::default(),
                 })
             }),
-            adapters: Vec::new(),
-
-            #[cfg(any(test, feature = "test-support"))]
-            fake_adapter: None,
         }
     }
 
-    pub fn lsp_adapters(&self) -> &[Arc<CachedLspAdapter>] {
-        &self.adapters
-    }
-
     pub fn with_queries(mut self, queries: LanguageQueries) -> Result<Self> {
         if let Some(query) = queries.highlights {
             self = self
@@ -1077,76 +1135,10 @@ impl Language {
         Arc::get_mut(self.grammar.as_mut().unwrap()).unwrap()
     }
 
-    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
-    }
-
-    #[cfg(any(test, feature = "test-support"))]
-    pub async fn set_fake_lsp_adapter(
-        &mut self,
-        fake_lsp_adapter: Arc<FakeLspAdapter>,
-    ) -> futures::channel::mpsc::UnboundedReceiver<lsp::FakeLanguageServer> {
-        let (servers_tx, servers_rx) = futures::channel::mpsc::unbounded();
-        self.fake_adapter = Some((servers_tx, fake_lsp_adapter.clone()));
-        let adapter = CachedLspAdapter::new(Arc::new(fake_lsp_adapter)).await;
-        self.adapters = vec![adapter];
-        servers_rx
-    }
-
     pub fn name(&self) -> Arc<str> {
         self.config.name.clone()
     }
 
-    pub async fn disk_based_diagnostic_sources(&self) -> &[String] {
-        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> {
-        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_completion(self: &Arc<Self>, completion: &mut lsp::CompletionItem) {
-        for adapter in &self.adapters {
-            adapter.process_completion(completion).await;
-        }
-    }
-
-    pub async fn label_for_completion(
-        self: &Arc<Self>,
-        completion: &lsp::CompletionItem,
-    ) -> Option<CodeLabel> {
-        self.adapters
-            .first()
-            .as_ref()?
-            .label_for_completion(completion, self)
-            .await
-    }
-
-    pub async fn label_for_symbol(
-        self: &Arc<Self>,
-        name: &str,
-        kind: lsp::SymbolKind,
-    ) -> Option<CodeLabel> {
-        self.adapters
-            .first()
-            .as_ref()?
-            .label_for_symbol(name, kind, self)
-            .await
-    }
-
     pub fn highlight_text<'a>(
         self: &'a Arc<Self>,
         text: &'a Rope,
@@ -1404,19 +1396,31 @@ impl Default for FakeLspAdapter {
             initialization_options: None,
             disk_based_diagnostics_sources: Vec::new(),
             prettier_plugins: Vec::new(),
+            language_server_binary: LanguageServerBinary {
+                path: "/the/fake/lsp/path".into(),
+                arguments: vec![],
+                env: Default::default(),
+            },
         }
     }
 }
 
 #[cfg(any(test, feature = "test-support"))]
 #[async_trait]
-impl LspAdapter for Arc<FakeLspAdapter> {
+impl LspAdapter for FakeLspAdapter {
     fn name(&self) -> LanguageServerName {
         LanguageServerName(self.name.into())
     }
 
-    fn short_name(&self) -> &'static str {
-        "FakeLspAdapter"
+    fn get_language_server_command<'a>(
+        self: Arc<Self>,
+        _: Arc<Language>,
+        _: Arc<Path>,
+        _: Arc<dyn LspAdapterDelegate>,
+        _: futures::lock::MutexGuard<'a, Option<LanguageServerBinary>>,
+        _: &'a mut AsyncAppContext,
+    ) -> Pin<Box<dyn 'a + Future<Output = Result<LanguageServerBinary>>>> {
+        async move { Ok(self.language_server_binary.clone()) }.boxed_local()
     }
 
     async fn fetch_latest_server_version(
@@ -1464,6 +1468,10 @@ impl LspAdapter for Arc<FakeLspAdapter> {
     fn prettier_plugins(&self) -> &[&'static str] {
         &self.prettier_plugins
     }
+
+    fn as_fake(&self) -> Option<&FakeLspAdapter> {
+        Some(self)
+    }
 }
 
 fn get_capture_indices(query: &Query, captures: &mut [(&str, &mut Option<u32>)]) {

crates/language/src/language_registry.rs 🔗

@@ -7,9 +7,9 @@ use collections::{hash_map, HashMap};
 use futures::{
     channel::{mpsc, oneshot},
     future::Shared,
-    Future, FutureExt as _, TryFutureExt as _,
+    Future, FutureExt as _,
 };
-use gpui::{AppContext, AsyncAppContext, BackgroundExecutor, Task};
+use gpui::{AppContext, BackgroundExecutor, Task};
 use lsp::{LanguageServerBinary, LanguageServerId};
 use parking_lot::{Mutex, RwLock};
 use postage::watch;
@@ -43,14 +43,19 @@ struct LanguageRegistryState {
     languages: Vec<Arc<Language>>,
     available_languages: Vec<AvailableLanguage>,
     grammars: HashMap<Arc<str>, AvailableGrammar>,
+    lsp_adapters: HashMap<Arc<str>, Vec<Arc<CachedLspAdapter>>>,
     loading_languages: HashMap<LanguageId, Vec<oneshot::Sender<Result<Arc<Language>>>>>,
     subscription: (watch::Sender<()>, watch::Receiver<()>),
     theme: Option<Arc<Theme>>,
     version: usize,
     reload_count: usize,
+
+    #[cfg(any(test, feature = "test-support"))]
+    fake_server_txs:
+        HashMap<Arc<str>, Vec<futures::channel::mpsc::UnboundedSender<lsp::FakeLanguageServer>>>,
 }
 
-#[derive(Clone)]
+#[derive(Clone, Debug, PartialEq, Eq)]
 pub enum LanguageServerBinaryStatus {
     CheckingForUpdate,
     Downloading,
@@ -72,7 +77,6 @@ struct AvailableLanguage {
     grammar: Option<Arc<str>>,
     matcher: LanguageMatcher,
     load: Arc<dyn Fn() -> Result<(LanguageConfig, LanguageQueries)> + 'static + Send + Sync>,
-    lsp_adapters: Vec<Arc<dyn LspAdapter>>,
     loaded: bool,
 }
 
@@ -112,7 +116,7 @@ pub struct LanguageQueries {
 
 #[derive(Clone, Default)]
 struct LspBinaryStatusSender {
-    txs: Arc<Mutex<Vec<mpsc::UnboundedSender<(Arc<Language>, LanguageServerBinaryStatus)>>>>,
+    txs: Arc<Mutex<Vec<mpsc::UnboundedSender<(LanguageServerName, LanguageServerBinaryStatus)>>>>,
 }
 
 impl LanguageRegistry {
@@ -124,10 +128,14 @@ impl LanguageRegistry {
                 available_languages: Default::default(),
                 grammars: Default::default(),
                 loading_languages: Default::default(),
+                lsp_adapters: Default::default(),
                 subscription: watch::channel(),
                 theme: Default::default(),
                 version: 0,
                 reload_count: 0,
+
+                #[cfg(any(test, feature = "test-support"))]
+                fake_server_txs: Default::default(),
             }),
             language_server_download_dir: None,
             login_shell_env_loaded: login_shell_env_loaded.shared(),
@@ -139,7 +147,9 @@ impl LanguageRegistry {
 
     #[cfg(any(test, feature = "test-support"))]
     pub fn test() -> Self {
-        Self::new(Task::ready(()))
+        let mut this = Self::new(Task::ready(()));
+        this.language_server_download_dir = Some(Path::new("/the-download-dir").into());
+        this
     }
 
     pub fn set_executor(&mut self, executor: BackgroundExecutor) {
@@ -162,24 +172,71 @@ impl LanguageRegistry {
             .remove_languages(languages_to_remove, grammars_to_remove)
     }
 
+    pub fn remove_lsp_adapter(&self, language_name: &str, name: &LanguageServerName) {
+        let mut state = self.state.write();
+        if let Some(adapters) = state.lsp_adapters.get_mut(language_name) {
+            adapters.retain(|adapter| &adapter.name != name)
+        }
+        state.version += 1;
+        state.reload_count += 1;
+        *state.subscription.0.borrow_mut() = ();
+    }
+
     #[cfg(any(feature = "test-support", test))]
     pub fn register_test_language(&self, config: LanguageConfig) {
         self.register_language(
             config.name.clone(),
             config.grammar.clone(),
             config.matcher.clone(),
-            vec![],
             move || Ok((config.clone(), Default::default())),
         )
     }
 
+    pub fn register_lsp_adapter(&self, language_name: Arc<str>, adapter: Arc<dyn LspAdapter>) {
+        self.state
+            .write()
+            .lsp_adapters
+            .entry(language_name)
+            .or_default()
+            .push(CachedLspAdapter::new(adapter));
+    }
+
+    #[cfg(any(feature = "test-support", test))]
+    pub fn register_fake_lsp_adapter(
+        &self,
+        language_name: &str,
+        adapter: crate::FakeLspAdapter,
+    ) -> futures::channel::mpsc::UnboundedReceiver<lsp::FakeLanguageServer> {
+        self.state
+            .write()
+            .lsp_adapters
+            .entry(language_name.into())
+            .or_default()
+            .push(CachedLspAdapter::new(Arc::new(adapter)));
+        self.fake_language_servers(language_name)
+    }
+
+    #[cfg(any(feature = "test-support", test))]
+    pub fn fake_language_servers(
+        &self,
+        language_name: &str,
+    ) -> futures::channel::mpsc::UnboundedReceiver<lsp::FakeLanguageServer> {
+        let (servers_tx, servers_rx) = futures::channel::mpsc::unbounded();
+        self.state
+            .write()
+            .fake_server_txs
+            .entry(language_name.into())
+            .or_default()
+            .push(servers_tx);
+        servers_rx
+    }
+
     /// Adds a language to the registry, which can be loaded if needed.
     pub fn register_language(
         &self,
         name: Arc<str>,
         grammar_name: Option<Arc<str>>,
         matcher: LanguageMatcher,
-        lsp_adapters: Vec<Arc<dyn LspAdapter>>,
         load: impl Fn() -> Result<(LanguageConfig, LanguageQueries)> + 'static + Send + Sync,
     ) {
         let load = Arc::new(load);
@@ -189,7 +246,6 @@ impl LanguageRegistry {
             if existing_language.name == name {
                 existing_language.grammar = grammar_name;
                 existing_language.matcher = matcher;
-                existing_language.lsp_adapters = lsp_adapters;
                 existing_language.load = load;
                 return;
             }
@@ -201,7 +257,6 @@ impl LanguageRegistry {
             grammar: grammar_name,
             matcher,
             load,
-            lsp_adapters,
             loaded: false,
         });
         state.version += 1;
@@ -376,10 +431,7 @@ impl LanguageRegistry {
                                         None
                                     };
 
-                                    Language::new_with_id(id, config, grammar)
-                                        .with_lsp_adapters(language.lsp_adapters)
-                                        .await
-                                        .with_queries(queries)
+                                    Language::new_with_id(id, config, grammar).with_queries(queries)
                                 }
                                 .await;
 
@@ -492,6 +544,23 @@ impl LanguageRegistry {
         self.state.read().languages.iter().cloned().collect()
     }
 
+    pub fn lsp_adapters(&self, language: &Arc<Language>) -> Vec<Arc<CachedLspAdapter>> {
+        self.state
+            .read()
+            .lsp_adapters
+            .get(&language.config.name)
+            .cloned()
+            .unwrap_or_default()
+    }
+
+    pub fn update_lsp_status(
+        &self,
+        server_name: LanguageServerName,
+        status: LanguageServerBinaryStatus,
+    ) {
+        self.lsp_binary_status_tx.send(server_name, status);
+    }
+
     pub fn create_pending_language_server(
         self: &Arc<Self>,
         stderr_capture: Arc<Mutex<Option<String>>>,
@@ -507,100 +576,85 @@ impl LanguageRegistry {
             adapter.name.0
         );
 
-        #[cfg(any(test, feature = "test-support"))]
-        if language.fake_adapter.is_some() {
-            let task = cx.spawn(|cx| async move {
-                let (servers_tx, fake_adapter) = language.fake_adapter.as_ref().unwrap();
-                let (server, mut fake_server) = lsp::FakeLanguageServer::new(
-                    fake_adapter.name.to_string(),
-                    fake_adapter.capabilities.clone(),
-                    cx.clone(),
-                );
-
-                if let Some(initializer) = &fake_adapter.initializer {
-                    initializer(&mut fake_server);
-                }
-
-                let servers_tx = servers_tx.clone();
-                cx.background_executor()
-                    .spawn(async move {
-                        if fake_server
-                            .try_receive_notification::<lsp::notification::Initialized>()
-                            .await
-                            .is_some()
-                        {
-                            servers_tx.unbounded_send(fake_server).ok();
-                        }
-                    })
-                    .detach();
-
-                Ok(server)
-            });
-
-            return Some(PendingLanguageServer {
-                server_id,
-                task,
-                container_dir: None,
-            });
-        }
-
         let download_dir = self
             .language_server_download_dir
             .clone()
             .ok_or_else(|| anyhow!("language server download directory has not been assigned before starting server"))
             .log_err()?;
-        let this = self.clone();
         let language = language.clone();
         let container_dir: Arc<Path> = Arc::from(download_dir.join(adapter.name.0.as_ref()));
         let root_path = root_path.clone();
-        let adapter = adapter.clone();
         let login_shell_env_loaded = self.login_shell_env_loaded.clone();
-        let lsp_binary_statuses = self.lsp_binary_status_tx.clone();
+        let this = Arc::downgrade(self);
 
-        let task = {
+        let task = cx.spawn({
             let container_dir = container_dir.clone();
-            cx.spawn(move |mut cx| async move {
-                // First we check whether the adapter can give us a user-installed binary.
-                // If so, we do *not* want to cache that, because each worktree might give us a different
-                // binary:
-                //
-                //      worktree 1: user-installed at `.bin/gopls`
-                //      worktree 2: user-installed at `~/bin/gopls`
-                //      worktree 3: no gopls found in PATH -> fallback to Zed installation
-                //
-                // We only want to cache when we fall back to the global one,
-                // because we don't want to download and overwrite our global one
-                // for each worktree we might have open.
-
-                let user_binary_task = check_user_installed_binary(
-                    adapter.clone(),
-                    language.clone(),
-                    delegate.clone(),
-                    &mut cx,
-                );
-                let binary = if let Some(user_binary) = user_binary_task.await {
-                    user_binary
-                } else {
-                    // If we want to install a binary globally, we need to wait for
-                    // the login shell to be set on our process.
-                    login_shell_env_loaded.await;
-
-                    get_or_install_binary(
-                        this,
-                        &adapter,
-                        language,
-                        &delegate,
-                        &cx,
+            move |mut cx| async move {
+                // If we want to install a binary globally, we need to wait for
+                // the login shell to be set on our process.
+                login_shell_env_loaded.await;
+
+                let binary = adapter
+                    .clone()
+                    .get_language_server_command(
+                        language.clone(),
                         container_dir,
-                        lsp_binary_statuses,
+                        delegate.clone(),
+                        &mut cx,
                     )
-                    .await?
-                };
+                    .await?;
 
                 if let Some(task) = adapter.will_start_server(&delegate, &mut cx) {
                     task.await?;
                 }
 
+                #[cfg(any(test, feature = "test-support"))]
+                if true {
+                    let capabilities = adapter
+                        .as_fake()
+                        .map(|fake_adapter| fake_adapter.capabilities.clone())
+                        .unwrap_or_default();
+
+                    let (server, mut fake_server) = lsp::FakeLanguageServer::new(
+                        binary,
+                        adapter.name.0.to_string(),
+                        capabilities,
+                        cx.clone(),
+                    );
+
+                    if let Some(fake_adapter) = adapter.as_fake() {
+                        if let Some(initializer) = &fake_adapter.initializer {
+                            initializer(&mut fake_server);
+                        }
+                    }
+
+                    cx.background_executor()
+                        .spawn(async move {
+                            if fake_server
+                                .try_receive_notification::<lsp::notification::Initialized>()
+                                .await
+                                .is_some()
+                            {
+                                if let Some(this) = this.upgrade() {
+                                    if let Some(txs) = this
+                                        .state
+                                        .write()
+                                        .fake_server_txs
+                                        .get_mut(language.name().as_ref())
+                                    {
+                                        for tx in txs {
+                                            tx.unbounded_send(fake_server.clone()).ok();
+                                        }
+                                    }
+                                }
+                            }
+                        })
+                        .detach();
+
+                    return Ok(server);
+                }
+
+                drop(this);
                 lsp::LanguageServer::new(
                     stderr_capture,
                     server_id,
@@ -609,8 +663,8 @@ impl LanguageRegistry {
                     adapter.code_action_kinds(),
                     cx,
                 )
-            })
-        };
+            }
+        });
 
         Some(PendingLanguageServer {
             server_id,
@@ -621,7 +675,7 @@ impl LanguageRegistry {
 
     pub fn language_server_binary_statuses(
         &self,
-    ) -> mpsc::UnboundedReceiver<(Arc<Language>, LanguageServerBinaryStatus)> {
+    ) -> mpsc::UnboundedReceiver<(LanguageServerName, LanguageServerBinaryStatus)> {
         self.lsp_binary_status_tx.subscribe()
     }
 
@@ -718,158 +772,16 @@ impl LanguageRegistryState {
 }
 
 impl LspBinaryStatusSender {
-    fn subscribe(&self) -> mpsc::UnboundedReceiver<(Arc<Language>, LanguageServerBinaryStatus)> {
+    fn subscribe(
+        &self,
+    ) -> mpsc::UnboundedReceiver<(LanguageServerName, LanguageServerBinaryStatus)> {
         let (tx, rx) = mpsc::unbounded();
         self.txs.lock().push(tx);
         rx
     }
 
-    fn send(&self, language: Arc<Language>, status: LanguageServerBinaryStatus) {
+    fn send(&self, name: LanguageServerName, status: LanguageServerBinaryStatus) {
         let mut txs = self.txs.lock();
-        txs.retain(|tx| {
-            tx.unbounded_send((language.clone(), status.clone()))
-                .is_ok()
-        });
-    }
-}
-
-async fn check_user_installed_binary(
-    adapter: Arc<CachedLspAdapter>,
-    language: Arc<Language>,
-    delegate: Arc<dyn LspAdapterDelegate>,
-    cx: &mut AsyncAppContext,
-) -> Option<LanguageServerBinary> {
-    let Some(task) = adapter.check_if_user_installed(&delegate, cx) else {
-        return None;
-    };
-
-    task.await.and_then(|binary| {
-        log::info!(
-            "found user-installed language server for {}. path: {:?}, arguments: {:?}",
-            language.name(),
-            binary.path,
-            binary.arguments
-        );
-        Some(binary)
-    })
-}
-
-async fn get_or_install_binary(
-    registry: Arc<LanguageRegistry>,
-    adapter: &Arc<CachedLspAdapter>,
-    language: Arc<Language>,
-    delegate: &Arc<dyn LspAdapterDelegate>,
-    cx: &AsyncAppContext,
-    container_dir: Arc<Path>,
-    lsp_binary_statuses: LspBinaryStatusSender,
-) -> Result<LanguageServerBinary> {
-    let entry = registry
-        .lsp_binary_paths
-        .lock()
-        .entry(adapter.name.clone())
-        .or_insert_with(|| {
-            let adapter = adapter.clone();
-            let language = language.clone();
-            let delegate = delegate.clone();
-            cx.spawn(|cx| {
-                get_binary(
-                    adapter,
-                    language,
-                    delegate,
-                    container_dir,
-                    lsp_binary_statuses,
-                    cx,
-                )
-                .map_err(Arc::new)
-            })
-            .shared()
-        })
-        .clone();
-
-    entry.await.map_err(|err| anyhow!("{:?}", err))
-}
-
-async fn get_binary(
-    adapter: Arc<CachedLspAdapter>,
-    language: Arc<Language>,
-    delegate: Arc<dyn LspAdapterDelegate>,
-    container_dir: Arc<Path>,
-    statuses: LspBinaryStatusSender,
-    mut cx: AsyncAppContext,
-) -> Result<LanguageServerBinary> {
-    if !container_dir.exists() {
-        smol::fs::create_dir_all(&container_dir)
-            .await
-            .context("failed to create container directory")?;
-    }
-
-    if let Some(task) = adapter.will_fetch_server(&delegate, &mut cx) {
-        task.await?;
-    }
-
-    let binary = fetch_latest_binary(
-        adapter.clone(),
-        language.clone(),
-        delegate.as_ref(),
-        &container_dir,
-        statuses.clone(),
-    )
-    .await;
-
-    if let Err(error) = binary.as_ref() {
-        if let Some(binary) = adapter
-            .cached_server_binary(container_dir.to_path_buf(), delegate.as_ref())
-            .await
-        {
-            statuses.send(language.clone(), LanguageServerBinaryStatus::Cached);
-            log::info!(
-                "failed to fetch newest version of language server {:?}. falling back to using {:?}",
-                adapter.name,
-                binary.path.display()
-            );
-            return Ok(binary);
-        }
-
-        statuses.send(
-            language.clone(),
-            LanguageServerBinaryStatus::Failed {
-                error: format!("{:?}", error),
-            },
-        );
+        txs.retain(|tx| tx.unbounded_send((name.clone(), status.clone())).is_ok());
     }
-
-    binary
-}
-
-async fn fetch_latest_binary(
-    adapter: Arc<CachedLspAdapter>,
-    language: Arc<Language>,
-    delegate: &dyn LspAdapterDelegate,
-    container_dir: &Path,
-    lsp_binary_statuses_tx: LspBinaryStatusSender,
-) -> Result<LanguageServerBinary> {
-    let container_dir: Arc<Path> = container_dir.into();
-
-    lsp_binary_statuses_tx.send(
-        language.clone(),
-        LanguageServerBinaryStatus::CheckingForUpdate,
-    );
-
-    log::info!(
-        "querying GitHub for latest version of language server {:?}",
-        adapter.name.0
-    );
-    let version_info = adapter.fetch_latest_server_version(delegate).await?;
-    lsp_binary_statuses_tx.send(language.clone(), LanguageServerBinaryStatus::Downloading);
-
-    log::info!(
-        "checking if Zed already installed or fetching version for language server {:?}",
-        adapter.name.0
-    );
-    let binary = adapter
-        .fetch_server_binary(version_info, container_dir.to_path_buf(), delegate)
-        .await?;
-    lsp_binary_statuses_tx.send(language.clone(), LanguageServerBinaryStatus::Downloaded);
-
-    Ok(binary)
 }

crates/language/src/proto.rs 🔗

@@ -2,7 +2,7 @@
 
 use crate::{
     diagnostic_set::DiagnosticEntry, CodeAction, CodeLabel, Completion, CursorShape, Diagnostic,
-    Language,
+    Language, LanguageRegistry,
 };
 use anyhow::{anyhow, Result};
 use clock::ReplicaId;
@@ -487,6 +487,7 @@ pub fn serialize_completion(completion: &Completion) -> proto::Completion {
 pub async fn deserialize_completion(
     completion: proto::Completion,
     language: Option<Arc<Language>>,
+    language_registry: &Arc<LanguageRegistry>,
 ) -> Result<Completion> {
     let old_start = completion
         .old_start
@@ -500,7 +501,11 @@ pub async fn deserialize_completion(
 
     let mut label = None;
     if let Some(language) = language {
-        label = language.label_for_completion(&lsp_completion).await;
+        if let Some(adapter) = language_registry.lsp_adapters(&language).first() {
+            label = adapter
+                .label_for_completion(&lsp_completion, &language)
+                .await;
+        }
     }
 
     Ok(Completion {

crates/language_tools/src/lsp_log_tests.rs 🔗

@@ -20,7 +20,20 @@ async fn test_lsp_logs(cx: &mut TestAppContext) {
 
     init_test(cx);
 
-    let mut rust_language = Language::new(
+    let fs = FakeFs::new(cx.background_executor.clone());
+    fs.insert_tree(
+        "/the-root",
+        json!({
+            "test.rs": "",
+            "package.json": "",
+        }),
+    )
+    .await;
+
+    let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
+
+    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+    language_registry.add(Arc::new(Language::new(
         LanguageConfig {
             name: "Rust".into(),
             matcher: LanguageMatcher {
@@ -30,27 +43,14 @@ async fn test_lsp_logs(cx: &mut TestAppContext) {
             ..Default::default()
         },
         Some(tree_sitter_rust::language()),
-    );
-    let mut fake_rust_servers = rust_language
-        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+    )));
+    let mut fake_rust_servers = language_registry.register_fake_lsp_adapter(
+        "Rust",
+        FakeLspAdapter {
             name: "the-rust-language-server",
             ..Default::default()
-        }))
-        .await;
-
-    let fs = FakeFs::new(cx.background_executor.clone());
-    fs.insert_tree(
-        "/the-root",
-        json!({
-            "test.rs": "",
-            "package.json": "",
-        }),
-    )
-    .await;
-    let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
-    project.update(cx, |project, _| {
-        project.languages().add(Arc::new(rust_language));
-    });
+        },
+    );
 
     let log_store = cx.new_model(|cx| LogStore::new(cx));
     log_store.update(cx, |store, cx| store.add_project(&project, cx));

crates/languages/src/astro.rs 🔗

@@ -36,10 +36,6 @@ impl LspAdapter for AstroLspAdapter {
         LanguageServerName("astro-language-server".into())
     }
 
-    fn short_name(&self) -> &'static str {
-        "astro"
-    }
-
     async fn fetch_latest_server_version(
         &self,
         _: &dyn LspAdapterDelegate,

crates/languages/src/c.rs 🔗

@@ -20,10 +20,6 @@ impl super::LspAdapter for CLspAdapter {
         LanguageServerName("clangd".into())
     }
 
-    fn short_name(&self) -> &'static str {
-        "clangd"
-    }
-
     async fn fetch_latest_server_version(
         &self,
         delegate: &dyn LspAdapterDelegate,
@@ -296,7 +292,7 @@ mod tests {
                 });
             });
         });
-        let language = crate::language("c", tree_sitter_c::language(), None).await;
+        let language = crate::language("c", tree_sitter_c::language());
 
         cx.new_model(|cx| {
             let mut buffer = Buffer::new(0, BufferId::new(cx.entity_id().as_u64()).unwrap(), "")

crates/languages/src/clojure.rs 🔗

@@ -18,10 +18,6 @@ impl super::LspAdapter for ClojureLspAdapter {
         LanguageServerName("clojure-lsp".into())
     }
 
-    fn short_name(&self) -> &'static str {
-        "clojure"
-    }
-
     async fn fetch_latest_server_version(
         &self,
         delegate: &dyn LspAdapterDelegate,

crates/languages/src/csharp.rs 🔗

@@ -21,10 +21,6 @@ impl super::LspAdapter for OmniSharpAdapter {
         LanguageServerName("OmniSharp".into())
     }
 
-    fn short_name(&self) -> &'static str {
-        "OmniSharp"
-    }
-
     async fn fetch_latest_server_version(
         &self,
         delegate: &dyn LspAdapterDelegate,

crates/languages/src/css.rs 🔗

@@ -37,10 +37,6 @@ impl LspAdapter for CssLspAdapter {
         LanguageServerName("vscode-css-language-server".into())
     }
 
-    fn short_name(&self) -> &'static str {
-        "css"
-    }
-
     async fn fetch_latest_server_version(
         &self,
         _: &dyn LspAdapterDelegate,

crates/languages/src/dart.rs 🔗

@@ -19,10 +19,6 @@ impl LspAdapter for DartLanguageServer {
         LanguageServerName("dart".into())
     }
 
-    fn short_name(&self) -> &'static str {
-        "dart"
-    }
-
     async fn fetch_latest_server_version(
         &self,
         _: &dyn LspAdapterDelegate,

crates/languages/src/deno.rs 🔗

@@ -62,10 +62,6 @@ impl LspAdapter for DenoLspAdapter {
         LanguageServerName("deno-language-server".into())
     }
 
-    fn short_name(&self) -> &'static str {
-        "deno-ts"
-    }
-
     async fn fetch_latest_server_version(
         &self,
         delegate: &dyn LspAdapterDelegate,

crates/languages/src/dockerfile.rs 🔗

@@ -36,10 +36,6 @@ impl LspAdapter for DockerfileLspAdapter {
         LanguageServerName("docker-langserver".into())
     }
 
-    fn short_name(&self) -> &'static str {
-        "dockerfile"
-    }
-
     async fn fetch_latest_server_version(
         &self,
         _: &dyn LspAdapterDelegate,

crates/languages/src/elixir.rs 🔗

@@ -71,10 +71,6 @@ impl LspAdapter for ElixirLspAdapter {
         LanguageServerName("elixir-ls".into())
     }
 
-    fn short_name(&self) -> &'static str {
-        "elixir-ls"
-    }
-
     fn will_start_server(
         &self,
         delegate: &Arc<dyn LspAdapterDelegate>,
@@ -302,10 +298,6 @@ impl LspAdapter for NextLspAdapter {
         LanguageServerName("next-ls".into())
     }
 
-    fn short_name(&self) -> &'static str {
-        "next-ls"
-    }
-
     async fn fetch_latest_server_version(
         &self,
         delegate: &dyn LspAdapterDelegate,
@@ -460,10 +452,6 @@ impl LspAdapter for LocalLspAdapter {
         LanguageServerName("local-ls".into())
     }
 
-    fn short_name(&self) -> &'static str {
-        "local-ls"
-    }
-
     async fn fetch_latest_server_version(
         &self,
         _: &dyn LspAdapterDelegate,

crates/languages/src/elm.rs 🔗

@@ -40,10 +40,6 @@ impl LspAdapter for ElmLspAdapter {
         LanguageServerName(SERVER_NAME.into())
     }
 
-    fn short_name(&self) -> &'static str {
-        "elmLS"
-    }
-
     async fn fetch_latest_server_version(
         &self,
         _: &dyn LspAdapterDelegate,

crates/languages/src/erlang.rs 🔗

@@ -12,10 +12,6 @@ impl LspAdapter for ErlangLspAdapter {
         LanguageServerName("erlang_ls".into())
     }
 
-    fn short_name(&self) -> &'static str {
-        "erlang_ls"
-    }
-
     async fn fetch_latest_server_version(
         &self,
         _: &dyn LspAdapterDelegate,

crates/languages/src/gleam.rs 🔗

@@ -27,10 +27,6 @@ impl LspAdapter for GleamLspAdapter {
         LanguageServerName("gleam".into())
     }
 
-    fn short_name(&self) -> &'static str {
-        "gleam"
-    }
-
     async fn fetch_latest_server_version(
         &self,
         delegate: &dyn LspAdapterDelegate,

crates/languages/src/go.rs 🔗

@@ -38,10 +38,6 @@ impl super::LspAdapter for GoLspAdapter {
         LanguageServerName("gopls".into())
     }
 
-    fn short_name(&self) -> &'static str {
-        "gopls"
-    }
-
     async fn fetch_latest_server_version(
         &self,
         delegate: &dyn LspAdapterDelegate,
@@ -58,23 +54,16 @@ impl super::LspAdapter for GoLspAdapter {
         Ok(Box::new(version) as Box<_>)
     }
 
-    fn check_if_user_installed(
+    async fn check_if_user_installed(
         &self,
-        delegate: &Arc<dyn LspAdapterDelegate>,
-        cx: &mut AsyncAppContext,
-    ) -> Option<Task<Option<LanguageServerBinary>>> {
-        let delegate = delegate.clone();
-
-        Some(cx.spawn(|cx| async move {
-            match cx.update(|cx| delegate.which_command(OsString::from("gopls"), cx)) {
-                Ok(task) => task.await.map(|(path, env)| LanguageServerBinary {
-                    path,
-                    arguments: server_binary_arguments(),
-                    env: Some(env),
-                }),
-                Err(_) => None,
-            }
-        }))
+        delegate: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        let (path, env) = delegate.which_command(OsString::from("gopls")).await?;
+        Some(LanguageServerBinary {
+            path,
+            arguments: server_binary_arguments(),
+            env: Some(env),
+        })
     }
 
     fn will_fetch_server(
@@ -423,12 +412,8 @@ mod tests {
 
     #[gpui::test]
     async fn test_go_label_for_completion() {
-        let language = language(
-            "go",
-            tree_sitter_go::language(),
-            Some(Arc::new(GoLspAdapter)),
-        )
-        .await;
+        let adapter = Arc::new(GoLspAdapter);
+        let language = language("go", tree_sitter_go::language());
 
         let theme = SyntaxTheme::new_test([
             ("type", Hsla::default()),
@@ -446,13 +431,16 @@ mod tests {
         let highlight_number = grammar.highlight_id_for_name("number").unwrap();
 
         assert_eq!(
-            language
-                .label_for_completion(&lsp::CompletionItem {
-                    kind: Some(lsp::CompletionItemKind::FUNCTION),
-                    label: "Hello".to_string(),
-                    detail: Some("func(a B) c.D".to_string()),
-                    ..Default::default()
-                })
+            adapter
+                .label_for_completion(
+                    &lsp::CompletionItem {
+                        kind: Some(lsp::CompletionItemKind::FUNCTION),
+                        label: "Hello".to_string(),
+                        detail: Some("func(a B) c.D".to_string()),
+                        ..Default::default()
+                    },
+                    &language
+                )
                 .await,
             Some(CodeLabel {
                 text: "Hello(a B) c.D".to_string(),
@@ -467,13 +455,16 @@ mod tests {
 
         // Nested methods
         assert_eq!(
-            language
-                .label_for_completion(&lsp::CompletionItem {
-                    kind: Some(lsp::CompletionItemKind::METHOD),
-                    label: "one.two.Three".to_string(),
-                    detail: Some("func() [3]interface{}".to_string()),
-                    ..Default::default()
-                })
+            adapter
+                .label_for_completion(
+                    &lsp::CompletionItem {
+                        kind: Some(lsp::CompletionItemKind::METHOD),
+                        label: "one.two.Three".to_string(),
+                        detail: Some("func() [3]interface{}".to_string()),
+                        ..Default::default()
+                    },
+                    &language
+                )
                 .await,
             Some(CodeLabel {
                 text: "one.two.Three() [3]interface{}".to_string(),
@@ -488,13 +479,16 @@ mod tests {
 
         // Nested fields
         assert_eq!(
-            language
-                .label_for_completion(&lsp::CompletionItem {
-                    kind: Some(lsp::CompletionItemKind::FIELD),
-                    label: "two.Three".to_string(),
-                    detail: Some("a.Bcd".to_string()),
-                    ..Default::default()
-                })
+            adapter
+                .label_for_completion(
+                    &lsp::CompletionItem {
+                        kind: Some(lsp::CompletionItemKind::FIELD),
+                        label: "two.Three".to_string(),
+                        detail: Some("a.Bcd".to_string()),
+                        ..Default::default()
+                    },
+                    &language
+                )
                 .await,
             Some(CodeLabel {
                 text: "two.Three a.Bcd".to_string(),

crates/languages/src/haskell.rs 🔗

@@ -12,10 +12,6 @@ impl LspAdapter for HaskellLanguageServer {
         LanguageServerName("hls".into())
     }
 
-    fn short_name(&self) -> &'static str {
-        "hls"
-    }
-
     async fn fetch_latest_server_version(
         &self,
         _: &dyn LspAdapterDelegate,

crates/languages/src/html.rs 🔗

@@ -37,10 +37,6 @@ impl LspAdapter for HtmlLspAdapter {
         LanguageServerName("vscode-html-language-server".into())
     }
 
-    fn short_name(&self) -> &'static str {
-        "html"
-    }
-
     async fn fetch_latest_server_version(
         &self,
         _: &dyn LspAdapterDelegate,

crates/languages/src/json.rs 🔗

@@ -90,10 +90,6 @@ impl LspAdapter for JsonLspAdapter {
         LanguageServerName("json-language-server".into())
     }
 
-    fn short_name(&self) -> &'static str {
-        "json"
-    }
-
     async fn fetch_latest_server_version(
         &self,
         _: &dyn LspAdapterDelegate,

crates/languages/src/lib.rs 🔗

@@ -122,15 +122,17 @@ pub fn init(
         ("dart", tree_sitter_dart::language()),
     ]);
 
-    let language = |asset_dir_name: &'static str, adapters| {
+    let language = |asset_dir_name: &'static str, adapters: Vec<Arc<dyn LspAdapter>>| {
         let config = load_config(asset_dir_name);
+        for adapter in adapters {
+            languages.register_lsp_adapter(config.name.clone(), adapter);
+        }
         languages.register_language(
             config.name.clone(),
             config.grammar.clone(),
             config.matcher.clone(),
-            adapters,
             move || Ok((config.clone(), load_queries(asset_dir_name))),
-        )
+        );
     };
 
     language(
@@ -329,15 +331,9 @@ pub fn init(
 }
 
 #[cfg(any(test, feature = "test-support"))]
-pub async fn language(
-    name: &str,
-    grammar: tree_sitter::Language,
-    lsp_adapter: Option<Arc<dyn LspAdapter>>,
-) -> Arc<Language> {
+pub fn language(name: &str, grammar: tree_sitter::Language) -> Arc<Language> {
     Arc::new(
         Language::new(load_config(name), Some(grammar))
-            .with_lsp_adapters(lsp_adapter.into_iter().collect())
-            .await
             .with_queries(load_queries(name))
             .unwrap(),
     )

crates/languages/src/lua.rs 🔗

@@ -22,10 +22,6 @@ impl super::LspAdapter for LuaLspAdapter {
         LanguageServerName("lua-language-server".into())
     }
 
-    fn short_name(&self) -> &'static str {
-        "lua"
-    }
-
     async fn fetch_latest_server_version(
         &self,
         delegate: &dyn LspAdapterDelegate,

crates/languages/src/nu.rs 🔗

@@ -12,10 +12,6 @@ impl LspAdapter for NuLanguageServer {
         LanguageServerName("nu".into())
     }
 
-    fn short_name(&self) -> &'static str {
-        "nu"
-    }
-
     async fn fetch_latest_server_version(
         &self,
         _: &dyn LspAdapterDelegate,

crates/languages/src/ocaml.rs 🔗

@@ -18,10 +18,6 @@ impl LspAdapter for OCamlLspAdapter {
         LanguageServerName("ocamllsp".into())
     }
 
-    fn short_name(&self) -> &'static str {
-        "ocaml"
-    }
-
     async fn fetch_latest_server_version(
         &self,
         _: &dyn LspAdapterDelegate,

crates/languages/src/php.rs 🔗

@@ -40,10 +40,6 @@ impl LspAdapter for IntelephenseLspAdapter {
         LanguageServerName("intelephense".into())
     }
 
-    fn short_name(&self) -> &'static str {
-        "php"
-    }
-
     async fn fetch_latest_server_version(
         &self,
         _delegate: &dyn LspAdapterDelegate,

crates/languages/src/prisma.rs 🔗

@@ -35,10 +35,6 @@ impl LspAdapter for PrismaLspAdapter {
         LanguageServerName("prisma-language-server".into())
     }
 
-    fn short_name(&self) -> &'static str {
-        "prisma-language-server"
-    }
-
     async fn fetch_latest_server_version(
         &self,
         _: &dyn LspAdapterDelegate,

crates/languages/src/purescript.rs 🔗

@@ -39,10 +39,6 @@ impl LspAdapter for PurescriptLspAdapter {
         LanguageServerName("purescript-language-server".into())
     }
 
-    fn short_name(&self) -> &'static str {
-        "purescript"
-    }
-
     async fn fetch_latest_server_version(
         &self,
         _: &dyn LspAdapterDelegate,

crates/languages/src/python.rs 🔗

@@ -34,10 +34,6 @@ impl LspAdapter for PythonLspAdapter {
         LanguageServerName("pyright".into())
     }
 
-    fn short_name(&self) -> &'static str {
-        "pyright"
-    }
-
     async fn fetch_latest_server_version(
         &self,
         _: &dyn LspAdapterDelegate,
@@ -188,7 +184,7 @@ mod tests {
     #[gpui::test]
     async fn test_python_autoindent(cx: &mut TestAppContext) {
         cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX);
-        let language = crate::language("python", tree_sitter_python::language(), None).await;
+        let language = crate::language("python", tree_sitter_python::language());
         cx.update(|cx| {
             let test_settings = SettingsStore::test(cx);
             cx.set_global(test_settings);

crates/languages/src/ruby.rs 🔗

@@ -12,10 +12,6 @@ impl LspAdapter for RubyLanguageServer {
         LanguageServerName("solargraph".into())
     }
 
-    fn short_name(&self) -> &'static str {
-        "solargraph"
-    }
-
     async fn fetch_latest_server_version(
         &self,
         _: &dyn LspAdapterDelegate,

crates/languages/src/rust.rs 🔗

@@ -23,10 +23,6 @@ impl LspAdapter for RustLspAdapter {
         LanguageServerName("rust-analyzer".into())
     }
 
-    fn short_name(&self) -> &'static str {
-        "rust"
-    }
-
     async fn fetch_latest_server_version(
         &self,
         delegate: &dyn LspAdapterDelegate,
@@ -360,12 +356,8 @@ mod tests {
 
     #[gpui::test]
     async fn test_rust_label_for_completion() {
-        let language = language(
-            "rust",
-            tree_sitter_rust::language(),
-            Some(Arc::new(RustLspAdapter)),
-        )
-        .await;
+        let adapter = Arc::new(RustLspAdapter);
+        let language = language("rust", tree_sitter_rust::language());
         let grammar = language.grammar().unwrap();
         let theme = SyntaxTheme::new_test([
             ("type", Hsla::default()),
@@ -382,13 +374,16 @@ mod tests {
         let highlight_field = grammar.highlight_id_for_name("property").unwrap();
 
         assert_eq!(
-            language
-                .label_for_completion(&lsp::CompletionItem {
-                    kind: Some(lsp::CompletionItemKind::FUNCTION),
-                    label: "hello(…)".to_string(),
-                    detail: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
-                    ..Default::default()
-                })
+            adapter
+                .label_for_completion(
+                    &lsp::CompletionItem {
+                        kind: Some(lsp::CompletionItemKind::FUNCTION),
+                        label: "hello(…)".to_string(),
+                        detail: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
+                        ..Default::default()
+                    },
+                    &language
+                )
                 .await,
             Some(CodeLabel {
                 text: "hello(&mut Option<T>) -> Vec<T>".to_string(),
@@ -404,13 +399,16 @@ mod tests {
             })
         );
         assert_eq!(
-            language
-                .label_for_completion(&lsp::CompletionItem {
-                    kind: Some(lsp::CompletionItemKind::FUNCTION),
-                    label: "hello(…)".to_string(),
-                    detail: Some("async fn(&mut Option<T>) -> Vec<T>".to_string()),
-                    ..Default::default()
-                })
+            adapter
+                .label_for_completion(
+                    &lsp::CompletionItem {
+                        kind: Some(lsp::CompletionItemKind::FUNCTION),
+                        label: "hello(…)".to_string(),
+                        detail: Some("async fn(&mut Option<T>) -> Vec<T>".to_string()),
+                        ..Default::default()
+                    },
+                    &language
+                )
                 .await,
             Some(CodeLabel {
                 text: "hello(&mut Option<T>) -> Vec<T>".to_string(),
@@ -426,13 +424,16 @@ mod tests {
             })
         );
         assert_eq!(
-            language
-                .label_for_completion(&lsp::CompletionItem {
-                    kind: Some(lsp::CompletionItemKind::FIELD),
-                    label: "len".to_string(),
-                    detail: Some("usize".to_string()),
-                    ..Default::default()
-                })
+            adapter
+                .label_for_completion(
+                    &lsp::CompletionItem {
+                        kind: Some(lsp::CompletionItemKind::FIELD),
+                        label: "len".to_string(),
+                        detail: Some("usize".to_string()),
+                        ..Default::default()
+                    },
+                    &language
+                )
                 .await,
             Some(CodeLabel {
                 text: "len: usize".to_string(),
@@ -442,13 +443,16 @@ mod tests {
         );
 
         assert_eq!(
-            language
-                .label_for_completion(&lsp::CompletionItem {
-                    kind: Some(lsp::CompletionItemKind::FUNCTION),
-                    label: "hello(…)".to_string(),
-                    detail: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
-                    ..Default::default()
-                })
+            adapter
+                .label_for_completion(
+                    &lsp::CompletionItem {
+                        kind: Some(lsp::CompletionItemKind::FUNCTION),
+                        label: "hello(…)".to_string(),
+                        detail: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
+                        ..Default::default()
+                    },
+                    &language
+                )
                 .await,
             Some(CodeLabel {
                 text: "hello(&mut Option<T>) -> Vec<T>".to_string(),
@@ -467,12 +471,8 @@ mod tests {
 
     #[gpui::test]
     async fn test_rust_label_for_symbol() {
-        let language = language(
-            "rust",
-            tree_sitter_rust::language(),
-            Some(Arc::new(RustLspAdapter)),
-        )
-        .await;
+        let adapter = Arc::new(RustLspAdapter);
+        let language = language("rust", tree_sitter_rust::language());
         let grammar = language.grammar().unwrap();
         let theme = SyntaxTheme::new_test([
             ("type", Hsla::default()),
@@ -488,8 +488,8 @@ mod tests {
         let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
 
         assert_eq!(
-            language
-                .label_for_symbol("hello", lsp::SymbolKind::FUNCTION)
+            adapter
+                .label_for_symbol("hello", lsp::SymbolKind::FUNCTION, &language)
                 .await,
             Some(CodeLabel {
                 text: "fn hello".to_string(),
@@ -499,8 +499,8 @@ mod tests {
         );
 
         assert_eq!(
-            language
-                .label_for_symbol("World", lsp::SymbolKind::TYPE_PARAMETER)
+            adapter
+                .label_for_symbol("World", lsp::SymbolKind::TYPE_PARAMETER, &language)
                 .await,
             Some(CodeLabel {
                 text: "type World".to_string(),
@@ -524,7 +524,7 @@ mod tests {
             });
         });
 
-        let language = crate::language("rust", tree_sitter_rust::language(), None).await;
+        let language = crate::language("rust", tree_sitter_rust::language());
 
         cx.new_model(|cx| {
             let mut buffer = Buffer::new(0, BufferId::new(cx.entity_id().as_u64()).unwrap(), "")

crates/languages/src/svelte.rs 🔗

@@ -36,10 +36,6 @@ impl LspAdapter for SvelteLspAdapter {
         LanguageServerName("svelte-language-server".into())
     }
 
-    fn short_name(&self) -> &'static str {
-        "svelte"
-    }
-
     async fn fetch_latest_server_version(
         &self,
         _: &dyn LspAdapterDelegate,

crates/languages/src/tailwind.rs 🔗

@@ -38,10 +38,6 @@ impl LspAdapter for TailwindLspAdapter {
         LanguageServerName("tailwindcss-language-server".into())
     }
 
-    fn short_name(&self) -> &'static str {
-        "tailwind"
-    }
-
     async fn fetch_latest_server_version(
         &self,
         _: &dyn LspAdapterDelegate,

crates/languages/src/terraform.rs 🔗

@@ -5,7 +5,7 @@ use futures::StreamExt;
 pub use language::*;
 use lsp::{CodeActionKind, LanguageServerBinary};
 use smol::fs::{self, File};
-use std::{any::Any, ffi::OsString, path::PathBuf, str};
+use std::{any::Any, ffi::OsString, path::PathBuf};
 use util::{
     async_maybe,
     fs::remove_matching,
@@ -25,10 +25,6 @@ impl LspAdapter for TerraformLspAdapter {
         LanguageServerName("terraform-ls".into())
     }
 
-    fn short_name(&self) -> &'static str {
-        "terraform-ls"
-    }
-
     async fn fetch_latest_server_version(
         &self,
         delegate: &dyn LspAdapterDelegate,

crates/languages/src/toml.rs 🔗

@@ -18,10 +18,6 @@ impl LspAdapter for TaploLspAdapter {
         LanguageServerName("taplo-ls".into())
     }
 
-    fn short_name(&self) -> &'static str {
-        "taplo-ls"
-    }
-
     async fn fetch_latest_server_version(
         &self,
         delegate: &dyn LspAdapterDelegate,

crates/languages/src/typescript.rs 🔗

@@ -56,10 +56,6 @@ impl LspAdapter for TypeScriptLspAdapter {
         LanguageServerName("typescript-language-server".into())
     }
 
-    fn short_name(&self) -> &'static str {
-        "tsserver"
-    }
-
     async fn fetch_latest_server_version(
         &self,
         _: &dyn LspAdapterDelegate,
@@ -283,10 +279,6 @@ impl LspAdapter for EsLintLspAdapter {
         LanguageServerName(Self::SERVER_NAME.into())
     }
 
-    fn short_name(&self) -> &'static str {
-        "eslint"
-    }
-
     async fn fetch_latest_server_version(
         &self,
         delegate: &dyn LspAdapterDelegate,
@@ -409,12 +401,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_outline(cx: &mut TestAppContext) {
-        let language = crate::language(
-            "typescript",
-            tree_sitter_typescript::language_typescript(),
-            None,
-        )
-        .await;
+        let language = crate::language("typescript", tree_sitter_typescript::language_typescript());
 
         let text = r#"
             function a() {

crates/languages/src/uiua.rs 🔗

@@ -12,10 +12,6 @@ impl LspAdapter for UiuaLanguageServer {
         LanguageServerName("uiua".into())
     }
 
-    fn short_name(&self) -> &'static str {
-        "uiua"
-    }
-
     async fn fetch_latest_server_version(
         &self,
         _: &dyn LspAdapterDelegate,

crates/languages/src/vue.rs 🔗

@@ -44,10 +44,6 @@ impl super::LspAdapter for VueLspAdapter {
         LanguageServerName("vue-language-server".into())
     }
 
-    fn short_name(&self) -> &'static str {
-        "vue-language-server"
-    }
-
     async fn fetch_latest_server_version(
         &self,
         _: &dyn LspAdapterDelegate,

crates/languages/src/yaml.rs 🔗

@@ -39,10 +39,6 @@ impl LspAdapter for YamlLspAdapter {
         LanguageServerName("yaml-language-server".into())
     }
 
-    fn short_name(&self) -> &'static str {
-        "yaml"
-    }
-
     async fn fetch_latest_server_version(
         &self,
         _: &dyn LspAdapterDelegate,

crates/languages/src/zig.rs 🔗

@@ -3,13 +3,11 @@ use async_compression::futures::bufread::GzipDecoder;
 use async_tar::Archive;
 use async_trait::async_trait;
 use futures::{io::BufReader, StreamExt};
-use gpui::{AsyncAppContext, Task};
 use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
 use lsp::LanguageServerBinary;
 use smol::fs;
 use std::env::consts::{ARCH, OS};
 use std::ffi::OsString;
-use std::sync::Arc;
 use std::{any::Any, path::PathBuf};
 use util::async_maybe;
 use util::github::latest_github_release;
@@ -23,10 +21,6 @@ impl LspAdapter for ZlsAdapter {
         LanguageServerName("zls".into())
     }
 
-    fn short_name(&self) -> &'static str {
-        "zls"
-    }
-
     async fn fetch_latest_server_version(
         &self,
         delegate: &dyn LspAdapterDelegate,
@@ -47,23 +41,16 @@ impl LspAdapter for ZlsAdapter {
         Ok(Box::new(version) as Box<_>)
     }
 
-    fn check_if_user_installed(
+    async fn check_if_user_installed(
         &self,
-        delegate: &Arc<dyn LspAdapterDelegate>,
-        cx: &mut AsyncAppContext,
-    ) -> Option<Task<Option<LanguageServerBinary>>> {
-        let delegate = delegate.clone();
-
-        Some(cx.spawn(|cx| async move {
-            match cx.update(|cx| delegate.which_command(OsString::from("zls"), cx)) {
-                Ok(task) => task.await.map(|(path, env)| LanguageServerBinary {
-                    path,
-                    arguments: vec![],
-                    env: Some(env),
-                }),
-                Err(_) => None,
-            }
-        }))
+        delegate: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        let (path, env) = delegate.which_command(OsString::from("zls")).await?;
+        Some(LanguageServerBinary {
+            path,
+            arguments: vec![],
+            env: Some(env),
+        })
     }
 
     async fn fetch_server_binary(

crates/lsp/src/lsp.rs 🔗

@@ -616,11 +616,11 @@ impl LanguageServer {
                 uri: root_uri,
                 name: Default::default(),
             }]),
-            client_info: Some(ClientInfo {
-                name: release_channel::ReleaseChannel::global(cx)
-                    .display_name()
-                    .to_string(),
-                version: Some(release_channel::AppVersion::global(cx).to_string()),
+            client_info: release_channel::ReleaseChannel::try_global(cx).map(|release_channel| {
+                ClientInfo {
+                    name: release_channel.display_name().to_string(),
+                    version: Some(release_channel::AppVersion::global(cx).to_string()),
+                }
             }),
             locale: None,
         };
@@ -1055,6 +1055,7 @@ impl Drop for Subscription {
 #[cfg(any(test, feature = "test-support"))]
 #[derive(Clone)]
 pub struct FakeLanguageServer {
+    pub binary: LanguageServerBinary,
     pub server: Arc<LanguageServer>,
     notifications_rx: channel::Receiver<(String, String)>,
 }
@@ -1063,6 +1064,7 @@ pub struct FakeLanguageServer {
 impl FakeLanguageServer {
     /// Construct a fake language server.
     pub fn new(
+        binary: LanguageServerBinary,
         name: String,
         capabilities: ServerCapabilities,
         cx: AsyncAppContext,
@@ -1084,6 +1086,7 @@ impl FakeLanguageServer {
             |_| {},
         );
         let fake = FakeLanguageServer {
+            binary,
             server: Arc::new(LanguageServer::new_internal(
                 LanguageServerId(0),
                 stdout_writer,
@@ -1302,8 +1305,16 @@ mod tests {
         cx.update(|cx| {
             release_channel::init("0.0.0", cx);
         });
-        let (server, mut fake) =
-            FakeLanguageServer::new("the-lsp".to_string(), Default::default(), cx.to_async());
+        let (server, mut fake) = FakeLanguageServer::new(
+            LanguageServerBinary {
+                path: "path/to/language-server".into(),
+                arguments: vec![],
+                env: None,
+            },
+            "the-lsp".to_string(),
+            Default::default(),
+            cx.to_async(),
+        );
 
         let (message_tx, message_rx) = channel::unbounded();
         let (diagnostics_tx, diagnostics_rx) = channel::unbounded();

crates/prettier/src/prettier.rs 🔗

@@ -2,7 +2,7 @@ use anyhow::Context;
 use collections::{HashMap, HashSet};
 use fs::Fs;
 use gpui::{AsyncAppContext, Model};
-use language::{language_settings::language_settings, Buffer, Diff};
+use language::{language_settings::language_settings, Buffer, Diff, LanguageRegistry};
 use lsp::{LanguageServer, LanguageServerId};
 use node_runtime::NodeRuntime;
 use serde::{Deserialize, Serialize};
@@ -25,6 +25,7 @@ pub struct RealPrettier {
     default: bool,
     prettier_dir: PathBuf,
     server: Arc<LanguageServer>,
+    language_registry: Arc<LanguageRegistry>,
 }
 
 #[cfg(any(test, feature = "test-support"))]
@@ -155,6 +156,7 @@ impl Prettier {
         _: LanguageServerId,
         prettier_dir: PathBuf,
         _: Arc<dyn NodeRuntime>,
+        _: Arc<LanguageRegistry>,
         _: AsyncAppContext,
     ) -> anyhow::Result<Self> {
         Ok(Self::Test(TestPrettier {
@@ -168,6 +170,7 @@ impl Prettier {
         server_id: LanguageServerId,
         prettier_dir: PathBuf,
         node: Arc<dyn NodeRuntime>,
+        language_registry: Arc<LanguageRegistry>,
         cx: AsyncAppContext,
     ) -> anyhow::Result<Self> {
         use lsp::LanguageServerBinary;
@@ -206,6 +209,7 @@ impl Prettier {
         Ok(Self::Real(RealPrettier {
             server,
             default: prettier_dir == DEFAULT_PRETTIER_DIR.as_path(),
+            language_registry,
             prettier_dir,
         }))
     }
@@ -223,10 +227,12 @@ impl Prettier {
                         let buffer_language = buffer.language();
                         let parser_with_plugins = buffer_language.and_then(|l| {
                             let prettier_parser = l.prettier_parser_name()?;
-                            let mut prettier_plugins = l
-                                .lsp_adapters()
+                            let mut prettier_plugins = local
+                                .language_registry
+                                .lsp_adapters(l)
                                 .iter()
                                 .flat_map(|adapter| adapter.prettier_plugins())
+                                .copied()
                                 .collect::<Vec<_>>();
                             prettier_plugins.dedup();
                             Some((prettier_parser, prettier_plugins))
@@ -264,7 +270,7 @@ impl Prettier {
 
                                 let mut plugins = plugins
                                     .into_iter()
-                                    .filter(|&&plugin_name| {
+                                    .filter(|&plugin_name| {
                                         if plugin_name == TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME {
                                             add_tailwind_back = true;
                                             false

crates/project/src/lsp_command.rs 🔗

@@ -1472,6 +1472,12 @@ impl LspCommand for GetCompletions {
             Default::default()
         };
 
+        let language_server_adapter = project
+            .update(&mut cx, |project, _cx| {
+                project.language_server_adapter_for_id(server_id)
+            })?
+            .ok_or_else(|| anyhow!("no such language server"))?;
+
         let completions = buffer.update(&mut cx, |buffer, cx| {
             let language_registry = project.read(cx).languages().clone();
             let language = buffer.language().cloned();
@@ -1559,12 +1565,17 @@ impl LspCommand for GetCompletions {
 
                     let language_registry = language_registry.clone();
                     let language = language.clone();
+                    let language_server_adapter = language_server_adapter.clone();
                     LineEnding::normalize(&mut new_text);
                     Some(async move {
                         let mut label = None;
-                        if let Some(language) = language.as_ref() {
-                            language.process_completion(&mut lsp_completion).await;
-                            label = language.label_for_completion(&lsp_completion).await;
+                        if let Some(language) = &language {
+                            language_server_adapter
+                                .process_completion(&mut lsp_completion)
+                                .await;
+                            label = language_server_adapter
+                                .label_for_completion(&lsp_completion, language)
+                                .await;
                         }
 
                         let documentation = if let Some(lsp_docs) = &lsp_completion.documentation {
@@ -1651,7 +1662,7 @@ impl LspCommand for GetCompletions {
     async fn response_from_proto(
         self,
         message: proto::GetCompletionsResponse,
-        _: Model<Project>,
+        project: Model<Project>,
         buffer: Model<Buffer>,
         mut cx: AsyncAppContext,
     ) -> Result<Vec<Completion>> {
@@ -1662,8 +1673,13 @@ impl LspCommand for GetCompletions {
             .await?;
 
         let language = buffer.update(&mut cx, |buffer, _| buffer.language().cloned())?;
+        let language_registry = project.update(&mut cx, |project, _| project.languages.clone())?;
         let completions = message.completions.into_iter().map(|completion| {
-            language::proto::deserialize_completion(completion, language.clone())
+            language::proto::deserialize_completion(
+                completion,
+                language.clone(),
+                &language_registry,
+            )
         });
         future::try_join_all(completions).await
     }

crates/project/src/prettier_support.rs 🔗

@@ -14,7 +14,7 @@ use futures::{
 use gpui::{AsyncAppContext, Model, ModelContext, Task, WeakModel};
 use language::{
     language_settings::{Formatter, LanguageSettings},
-    Buffer, Language, LanguageServerName, LocalFile,
+    Buffer, Language, LanguageRegistry, LanguageServerName, LocalFile,
 };
 use lsp::{LanguageServer, LanguageServerId};
 use node_runtime::NodeRuntime;
@@ -26,7 +26,8 @@ use crate::{
 };
 
 pub fn prettier_plugins_for_language(
-    language: &Language,
+    language_registry: &Arc<LanguageRegistry>,
+    language: &Arc<Language>,
     language_settings: &LanguageSettings,
 ) -> Option<HashSet<&'static str>> {
     match &language_settings.formatter {
@@ -38,8 +39,8 @@ pub fn prettier_plugins_for_language(
         prettier_plugins
             .get_or_insert_with(|| HashSet::default())
             .extend(
-                language
-                    .lsp_adapters()
+                language_registry
+                    .lsp_adapters(language)
                     .iter()
                     .flat_map(|adapter| adapter.prettier_plugins()),
             )
@@ -303,15 +304,20 @@ fn start_prettier(
 ) -> PrettierTask {
     cx.spawn(|project, mut cx| async move {
         log::info!("Starting prettier at path {prettier_dir:?}");
-        let new_server_id = project.update(&mut cx, |project, _| {
-            project.languages.next_language_server_id()
-        })?;
-
-        let new_prettier = Prettier::start(new_server_id, prettier_dir, node, cx.clone())
-            .await
-            .context("default prettier spawn")
-            .map(Arc::new)
-            .map_err(Arc::new)?;
+        let language_registry = project.update(&mut cx, |project, _| project.languages.clone())?;
+        let new_server_id = language_registry.next_language_server_id();
+
+        let new_prettier = Prettier::start(
+            new_server_id,
+            prettier_dir,
+            node,
+            language_registry,
+            cx.clone(),
+        )
+        .await
+        .context("default prettier spawn")
+        .map(Arc::new)
+        .map_err(Arc::new)?;
         register_new_prettier(&project, &new_prettier, worktree_id, new_server_id, &mut cx);
         Ok(new_prettier)
     })

crates/project/src/project.rs 🔗

@@ -10,6 +10,7 @@ pub mod terminals;
 mod project_tests;
 
 use anyhow::{anyhow, bail, Context as _, Result};
+use async_trait::async_trait;
 use client::{proto, Client, Collaborator, TypedEnvelope, UserStore};
 use clock::ReplicaId;
 use collections::{hash_map, BTreeMap, HashMap, HashSet, VecDeque};
@@ -847,10 +848,12 @@ impl Project {
         let current_lsp_settings = &self.current_lsp_settings;
         for (worktree_id, started_lsp_name) in self.language_server_ids.keys() {
             let language = languages.iter().find_map(|l| {
-                let adapter = l
-                    .lsp_adapters()
+                let adapter = self
+                    .languages
+                    .lsp_adapters(l)
                     .iter()
-                    .find(|adapter| &adapter.name == started_lsp_name)?;
+                    .find(|adapter| &adapter.name == started_lsp_name)?
+                    .clone();
                 Some((l, adapter))
             });
             if let Some((language, adapter)) = language {
@@ -889,9 +892,11 @@ impl Project {
 
         let mut prettier_plugins_by_worktree = HashMap::default();
         for (worktree, language, settings) in language_formatters_to_check {
-            if let Some(plugins) =
-                prettier_support::prettier_plugins_for_language(&language, &settings)
-            {
+            if let Some(plugins) = prettier_support::prettier_plugins_for_language(
+                &self.languages,
+                &language,
+                &settings,
+            ) {
                 prettier_plugins_by_worktree
                     .entry(worktree)
                     .or_insert_with(|| HashSet::default())
@@ -2047,7 +2052,7 @@ impl Project {
             }
 
             if let Some(language) = language {
-                for adapter in language.lsp_adapters() {
+                for adapter in self.languages.lsp_adapters(&language) {
                     let language_id = adapter.language_ids.get(language.name().as_ref()).cloned();
                     let server = self
                         .language_server_ids
@@ -2118,10 +2123,12 @@ impl Project {
             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);
+            if let Some(language) = buffer.language().cloned() {
+                for adapter in self.languages.lsp_adapters(&language) {
+                    if let Some(server_id) = ids.get(&(worktree_id, adapter.name.clone())) {
+                        buffer.update_diagnostics(*server_id, Default::default(), cx);
+                    }
+                }
             }
 
             self.buffer_snapshots.remove(&buffer.remote_id());
@@ -2701,9 +2708,11 @@ impl Project {
         let settings = language_settings(Some(&new_language), buffer_file.as_ref(), cx).clone();
         let buffer_file = File::from_dyn(buffer_file.as_ref());
         let worktree = buffer_file.as_ref().map(|f| f.worktree_id(cx));
-        if let Some(prettier_plugins) =
-            prettier_support::prettier_plugins_for_language(&new_language, &settings)
-        {
+        if let Some(prettier_plugins) = prettier_support::prettier_plugins_for_language(
+            &self.languages,
+            &new_language,
+            &settings,
+        ) {
             self.install_default_prettier(worktree, prettier_plugins, cx);
         };
         if let Some(file) = buffer_file {
@@ -2726,7 +2735,7 @@ impl Project {
             return;
         }
 
-        for adapter in language.lsp_adapters() {
+        for adapter in self.languages.clone().lsp_adapters(&language) {
             self.start_language_server(worktree, adapter.clone(), language.clone(), cx);
         }
     }
@@ -3240,7 +3249,11 @@ impl Project {
                 };
 
                 if file.worktree.read(cx).id() != key.0
-                    || !language.lsp_adapters().iter().any(|a| a.name == key.1)
+                    || !self
+                        .languages
+                        .lsp_adapters(&language)
+                        .iter()
+                        .any(|a| a.name == key.1)
                 {
                     continue;
                 }
@@ -3433,8 +3446,10 @@ impl Project {
     ) {
         let worktree_id = worktree.read(cx).id();
 
-        let stop_tasks = language
-            .lsp_adapters()
+        let stop_tasks = self
+            .languages
+            .clone()
+            .lsp_adapters(&language)
             .iter()
             .map(|adapter| {
                 let stop_task = self.stop_language_server(worktree_id, adapter.name.clone(), cx);
@@ -4785,14 +4800,15 @@ impl Project {
                                     .languages
                                     .language_for_file(&project_path.path, None)
                                     .unwrap_or_else(move |_| adapter_language);
-                                let language_server_name = adapter.name.clone();
+                                let adapter = adapter.clone();
                                 Some(async move {
                                     let language = language.await;
-                                    let label =
-                                        language.label_for_symbol(&symbol_name, symbol_kind).await;
+                                    let label = adapter
+                                        .label_for_symbol(&symbol_name, symbol_kind, &language)
+                                        .await;
 
                                     Symbol {
-                                        language_server_name,
+                                        language_server_name: adapter.name.clone(),
                                         source_worktree_id,
                                         path: project_path,
                                         label: label.unwrap_or_else(|| {
@@ -7972,6 +7988,7 @@ impl Project {
         _: Arc<Client>,
         mut cx: AsyncAppContext,
     ) -> Result<proto::ApplyCompletionAdditionalEditsResponse> {
+        let languages = this.update(&mut cx, |this, _| this.languages.clone())?;
         let (buffer, completion) = this.update(&mut cx, |this, cx| {
             let buffer_id = BufferId::new(envelope.payload.buffer_id)?;
             let buffer = this
@@ -7986,6 +8003,7 @@ impl Project {
                     .completion
                     .ok_or_else(|| anyhow!("invalid completion"))?,
                 language.cloned(),
+                &languages,
             );
             Ok::<_, anyhow::Error>((buffer, completion))
         })??;
@@ -8713,6 +8731,9 @@ impl Project {
                 .language_for_file(&path.path, None)
                 .await
                 .log_err();
+            let adapter = language
+                .as_ref()
+                .and_then(|language| languages.lsp_adapters(language).first().cloned());
             Ok(Symbol {
                 language_server_name: LanguageServerName(
                     serialized_symbol.language_server_name.into(),
@@ -8720,10 +8741,10 @@ impl Project {
                 source_worktree_id,
                 path,
                 label: {
-                    match language {
-                        Some(language) => {
-                            language
-                                .label_for_symbol(&serialized_symbol.name, kind)
+                    match language.as_ref().zip(adapter.as_ref()) {
+                        Some((language, adapter)) => {
+                            adapter
+                                .label_for_symbol(&serialized_symbol.name, kind, language)
                                 .await
                         }
                         None => None,
@@ -8975,6 +8996,17 @@ impl Project {
         self.supplementary_language_servers.iter()
     }
 
+    pub fn language_server_adapter_for_id(
+        &self,
+        id: LanguageServerId,
+    ) -> Option<Arc<CachedLspAdapter>> {
+        if let Some(LanguageServerState::Running { adapter, .. }) = self.language_servers.get(&id) {
+            Some(adapter.clone())
+        } else {
+            None
+        }
+    }
+
     pub fn language_server_for_id(&self, id: LanguageServerId) -> Option<Arc<LanguageServer>> {
         if let Some(LanguageServerState::Running { server, .. }) = self.language_servers.get(&id) {
             Some(server.clone())
@@ -9025,8 +9057,8 @@ impl Project {
     ) -> Vec<LanguageServerId> {
         if let Some((file, language)) = File::from_dyn(buffer.file()).zip(buffer.language()) {
             let worktree_id = file.worktree_id(cx);
-            language
-                .lsp_adapters()
+            self.languages
+                .lsp_adapters(&language)
                 .iter()
                 .flat_map(|adapter| {
                     let key = (worktree_id, adapter.name.clone());
@@ -9190,20 +9222,25 @@ impl<P: AsRef<Path>> From<(WorktreeId, P)> for ProjectPath {
 
 struct ProjectLspAdapterDelegate {
     project: Model<Project>,
-    worktree: Model<Worktree>,
+    worktree: worktree::Snapshot,
+    fs: Arc<dyn Fs>,
     http_client: Arc<dyn HttpClient>,
+    language_registry: Arc<LanguageRegistry>,
 }
 
 impl ProjectLspAdapterDelegate {
     fn new(project: &Project, worktree: &Model<Worktree>, cx: &ModelContext<Project>) -> Arc<Self> {
         Arc::new(Self {
             project: cx.handle(),
-            worktree: worktree.clone(),
+            worktree: worktree.read(cx).snapshot(),
+            fs: project.fs.clone(),
             http_client: project.client.http_client(),
+            language_registry: project.languages.clone(),
         })
     }
 }
 
+#[async_trait]
 impl LspAdapterDelegate for ProjectLspAdapterDelegate {
     fn show_notification(&self, message: &str, cx: &mut AppContext) {
         self.project
@@ -9214,41 +9251,50 @@ impl LspAdapterDelegate for ProjectLspAdapterDelegate {
         self.http_client.clone()
     }
 
-    fn which_command(
-        &self,
-        command: OsString,
-        cx: &AppContext,
-    ) -> Task<Option<(PathBuf, HashMap<String, String>)>> {
-        let worktree_abs_path = self.worktree.read(cx).abs_path();
-        let command = command.to_owned();
+    async fn which_command(&self, command: OsString) -> Option<(PathBuf, HashMap<String, String>)> {
+        let worktree_abs_path = self.worktree.abs_path();
 
-        cx.background_executor().spawn(async move {
-            let shell_env = load_shell_environment(&worktree_abs_path)
-                .await
-                .with_context(|| {
-                    format!(
-                        "failed to determine load login shell environment in {worktree_abs_path:?}"
-                    )
-                })
-                .log_err();
-
-            if let Some(shell_env) = shell_env {
-                let shell_path = shell_env.get("PATH");
-                match which::which_in(&command, shell_path, &worktree_abs_path) {
-                    Ok(command_path) => Some((command_path, shell_env)),
-                    Err(error) => {
-                        log::warn!(
-                            "failed to determine path for command {:?} in shell PATH {:?}: {error}",
-                            command.to_string_lossy(),
-                            shell_path.map(String::as_str).unwrap_or("")
-                        );
-                        None
-                    }
+        let shell_env = load_shell_environment(&worktree_abs_path)
+            .await
+            .with_context(|| {
+                format!("failed to determine load login shell environment in {worktree_abs_path:?}")
+            })
+            .log_err();
+
+        if let Some(shell_env) = shell_env {
+            let shell_path = shell_env.get("PATH");
+            match which::which_in(&command, shell_path, &worktree_abs_path) {
+                Ok(command_path) => Some((command_path, shell_env)),
+                Err(error) => {
+                    log::warn!(
+                        "failed to determine path for command {:?} in shell PATH {:?}: {error}",
+                        command.to_string_lossy(),
+                        shell_path.map(String::as_str).unwrap_or("")
+                    );
+                    None
                 }
-            } else {
-                None
             }
-        })
+        } else {
+            None
+        }
+    }
+
+    fn update_status(
+        &self,
+        server_name: LanguageServerName,
+        status: language::LanguageServerBinaryStatus,
+    ) {
+        self.language_registry
+            .update_lsp_status(server_name, status);
+    }
+
+    async fn read_text_file(&self, path: PathBuf) -> Result<String> {
+        if self.worktree.entry_for_path(&path).is_none() {
+            return Err(anyhow!("no such path {path:?}"));
+        }
+        let path = self.worktree.absolutize(path.as_ref())?;
+        let content = self.fs.load(&path).await?;
+        Ok(content)
     }
 }
 

crates/project/src/project_tests.rs 🔗

@@ -189,30 +189,24 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
 async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
     init_test(cx);
 
-    let mut rust_language = Language::new(
-        LanguageConfig {
-            name: "Rust".into(),
-            matcher: LanguageMatcher {
-                path_suffixes: vec!["rs".to_string()],
-                ..Default::default()
-            },
-            ..Default::default()
-        },
-        Some(tree_sitter_rust::language()),
-    );
-    let mut json_language = Language::new(
-        LanguageConfig {
-            name: "JSON".into(),
-            matcher: LanguageMatcher {
-                path_suffixes: vec!["json".to_string()],
-                ..Default::default()
-            },
-            ..Default::default()
-        },
-        None,
-    );
-    let mut fake_rust_servers = rust_language
-        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+    let fs = FakeFs::new(cx.executor());
+    fs.insert_tree(
+        "/the-root",
+        json!({
+            "test.rs": "const A: i32 = 1;",
+            "test2.rs": "",
+            "Cargo.toml": "a = 1",
+            "package.json": "{\"a\": 1}",
+        }),
+    )
+    .await;
+
+    let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
+    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+
+    let mut fake_rust_servers = language_registry.register_fake_lsp_adapter(
+        "Rust",
+        FakeLspAdapter {
             name: "the-rust-language-server",
             capabilities: lsp::ServerCapabilities {
                 completion_provider: Some(lsp::CompletionOptions {
@@ -222,10 +216,11 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
                 ..Default::default()
             },
             ..Default::default()
-        }))
-        .await;
-    let mut fake_json_servers = json_language
-        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+        },
+    );
+    let mut fake_json_servers = language_registry.register_fake_lsp_adapter(
+        "JSON",
+        FakeLspAdapter {
             name: "the-json-language-server",
             capabilities: lsp::ServerCapabilities {
                 completion_provider: Some(lsp::CompletionOptions {
@@ -235,22 +230,8 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
                 ..Default::default()
             },
             ..Default::default()
-        }))
-        .await;
-
-    let fs = FakeFs::new(cx.executor());
-    fs.insert_tree(
-        "/the-root",
-        json!({
-            "test.rs": "const A: i32 = 1;",
-            "test2.rs": "",
-            "Cargo.toml": "a = 1",
-            "package.json": "{\"a\": 1}",
-        }),
-    )
-    .await;
-
-    let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
+        },
+    );
 
     // Open a buffer without an associated language server.
     let toml_buffer = project
@@ -273,10 +254,8 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
 
     // Now we add the languages to the project, and ensure they get assigned to all
     // the relevant open buffers.
-    project.update(cx, |project, _| {
-        project.languages.add(Arc::new(json_language));
-        project.languages.add(Arc::new(rust_language));
-    });
+    language_registry.add(json_lang());
+    language_registry.add(rust_lang());
     cx.executor().run_until_parked();
     rust_buffer.update(cx, |buffer, _| {
         assert_eq!(buffer.language().map(|l| l.name()), Some("Rust".into()));
@@ -581,24 +560,6 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
 async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppContext) {
     init_test(cx);
 
-    let mut language = Language::new(
-        LanguageConfig {
-            name: "Rust".into(),
-            matcher: LanguageMatcher {
-                path_suffixes: vec!["rs".to_string()],
-                ..Default::default()
-            },
-            ..Default::default()
-        },
-        Some(tree_sitter_rust::language()),
-    );
-    let mut fake_servers = language
-        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
-            name: "the-language-server",
-            ..Default::default()
-        }))
-        .await;
-
     let fs = FakeFs::new(cx.executor());
     fs.insert_tree(
         "/the-root",
@@ -630,9 +591,16 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
     .await;
 
     let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
-    project.update(cx, |project, _| {
-        project.languages.add(Arc::new(language));
-    });
+    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+    language_registry.add(rust_lang());
+    let mut fake_servers = language_registry.register_fake_lsp_adapter(
+        "Rust",
+        FakeLspAdapter {
+            name: "the-language-server",
+            ..Default::default()
+        },
+    );
+
     cx.executor().run_until_parked();
 
     // Start the language server by opening a buffer with a compatible file extension.
@@ -1019,24 +987,6 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
     init_test(cx);
 
     let progress_token = "the-progress-token";
-    let mut language = Language::new(
-        LanguageConfig {
-            name: "Rust".into(),
-            matcher: LanguageMatcher {
-                path_suffixes: vec!["rs".to_string()],
-                ..Default::default()
-            },
-            ..Default::default()
-        },
-        Some(tree_sitter_rust::language()),
-    );
-    let mut fake_servers = language
-        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
-            disk_based_diagnostics_progress_token: Some(progress_token.into()),
-            disk_based_diagnostics_sources: vec!["disk".into()],
-            ..Default::default()
-        }))
-        .await;
 
     let fs = FakeFs::new(cx.executor());
     fs.insert_tree(
@@ -1049,7 +999,18 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
     .await;
 
     let project = Project::test(fs, ["/dir".as_ref()], cx).await;
-    project.update(cx, |project, _| project.languages.add(Arc::new(language)));
+    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+
+    language_registry.add(rust_lang());
+    let mut fake_servers = language_registry.register_fake_lsp_adapter(
+        "Rust",
+        FakeLspAdapter {
+            disk_based_diagnostics_progress_token: Some(progress_token.into()),
+            disk_based_diagnostics_sources: vec!["disk".into()],
+            ..Default::default()
+        },
+    );
+
     let worktree_id = project.update(cx, |p, cx| p.worktrees().next().unwrap().read(cx).id());
 
     // Cause worktree to start the fake language server
@@ -1155,29 +1116,23 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC
     init_test(cx);
 
     let progress_token = "the-progress-token";
-    let mut language = Language::new(
-        LanguageConfig {
-            matcher: LanguageMatcher {
-                path_suffixes: vec!["rs".to_string()],
-                ..Default::default()
-            },
-            ..Default::default()
-        },
-        None,
-    );
-    let mut fake_servers = language
-        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
-            disk_based_diagnostics_sources: vec!["disk".into()],
-            disk_based_diagnostics_progress_token: Some(progress_token.into()),
-            ..Default::default()
-        }))
-        .await;
 
     let fs = FakeFs::new(cx.executor());
     fs.insert_tree("/dir", json!({ "a.rs": "" })).await;
 
     let project = Project::test(fs, ["/dir".as_ref()], cx).await;
-    project.update(cx, |project, _| project.languages.add(Arc::new(language)));
+
+    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+    language_registry.add(rust_lang());
+    let mut fake_servers = language_registry.register_fake_lsp_adapter(
+        "Rust",
+        FakeLspAdapter {
+            name: "the-language-server",
+            disk_based_diagnostics_sources: vec!["disk".into()],
+            disk_based_diagnostics_progress_token: Some(progress_token.into()),
+            ..Default::default()
+        },
+    );
 
     let buffer = project
         .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
@@ -1239,27 +1194,15 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC
 async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAppContext) {
     init_test(cx);
 
-    let mut language = Language::new(
-        LanguageConfig {
-            matcher: LanguageMatcher {
-                path_suffixes: vec!["rs".to_string()],
-                ..Default::default()
-            },
-            ..Default::default()
-        },
-        None,
-    );
-    let mut fake_servers = language
-        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
-            ..Default::default()
-        }))
-        .await;
-
     let fs = FakeFs::new(cx.executor());
     fs.insert_tree("/dir", json!({ "a.rs": "x" })).await;
 
     let project = Project::test(fs, ["/dir".as_ref()], cx).await;
-    project.update(cx, |project, _| project.languages.add(Arc::new(language)));
+
+    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+    language_registry.add(rust_lang());
+    let mut fake_servers =
+        language_registry.register_fake_lsp_adapter("Rust", FakeLspAdapter::default());
 
     let buffer = project
         .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
@@ -1331,28 +1274,15 @@ async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAp
 async fn test_restarted_server_reporting_invalid_buffer_version(cx: &mut gpui::TestAppContext) {
     init_test(cx);
 
-    let mut language = Language::new(
-        LanguageConfig {
-            matcher: LanguageMatcher {
-                path_suffixes: vec!["rs".to_string()],
-                ..Default::default()
-            },
-            ..Default::default()
-        },
-        None,
-    );
-    let mut fake_servers = language
-        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
-            name: "the-lsp",
-            ..Default::default()
-        }))
-        .await;
-
     let fs = FakeFs::new(cx.executor());
     fs.insert_tree("/dir", json!({ "a.rs": "" })).await;
 
     let project = Project::test(fs, ["/dir".as_ref()], cx).await;
-    project.update(cx, |project, _| project.languages.add(Arc::new(language)));
+    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+
+    language_registry.add(rust_lang());
+    let mut fake_servers =
+        language_registry.register_fake_lsp_adapter("Rust", FakeLspAdapter::default());
 
     let buffer = project
         .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
@@ -1383,50 +1313,29 @@ async fn test_restarted_server_reporting_invalid_buffer_version(cx: &mut gpui::T
 async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) {
     init_test(cx);
 
-    let mut rust = Language::new(
-        LanguageConfig {
-            name: Arc::from("Rust"),
-            matcher: LanguageMatcher {
-                path_suffixes: vec!["rs".to_string()],
-                ..Default::default()
-            },
-            ..Default::default()
-        },
-        None,
-    );
-    let mut fake_rust_servers = rust
-        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
-            name: "rust-lsp",
-            ..Default::default()
-        }))
+    let fs = FakeFs::new(cx.executor());
+    fs.insert_tree("/dir", json!({ "a.rs": "", "b.js": "" }))
         .await;
-    let mut js = Language::new(
-        LanguageConfig {
-            name: Arc::from("JavaScript"),
-            matcher: LanguageMatcher {
-                path_suffixes: vec!["js".to_string()],
-                ..Default::default()
-            },
+
+    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
+    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+
+    let mut fake_rust_servers = language_registry.register_fake_lsp_adapter(
+        "Rust",
+        FakeLspAdapter {
+            name: "rust-lsp",
             ..Default::default()
         },
-        None,
     );
-    let mut fake_js_servers = js
-        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+    let mut fake_js_servers = language_registry.register_fake_lsp_adapter(
+        "JavaScript",
+        FakeLspAdapter {
             name: "js-lsp",
             ..Default::default()
-        }))
-        .await;
-
-    let fs = FakeFs::new(cx.executor());
-    fs.insert_tree("/dir", json!({ "a.rs": "", "b.js": "" }))
-        .await;
-
-    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
-    project.update(cx, |project, _| {
-        project.languages.add(Arc::new(rust));
-        project.languages.add(Arc::new(js));
-    });
+        },
+    );
+    language_registry.add(rust_lang());
+    language_registry.add(js_lang());
 
     let _rs_buffer = project
         .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
@@ -1518,24 +1427,6 @@ async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) {
 async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
     init_test(cx);
 
-    let mut language = Language::new(
-        LanguageConfig {
-            name: "Rust".into(),
-            matcher: LanguageMatcher {
-                path_suffixes: vec!["rs".to_string()],
-                ..Default::default()
-            },
-            ..Default::default()
-        },
-        Some(tree_sitter_rust::language()),
-    );
-    let mut fake_servers = language
-        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
-            disk_based_diagnostics_sources: vec!["disk".into()],
-            ..Default::default()
-        }))
-        .await;
-
     let text = "
         fn a() { A }
         fn b() { BB }
@@ -1547,7 +1438,16 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
     fs.insert_tree("/dir", json!({ "a.rs": text })).await;
 
     let project = Project::test(fs, ["/dir".as_ref()], cx).await;
-    project.update(cx, |project, _| project.languages.add(Arc::new(language)));
+    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+
+    language_registry.add(rust_lang());
+    let mut fake_servers = language_registry.register_fake_lsp_adapter(
+        "Rust",
+        FakeLspAdapter {
+            disk_based_diagnostics_sources: vec!["disk".into()],
+            ..Default::default()
+        },
+    );
 
     let buffer = project
         .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
@@ -1932,19 +1832,6 @@ async fn test_diagnostics_from_multiple_language_servers(cx: &mut gpui::TestAppC
 async fn test_edits_from_lsp2_with_past_version(cx: &mut gpui::TestAppContext) {
     init_test(cx);
 
-    let mut language = Language::new(
-        LanguageConfig {
-            name: "Rust".into(),
-            matcher: LanguageMatcher {
-                path_suffixes: vec!["rs".to_string()],
-                ..Default::default()
-            },
-            ..Default::default()
-        },
-        Some(tree_sitter_rust::language()),
-    );
-    let mut fake_servers = language.set_fake_lsp_adapter(Default::default()).await;
-
     let text = "
         fn a() {
             f1();
@@ -1968,7 +1855,12 @@ async fn test_edits_from_lsp2_with_past_version(cx: &mut gpui::TestAppContext) {
     .await;
 
     let project = Project::test(fs, ["/dir".as_ref()], cx).await;
-    project.update(cx, |project, _| project.languages.add(Arc::new(language)));
+
+    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+    language_registry.add(rust_lang());
+    let mut fake_servers =
+        language_registry.register_fake_lsp_adapter("Rust", FakeLspAdapter::default());
+
     let buffer = project
         .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
         .await
@@ -2322,19 +2214,6 @@ fn chunks_with_diagnostics<T: ToOffset + ToPoint>(
 async fn test_definition(cx: &mut gpui::TestAppContext) {
     init_test(cx);
 
-    let mut language = Language::new(
-        LanguageConfig {
-            name: "Rust".into(),
-            matcher: LanguageMatcher {
-                path_suffixes: vec!["rs".to_string()],
-                ..Default::default()
-            },
-            ..Default::default()
-        },
-        Some(tree_sitter_rust::language()),
-    );
-    let mut fake_servers = language.set_fake_lsp_adapter(Default::default()).await;
-
     let fs = FakeFs::new(cx.executor());
     fs.insert_tree(
         "/dir",
@@ -2346,7 +2225,11 @@ async fn test_definition(cx: &mut gpui::TestAppContext) {
     .await;
 
     let project = Project::test(fs, ["/dir/b.rs".as_ref()], cx).await;
-    project.update(cx, |project, _| project.languages.add(Arc::new(language)));
+
+    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+    language_registry.add(rust_lang());
+    let mut fake_servers =
+        language_registry.register_fake_lsp_adapter("Rust", FakeLspAdapter::default());
 
     let buffer = project
         .update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx))
@@ -2426,30 +2309,6 @@ async fn test_definition(cx: &mut gpui::TestAppContext) {
 async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) {
     init_test(cx);
 
-    let mut language = Language::new(
-        LanguageConfig {
-            name: "TypeScript".into(),
-            matcher: LanguageMatcher {
-                path_suffixes: vec!["ts".to_string()],
-                ..Default::default()
-            },
-            ..Default::default()
-        },
-        Some(tree_sitter_typescript::language_typescript()),
-    );
-    let mut fake_language_servers = language
-        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
-            capabilities: lsp::ServerCapabilities {
-                completion_provider: Some(lsp::CompletionOptions {
-                    trigger_characters: Some(vec![":".to_string()]),
-                    ..Default::default()
-                }),
-                ..Default::default()
-            },
-            ..Default::default()
-        }))
-        .await;
-
     let fs = FakeFs::new(cx.executor());
     fs.insert_tree(
         "/dir",
@@ -2460,7 +2319,23 @@ async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) {
     .await;
 
     let project = Project::test(fs, ["/dir".as_ref()], cx).await;
-    project.update(cx, |project, _| project.languages.add(Arc::new(language)));
+
+    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+    language_registry.add(typescript_lang());
+    let mut fake_language_servers = language_registry.register_fake_lsp_adapter(
+        "TypeScript",
+        FakeLspAdapter {
+            capabilities: lsp::ServerCapabilities {
+                completion_provider: Some(lsp::CompletionOptions {
+                    trigger_characters: Some(vec![":".to_string()]),
+                    ..Default::default()
+                }),
+                ..Default::default()
+            },
+            ..Default::default()
+        },
+    );
+
     let buffer = project
         .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
         .await
@@ -2526,30 +2401,6 @@ async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) {
 async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) {
     init_test(cx);
 
-    let mut language = Language::new(
-        LanguageConfig {
-            name: "TypeScript".into(),
-            matcher: LanguageMatcher {
-                path_suffixes: vec!["ts".to_string()],
-                ..Default::default()
-            },
-            ..Default::default()
-        },
-        Some(tree_sitter_typescript::language_typescript()),
-    );
-    let mut fake_language_servers = language
-        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
-            capabilities: lsp::ServerCapabilities {
-                completion_provider: Some(lsp::CompletionOptions {
-                    trigger_characters: Some(vec![":".to_string()]),
-                    ..Default::default()
-                }),
-                ..Default::default()
-            },
-            ..Default::default()
-        }))
-        .await;
-
     let fs = FakeFs::new(cx.executor());
     fs.insert_tree(
         "/dir",
@@ -2560,7 +2411,23 @@ async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) {
     .await;
 
     let project = Project::test(fs, ["/dir".as_ref()], cx).await;
-    project.update(cx, |project, _| project.languages.add(Arc::new(language)));
+
+    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+    language_registry.add(typescript_lang());
+    let mut fake_language_servers = language_registry.register_fake_lsp_adapter(
+        "TypeScript",
+        FakeLspAdapter {
+            capabilities: lsp::ServerCapabilities {
+                completion_provider: Some(lsp::CompletionOptions {
+                    trigger_characters: Some(vec![":".to_string()]),
+                    ..Default::default()
+                }),
+                ..Default::default()
+            },
+            ..Default::default()
+        },
+    );
+
     let buffer = project
         .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
         .await
@@ -2595,19 +2462,6 @@ async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) {
 async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) {
     init_test(cx);
 
-    let mut language = Language::new(
-        LanguageConfig {
-            name: "TypeScript".into(),
-            matcher: LanguageMatcher {
-                path_suffixes: vec!["ts".to_string()],
-                ..Default::default()
-            },
-            ..Default::default()
-        },
-        None,
-    );
-    let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
-
     let fs = FakeFs::new(cx.executor());
     fs.insert_tree(
         "/dir",
@@ -2618,7 +2472,12 @@ async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) {
     .await;
 
     let project = Project::test(fs, ["/dir".as_ref()], cx).await;
-    project.update(cx, |project, _| project.languages.add(Arc::new(language)));
+
+    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+    language_registry.add(typescript_lang());
+    let mut fake_language_servers =
+        language_registry.register_fake_lsp_adapter("TypeScript", Default::default());
+
     let buffer = project
         .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
         .await
@@ -2904,16 +2763,7 @@ async fn test_save_as(cx: &mut gpui::TestAppContext) {
     let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
 
     let languages = project.update(cx, |project, _| project.languages().clone());
-    languages.register_native_grammars([("rust", tree_sitter_rust::language())]);
-    languages.register_test_language(LanguageConfig {
-        name: "Rust".into(),
-        grammar: Some("rust".into()),
-        matcher: LanguageMatcher {
-            path_suffixes: vec!["rs".into()],
-            ..Default::default()
-        },
-        ..Default::default()
-    });
+    languages.add(rust_lang());
 
     let buffer = project.update(cx, |project, cx| {
         project.create_buffer("", None, cx).unwrap()
@@ -3733,30 +3583,6 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
 async fn test_rename(cx: &mut gpui::TestAppContext) {
     init_test(cx);
 
-    let mut language = Language::new(
-        LanguageConfig {
-            name: "Rust".into(),
-            matcher: LanguageMatcher {
-                path_suffixes: vec!["rs".to_string()],
-                ..Default::default()
-            },
-            ..Default::default()
-        },
-        Some(tree_sitter_rust::language()),
-    );
-    let mut fake_servers = language
-        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
-            capabilities: lsp::ServerCapabilities {
-                rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
-                    prepare_provider: Some(true),
-                    work_done_progress_options: Default::default(),
-                })),
-                ..Default::default()
-            },
-            ..Default::default()
-        }))
-        .await;
-
     let fs = FakeFs::new(cx.executor());
     fs.insert_tree(
         "/dir",
@@ -3768,7 +3594,23 @@ async fn test_rename(cx: &mut gpui::TestAppContext) {
     .await;
 
     let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
-    project.update(cx, |project, _| project.languages.add(Arc::new(language)));
+
+    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+    language_registry.add(rust_lang());
+    let mut fake_servers = language_registry.register_fake_lsp_adapter(
+        "Rust",
+        FakeLspAdapter {
+            capabilities: lsp::ServerCapabilities {
+                rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
+                    prepare_provider: Some(true),
+                    work_done_progress_options: Default::default(),
+                })),
+                ..Default::default()
+            },
+            ..Default::default()
+        },
+    );
+
     let buffer = project
         .update(cx, |project, cx| {
             project.open_local_buffer("/dir/one.rs", cx)
@@ -4475,3 +4317,59 @@ fn init_test(cx: &mut gpui::TestAppContext) {
         Project::init_settings(cx);
     });
 }
+
+fn json_lang() -> Arc<Language> {
+    Arc::new(Language::new(
+        LanguageConfig {
+            name: "JSON".into(),
+            matcher: LanguageMatcher {
+                path_suffixes: vec!["json".to_string()],
+                ..Default::default()
+            },
+            ..Default::default()
+        },
+        None,
+    ))
+}
+
+fn js_lang() -> Arc<Language> {
+    Arc::new(Language::new(
+        LanguageConfig {
+            name: Arc::from("JavaScript"),
+            matcher: LanguageMatcher {
+                path_suffixes: vec!["js".to_string()],
+                ..Default::default()
+            },
+            ..Default::default()
+        },
+        None,
+    ))
+}
+
+fn rust_lang() -> Arc<Language> {
+    Arc::new(Language::new(
+        LanguageConfig {
+            name: "Rust".into(),
+            matcher: LanguageMatcher {
+                path_suffixes: vec!["rs".to_string()],
+                ..Default::default()
+            },
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::language()),
+    ))
+}
+
+fn typescript_lang() -> Arc<Language> {
+    Arc::new(Language::new(
+        LanguageConfig {
+            name: "TypeScript".into(),
+            matcher: LanguageMatcher {
+                path_suffixes: vec!["ts".to_string()],
+                ..Default::default()
+            },
+            ..Default::default()
+        },
+        Some(tree_sitter_typescript::language_typescript()),
+    ))
+}

crates/project/src/task_inventory.rs 🔗

@@ -219,7 +219,7 @@ impl Inventory {
     }
 }
 
-#[cfg(feature = "test-support")]
+#[cfg(any(test, feature = "test-support"))]
 pub mod test_inventory {
     use std::{
         path::{Path, PathBuf},

crates/project_symbols/src/project_symbols.rs 🔗

@@ -271,7 +271,13 @@ mod tests {
     async fn test_project_symbols(cx: &mut TestAppContext) {
         init_test(cx);
 
-        let mut language = Language::new(
+        let fs = FakeFs::new(cx.executor());
+        fs.insert_tree("/dir", json!({ "test.rs": "" })).await;
+
+        let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
+
+        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+        language_registry.add(Arc::new(Language::new(
             LanguageConfig {
                 name: "Rust".into(),
                 matcher: LanguageMatcher {
@@ -281,16 +287,9 @@ mod tests {
                 ..Default::default()
             },
             None,
-        );
-        let mut fake_servers = language
-            .set_fake_lsp_adapter(Arc::<FakeLspAdapter>::default())
-            .await;
-
-        let fs = FakeFs::new(cx.executor());
-        fs.insert_tree("/dir", json!({ "test.rs": "" })).await;
-
-        let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
-        project.update(cx, |project, _| project.languages().add(Arc::new(language)));
+        )));
+        let mut fake_servers =
+            language_registry.register_fake_lsp_adapter("Rust", FakeLspAdapter::default());
 
         let _buffer = project
             .update(cx, |project, cx| {

crates/vim/src/command.rs 🔗

@@ -490,9 +490,7 @@ mod test {
 
         assert_eq!(fs.load(&path).await.unwrap(), "@\n");
 
-        fs.as_fake()
-            .write_file_internal(path, "oops\n".to_string())
-            .unwrap();
+        fs.as_fake().insert_file(path, b"oops\n".to_vec()).await;
 
         // conflict!
         cx.simulate_keystrokes(["i", "@", "escape"]);

crates/zed/src/main.rs 🔗

@@ -178,6 +178,7 @@ fn main() {
         extension::init(
             fs.clone(),
             http.clone(),
+            node_runtime.clone(),
             languages.clone(),
             ThemeRegistry::global(cx),
             cx,

crates/zed/src/zed.rs 🔗

@@ -1594,7 +1594,7 @@ mod tests {
         app_state
             .fs
             .as_fake()
-            .insert_file("/root/a.txt", "changed".to_string())
+            .insert_file("/root/a.txt", b"changed".to_vec())
             .await;
 
         cx.run_until_parked();

extensions/gleam/Cargo.toml 🔗

@@ -0,0 +1,13 @@
+[package]
+name = "zed_gleam"
+version = "0.0.1"
+edition = "2021"
+
+[dependencies]
+zed_extension_api = { path = "../../crates/extension_api" }
+
+[lib]
+path = "src/gleam.rs"
+crate-type = ["cdylib"]
+
+[package.metadata.component]

extensions/gleam/extension.toml 🔗

@@ -0,0 +1,13 @@
+id = "gleam"
+name = "Gleam"
+description = "Gleam support for Zed"
+version = "0.0.1"
+authors = ["Marshall Bowers <elliott.codes@gmail.com>"]
+
+[language_servers.gleam]
+name = "Gleam LSP"
+language = "Gleam"
+
+[grammars.gleam]
+repository = "https://github.com/gleam-lang/tree-sitter-gleam"
+commit = "58b7cac8fc14c92b0677c542610d8738c373fa81"

extensions/gleam/languages/gleam/config.toml 🔗

@@ -0,0 +1,11 @@
+name = "Gleam"
+grammar = "gleam"
+path_suffixes = ["gleam"]
+line_comments = ["// ", "/// "]
+autoclose_before = ";:.,=}])>"
+brackets = [
+    { start = "{", end = "}", close = true, newline = true },
+    { start = "[", end = "]", close = true, newline = true },
+    { start = "(", end = ")", close = true, newline = true },
+    { start = "\"", end = "\"", close = true, newline = false, not_in = ["string", "comment"] },
+]

extensions/gleam/languages/gleam/highlights.scm 🔗

@@ -0,0 +1,130 @@
+; Comments
+(module_comment) @comment
+(statement_comment) @comment
+(comment) @comment
+
+; Constants
+(constant
+  name: (identifier) @constant)
+
+; Modules
+(module) @module
+(import alias: (identifier) @module)
+(remote_type_identifier
+  module: (identifier) @module)
+(remote_constructor_name
+  module: (identifier) @module)
+((field_access
+  record: (identifier) @module
+  field: (label) @function)
+ (#is-not? local))
+
+; Functions
+(unqualified_import (identifier) @function)
+(unqualified_import "type" (type_identifier) @type)
+(unqualified_import (type_identifier) @constructor)
+(function
+  name: (identifier) @function)
+(external_function
+  name: (identifier) @function)
+(function_parameter
+  name: (identifier) @variable.parameter)
+((function_call
+   function: (identifier) @function)
+ (#is-not? local))
+((binary_expression
+   operator: "|>"
+   right: (identifier) @function)
+ (#is-not? local))
+
+; "Properties"
+; Assumed to be intended to refer to a name for a field; something that comes
+; before ":" or after "."
+; e.g. record field names, tuple indices, names for named arguments, etc
+(label) @property
+(tuple_access
+  index: (integer) @property)
+
+; Attributes
+(attribute
+  "@" @attribute
+  name: (identifier) @attribute)
+
+(attribute_value (identifier) @constant)
+
+; Type names
+(remote_type_identifier) @type
+(type_identifier) @type
+
+; Data constructors
+(constructor_name) @constructor
+
+; Literals
+(string) @string
+((escape_sequence) @warning
+ ; Deprecated in v0.33.0-rc2:
+ (#eq? @warning "\\e"))
+(escape_sequence) @string.escape
+(bit_string_segment_option) @function.builtin
+(integer) @number
+(float) @number
+
+; Reserved identifiers
+; TODO: when tree-sitter supports `#any-of?` in the Rust bindings,
+; refactor this to use `#any-of?` rather than `#match?`
+((identifier) @warning
+ (#match? @warning "^(auto|delegate|derive|else|implement|macro|test|echo)$"))
+
+; Variables
+(identifier) @variable
+(discard) @comment.unused
+
+; Keywords
+[
+  (visibility_modifier) ; "pub"
+  (opacity_modifier) ; "opaque"
+  "as"
+  "assert"
+  "case"
+  "const"
+  ; DEPRECATED: 'external' was removed in v0.30.
+  "external"
+  "fn"
+  "if"
+  "import"
+  "let"
+  "panic"
+  "todo"
+  "type"
+  "use"
+] @keyword
+
+; Operators
+(binary_expression
+  operator: _ @operator)
+(boolean_negation "!" @operator)
+(integer_negation "-" @operator)
+
+; Punctuation
+[
+  "("
+  ")"
+  "["
+  "]"
+  "{"
+  "}"
+  "<<"
+  ">>"
+] @punctuation.bracket
+[
+  "."
+  ","
+  ;; Controversial -- maybe some are operators?
+  ":"
+  "#"
+  "="
+  "->"
+  ".."
+  "-"
+  "<-"
+] @punctuation.delimiter

extensions/gleam/languages/gleam/outline.scm 🔗

@@ -0,0 +1,31 @@
+(external_type
+    (visibility_modifier)? @context
+    "type" @context
+    (type_name) @name) @item
+
+(type_definition
+    (visibility_modifier)? @context
+    (opacity_modifier)? @context
+    "type" @context
+    (type_name) @name) @item
+
+(data_constructor
+    (constructor_name) @name) @item
+
+(data_constructor_argument
+    (label) @name) @item
+
+(type_alias
+    (visibility_modifier)? @context
+    "type" @context
+    (type_name) @name) @item
+
+(function
+    (visibility_modifier)? @context
+    "fn" @context
+    name: (_) @name) @item
+
+(constant
+    (visibility_modifier)? @context
+    "const" @context
+    name: (_) @name) @item

extensions/gleam/src/bindings.rs 🔗

@@ -0,0 +1,11 @@
+// Generated by `wit-bindgen` 0.16.0. DO NOT EDIT!
+
+#[cfg(target_arch = "wasm32")]
+#[link_section = "component-type:zed_gleam"]
+#[doc(hidden)]
+pub static __WIT_BINDGEN_COMPONENT_TYPE: [u8; 169] = [3, 0, 9, 122, 101, 100, 95, 103, 108, 101, 97, 109, 0, 97, 115, 109, 13, 0, 1, 0, 7, 40, 1, 65, 2, 1, 65, 0, 4, 1, 29, 99, 111, 109, 112, 111, 110, 101, 110, 116, 58, 122, 101, 100, 95, 103, 108, 101, 97, 109, 47, 122, 101, 100, 95, 103, 108, 101, 97, 109, 4, 0, 11, 15, 1, 0, 9, 122, 101, 100, 95, 103, 108, 101, 97, 109, 3, 0, 0, 0, 16, 12, 112, 97, 99, 107, 97, 103, 101, 45, 100, 111, 99, 115, 0, 123, 125, 0, 70, 9, 112, 114, 111, 100, 117, 99, 101, 114, 115, 1, 12, 112, 114, 111, 99, 101, 115, 115, 101, 100, 45, 98, 121, 2, 13, 119, 105, 116, 45, 99, 111, 109, 112, 111, 110, 101, 110, 116, 6, 48, 46, 49, 56, 46, 50, 16, 119, 105, 116, 45, 98, 105, 110, 100, 103, 101, 110, 45, 114, 117, 115, 116, 6, 48, 46, 49, 54, 46, 48];
+
+#[inline(never)]
+#[doc(hidden)]
+#[cfg(target_arch = "wasm32")]
+pub fn __link_section() {}

extensions/gleam/src/gleam.rs 🔗

@@ -0,0 +1,91 @@
+use zed_extension_api::{self as zed, Result};
+
+struct GleamExtension {
+    cached_binary_path: Option<String>,
+}
+
+impl zed::Extension for GleamExtension {
+    fn new() -> Self {
+        Self {
+            cached_binary_path: None,
+        }
+    }
+
+    fn language_server_command(
+        &mut self,
+        config: zed::LanguageServerConfig,
+        _worktree: &zed::Worktree,
+    ) -> Result<zed::Command> {
+        let binary_path = if let Some(path) = &self.cached_binary_path {
+            zed::set_language_server_installation_status(
+                &config.name,
+                &zed::LanguageServerInstallationStatus::Cached,
+            );
+
+            path.clone()
+        } else {
+            zed::set_language_server_installation_status(
+                &config.name,
+                &zed::LanguageServerInstallationStatus::CheckingForUpdate,
+            );
+            let release = zed::latest_github_release(
+                "gleam-lang/gleam",
+                zed::GithubReleaseOptions {
+                    require_assets: true,
+                    pre_release: false,
+                },
+            )?;
+
+            let (platform, arch) = zed::current_platform();
+            let asset_name = format!(
+                "gleam-{version}-{arch}-{os}.tar.gz",
+                version = release.version,
+                arch = match arch {
+                    zed::Architecture::Aarch64 => "aarch64",
+                    zed::Architecture::X86 => "x86",
+                    zed::Architecture::X8664 => "x86_64",
+                },
+                os = match platform {
+                    zed::Os::Mac => "apple-darwin",
+                    zed::Os::Linux => "unknown-linux-musl",
+                    zed::Os::Windows => "pc-windows-msvc",
+                },
+            );
+
+            let asset = release
+                .assets
+                .iter()
+                .find(|asset| asset.name == asset_name)
+                .ok_or_else(|| format!("no asset found matching {:?}", asset_name))?;
+
+            zed::set_language_server_installation_status(
+                &config.name,
+                &zed::LanguageServerInstallationStatus::Downloading,
+            );
+            let version_dir = format!("gleam-{}", release.version);
+            zed::download_file(
+                &asset.download_url,
+                &version_dir,
+                zed::DownloadedFileType::GzipTar,
+            )
+            .map_err(|e| format!("failed to download file: {e}"))?;
+
+            zed::set_language_server_installation_status(
+                &config.name,
+                &zed::LanguageServerInstallationStatus::Downloaded,
+            );
+
+            let binary_path = format!("{version_dir}/gleam");
+            self.cached_binary_path = Some(binary_path.clone());
+            binary_path
+        };
+
+        Ok(zed::Command {
+            command: binary_path,
+            args: vec!["lsp".to_string()],
+            env: Default::default(),
+        })
+    }
+}
+
+zed::register_extension!(GleamExtension);