From 268fa1cbaf91929c3fe3e387a6260be5612b5c97 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 1 Mar 2024 16:00:55 -0800 Subject: [PATCH] Add initial support for defining language server adapters in WebAssembly-based extensions (#8645) 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 Co-authored-by: Nathan Co-authored-by: Marshall Bowers --- .github/workflows/ci.yml | 6 + .gitignore | 1 + Cargo.lock | 681 +++++++++++++++++- Cargo.toml | 4 + .../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 ++--- .../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 | 215 +++--- .../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 | 627 ++++++++++------ 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 | 390 ++++------ crates/language/src/proto.rs | 9 +- crates/language_tools/src/lsp_log_tests.rs | 42 +- 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 | 30 +- crates/project/src/project.rs | 168 +++-- crates/project/src/project_tests.rs | 560 ++++++-------- 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 + .../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, 3714 insertions(+), 1973 deletions(-) create mode 100644 crates/extension/src/extension_lsp_adapter.rs create mode 100644 crates/extension/src/wasm_host.rs create mode 100644 crates/extension_api/Cargo.toml create mode 120000 crates/extension_api/LICENSE-APACHE create mode 100644 crates/extension_api/build.rs create mode 100644 crates/extension_api/src/extension_api.rs create mode 100644 crates/extension_api/wit/extension.wit create mode 100644 extensions/gleam/Cargo.toml create mode 100644 extensions/gleam/extension.toml create mode 100644 extensions/gleam/languages/gleam/config.toml create mode 100644 extensions/gleam/languages/gleam/highlights.scm create mode 100644 extensions/gleam/languages/gleam/indents.scm create mode 100644 extensions/gleam/languages/gleam/outline.scm create mode 100644 extensions/gleam/src/bindings.rs create mode 100644 extensions/gleam/src/gleam.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 617fdaf00e..6f3ed4a18d 100644 --- a/.github/workflows/ci.yml +++ b/.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 diff --git a/.gitignore b/.gitignore index 9b6df52dd1..584337b840 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ /assets/*licenses.md **/venv .build +*.wasm Packages *.xcodeproj xcuserdata/ diff --git a/Cargo.lock b/Cargo.lock index 43584772ee..d69885c0d7 100644 --- a/Cargo.lock +++ b/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" diff --git a/Cargo.toml b/Cargo.toml index 5e1eea7414..fa53be6645 100644 --- a/Cargo.toml +++ b/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" diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index 4f070d31c6..7c4c5b1913 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/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, + name: LanguageServerName, status: LanguageServerBinaryStatus, } @@ -58,13 +58,10 @@ impl ActivityIndicator { let this = cx.new_view(|cx: &mut ViewContext| { 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) diff --git a/crates/collab/src/tests.rs b/crates/collab/src/tests.rs index 36d3dca711..6185b8d582 100644 --- a/crates/collab/src/tests.rs +++ b/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, cx: &mut TestAppContext) -> RoomPartici fn channel_id(room: &Model, cx: &mut TestAppContext) -> Option { cx.read(|cx| room.read(cx).channel_id()) } + +fn rust_lang() -> Arc { + 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()), + )) +} diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index a4de4cc432..9bf92e87f2 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/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() diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 746f5aeeaf..db142afcaf 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/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() diff --git a/crates/collab/src/tests/random_project_collaboration_tests.rs b/crates/collab/src/tests/random_project_collaboration_tests.rs index 37103a3382..9957b785f2 100644 --- a/crates/collab/src/tests/random_project_collaboration_tests.rs +++ b/crates/collab/src/tests/random_project_collaboration_tests.rs @@ -1021,7 +1021,7 @@ impl RandomizedTest for ProjectCollaborationTest { } async fn on_client_added(client: &Rc, _: &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, TestAppContext)]) { diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 3356b00f52..90a8f57130 100644 --- a/crates/copilot/src/copilot.rs +++ b/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 { diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index e826037eeb..98e1ff2991 100644 --- a/crates/editor/src/editor_tests.rs +++ b/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 = "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 { + 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()), + )) +} diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 8b602263ff..0db43d24de 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/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,27 +3367,6 @@ pub mod tests { async fn prepare_test_objects( cx: &mut TestAppContext, ) -> (&'static str, WindowHandle, FakeLanguageServer) { - 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", @@ -3428,7 +3378,30 @@ 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(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()), + ))); + 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) diff --git a/crates/editor/src/test/editor_lsp_test_context.rs b/crates/editor/src/test/editor_lsp_test_context.rs index fe9a80b01b..848e47a2ea 100644 --- a/crates/editor/src/test/editor_lsp_test_context.rs +++ b/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 diff --git a/crates/extension/Cargo.toml b/crates/extension/Cargo.toml index 97eb7adbf3..7c0e2d7afa 100644 --- a/crates/extension/Cargo.toml +++ b/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"] } diff --git a/crates/extension/src/extension_lsp_adapter.rs b/crates/extension/src/extension_lsp_adapter.rs new file mode 100644 index 0000000000..7e3cc9cdc5 --- /dev/null +++ b/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, + _: Arc, + _: Arc, + delegate: Arc, + _: futures::lock::MutexGuard<'a, Option>, + _: &'a mut AsyncAppContext, + ) -> Pin>>> { + 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> { + unreachable!("get_language_server_command is overridden") + } + + async fn fetch_server_binary( + &self, + _: Box, + _: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Result { + unreachable!("get_language_server_command is overridden") + } + + async fn cached_server_binary( + &self, + _: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + unreachable!("get_language_server_command is overridden") + } + + async fn installation_test_binary(&self, _: PathBuf) -> Option { + None + } +} diff --git a/crates/extension/src/extension_store.rs b/crates/extension/src/extension_store.rs index d8434ea895..1386ae016c 100644 --- a/crates/extension/src/extension_store.rs +++ b/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, + pub data: Vec, } #[derive(Clone, Deserialize)] -pub struct Extension { +pub struct ExtensionApiResponse { pub id: Arc, - pub version: Arc, pub name: String, + pub version: Arc, pub description: Option, pub authors: Vec, 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, + + #[serde(default)] + pub description: Option, + #[serde(default)] + pub repository: Option, + #[serde(default)] + pub authors: Vec, + + #[serde(default)] + pub themes: BTreeMap, PathBuf>, + #[serde(default)] + pub languages: BTreeMap, PathBuf>, + #[serde(default)] + pub grammars: BTreeMap, PathBuf>, +} + +#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] +pub struct ExtensionManifest { + pub id: Arc, + pub name: String, + pub version: Arc, + + #[serde(default)] + pub description: Option, + #[serde(default)] + pub repository: Option, + #[serde(default)] + pub authors: Vec, + #[serde(default)] + pub lib: LibManifestEntry, + + #[serde(default)] + pub themes: Vec, + #[serde(default)] + pub languages: Vec, + #[serde(default)] + pub grammars: BTreeMap, GrammarManifestEntry>, + #[serde(default)] + pub language_servers: BTreeMap, +} + +#[derive(Clone, Default, PartialEq, Eq, Debug, Deserialize, Serialize)] +pub struct LibManifestEntry { + path: Option, +} + +#[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, +} + #[derive(Clone)] pub enum ExtensionStatus { NotInstalled, @@ -67,7 +137,7 @@ impl ExtensionStatus { } pub struct ExtensionStore { - manifest: Arc>, + extension_index: ExtensionIndex, fs: Arc, http_client: Arc, extensions_dir: PathBuf, @@ -76,7 +146,9 @@ pub struct ExtensionStore { manifest_path: PathBuf, language_registry: Arc, theme_registry: Arc, - extension_changes: ExtensionChanges, + modified_extensions: HashSet>, + wasm_host: Arc, + wasm_extensions: Vec<(Arc, WasmExtension)>, reload_task: Option>>, needs_reload: bool, _watch_extensions_dir: [Task<()>; 2], @@ -86,56 +158,44 @@ struct GlobalExtensionStore(Model); impl Global for GlobalExtensionStore {} -#[derive(Debug, Deserialize, Serialize, Default)] -pub struct Manifest { - pub extensions: BTreeMap, Arc>, - pub grammars: BTreeMap, GrammarManifestEntry>, - pub languages: BTreeMap, LanguageManifestEntry>, - pub themes: BTreeMap, ThemeManifestEntry>, +#[derive(Debug, Deserialize, Serialize, Default, PartialEq, Eq)] +pub struct ExtensionIndex { + pub extensions: BTreeMap, Arc>, + pub themes: BTreeMap, ExtensionIndexEntry>, + pub languages: BTreeMap, 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, path: PathBuf, } #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Deserialize, Serialize)] -pub struct LanguageManifestEntry { - extension: String, +pub struct ExtensionIndexLanguageEntry { + extension: Arc, path: PathBuf, matcher: LanguageMatcher, grammar: Option>, } -#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] -pub struct ThemeManifestEntry { - extension: String, - path: PathBuf, -} - -#[derive(Default)] -struct ExtensionChanges { - languages: HashSet>, - grammars: HashSet>, - themes: HashSet>, -} - actions!(zed, [ReloadExtensions]); pub fn init( fs: Arc, http_client: Arc, + node_runtime: Arc, language_registry: Arc, theme_registry: Arc, 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, http_client: Arc, + node_runtime: Arc, language_registry: Arc, theme_registry: Arc, cx: &mut ModelContext, ) -> 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, - ) -> Task>> { + ) -> Task>> { 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) { + fn extensions_updated( + &mut self, + new_index: ExtensionIndex, + cx: &mut ModelContext, + ) -> Task> { 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 old_index = &self.extension_index; + let (extensions_to_unload, extensions_to_load) = diff( + old_index.extensions.iter(), + new_index.extensions.iter(), + &self.modified_extensions, ); - 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, - ); - 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::>(); + 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::>(); + 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::>(); + + 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::>(); + 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::>(); - 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(); - - 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; - } - } - }) - .detach(); - - *self.manifest.write() = manifest; + self.extension_index = new_index; cx.notify(); + + 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)); + } + + 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) -> [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(); + for event in events { + let Ok(event_path) = event.path.strip_prefix(&extensions_dir) else { + continue; + }; - { - 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 (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, 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 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::(&manifest_content) + .with_context(|| { + format!("invalid extension.json for extension {extension_name}") + })?; - 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(), - }, - ); - } - } + 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::>(); + themes.sort(); + themes.dedup(); + themes + }, + languages: { + let mut languages = manifest_json.languages.into_values().collect::>(); + 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::(&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,35 +860,39 @@ 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(), + }, + ); } } } + 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); + } + + index + .extensions + .insert(extension_name.into(), Arc::new(extension_manifest)); + Ok(()) } } -impl ExtensionChanges { - fn clear(&mut self) { - self.grammars.clear(); - self.languages.clear(); - self.themes.clear(); - } - - fn merge(&mut self, other: Self) { - self.grammars.extend(other.grammars); - self.languages.extend(other.languages); - self.themes.extend(other.themes); - } -} - fn load_plugin_queries(root_path: &Path) -> LanguageQueries { let mut result = LanguageQueries::default(); if let Some(entries) = std::fs::read_dir(root_path).log_err() { diff --git a/crates/extension/src/extension_store_test.rs b/crates/extension/src/extension_store_test.rs index a9ff4fe443..4f72bf2f87 100644 --- a/crates/extension/src/extension_store_test.rs +++ b/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::::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); + }); +} diff --git a/crates/extension/src/wasm_host.rs b/crates/extension/src/wasm_host.rs new file mode 100644 index 0000000000..611cd9c9b0 --- /dev/null +++ b/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; + +pub(crate) struct WasmHost { + engine: Engine, + linker: Arc>, + http_client: Arc, + node_runtime: Arc, + language_registry: Arc, + fs: Arc, + pub(crate) work_dir: PathBuf, +} + +#[derive(Clone)] +pub struct WasmExtension { + tx: UnboundedSender, + #[allow(unused)] + zed_api_version: SemanticVersion, +} + +pub(crate) struct WasmState { + manifest: Arc, + table: ResourceTable, + ctx: WasiCtx, + host: Arc, +} + +type ExtensionCall = Box< + dyn Send + + for<'a> FnOnce(&'a mut wit::Extension, &'a mut Store) -> BoxFuture<'a, ()>, +>; + +static WASM_ENGINE: OnceLock = OnceLock::new(); + +impl WasmHost { + pub fn new( + fs: Arc, + http_client: Arc, + node_runtime: Arc, + language_registry: Arc, + work_dir: PathBuf, + ) -> Arc { + 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, + wasm_bytes: Vec, + manifest: Arc, + executor: BackgroundExecutor, + ) -> impl 'static + Future> { + 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::(); + 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(&self, f: Fn) -> T + where + T: 'static + Send, + Fn: 'static + + Send + + for<'a> FnOnce(&'a mut wit::Extension, &'a mut Store) -> 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>, + path: String, + ) -> wasmtime::Result> { + 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) -> 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> { + async fn inner(this: &mut WasmState, package_name: String) -> anyhow::Result { + 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> { + async fn inner( + this: &mut WasmState, + repo: String, + options: wit::GithubReleaseOptions, + ) -> anyhow::Result { + 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> { + 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 + } +} diff --git a/crates/extension_api/Cargo.toml b/crates/extension_api/Cargo.toml new file mode 100644 index 0000000000..1adbd0c0ee --- /dev/null +++ b/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" } diff --git a/crates/extension_api/LICENSE-APACHE b/crates/extension_api/LICENSE-APACHE new file mode 120000 index 0000000000..1cd601d0a3 --- /dev/null +++ b/crates/extension_api/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/extension_api/build.rs b/crates/extension_api/build.rs new file mode 100644 index 0000000000..4637257dee --- /dev/null +++ b/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::().unwrap().to_be_bytes(); + let minor = parts.next().unwrap().parse::().unwrap().to_be_bytes(); + let patch = parts.next().unwrap().parse::().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(); +} diff --git a/crates/extension_api/src/extension_api.rs b/crates/extension_api/src/extension_api.rs new file mode 100644 index 0000000000..20332a85e3 --- /dev/null +++ b/crates/extension_api/src/extension_api.rs @@ -0,0 +1,62 @@ +pub struct Guest; +pub use wit::*; + +pub type Result = core::result::Result; + +pub trait Extension: Send + Sync { + fn new() -> Self + where + Self: Sized; + + fn language_server_command( + &mut self, + config: wit::LanguageServerConfig, + worktree: &wit::Worktree, + ) -> Result; +} + +#[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) { + unsafe { EXTENSION = Some((build_extension)()) } +} + +fn extension() -> &'static mut dyn Extension { + unsafe { EXTENSION.as_deref_mut().unwrap() } +} + +static mut EXTENSION: Option> = 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 { + extension().language_server_command(config, worktree) + } +} diff --git a/crates/extension_api/wit/extension.wit b/crates/extension_api/wit/extension.wit new file mode 100644 index 0000000000..1b00874698 --- /dev/null +++ b/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, + } + + 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; + + /// Gets the latest version of the given NPM package. + import npm-package-latest-version: func(package-name: string) -> result; + + /// Gets the latest release for the given GitHub repository. + import latest-github-release: func(repo: string, options: github-release-options) -> result; + + /// 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, + env: list>, + } + + resource worktree { + read-text-file: func(path: string) -> result; + } + + record language-server-config { + name: string, + language-name: string, + } + + export language-server-command: func(config: language-server-config, worktree: borrow) -> result; +} diff --git a/crates/extensions_ui/src/extensions_ui.rs b/crates/extensions_ui/src/extensions_ui.rs index 3e663798a1..45778c85d6 100644 --- a/crates/extensions_ui/src/extensions_ui.rs +++ b/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, is_fetching_extensions: bool, filter: ExtensionFilter, - extension_entries: Vec, + extension_entries: Vec, query_editor: View, query_contains_error: bool, _subscription: gpui::Subscription, @@ -78,7 +78,7 @@ impl ExtensionsPage { }) } - fn filtered_extension_entries(&self, cx: &mut ViewContext) -> Vec { + fn filtered_extension_entries(&self, cx: &mut ViewContext) -> Vec { 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) -> Div { + fn render_entry(&self, extension: &ExtensionApiResponse, cx: &mut ViewContext) -> Div { let status = ExtensionStore::global(cx) .read(cx) .extension_status(&extension.id); diff --git a/crates/fs/Cargo.toml b/crates/fs/Cargo.toml index aedb28d5b3..4b48dbc2bf 100644 --- a/crates/fs/Cargo.toml +++ b/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 diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 2041ebb468..87264d26e2 100644 --- a/crates/fs/src/fs.rs +++ b/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>, + ) -> 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>, + ) -> 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, }, Dir { inode: u64, @@ -575,7 +605,7 @@ impl FakeFs { }) } - pub async fn insert_file(&self, path: impl AsRef, content: String) { + pub async fn insert_file(&self, path: impl AsRef, content: Vec) { 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, content: String) -> Result<()> { + fn write_file_internal(&self, path: impl AsRef, content: Vec) -> 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) -> Result> { + 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 + Send, + src_path: impl 'a + AsRef + 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(&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> { 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) -> 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>, + ) -> 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> { - 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 { - 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::(); 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(()) } diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 8d10bfc7f8..40cf940699 100644 --- a/crates/language/src/language.rs +++ b/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 = 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); -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Location { pub buffer: Model, pub range: Range, @@ -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, pub disk_based_diagnostics_progress_token: Option, pub language_ids: HashMap, pub adapter: Arc, pub reinstall_attempt_count: AtomicU64, + cached_binary: futures::lock::Mutex>, } impl CachedLspAdapter { - pub async fn new(adapter: Arc) -> Arc { + pub fn new(adapter: Arc) -> Arc { 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, + pub async fn get_language_server_command( + self: Arc, + language: Arc, + container_dir: Arc, + delegate: Arc, cx: &mut AsyncAppContext, - ) -> Option>> { - self.adapter.check_if_user_installed(delegate, cx) - } - - pub async fn fetch_latest_server_version( - &self, - delegate: &dyn LspAdapterDelegate, - ) -> Result> { - self.adapter.fetch_latest_server_version(delegate).await - } - - pub fn will_fetch_server( - &self, - delegate: &Arc, - cx: &mut AsyncAppContext, - ) -> Option>> { - self.adapter.will_fetch_server(delegate, cx) + ) -> Result { + 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, - container_dir: PathBuf, - delegate: &dyn LspAdapterDelegate, - ) -> Result { - self.adapter - .fetch_server_binary(version, container_dir, delegate) - .await - } - - pub async fn cached_server_binary( - &self, - container_dir: PathBuf, - delegate: &dyn LspAdapterDelegate, - ) -> Option { - 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; - fn which_command( - &self, - command: OsString, - cx: &AppContext, - ) -> Task)>>; + fn update_status(&self, language: LanguageServerName, status: LanguageServerBinaryStatus); + + async fn which_command(&self, command: OsString) -> Option<(PathBuf, HashMap)>; + async fn read_text_file(&self, path: PathBuf) -> Result; } #[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, + language: Arc, + container_dir: Arc, + delegate: Arc, + mut cached_binary: futures::lock::MutexGuard<'a, Option>, + cx: &'a mut AsyncAppContext, + ) -> Pin>>> { + 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); + } - fn check_if_user_installed( + 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")?; + } + + 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, - _: &mut AsyncAppContext, - ) -> Option>> { + _: &dyn LspAdapterDelegate, + ) -> Option { 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, pub disk_based_diagnostics_sources: Vec, 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>, - pub(crate) adapters: Vec>, - - #[cfg(any(test, feature = "test-support"))] - fake_adapter: Option<( - futures::channel::mpsc::UnboundedSender, - Arc, - )>, } #[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] { - &self.adapters - } - pub fn with_queries(mut self, queries: LanguageQueries) -> Result { 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>) -> 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, - ) -> futures::channel::mpsc::UnboundedReceiver { - 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 { 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, completion: &mut lsp::CompletionItem) { - for adapter in &self.adapters { - adapter.process_completion(completion).await; - } - } - - pub async fn label_for_completion( - self: &Arc, - completion: &lsp::CompletionItem, - ) -> Option { - self.adapters - .first() - .as_ref()? - .label_for_completion(completion, self) - .await - } - - pub async fn label_for_symbol( - self: &Arc, - name: &str, - kind: lsp::SymbolKind, - ) -> Option { - self.adapters - .first() - .as_ref()? - .label_for_symbol(name, kind, self) - .await - } - pub fn highlight_text<'a>( self: &'a Arc, 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 { +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, + _: Arc, + _: Arc, + _: Arc, + _: futures::lock::MutexGuard<'a, Option>, + _: &'a mut AsyncAppContext, + ) -> Pin>>> { + async move { Ok(self.language_server_binary.clone()) }.boxed_local() } async fn fetch_latest_server_version( @@ -1464,6 +1468,10 @@ impl LspAdapter for Arc { 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)]) { diff --git a/crates/language/src/language_registry.rs b/crates/language/src/language_registry.rs index 95cdc3ff31..8bc29f943f 100644 --- a/crates/language/src/language_registry.rs +++ b/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>, available_languages: Vec, grammars: HashMap, AvailableGrammar>, + lsp_adapters: HashMap, Vec>>, loading_languages: HashMap>>>>, subscription: (watch::Sender<()>, watch::Receiver<()>), theme: Option>, version: usize, reload_count: usize, + + #[cfg(any(test, feature = "test-support"))] + fake_server_txs: + HashMap, Vec>>, } -#[derive(Clone)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum LanguageServerBinaryStatus { CheckingForUpdate, Downloading, @@ -72,7 +77,6 @@ struct AvailableLanguage { grammar: Option>, matcher: LanguageMatcher, load: Arc Result<(LanguageConfig, LanguageQueries)> + 'static + Send + Sync>, - lsp_adapters: Vec>, loaded: bool, } @@ -112,7 +116,7 @@ pub struct LanguageQueries { #[derive(Clone, Default)] struct LspBinaryStatusSender { - txs: Arc, LanguageServerBinaryStatus)>>>>, + txs: Arc>>>, } 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, adapter: Arc) { + 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 { + 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 { + 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, grammar_name: Option>, matcher: LanguageMatcher, - lsp_adapters: Vec>, 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) -> Vec> { + 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, stderr_capture: Arc>>, @@ -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::() - .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 = 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. + 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 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, + 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::() + .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, 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, LanguageServerBinaryStatus)> { + fn subscribe( + &self, + ) -> mpsc::UnboundedReceiver<(LanguageServerName, LanguageServerBinaryStatus)> { let (tx, rx) = mpsc::unbounded(); self.txs.lock().push(tx); rx } - fn send(&self, language: Arc, 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() - }); + txs.retain(|tx| tx.unbounded_send((name.clone(), status.clone())).is_ok()); } } - -async fn check_user_installed_binary( - adapter: Arc, - language: Arc, - delegate: Arc, - cx: &mut AsyncAppContext, -) -> Option { - 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, - adapter: &Arc, - language: Arc, - delegate: &Arc, - cx: &AsyncAppContext, - container_dir: Arc, - lsp_binary_statuses: LspBinaryStatusSender, -) -> Result { - 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, - language: Arc, - delegate: Arc, - container_dir: Arc, - statuses: LspBinaryStatusSender, - mut cx: AsyncAppContext, -) -> Result { - 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), - }, - ); - } - - binary -} - -async fn fetch_latest_binary( - adapter: Arc, - language: Arc, - delegate: &dyn LspAdapterDelegate, - container_dir: &Path, - lsp_binary_statuses_tx: LspBinaryStatusSender, -) -> Result { - let container_dir: Arc = 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) -} diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index eec2dcbb29..eae72092ba 100644 --- a/crates/language/src/proto.rs +++ b/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>, + language_registry: &Arc, ) -> Result { 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 { diff --git a/crates/language_tools/src/lsp_log_tests.rs b/crates/language_tools/src/lsp_log_tests.rs index b00d1bb79a..6058f454b5 100644 --- a/crates/language_tools/src/lsp_log_tests.rs +++ b/crates/language_tools/src/lsp_log_tests.rs @@ -20,24 +20,6 @@ async fn test_lsp_logs(cx: &mut 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 fake_rust_servers = rust_language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - name: "the-rust-language-server", - ..Default::default() - })) - .await; - let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( "/the-root", @@ -47,10 +29,28 @@ async fn test_lsp_logs(cx: &mut TestAppContext) { }), ) .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 language_registry = project.read_with(cx, |project, _| project.languages().clone()); + language_registry.add(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()), + ))); + let mut fake_rust_servers = language_registry.register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { + name: "the-rust-language-server", + ..Default::default() + }, + ); let log_store = cx.new_model(|cx| LogStore::new(cx)); log_store.update(cx, |store, cx| store.add_project(&project, cx)); diff --git a/crates/languages/src/astro.rs b/crates/languages/src/astro.rs index 3ce39b8fc3..6fa3ada89a 100644 --- a/crates/languages/src/astro.rs +++ b/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, diff --git a/crates/languages/src/c.rs b/crates/languages/src/c.rs index a19361c6b4..bad4e0a076 100644 --- a/crates/languages/src/c.rs +++ b/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(), "") diff --git a/crates/languages/src/clojure.rs b/crates/languages/src/clojure.rs index 7cb333c179..139d741db2 100644 --- a/crates/languages/src/clojure.rs +++ b/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, diff --git a/crates/languages/src/csharp.rs b/crates/languages/src/csharp.rs index 149aa754c7..297e397cdd 100644 --- a/crates/languages/src/csharp.rs +++ b/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, diff --git a/crates/languages/src/css.rs b/crates/languages/src/css.rs index 8e4bd55c16..e90158652b 100644 --- a/crates/languages/src/css.rs +++ b/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, diff --git a/crates/languages/src/dart.rs b/crates/languages/src/dart.rs index bd906e67b1..5c0d0c21a1 100644 --- a/crates/languages/src/dart.rs +++ b/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, diff --git a/crates/languages/src/deno.rs b/crates/languages/src/deno.rs index 95493b2912..abb9dccbfb 100644 --- a/crates/languages/src/deno.rs +++ b/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, diff --git a/crates/languages/src/dockerfile.rs b/crates/languages/src/dockerfile.rs index 4717c9d0e2..2b4fc792e2 100644 --- a/crates/languages/src/dockerfile.rs +++ b/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, diff --git a/crates/languages/src/elixir.rs b/crates/languages/src/elixir.rs index a70543f9d3..471f466c84 100644 --- a/crates/languages/src/elixir.rs +++ b/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, @@ -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, diff --git a/crates/languages/src/elm.rs b/crates/languages/src/elm.rs index 10de8dcd2f..e857b6edd2 100644 --- a/crates/languages/src/elm.rs +++ b/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, diff --git a/crates/languages/src/erlang.rs b/crates/languages/src/erlang.rs index 8d276cb8b6..2b6d33ed41 100644 --- a/crates/languages/src/erlang.rs +++ b/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, diff --git a/crates/languages/src/gleam.rs b/crates/languages/src/gleam.rs index 6efb3cd38a..9eb22c179e 100644 --- a/crates/languages/src/gleam.rs +++ b/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, diff --git a/crates/languages/src/go.rs b/crates/languages/src/go.rs index ba7aedb71b..dc0abf1747 100644 --- a/crates/languages/src/go.rs +++ b/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, - cx: &mut AsyncAppContext, - ) -> Option>> { - 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 { + 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(), diff --git a/crates/languages/src/haskell.rs b/crates/languages/src/haskell.rs index c7e64685b4..0e8c59d623 100644 --- a/crates/languages/src/haskell.rs +++ b/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, diff --git a/crates/languages/src/html.rs b/crates/languages/src/html.rs index 0177d0956e..1b152aa5f5 100644 --- a/crates/languages/src/html.rs +++ b/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, diff --git a/crates/languages/src/json.rs b/crates/languages/src/json.rs index cee48ef4ec..ff8746726e 100644 --- a/crates/languages/src/json.rs +++ b/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, diff --git a/crates/languages/src/lib.rs b/crates/languages/src/lib.rs index 6bbaa8090d..f47bf33214 100644 --- a/crates/languages/src/lib.rs +++ b/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>| { 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 { +pub fn language(name: &str, grammar: tree_sitter::Language) -> Arc { Arc::new( Language::new(load_config(name), Some(grammar)) - .with_lsp_adapters(lsp_adapter.into_iter().collect()) - .await .with_queries(load_queries(name)) .unwrap(), ) diff --git a/crates/languages/src/lua.rs b/crates/languages/src/lua.rs index 9a3447e678..cad9004480 100644 --- a/crates/languages/src/lua.rs +++ b/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, diff --git a/crates/languages/src/nu.rs b/crates/languages/src/nu.rs index 374ecd6042..691ecfd52f 100644 --- a/crates/languages/src/nu.rs +++ b/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, diff --git a/crates/languages/src/ocaml.rs b/crates/languages/src/ocaml.rs index 3a292884c0..3b965b9ba0 100644 --- a/crates/languages/src/ocaml.rs +++ b/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, diff --git a/crates/languages/src/php.rs b/crates/languages/src/php.rs index a41cd13760..1405614210 100644 --- a/crates/languages/src/php.rs +++ b/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, diff --git a/crates/languages/src/prisma.rs b/crates/languages/src/prisma.rs index d07355cb16..cf2d2ca682 100644 --- a/crates/languages/src/prisma.rs +++ b/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, diff --git a/crates/languages/src/purescript.rs b/crates/languages/src/purescript.rs index 931809a1ed..ffca892cdc 100644 --- a/crates/languages/src/purescript.rs +++ b/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, diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index 1c56a57f04..36c1c7031f 100644 --- a/crates/languages/src/python.rs +++ b/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); diff --git a/crates/languages/src/ruby.rs b/crates/languages/src/ruby.rs index 068a6e97d0..ced781bfbf 100644 --- a/crates/languages/src/ruby.rs +++ b/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, diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index 51914ee3fa..2d3925e7d6 100644 --- a/crates/languages/src/rust.rs +++ b/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) -> Vec".to_string()), - ..Default::default() - }) + adapter + .label_for_completion( + &lsp::CompletionItem { + kind: Some(lsp::CompletionItemKind::FUNCTION), + label: "hello(…)".to_string(), + detail: Some("fn(&mut Option) -> Vec".to_string()), + ..Default::default() + }, + &language + ) .await, Some(CodeLabel { text: "hello(&mut Option) -> Vec".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) -> Vec".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) -> Vec".to_string()), + ..Default::default() + }, + &language + ) .await, Some(CodeLabel { text: "hello(&mut Option) -> Vec".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) -> Vec".to_string()), - ..Default::default() - }) + adapter + .label_for_completion( + &lsp::CompletionItem { + kind: Some(lsp::CompletionItemKind::FUNCTION), + label: "hello(…)".to_string(), + detail: Some("fn(&mut Option) -> Vec".to_string()), + ..Default::default() + }, + &language + ) .await, Some(CodeLabel { text: "hello(&mut Option) -> Vec".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(), "") diff --git a/crates/languages/src/svelte.rs b/crates/languages/src/svelte.rs index 3a09991e4b..5167663724 100644 --- a/crates/languages/src/svelte.rs +++ b/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, diff --git a/crates/languages/src/tailwind.rs b/crates/languages/src/tailwind.rs index e2937ac505..e91ab535f8 100644 --- a/crates/languages/src/tailwind.rs +++ b/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, diff --git a/crates/languages/src/terraform.rs b/crates/languages/src/terraform.rs index dba2fdcc1a..6d685ca55c 100644 --- a/crates/languages/src/terraform.rs +++ b/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, diff --git a/crates/languages/src/toml.rs b/crates/languages/src/toml.rs index c8eda04800..1ca6bb8d1d 100644 --- a/crates/languages/src/toml.rs +++ b/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, diff --git a/crates/languages/src/typescript.rs b/crates/languages/src/typescript.rs index 2370dd991f..33083c2fd1 100644 --- a/crates/languages/src/typescript.rs +++ b/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() { diff --git a/crates/languages/src/uiua.rs b/crates/languages/src/uiua.rs index 2210f63888..229c0804f5 100644 --- a/crates/languages/src/uiua.rs +++ b/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, diff --git a/crates/languages/src/vue.rs b/crates/languages/src/vue.rs index 2fc9a91907..f90364c66b 100644 --- a/crates/languages/src/vue.rs +++ b/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, diff --git a/crates/languages/src/yaml.rs b/crates/languages/src/yaml.rs index 3020e13ef1..0e543410d4 100644 --- a/crates/languages/src/yaml.rs +++ b/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, diff --git a/crates/languages/src/zig.rs b/crates/languages/src/zig.rs index d60d829a6f..d3ad22aa8b 100644 --- a/crates/languages/src/zig.rs +++ b/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, - cx: &mut AsyncAppContext, - ) -> Option>> { - 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 { + let (path, env) = delegate.which_command(OsString::from("zls")).await?; + Some(LanguageServerBinary { + path, + arguments: vec![], + env: Some(env), + }) } async fn fetch_server_binary( diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index caefa57c86..03823e223b 100644 --- a/crates/lsp/src/lsp.rs +++ b/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, 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(); diff --git a/crates/prettier/src/prettier.rs b/crates/prettier/src/prettier.rs index 5aa78eef32..a2a168fb2d 100644 --- a/crates/prettier/src/prettier.rs +++ b/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, + language_registry: Arc, } #[cfg(any(test, feature = "test-support"))] @@ -155,6 +156,7 @@ impl Prettier { _: LanguageServerId, prettier_dir: PathBuf, _: Arc, + _: Arc, _: AsyncAppContext, ) -> anyhow::Result { Ok(Self::Test(TestPrettier { @@ -168,6 +170,7 @@ impl Prettier { server_id: LanguageServerId, prettier_dir: PathBuf, node: Arc, + language_registry: Arc, cx: AsyncAppContext, ) -> anyhow::Result { 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::>(); 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 diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 59e958d082..128a11d5f3 100644 --- a/crates/project/src/lsp_command.rs +++ b/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: Model, buffer: Model, mut cx: AsyncAppContext, ) -> Result> { @@ -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 } diff --git a/crates/project/src/prettier_support.rs b/crates/project/src/prettier_support.rs index 496f435d49..48a21f9158 100644 --- a/crates/project/src/prettier_support.rs +++ b/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, + language: &Arc, language_settings: &LanguageSettings, ) -> Option> { 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 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, cx.clone()) - .await - .context("default prettier spawn") - .map(Arc::new) - .map_err(Arc::new)?; + 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) }) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 9faf058ac6..f3989390ce 100644 --- a/crates/project/src/project.rs +++ b/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, mut cx: AsyncAppContext, ) -> Result { + 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> { + 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> { if let Some(LanguageServerState::Running { server, .. }) = self.language_servers.get(&id) { Some(server.clone()) @@ -9025,8 +9057,8 @@ impl Project { ) -> Vec { 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> From<(WorktreeId, P)> for ProjectPath { struct ProjectLspAdapterDelegate { project: Model, - worktree: Model, + worktree: worktree::Snapshot, + fs: Arc, http_client: Arc, + language_registry: Arc, } impl ProjectLspAdapterDelegate { fn new(project: &Project, worktree: &Model, cx: &ModelContext) -> Arc { 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)>> { - 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)> { + 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(); + 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 - } + 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 { + 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) } } diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 69bff1a5e6..1af8fca291 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -189,55 +189,6 @@ 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 { - name: "the-rust-language-server", - capabilities: lsp::ServerCapabilities { - completion_provider: Some(lsp::CompletionOptions { - trigger_characters: Some(vec![".".to_string(), "::".to_string()]), - ..Default::default() - }), - ..Default::default() - }, - ..Default::default() - })) - .await; - let mut fake_json_servers = json_language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - name: "the-json-language-server", - 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( "/the-root", @@ -251,6 +202,36 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { .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 { + trigger_characters: Some(vec![".".to_string(), "::".to_string()]), + ..Default::default() + }), + ..Default::default() + }, + ..Default::default() + }, + ); + 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 { + trigger_characters: Some(vec![":".to_string()]), + ..Default::default() + }), + ..Default::default() + }, + ..Default::default() + }, + ); // 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() - })) - .await; - let mut js = Language::new( - LanguageConfig { - name: Arc::from("JavaScript"), - matcher: LanguageMatcher { - path_suffixes: vec!["js".to_string()], - ..Default::default() - }, - ..Default::default() - }, - None, - ); - let mut fake_js_servers = js - .set_fake_lsp_adapter(Arc::new(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)); - }); + 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() + }, + ); + let mut fake_js_servers = language_registry.register_fake_lsp_adapter( + "JavaScript", + FakeLspAdapter { + name: "js-lsp", + ..Default::default() + }, + ); + 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( 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 { + 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 { + 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 { + 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 { + 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()), + )) +} diff --git a/crates/project/src/task_inventory.rs b/crates/project/src/task_inventory.rs index 2c4b08c15c..54b3e80a8b 100644 --- a/crates/project/src/task_inventory.rs +++ b/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}, diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index 9ee052b9f7..035eca6ffd 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/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::::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| { diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index 9c3cc5311a..af571dd632 100644 --- a/crates/vim/src/command.rs +++ b/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"]); diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 73029b7e3c..00af090d33 100644 --- a/crates/zed/src/main.rs +++ b/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, diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 5571273566..fdbf173b2d 100644 --- a/crates/zed/src/zed.rs +++ b/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(); diff --git a/extensions/gleam/Cargo.toml b/extensions/gleam/Cargo.toml new file mode 100644 index 0000000000..1e8ff5cdea --- /dev/null +++ b/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] diff --git a/extensions/gleam/extension.toml b/extensions/gleam/extension.toml new file mode 100644 index 0000000000..76cce0f291 --- /dev/null +++ b/extensions/gleam/extension.toml @@ -0,0 +1,13 @@ +id = "gleam" +name = "Gleam" +description = "Gleam support for Zed" +version = "0.0.1" +authors = ["Marshall Bowers "] + +[language_servers.gleam] +name = "Gleam LSP" +language = "Gleam" + +[grammars.gleam] +repository = "https://github.com/gleam-lang/tree-sitter-gleam" +commit = "58b7cac8fc14c92b0677c542610d8738c373fa81" diff --git a/extensions/gleam/languages/gleam/config.toml b/extensions/gleam/languages/gleam/config.toml new file mode 100644 index 0000000000..0a472172ad --- /dev/null +++ b/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"] }, +] diff --git a/extensions/gleam/languages/gleam/highlights.scm b/extensions/gleam/languages/gleam/highlights.scm new file mode 100644 index 0000000000..a95f6cb031 --- /dev/null +++ b/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 diff --git a/extensions/gleam/languages/gleam/indents.scm b/extensions/gleam/languages/gleam/indents.scm new file mode 100644 index 0000000000..112b414aa4 --- /dev/null +++ b/extensions/gleam/languages/gleam/indents.scm @@ -0,0 +1,3 @@ +(_ "[" "]" @end) @indent +(_ "{" "}" @end) @indent +(_ "(" ")" @end) @indent diff --git a/extensions/gleam/languages/gleam/outline.scm b/extensions/gleam/languages/gleam/outline.scm new file mode 100644 index 0000000000..5df7a6af80 --- /dev/null +++ b/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 diff --git a/extensions/gleam/src/bindings.rs b/extensions/gleam/src/bindings.rs new file mode 100644 index 0000000000..a5e0b040dc --- /dev/null +++ b/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() {} diff --git a/extensions/gleam/src/gleam.rs b/extensions/gleam/src/gleam.rs new file mode 100644 index 0000000000..ffc8515802 --- /dev/null +++ b/extensions/gleam/src/gleam.rs @@ -0,0 +1,91 @@ +use zed_extension_api::{self as zed, Result}; + +struct GleamExtension { + cached_binary_path: Option, +} + +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 { + 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);