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 <marshall@zed.dev>
Co-authored-by: Nathan <nathan@zed.dev>
Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
This commit is contained in:
Max Brunsfeld 2024-03-01 16:00:55 -08:00 committed by GitHub
parent f3f2225a8e
commit 268fa1cbaf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
84 changed files with 3714 additions and 1973 deletions

View File

@ -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

1
.gitignore vendored
View File

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

681
Cargo.lock generated
View File

@ -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"

View File

@ -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"

View File

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

View File

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

View File

@ -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()

View File

@ -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()

View File

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

View File

@ -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 {

View File

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

View File

@ -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<Editor>, 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)

View File

@ -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

View File

@ -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"] }

View File

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

View File

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

View File

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

View File

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

View File

@ -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" }

View File

@ -0,0 +1 @@
../../LICENSE-APACHE

View File

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

View File

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

View File

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

View File

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

View File

@ -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

View File

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

View File

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

View File

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

View File

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

View File

@ -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));

View File

@ -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,

View File

@ -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(), "")

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

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

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

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

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

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

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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);

View File

@ -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,

View File

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

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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() {

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<T: ToOffset + ToPoint>(
async fn test_definition(cx: &mut gpui::TestAppContext) {
init_test(cx);
let mut language = Language::new(
LanguageConfig {
name: "Rust".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
..Default::default()
},
Some(tree_sitter_rust::language()),
);
let mut fake_servers = language.set_fake_lsp_adapter(Default::default()).await;
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/dir",
@ -2346,7 +2225,11 @@ async fn test_definition(cx: &mut gpui::TestAppContext) {
.await;
let project = Project::test(fs, ["/dir/b.rs".as_ref()], cx).await;
project.update(cx, |project, _| project.languages.add(Arc::new(language)));
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(rust_lang());
let mut fake_servers =
language_registry.register_fake_lsp_adapter("Rust", FakeLspAdapter::default());
let buffer = project
.update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx))
@ -2426,30 +2309,6 @@ async fn test_definition(cx: &mut gpui::TestAppContext) {
async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) {
init_test(cx);
let mut language = Language::new(
LanguageConfig {
name: "TypeScript".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["ts".to_string()],
..Default::default()
},
..Default::default()
},
Some(tree_sitter_typescript::language_typescript()),
);
let mut fake_language_servers = language
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
capabilities: lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions {
trigger_characters: Some(vec![":".to_string()]),
..Default::default()
}),
..Default::default()
},
..Default::default()
}))
.await;
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/dir",
@ -2460,7 +2319,23 @@ async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) {
.await;
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
project.update(cx, |project, _| project.languages.add(Arc::new(language)));
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(typescript_lang());
let mut fake_language_servers = language_registry.register_fake_lsp_adapter(
"TypeScript",
FakeLspAdapter {
capabilities: lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions {
trigger_characters: Some(vec![":".to_string()]),
..Default::default()
}),
..Default::default()
},
..Default::default()
},
);
let buffer = project
.update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
.await
@ -2526,30 +2401,6 @@ async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) {
async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) {
init_test(cx);
let mut language = Language::new(
LanguageConfig {
name: "TypeScript".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["ts".to_string()],
..Default::default()
},
..Default::default()
},
Some(tree_sitter_typescript::language_typescript()),
);
let mut fake_language_servers = language
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
capabilities: lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions {
trigger_characters: Some(vec![":".to_string()]),
..Default::default()
}),
..Default::default()
},
..Default::default()
}))
.await;
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/dir",
@ -2560,7 +2411,23 @@ async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) {
.await;
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
project.update(cx, |project, _| project.languages.add(Arc::new(language)));
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(typescript_lang());
let mut fake_language_servers = language_registry.register_fake_lsp_adapter(
"TypeScript",
FakeLspAdapter {
capabilities: lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions {
trigger_characters: Some(vec![":".to_string()]),
..Default::default()
}),
..Default::default()
},
..Default::default()
},
);
let buffer = project
.update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
.await
@ -2595,19 +2462,6 @@ async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) {
async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) {
init_test(cx);
let mut language = Language::new(
LanguageConfig {
name: "TypeScript".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["ts".to_string()],
..Default::default()
},
..Default::default()
},
None,
);
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/dir",
@ -2618,7 +2472,12 @@ async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) {
.await;
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
project.update(cx, |project, _| project.languages.add(Arc::new(language)));
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(typescript_lang());
let mut fake_language_servers =
language_registry.register_fake_lsp_adapter("TypeScript", Default::default());
let buffer = project
.update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
.await
@ -2904,16 +2763,7 @@ async fn test_save_as(cx: &mut gpui::TestAppContext) {
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
let languages = project.update(cx, |project, _| project.languages().clone());
languages.register_native_grammars([("rust", tree_sitter_rust::language())]);
languages.register_test_language(LanguageConfig {
name: "Rust".into(),
grammar: Some("rust".into()),
matcher: LanguageMatcher {
path_suffixes: vec!["rs".into()],
..Default::default()
},
..Default::default()
});
languages.add(rust_lang());
let buffer = project.update(cx, |project, cx| {
project.create_buffer("", None, cx).unwrap()
@ -3733,30 +3583,6 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
async fn test_rename(cx: &mut gpui::TestAppContext) {
init_test(cx);
let mut language = Language::new(
LanguageConfig {
name: "Rust".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
..Default::default()
},
Some(tree_sitter_rust::language()),
);
let mut fake_servers = language
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
capabilities: lsp::ServerCapabilities {
rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
prepare_provider: Some(true),
work_done_progress_options: Default::default(),
})),
..Default::default()
},
..Default::default()
}))
.await;
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/dir",
@ -3768,7 +3594,23 @@ async fn test_rename(cx: &mut gpui::TestAppContext) {
.await;
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
project.update(cx, |project, _| project.languages.add(Arc::new(language)));
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(rust_lang());
let mut fake_servers = language_registry.register_fake_lsp_adapter(
"Rust",
FakeLspAdapter {
capabilities: lsp::ServerCapabilities {
rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
prepare_provider: Some(true),
work_done_progress_options: Default::default(),
})),
..Default::default()
},
..Default::default()
},
);
let buffer = project
.update(cx, |project, cx| {
project.open_local_buffer("/dir/one.rs", cx)
@ -4475,3 +4317,59 @@ fn init_test(cx: &mut gpui::TestAppContext) {
Project::init_settings(cx);
});
}
fn json_lang() -> Arc<Language> {
Arc::new(Language::new(
LanguageConfig {
name: "JSON".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["json".to_string()],
..Default::default()
},
..Default::default()
},
None,
))
}
fn js_lang() -> Arc<Language> {
Arc::new(Language::new(
LanguageConfig {
name: Arc::from("JavaScript"),
matcher: LanguageMatcher {
path_suffixes: vec!["js".to_string()],
..Default::default()
},
..Default::default()
},
None,
))
}
fn rust_lang() -> Arc<Language> {
Arc::new(Language::new(
LanguageConfig {
name: "Rust".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
..Default::default()
},
Some(tree_sitter_rust::language()),
))
}
fn typescript_lang() -> Arc<Language> {
Arc::new(Language::new(
LanguageConfig {
name: "TypeScript".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["ts".to_string()],
..Default::default()
},
..Default::default()
},
Some(tree_sitter_typescript::language_typescript()),
))
}

View File

@ -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},

View File

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

View File

@ -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"]);

View File

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

View File

@ -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();

View File

@ -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]

View File

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

View File

@ -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"] },
]

View File

@ -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

View File

@ -0,0 +1,3 @@
(_ "[" "]" @end) @indent
(_ "{" "}" @end) @indent
(_ "(" ")" @end) @indent

View File

@ -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

View File

@ -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() {}

View File

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