Detailed changes
@@ -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
@@ -10,6 +10,7 @@
/assets/*licenses.md
**/venv
.build
+*.wasm
Packages
*.xcodeproj
xcuserdata/
@@ -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"
@@ -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"
@@ -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)
@@ -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()),
+ ))
+}
@@ -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()
@@ -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()
@@ -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)]) {
@@ -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 {
@@ -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()),
+ ))
+}
@@ -1553,12 +1553,14 @@ pub mod tests {
}),
)
.await;
+
let project = Project::test(fs, ["/a".as_ref()], cx).await;
+ let language_registry = project.read_with(cx, |project, _| project.languages().clone());
let mut rs_fake_servers = None;
let mut md_fake_servers = None;
for (name, path_suffix) in [("Rust", "rs"), ("Markdown", "md")] {
- let mut language = Language::new(
+ language_registry.add(Arc::new(Language::new(
LanguageConfig {
name: name.into(),
matcher: LanguageMatcher {
@@ -1568,25 +1570,23 @@ pub mod tests {
..Default::default()
},
Some(tree_sitter_rust::language()),
- );
- let fake_servers = language
- .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+ )));
+ let fake_servers = language_registry.register_fake_lsp_adapter(
+ name,
+ FakeLspAdapter {
name,
capabilities: lsp::ServerCapabilities {
inlay_hint_provider: Some(lsp::OneOf::Left(true)),
..Default::default()
},
..Default::default()
- }))
- .await;
+ },
+ );
match name {
"Rust" => rs_fake_servers = Some(fake_servers),
"Markdown" => md_fake_servers = Some(fake_servers),
_ => unreachable!(),
}
- project.update(cx, |project, _| {
- project.languages().add(Arc::new(language));
- });
}
let rs_buffer = project
@@ -2253,26 +2253,6 @@ pub mod tests {
})
});
- let mut language = Language::new(
- LanguageConfig {
- name: "Rust".into(),
- matcher: LanguageMatcher {
- path_suffixes: vec!["rs".to_string()],
- ..Default::default()
- },
- ..Default::default()
- },
- Some(tree_sitter_rust::language()),
- );
- let mut fake_servers = language
- .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
- capabilities: lsp::ServerCapabilities {
- inlay_hint_provider: Some(lsp::OneOf::Left(true)),
- ..Default::default()
- },
- ..Default::default()
- }))
- .await;
let fs = FakeFs::new(cx.background_executor.clone());
fs.insert_tree(
"/a",
@@ -2282,8 +2262,22 @@ pub mod tests {
}),
)
.await;
+
let project = Project::test(fs, ["/a".as_ref()], cx).await;
- project.update(cx, |project, _| project.languages().add(Arc::new(language)));
+
+ let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+ language_registry.add(crate::editor_tests::rust_lang());
+ let mut fake_servers = language_registry.register_fake_lsp_adapter(
+ "Rust",
+ FakeLspAdapter {
+ capabilities: lsp::ServerCapabilities {
+ inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+ ..Default::default()
+ },
+ ..Default::default()
+ },
+ );
+
let buffer = project
.update(cx, |project, cx| {
project.open_local_buffer("/a/main.rs", cx)
@@ -2554,27 +2548,6 @@ pub mod tests {
})
});
- let mut language = Language::new(
- LanguageConfig {
- name: "Rust".into(),
- matcher: LanguageMatcher {
- path_suffixes: vec!["rs".to_string()],
- ..Default::default()
- },
- ..Default::default()
- },
- Some(tree_sitter_rust::language()),
- );
- let mut fake_servers = language
- .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
- capabilities: lsp::ServerCapabilities {
- inlay_hint_provider: Some(lsp::OneOf::Left(true)),
- ..Default::default()
- },
- ..Default::default()
- }))
- .await;
- let language = Arc::new(language);
let fs = FakeFs::new(cx.background_executor.clone());
fs.insert_tree(
"/a",
@@ -2584,10 +2557,23 @@ pub mod tests {
}),
)
.await;
+
let project = Project::test(fs, ["/a".as_ref()], cx).await;
- project.update(cx, |project, _| {
- project.languages().add(Arc::clone(&language))
- });
+
+ let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+ let language = crate::editor_tests::rust_lang();
+ language_registry.add(language);
+ let mut fake_servers = language_registry.register_fake_lsp_adapter(
+ "Rust",
+ FakeLspAdapter {
+ capabilities: lsp::ServerCapabilities {
+ inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+ ..Default::default()
+ },
+ ..Default::default()
+ },
+ );
+
let worktree_id = project.update(cx, |project, cx| {
project.worktrees().next().unwrap().read(cx).id()
});
@@ -2911,27 +2897,6 @@ pub mod tests {
})
});
- let mut language = Language::new(
- LanguageConfig {
- name: "Rust".into(),
- matcher: LanguageMatcher {
- path_suffixes: vec!["rs".to_string()],
- ..Default::default()
- },
- ..Default::default()
- },
- Some(tree_sitter_rust::language()),
- );
- let mut fake_servers = language
- .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
- capabilities: lsp::ServerCapabilities {
- inlay_hint_provider: Some(lsp::OneOf::Left(true)),
- ..Default::default()
- },
- ..Default::default()
- }))
- .await;
- let language = Arc::new(language);
let fs = FakeFs::new(cx.background_executor.clone());
fs.insert_tree(
"/a",
@@ -2941,10 +2906,22 @@ pub mod tests {
}),
)
.await;
+
let project = Project::test(fs, ["/a".as_ref()], cx).await;
- project.update(cx, |project, _| {
- project.languages().add(Arc::clone(&language))
- });
+
+ let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+ language_registry.add(crate::editor_tests::rust_lang());
+ let mut fake_servers = language_registry.register_fake_lsp_adapter(
+ "Rust",
+ FakeLspAdapter {
+ capabilities: lsp::ServerCapabilities {
+ inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+ ..Default::default()
+ },
+ ..Default::default()
+ },
+ );
+
let worktree_id = project.update(cx, |project, cx| {
project.worktrees().next().unwrap().read(cx).id()
});
@@ -3149,26 +3126,6 @@ pub mod tests {
})
});
- let mut language = Language::new(
- LanguageConfig {
- name: "Rust".into(),
- matcher: LanguageMatcher {
- path_suffixes: vec!["rs".to_string()],
- ..Default::default()
- },
- ..Default::default()
- },
- Some(tree_sitter_rust::language()),
- );
- let mut fake_servers = language
- .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
- capabilities: lsp::ServerCapabilities {
- inlay_hint_provider: Some(lsp::OneOf::Left(true)),
- ..Default::default()
- },
- ..Default::default()
- }))
- .await;
let fs = FakeFs::new(cx.background_executor.clone());
fs.insert_tree(
"/a",
@@ -3178,8 +3135,22 @@ pub mod tests {
}),
)
.await;
+
let project = Project::test(fs, ["/a".as_ref()], cx).await;
- project.update(cx, |project, _| project.languages().add(Arc::new(language)));
+
+ let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+ language_registry.add(crate::editor_tests::rust_lang());
+ let mut fake_servers = language_registry.register_fake_lsp_adapter(
+ "Rust",
+ FakeLspAdapter {
+ capabilities: lsp::ServerCapabilities {
+ inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+ ..Default::default()
+ },
+ ..Default::default()
+ },
+ );
+
let buffer = project
.update(cx, |project, cx| {
project.open_local_buffer("/a/main.rs", cx)
@@ -3396,7 +3367,20 @@ pub mod tests {
async fn prepare_test_objects(
cx: &mut TestAppContext,
) -> (&'static str, WindowHandle<Editor>, FakeLanguageServer) {
- let mut language = Language::new(
+ let fs = FakeFs::new(cx.background_executor.clone());
+ fs.insert_tree(
+ "/a",
+ json!({
+ "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
+ "other.rs": "// Test file",
+ }),
+ )
+ .await;
+
+ let project = Project::test(fs, ["/a".as_ref()], cx).await;
+
+ let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+ language_registry.add(Arc::new(Language::new(
LanguageConfig {
name: "Rust".into(),
matcher: LanguageMatcher {
@@ -3406,29 +3390,18 @@ pub mod tests {
..Default::default()
},
Some(tree_sitter_rust::language()),
- );
- let mut fake_servers = language
- .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+ )));
+ let mut fake_servers = language_registry.register_fake_lsp_adapter(
+ "Rust",
+ FakeLspAdapter {
capabilities: lsp::ServerCapabilities {
inlay_hint_provider: Some(lsp::OneOf::Left(true)),
..Default::default()
},
..Default::default()
- }))
- .await;
-
- let fs = FakeFs::new(cx.background_executor.clone());
- fs.insert_tree(
- "/a",
- json!({
- "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
- "other.rs": "// Test file",
- }),
- )
- .await;
+ },
+ );
- let project = Project::test(fs, ["/a".as_ref()], cx).await;
- project.update(cx, |project, _| project.languages().add(Arc::new(language)));
let buffer = project
.update(cx, |project, cx| {
project.open_local_buffer("/a/main.rs", cx)
@@ -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
@@ -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"] }
@@ -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
+ }
+}
@@ -1,48 +1,118 @@
+mod extension_lsp_adapter;
+mod wasm_host;
+
+#[cfg(test)]
+mod extension_store_test;
+
use anyhow::{anyhow, bail, Context as _, Result};
use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use collections::{BTreeMap, HashSet};
use fs::{Fs, RemoveOptions};
-use futures::channel::mpsc::unbounded;
-use futures::StreamExt as _;
-use futures::{io::BufReader, AsyncReadExt as _};
+use futures::{channel::mpsc::unbounded, io::BufReader, AsyncReadExt as _, StreamExt as _};
use gpui::{actions, AppContext, Context, Global, Model, ModelContext, Task};
use language::{
- LanguageConfig, LanguageMatcher, LanguageQueries, LanguageRegistry, QUERY_FILENAME_PREFIXES,
+ LanguageConfig, LanguageMatcher, LanguageQueries, LanguageRegistry, LanguageServerName,
+ QUERY_FILENAME_PREFIXES,
};
-use parking_lot::RwLock;
+use node_runtime::NodeRuntime;
use serde::{Deserialize, Serialize};
-use std::cmp::Ordering;
use std::{
+ cmp::Ordering,
ffi::OsStr,
- path::{Path, PathBuf},
+ path::{self, Path, PathBuf},
sync::Arc,
time::Duration,
};
use theme::{ThemeRegistry, ThemeSettings};
-use util::http::{AsyncBody, HttpClientWithUrl};
-use util::TryFutureExt;
-use util::{http::HttpClient, paths::EXTENSIONS_DIR, ResultExt};
+use util::{
+ http::{AsyncBody, HttpClient, HttpClientWithUrl},
+ paths::EXTENSIONS_DIR,
+ ResultExt, TryFutureExt,
+};
+use wasm_host::{WasmExtension, WasmHost};
-#[cfg(test)]
-mod extension_store_test;
+use crate::{extension_lsp_adapter::ExtensionLspAdapter, wasm_host::wit};
#[derive(Deserialize)]
pub struct ExtensionsApiResponse {
- pub data: Vec<Extension>,
+ pub data: Vec<ExtensionApiResponse>,
}
#[derive(Clone, Deserialize)]
-pub struct Extension {
+pub struct ExtensionApiResponse {
pub id: Arc<str>,
- pub version: Arc<str>,
pub name: String,
+ pub version: Arc<str>,
pub description: Option<String>,
pub authors: Vec<String>,
pub repository: String,
pub download_count: usize,
}
+/// This is the old version of the extension manifest, from when it was `extension.json`.
+#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
+pub struct OldExtensionManifest {
+ pub name: String,
+ pub version: Arc<str>,
+
+ #[serde(default)]
+ pub description: Option<String>,
+ #[serde(default)]
+ pub repository: Option<String>,
+ #[serde(default)]
+ pub authors: Vec<String>,
+
+ #[serde(default)]
+ pub themes: BTreeMap<Arc<str>, PathBuf>,
+ #[serde(default)]
+ pub languages: BTreeMap<Arc<str>, PathBuf>,
+ #[serde(default)]
+ pub grammars: BTreeMap<Arc<str>, PathBuf>,
+}
+
+#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
+pub struct ExtensionManifest {
+ pub id: Arc<str>,
+ pub name: String,
+ pub version: Arc<str>,
+
+ #[serde(default)]
+ pub description: Option<String>,
+ #[serde(default)]
+ pub repository: Option<String>,
+ #[serde(default)]
+ pub authors: Vec<String>,
+ #[serde(default)]
+ pub lib: LibManifestEntry,
+
+ #[serde(default)]
+ pub themes: Vec<PathBuf>,
+ #[serde(default)]
+ pub languages: Vec<PathBuf>,
+ #[serde(default)]
+ pub grammars: BTreeMap<Arc<str>, GrammarManifestEntry>,
+ #[serde(default)]
+ pub language_servers: BTreeMap<LanguageServerName, LanguageServerManifestEntry>,
+}
+
+#[derive(Clone, Default, PartialEq, Eq, Debug, Deserialize, Serialize)]
+pub struct LibManifestEntry {
+ path: Option<PathBuf>,
+}
+
+#[derive(Clone, Default, PartialEq, Eq, Debug, Deserialize, Serialize)]
+pub struct GrammarManifestEntry {
+ repository: String,
+ #[serde(alias = "commit")]
+ rev: String,
+}
+
+#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
+pub struct LanguageServerManifestEntry {
+ language: Arc<str>,
+}
+
#[derive(Clone)]
pub enum ExtensionStatus {
NotInstalled,
@@ -67,7 +137,7 @@ impl ExtensionStatus {
}
pub struct ExtensionStore {
- manifest: Arc<RwLock<Manifest>>,
+ extension_index: ExtensionIndex,
fs: Arc<dyn Fs>,
http_client: Arc<HttpClientWithUrl>,
extensions_dir: PathBuf,
@@ -76,7 +146,9 @@ pub struct ExtensionStore {
manifest_path: PathBuf,
language_registry: Arc<LanguageRegistry>,
theme_registry: Arc<ThemeRegistry>,
- extension_changes: ExtensionChanges,
+ modified_extensions: HashSet<Arc<str>>,
+ wasm_host: Arc<WasmHost>,
+ wasm_extensions: Vec<(Arc<ExtensionManifest>, WasmExtension)>,
reload_task: Option<Task<Option<()>>>,
needs_reload: bool,
_watch_extensions_dir: [Task<()>; 2],
@@ -86,56 +158,44 @@ struct GlobalExtensionStore(Model<ExtensionStore>);
impl Global for GlobalExtensionStore {}
-#[derive(Debug, Deserialize, Serialize, Default)]
-pub struct Manifest {
- pub extensions: BTreeMap<Arc<str>, Arc<str>>,
- pub grammars: BTreeMap<Arc<str>, GrammarManifestEntry>,
- pub languages: BTreeMap<Arc<str>, LanguageManifestEntry>,
- pub themes: BTreeMap<Arc<str>, ThemeManifestEntry>,
+#[derive(Debug, Deserialize, Serialize, Default, PartialEq, Eq)]
+pub struct ExtensionIndex {
+ pub extensions: BTreeMap<Arc<str>, Arc<ExtensionManifest>>,
+ pub themes: BTreeMap<Arc<str>, ExtensionIndexEntry>,
+ pub languages: BTreeMap<Arc<str>, ExtensionIndexLanguageEntry>,
}
-#[derive(PartialEq, Eq, Debug, PartialOrd, Ord, Deserialize, Serialize)]
-pub struct GrammarManifestEntry {
- extension: String,
+#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Deserialize, Serialize)]
+pub struct ExtensionIndexEntry {
+ extension: Arc<str>,
path: PathBuf,
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Deserialize, Serialize)]
-pub struct LanguageManifestEntry {
- extension: String,
+pub struct ExtensionIndexLanguageEntry {
+ extension: Arc<str>,
path: PathBuf,
matcher: LanguageMatcher,
grammar: Option<Arc<str>>,
}
-#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)]
-pub struct ThemeManifestEntry {
- extension: String,
- path: PathBuf,
-}
-
-#[derive(Default)]
-struct ExtensionChanges {
- languages: HashSet<Arc<str>>,
- grammars: HashSet<Arc<str>>,
- themes: HashSet<Arc<str>>,
-}
-
actions!(zed, [ReloadExtensions]);
pub fn init(
fs: Arc<fs::RealFs>,
http_client: Arc<HttpClientWithUrl>,
+ node_runtime: Arc<dyn NodeRuntime>,
language_registry: Arc<LanguageRegistry>,
theme_registry: Arc<ThemeRegistry>,
cx: &mut AppContext,
) {
- let store = cx.new_model(|cx| {
+ let store = cx.new_model(move |cx| {
ExtensionStore::new(
EXTENSIONS_DIR.clone(),
- fs.clone(),
- http_client.clone(),
- language_registry.clone(),
+ fs,
+ http_client,
+ node_runtime,
+ language_registry,
theme_registry,
cx,
)
@@ -158,19 +218,28 @@ impl ExtensionStore {
extensions_dir: PathBuf,
fs: Arc<dyn Fs>,
http_client: Arc<HttpClientWithUrl>,
+ node_runtime: Arc<dyn NodeRuntime>,
language_registry: Arc<LanguageRegistry>,
theme_registry: Arc<ThemeRegistry>,
cx: &mut ModelContext<Self>,
) -> Self {
let mut this = Self {
- manifest: Default::default(),
+ extension_index: Default::default(),
extensions_dir: extensions_dir.join("installed"),
manifest_path: extensions_dir.join("manifest.json"),
extensions_being_installed: Default::default(),
extensions_being_uninstalled: Default::default(),
reload_task: None,
+ wasm_host: WasmHost::new(
+ fs.clone(),
+ http_client.clone(),
+ node_runtime,
+ language_registry.clone(),
+ extensions_dir.join("work"),
+ ),
+ wasm_extensions: Vec::new(),
needs_reload: false,
- extension_changes: ExtensionChanges::default(),
+ modified_extensions: Default::default(),
fs,
http_client,
language_registry,
@@ -194,7 +263,8 @@ impl ExtensionStore {
if let Some(manifest_content) = manifest_content.log_err() {
if let Some(manifest) = serde_json::from_str(&manifest_content).log_err() {
- self.manifest_updated(manifest, cx);
+ // TODO: don't detach
+ self.extensions_updated(manifest, cx).detach();
}
}
@@ -221,11 +291,15 @@ impl ExtensionStore {
return ExtensionStatus::Removing;
}
- let installed_version = self.manifest.read().extensions.get(extension_id).cloned();
+ let installed_version = self
+ .extension_index
+ .extensions
+ .get(extension_id)
+ .map(|manifest| manifest.version.clone());
let is_installing = self.extensions_being_installed.contains(extension_id);
match (installed_version, is_installing) {
(Some(_), true) => ExtensionStatus::Upgrading,
- (Some(version), false) => ExtensionStatus::Installed(version.clone()),
+ (Some(version), false) => ExtensionStatus::Installed(version),
(None, true) => ExtensionStatus::Installing,
(None, false) => ExtensionStatus::NotInstalled,
}
@@ -235,7 +309,7 @@ impl ExtensionStore {
&self,
search: Option<&str>,
cx: &mut ModelContext<Self>,
- ) -> Task<Result<Vec<Extension>>> {
+ ) -> Task<Result<Vec<ExtensionApiResponse>>> {
let url = self.http_client.build_zed_api_url(&format!(
"/extensions{query}",
query = search
@@ -335,7 +409,11 @@ impl ExtensionStore {
/// no longer in the manifest, or whose files have changed on disk.
/// Then it loads any themes, languages, or grammars that are newly
/// added to the manifest, or whose files have changed on disk.
- fn manifest_updated(&mut self, manifest: Manifest, cx: &mut ModelContext<Self>) {
+ fn extensions_updated(
+ &mut self,
+ new_index: ExtensionIndex,
+ cx: &mut ModelContext<Self>,
+ ) -> Task<Result<()>> {
fn diff<'a, T, I1, I2>(
old_keys: I1,
new_keys: I2,
@@ -379,54 +457,104 @@ impl ExtensionStore {
}
}
- let old_manifest = self.manifest.read();
- let (languages_to_remove, languages_to_add) = diff(
- old_manifest.languages.iter(),
- manifest.languages.iter(),
- &self.extension_changes.languages,
- );
- let (grammars_to_remove, grammars_to_add) = diff(
- old_manifest.grammars.iter(),
- manifest.grammars.iter(),
- &self.extension_changes.grammars,
- );
- let (themes_to_remove, themes_to_add) = diff(
- old_manifest.themes.iter(),
- manifest.themes.iter(),
- &self.extension_changes.themes,
+ let old_index = &self.extension_index;
+ let (extensions_to_unload, extensions_to_load) = diff(
+ old_index.extensions.iter(),
+ new_index.extensions.iter(),
+ &self.modified_extensions,
);
- self.extension_changes.clear();
- drop(old_manifest);
+ self.modified_extensions.clear();
- let themes_to_remove = &themes_to_remove
- .into_iter()
- .map(|theme| theme.into())
+ let themes_to_remove = old_index
+ .themes
+ .iter()
+ .filter_map(|(name, entry)| {
+ if extensions_to_unload.contains(&entry.extension) {
+ Some(name.clone().into())
+ } else {
+ None
+ }
+ })
+ .collect::<Vec<_>>();
+ let languages_to_remove = old_index
+ .languages
+ .iter()
+ .filter_map(|(name, entry)| {
+ if extensions_to_unload.contains(&entry.extension) {
+ Some(name.clone())
+ } else {
+ None
+ }
+ })
+ .collect::<Vec<_>>();
+ let empty = Default::default();
+ let grammars_to_remove = extensions_to_unload
+ .iter()
+ .flat_map(|extension_id| {
+ old_index
+ .extensions
+ .get(extension_id)
+ .map_or(&empty, |extension| &extension.grammars)
+ .keys()
+ .cloned()
+ })
.collect::<Vec<_>>();
+
+ self.wasm_extensions
+ .retain(|(extension, _)| !extensions_to_unload.contains(&extension.id));
+
+ for extension_id in &extensions_to_unload {
+ if let Some(extension) = old_index.extensions.get(extension_id) {
+ for (language_server_name, config) in extension.language_servers.iter() {
+ self.language_registry
+ .remove_lsp_adapter(config.language.as_ref(), language_server_name);
+ }
+ }
+ }
+
self.theme_registry.remove_user_themes(&themes_to_remove);
self.language_registry
.remove_languages(&languages_to_remove, &grammars_to_remove);
- self.language_registry
- .register_wasm_grammars(grammars_to_add.iter().map(|grammar_name| {
- let grammar = manifest.grammars.get(grammar_name).unwrap();
+ let languages_to_add = new_index
+ .languages
+ .iter()
+ .filter(|(_, entry)| extensions_to_load.contains(&entry.extension))
+ .collect::<Vec<_>>();
+ let mut grammars_to_add = Vec::new();
+ let mut themes_to_add = Vec::new();
+ for extension_id in &extensions_to_load {
+ let Some(extension) = new_index.extensions.get(extension_id) else {
+ continue;
+ };
+
+ grammars_to_add.extend(extension.grammars.keys().map(|grammar_name| {
let mut grammar_path = self.extensions_dir.clone();
- grammar_path.extend([grammar.extension.as_ref(), grammar.path.as_path()]);
+ grammar_path.extend([extension_id.as_ref(), "grammars"]);
+ grammar_path.push(grammar_name.as_ref());
+ grammar_path.set_extension("wasm");
(grammar_name.clone(), grammar_path)
}));
+ themes_to_add.extend(extension.themes.iter().map(|theme_path| {
+ let mut path = self.extensions_dir.clone();
+ path.extend([Path::new(extension_id.as_ref()), theme_path.as_path()]);
+ path
+ }));
+ }
- for language_name in &languages_to_add {
- if language_name.as_ref() == "Swift" {
- continue;
- }
+ self.language_registry
+ .register_wasm_grammars(grammars_to_add);
- let language = manifest.languages.get(language_name.as_ref()).unwrap();
+ for (language_name, language) in languages_to_add {
let mut language_path = self.extensions_dir.clone();
- language_path.extend([language.extension.as_ref(), language.path.as_path()]);
+ language_path.extend([
+ Path::new(language.extension.as_ref()),
+ language.path.as_path(),
+ ]);
self.language_registry.register_language(
language_name.clone(),
language.grammar.clone(),
language.matcher.clone(),
- vec![],
move || {
let config = std::fs::read_to_string(language_path.join("config.toml"))?;
let config: LanguageConfig = ::toml::from_str(&config)?;
@@ -436,107 +564,119 @@ impl ExtensionStore {
);
}
- let (reload_theme_tx, mut reload_theme_rx) = unbounded();
let fs = self.fs.clone();
+ let wasm_host = self.wasm_host.clone();
let root_dir = self.extensions_dir.clone();
let theme_registry = self.theme_registry.clone();
- let themes = themes_to_add
+ let extension_manifests = extensions_to_load
.iter()
- .filter_map(|name| manifest.themes.get(name).cloned())
+ .filter_map(|name| new_index.extensions.get(name).cloned())
.collect::<Vec<_>>();
- cx.background_executor()
- .spawn(async move {
- for theme in &themes {
- let mut theme_path = root_dir.clone();
- theme_path.extend([theme.extension.as_ref(), theme.path.as_path()]);
-
- theme_registry
- .load_user_theme(&theme_path, fs.clone())
- .await
- .log_err();
- }
- reload_theme_tx.unbounded_send(()).ok();
- })
- .detach();
+ self.extension_index = new_index;
+ cx.notify();
- cx.spawn(|_, cx| async move {
- while let Some(_) = reload_theme_rx.next().await {
- if cx
- .update(|cx| ThemeSettings::reload_current_theme(cx))
- .is_err()
- {
- break;
- }
+ cx.spawn(|this, mut cx| async move {
+ cx.background_executor()
+ .spawn({
+ let fs = fs.clone();
+ async move {
+ for theme_path in &themes_to_add {
+ theme_registry
+ .load_user_theme(&theme_path, fs.clone())
+ .await
+ .log_err();
+ }
+ }
+ })
+ .await;
+
+ let mut wasm_extensions = Vec::new();
+ for extension_manifest in extension_manifests {
+ let Some(wasm_path) = &extension_manifest.lib.path else {
+ continue;
+ };
+
+ let mut path = root_dir.clone();
+ path.extend([
+ Path::new(extension_manifest.id.as_ref()),
+ wasm_path.as_path(),
+ ]);
+ let mut wasm_file = fs
+ .open_sync(&path)
+ .await
+ .context("failed to open wasm file")?;
+ let mut wasm_bytes = Vec::new();
+ wasm_file
+ .read_to_end(&mut wasm_bytes)
+ .context("failed to read wasm")?;
+ let wasm_extension = wasm_host
+ .load_extension(
+ wasm_bytes,
+ extension_manifest.clone(),
+ cx.background_executor().clone(),
+ )
+ .await
+ .context("failed to load wasm extension")?;
+ wasm_extensions.push((extension_manifest.clone(), wasm_extension));
}
- })
- .detach();
- *self.manifest.write() = manifest;
- cx.notify();
+ this.update(&mut cx, |this, cx| {
+ for (manifest, wasm_extension) in &wasm_extensions {
+ for (language_server_name, language_server_config) in &manifest.language_servers
+ {
+ this.language_registry.register_lsp_adapter(
+ language_server_config.language.clone(),
+ Arc::new(ExtensionLspAdapter {
+ extension: wasm_extension.clone(),
+ work_dir: this.wasm_host.work_dir.join(manifest.id.as_ref()),
+ config: wit::LanguageServerConfig {
+ name: language_server_name.0.to_string(),
+ language_name: language_server_config.language.to_string(),
+ },
+ }),
+ );
+ }
+ }
+ this.wasm_extensions.extend(wasm_extensions);
+ ThemeSettings::reload_current_theme(cx)
+ })
+ .ok();
+ Ok(())
+ })
}
fn watch_extensions_dir(&self, cx: &mut ModelContext<Self>) -> [Task<()>; 2] {
- let manifest = self.manifest.clone();
let fs = self.fs.clone();
let extensions_dir = self.extensions_dir.clone();
-
- let (changes_tx, mut changes_rx) = unbounded();
+ let (changed_extensions_tx, mut changed_extensions_rx) = unbounded();
let events_task = cx.background_executor().spawn(async move {
let mut events = fs.watch(&extensions_dir, Duration::from_millis(250)).await;
while let Some(events) = events.next().await {
- let mut changed_grammars = HashSet::default();
- let mut changed_languages = HashSet::default();
- let mut changed_themes = HashSet::default();
-
- {
- let manifest = manifest.read();
- for event in events {
- for (grammar_name, grammar) in &manifest.grammars {
- let mut grammar_path = extensions_dir.clone();
- grammar_path
- .extend([grammar.extension.as_ref(), grammar.path.as_path()]);
- if event.path.starts_with(&grammar_path) || event.path == grammar_path {
- changed_grammars.insert(grammar_name.clone());
- }
- }
-
- for (language_name, language) in &manifest.languages {
- let mut language_path = extensions_dir.clone();
- language_path
- .extend([language.extension.as_ref(), language.path.as_path()]);
- if event.path.starts_with(&language_path) || event.path == language_path
- {
- changed_languages.insert(language_name.clone());
- }
- }
+ for event in events {
+ let Ok(event_path) = event.path.strip_prefix(&extensions_dir) else {
+ continue;
+ };
- for (theme_name, theme) in &manifest.themes {
- let mut theme_path = extensions_dir.clone();
- theme_path.extend([theme.extension.as_ref(), theme.path.as_path()]);
- if event.path.starts_with(&theme_path) || event.path == theme_path {
- changed_themes.insert(theme_name.clone());
- }
+ if let Some(path::Component::Normal(extension_dir_name)) =
+ event_path.components().next()
+ {
+ if let Some(extension_id) = extension_dir_name.to_str() {
+ changed_extensions_tx
+ .unbounded_send(Arc::from(extension_id))
+ .ok();
}
}
}
-
- changes_tx
- .unbounded_send(ExtensionChanges {
- languages: changed_languages,
- grammars: changed_grammars,
- themes: changed_themes,
- })
- .ok();
}
});
let reload_task = cx.spawn(|this, mut cx| async move {
- while let Some(changes) = changes_rx.next().await {
+ while let Some(changed_extension_id) = changed_extensions_rx.next().await {
if this
.update(&mut cx, |this, cx| {
- this.extension_changes.merge(changes);
+ this.modified_extensions.insert(changed_extension_id);
this.reload(cx);
})
.is_err()
@@ -556,16 +696,18 @@ impl ExtensionStore {
}
let fs = self.fs.clone();
+ let work_dir = self.wasm_host.work_dir.clone();
let extensions_dir = self.extensions_dir.clone();
let manifest_path = self.manifest_path.clone();
self.needs_reload = false;
self.reload_task = Some(cx.spawn(|this, mut cx| {
async move {
- let manifest = cx
+ let extension_index = cx
.background_executor()
.spawn(async move {
- let mut manifest = Manifest::default();
+ let mut index = ExtensionIndex::default();
+ fs.create_dir(&work_dir).await.log_err();
fs.create_dir(&extensions_dir).await.log_err();
let extension_paths = fs.read_dir(&extensions_dir).await;
@@ -574,20 +716,16 @@ impl ExtensionStore {
let Ok(extension_dir) = extension_dir else {
continue;
};
- Self::add_extension_to_manifest(
- fs.clone(),
- extension_dir,
- &mut manifest,
- )
- .await
- .log_err();
+ Self::add_extension_to_index(fs.clone(), extension_dir, &mut index)
+ .await
+ .log_err();
}
}
- if let Ok(manifest_json) = serde_json::to_string_pretty(&manifest) {
+ if let Ok(index_json) = serde_json::to_string_pretty(&index) {
fs.save(
&manifest_path,
- &manifest_json.as_str().into(),
+ &index_json.as_str().into(),
Default::default(),
)
.await
@@ -595,12 +733,17 @@ impl ExtensionStore {
.log_err();
}
- manifest
+ index
})
.await;
+ if let Ok(task) = this.update(&mut cx, |this, cx| {
+ this.extensions_updated(extension_index, cx)
+ }) {
+ task.await.log_err();
+ }
+
this.update(&mut cx, |this, cx| {
- this.manifest_updated(manifest, cx);
this.reload_task.take();
if this.needs_reload {
this.reload(cx);
@@ -611,52 +754,65 @@ impl ExtensionStore {
}));
}
- async fn add_extension_to_manifest(
+ async fn add_extension_to_index(
fs: Arc<dyn Fs>,
extension_dir: PathBuf,
- manifest: &mut Manifest,
+ index: &mut ExtensionIndex,
) -> Result<()> {
let extension_name = extension_dir
.file_name()
.and_then(OsStr::to_str)
.ok_or_else(|| anyhow!("invalid extension name"))?;
- #[derive(Deserialize)]
- struct ExtensionJson {
- pub version: String,
- }
-
- let extension_json_path = extension_dir.join("extension.json");
- let extension_json = fs
- .load(&extension_json_path)
- .await
- .context("failed to load extension.json")?;
- let extension_json: ExtensionJson =
- serde_json::from_str(&extension_json).context("invalid extension.json")?;
-
- manifest
- .extensions
- .insert(extension_name.into(), extension_json.version.into());
-
- if let Ok(mut grammar_paths) = fs.read_dir(&extension_dir.join("grammars")).await {
- while let Some(grammar_path) = grammar_paths.next().await {
- let grammar_path = grammar_path?;
- let Ok(relative_path) = grammar_path.strip_prefix(&extension_dir) else {
- continue;
- };
- let Some(grammar_name) = grammar_path.file_stem().and_then(OsStr::to_str) else {
- continue;
- };
-
- manifest.grammars.insert(
- grammar_name.into(),
- GrammarManifestEntry {
- extension: extension_name.into(),
- path: relative_path.into(),
- },
- );
- }
- }
+ let mut extension_manifest_path = extension_dir.join("extension.json");
+ let mut extension_manifest;
+ if fs.is_file(&extension_manifest_path).await {
+ let manifest_content = fs
+ .load(&extension_manifest_path)
+ .await
+ .with_context(|| format!("failed to load {extension_name} extension.json"))?;
+ let manifest_json = serde_json::from_str::<OldExtensionManifest>(&manifest_content)
+ .with_context(|| {
+ format!("invalid extension.json for extension {extension_name}")
+ })?;
+
+ extension_manifest = ExtensionManifest {
+ id: extension_name.into(),
+ name: manifest_json.name,
+ version: manifest_json.version,
+ description: manifest_json.description,
+ repository: manifest_json.repository,
+ authors: manifest_json.authors,
+ lib: Default::default(),
+ themes: {
+ let mut themes = manifest_json.themes.into_values().collect::<Vec<_>>();
+ themes.sort();
+ themes.dedup();
+ themes
+ },
+ languages: {
+ let mut languages = manifest_json.languages.into_values().collect::<Vec<_>>();
+ languages.sort();
+ languages.dedup();
+ languages
+ },
+ grammars: manifest_json
+ .grammars
+ .into_iter()
+ .map(|(grammar_name, _)| (grammar_name, Default::default()))
+ .collect(),
+ language_servers: Default::default(),
+ };
+ } else {
+ extension_manifest_path.set_extension("toml");
+ let manifest_content = fs
+ .load(&extension_manifest_path)
+ .await
+ .with_context(|| format!("failed to load {extension_name} extension.toml"))?;
+ extension_manifest = ::toml::from_str(&manifest_content).with_context(|| {
+ format!("invalid extension.json for extension {extension_name}")
+ })?;
+ };
if let Ok(mut language_paths) = fs.read_dir(&extension_dir.join("languages")).await {
while let Some(language_path) = language_paths.next().await {
@@ -673,11 +829,16 @@ impl ExtensionStore {
let config = fs.load(&language_path.join("config.toml")).await?;
let config = ::toml::from_str::<LanguageConfig>(&config)?;
- manifest.languages.insert(
+ let relative_path = relative_path.to_path_buf();
+ if !extension_manifest.languages.contains(&relative_path) {
+ extension_manifest.languages.push(relative_path.clone());
+ }
+
+ index.languages.insert(
config.name.clone(),
- LanguageManifestEntry {
+ ExtensionIndexLanguageEntry {
extension: extension_name.into(),
- path: relative_path.into(),
+ path: relative_path,
matcher: config.matcher,
grammar: config.grammar,
},
@@ -699,32 +860,36 @@ impl ExtensionStore {
continue;
};
- for theme in theme_family.themes {
- let location = ThemeManifestEntry {
- extension: extension_name.into(),
- path: relative_path.into(),
- };
+ let relative_path = relative_path.to_path_buf();
+ if !extension_manifest.themes.contains(&relative_path) {
+ extension_manifest.themes.push(relative_path.clone());
+ }
- manifest.themes.insert(theme.name.into(), location);
+ for theme in theme_family.themes {
+ index.themes.insert(
+ theme.name.into(),
+ ExtensionIndexEntry {
+ extension: extension_name.into(),
+ path: relative_path.clone(),
+ },
+ );
}
}
}
- Ok(())
- }
-}
+ let default_extension_wasm_path = extension_dir.join("extension.wasm");
+ if fs.is_file(&default_extension_wasm_path).await {
+ extension_manifest
+ .lib
+ .path
+ .get_or_insert(default_extension_wasm_path);
+ }
-impl ExtensionChanges {
- fn clear(&mut self) {
- self.grammars.clear();
- self.languages.clear();
- self.themes.clear();
- }
+ index
+ .extensions
+ .insert(extension_name.into(), Arc::new(extension_manifest));
- fn merge(&mut self, other: Self) {
- self.grammars.extend(other.grammars);
- self.languages.extend(other.languages);
- self.themes.extend(other.themes);
+ Ok(())
}
}
@@ -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);
+ });
+}
@@ -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
+ }
+}
@@ -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" }
@@ -0,0 +1 @@
+../../LICENSE-APACHE
@@ -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();
+}
@@ -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)
+ }
+}
@@ -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>;
+}
@@ -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);
@@ -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
@@ -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(())
}
@@ -22,6 +22,7 @@ pub mod markdown;
use anyhow::{anyhow, Context, Result};
use async_trait::async_trait;
use collections::{HashMap, HashSet};
+use futures::Future;
use gpui::{AppContext, AsyncAppContext, Model, Task};
pub use highlight_map::HighlightMap;
use lazy_static::lazy_static;
@@ -35,6 +36,7 @@ use schemars::{
};
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use serde_json::Value;
+use smol::future::FutureExt as _;
use std::{
any::Any,
cell::RefCell,
@@ -44,6 +46,7 @@ use std::{
mem,
ops::Range,
path::{Path, PathBuf},
+ pin::Pin,
str,
sync::{
atomic::{AtomicU64, AtomicUsize, Ordering::SeqCst},
@@ -86,7 +89,9 @@ thread_local! {
lazy_static! {
static ref NEXT_LANGUAGE_ID: AtomicUsize = Default::default();
static ref NEXT_GRAMMAR_ID: AtomicUsize = Default::default();
- static ref WASM_ENGINE: wasmtime::Engine = wasmtime::Engine::default();
+ static ref WASM_ENGINE: wasmtime::Engine = {
+ wasmtime::Engine::new(&wasmtime::Config::new()).unwrap()
+ };
/// A shared grammar for plain text, exposed for reuse by downstream crates.
pub static ref PLAIN_TEXT: Arc<Language> = Arc::new(Language::new(
@@ -106,10 +111,10 @@ pub trait ToLspPosition {
}
/// A name of a language server.
-#[derive(Clone, Debug, PartialEq, Eq, Hash)]
+#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
pub struct LanguageServerName(pub Arc<str>);
-#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Location {
pub buffer: Model<Buffer>,
pub range: Range<Anchor>,
@@ -120,54 +125,44 @@ pub struct Location {
/// once at startup, and caches the results.
pub struct CachedLspAdapter {
pub name: LanguageServerName,
- pub short_name: &'static str,
pub disk_based_diagnostic_sources: Vec<String>,
pub disk_based_diagnostics_progress_token: Option<String>,
pub language_ids: HashMap<String, String>,
pub adapter: Arc<dyn LspAdapter>,
pub reinstall_attempt_count: AtomicU64,
+ cached_binary: futures::lock::Mutex<Option<LanguageServerBinary>>,
}
impl CachedLspAdapter {
- pub async fn new(adapter: Arc<dyn LspAdapter>) -> Arc<Self> {
+ pub fn new(adapter: Arc<dyn LspAdapter>) -> Arc<Self> {
let name = adapter.name();
- let short_name = adapter.short_name();
let disk_based_diagnostic_sources = adapter.disk_based_diagnostic_sources();
let disk_based_diagnostics_progress_token = adapter.disk_based_diagnostics_progress_token();
let language_ids = adapter.language_ids();
Arc::new(CachedLspAdapter {
name,
- short_name,
disk_based_diagnostic_sources,
disk_based_diagnostics_progress_token,
language_ids,
adapter,
+ cached_binary: Default::default(),
reinstall_attempt_count: AtomicU64::new(0),
})
}
- pub fn check_if_user_installed(
- &self,
- delegate: &Arc<dyn LspAdapterDelegate>,
+ pub async fn get_language_server_command(
+ self: Arc<Self>,
+ language: Arc<Language>,
+ container_dir: Arc<Path>,
+ delegate: Arc<dyn LspAdapterDelegate>,
cx: &mut AsyncAppContext,
- ) -> Option<Task<Option<LanguageServerBinary>>> {
- self.adapter.check_if_user_installed(delegate, cx)
- }
-
- pub async fn fetch_latest_server_version(
- &self,
- delegate: &dyn LspAdapterDelegate,
- ) -> Result<Box<dyn 'static + Send + Any>> {
- self.adapter.fetch_latest_server_version(delegate).await
- }
-
- pub fn will_fetch_server(
- &self,
- delegate: &Arc<dyn LspAdapterDelegate>,
- cx: &mut AsyncAppContext,
- ) -> Option<Task<Result<()>>> {
- self.adapter.will_fetch_server(delegate, cx)
+ ) -> Result<LanguageServerBinary> {
+ let cached_binary = self.cached_binary.lock().await;
+ self.adapter
+ .clone()
+ .get_language_server_command(language, container_dir, delegate, cached_binary, cx)
+ .await
}
pub fn will_start_server(
@@ -178,27 +173,6 @@ impl CachedLspAdapter {
self.adapter.will_start_server(delegate, cx)
}
- pub async fn fetch_server_binary(
- &self,
- version: Box<dyn 'static + Send + Any>,
- container_dir: PathBuf,
- delegate: &dyn LspAdapterDelegate,
- ) -> Result<LanguageServerBinary> {
- self.adapter
- .fetch_server_binary(version, container_dir, delegate)
- .await
- }
-
- pub async fn cached_server_binary(
- &self,
- container_dir: PathBuf,
- delegate: &dyn LspAdapterDelegate,
- ) -> Option<LanguageServerBinary> {
- self.adapter
- .cached_server_binary(container_dir, delegate)
- .await
- }
-
pub fn can_be_reinstalled(&self) -> bool {
self.adapter.can_be_reinstalled()
}
@@ -248,31 +222,124 @@ impl CachedLspAdapter {
pub fn prettier_plugins(&self) -> &[&'static str] {
self.adapter.prettier_plugins()
}
+
+ #[cfg(any(test, feature = "test-support"))]
+ fn as_fake(&self) -> Option<&FakeLspAdapter> {
+ self.adapter.as_fake()
+ }
}
/// [`LspAdapterDelegate`] allows [`LspAdapter]` implementations to interface with the application
// e.g. to display a notification or fetch data from the web.
+#[async_trait]
pub trait LspAdapterDelegate: Send + Sync {
fn show_notification(&self, message: &str, cx: &mut AppContext);
fn http_client(&self) -> Arc<dyn HttpClient>;
- fn which_command(
- &self,
- command: OsString,
- cx: &AppContext,
- ) -> Task<Option<(PathBuf, HashMap<String, String>)>>;
+ fn update_status(&self, language: LanguageServerName, status: LanguageServerBinaryStatus);
+
+ async fn which_command(&self, command: OsString) -> Option<(PathBuf, HashMap<String, String>)>;
+ async fn read_text_file(&self, path: PathBuf) -> Result<String>;
}
#[async_trait]
pub trait LspAdapter: 'static + Send + Sync {
fn name(&self) -> LanguageServerName;
- fn short_name(&self) -> &'static str;
+ fn get_language_server_command<'a>(
+ self: Arc<Self>,
+ language: Arc<Language>,
+ container_dir: Arc<Path>,
+ delegate: Arc<dyn LspAdapterDelegate>,
+ mut cached_binary: futures::lock::MutexGuard<'a, Option<LanguageServerBinary>>,
+ cx: &'a mut AsyncAppContext,
+ ) -> Pin<Box<dyn 'a + Future<Output = Result<LanguageServerBinary>>>> {
+ async move {
+ // First we check whether the adapter can give us a user-installed binary.
+ // If so, we do *not* want to cache that, because each worktree might give us a different
+ // binary:
+ //
+ // worktree 1: user-installed at `.bin/gopls`
+ // worktree 2: user-installed at `~/bin/gopls`
+ // worktree 3: no gopls found in PATH -> fallback to Zed installation
+ //
+ // We only want to cache when we fall back to the global one,
+ // because we don't want to download and overwrite our global one
+ // for each worktree we might have open.
+ if let Some(binary) = self.check_if_user_installed(delegate.as_ref()).await {
+ log::info!(
+ "found user-installed language server for {}. path: {:?}, arguments: {:?}",
+ language.name(),
+ binary.path,
+ binary.arguments
+ );
+ return Ok(binary);
+ }
+
+ if let Some(cached_binary) = cached_binary.as_ref() {
+ return Ok(cached_binary.clone());
+ }
+
+ if !container_dir.exists() {
+ smol::fs::create_dir_all(&container_dir)
+ .await
+ .context("failed to create container directory")?;
+ }
- fn check_if_user_installed(
+ if let Some(task) = self.will_fetch_server(&delegate, cx) {
+ task.await?;
+ }
+
+ let name = self.name();
+ log::info!("fetching latest version of language server {:?}", name.0);
+ delegate.update_status(
+ name.clone(),
+ LanguageServerBinaryStatus::CheckingForUpdate,
+ );
+ let version_info = self.fetch_latest_server_version(delegate.as_ref()).await?;
+
+ log::info!("downloading language server {:?}", name.0);
+ delegate.update_status(self.name(), LanguageServerBinaryStatus::Downloading);
+ let mut binary = self
+ .fetch_server_binary(version_info, container_dir.to_path_buf(), delegate.as_ref())
+ .await;
+
+ delegate.update_status(name.clone(), LanguageServerBinaryStatus::Downloaded);
+
+ if let Err(error) = binary.as_ref() {
+ if let Some(prev_downloaded_binary) = self
+ .cached_server_binary(container_dir.to_path_buf(), delegate.as_ref())
+ .await
+ {
+ delegate.update_status(name.clone(), LanguageServerBinaryStatus::Cached);
+ log::info!(
+ "failed to fetch newest version of language server {:?}. falling back to using {:?}",
+ name.clone(),
+ prev_downloaded_binary.path.display()
+ );
+ binary = Ok(prev_downloaded_binary);
+ } else {
+ delegate.update_status(
+ name.clone(),
+ LanguageServerBinaryStatus::Failed {
+ error: format!("{:?}", error),
+ },
+ );
+ }
+ }
+
+ if let Ok(binary) = &binary {
+ *cached_binary = Some(binary.clone());
+ }
+
+ binary
+ }
+ .boxed_local()
+ }
+
+ async fn check_if_user_installed(
&self,
- _: &Arc<dyn LspAdapterDelegate>,
- _: &mut AsyncAppContext,
- ) -> Option<Task<Option<LanguageServerBinary>>> {
+ _: &dyn LspAdapterDelegate,
+ ) -> Option<LanguageServerBinary> {
None
}
@@ -384,6 +451,11 @@ pub trait LspAdapter: 'static + Send + Sync {
fn prettier_plugins(&self) -> &[&'static str] {
&[]
}
+
+ #[cfg(any(test, feature = "test-support"))]
+ fn as_fake(&self) -> Option<&FakeLspAdapter> {
+ None
+ }
}
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -578,6 +650,7 @@ pub struct FakeLspAdapter {
pub disk_based_diagnostics_progress_token: Option<String>,
pub disk_based_diagnostics_sources: Vec<String>,
pub prettier_plugins: Vec<&'static str>,
+ pub language_server_binary: LanguageServerBinary,
}
/// Configuration of handling bracket pairs for a given language.
@@ -654,13 +727,6 @@ pub struct Language {
pub(crate) id: LanguageId,
pub(crate) config: LanguageConfig,
pub(crate) grammar: Option<Arc<Grammar>>,
- pub(crate) adapters: Vec<Arc<CachedLspAdapter>>,
-
- #[cfg(any(test, feature = "test-support"))]
- fake_adapter: Option<(
- futures::channel::mpsc::UnboundedSender<lsp::FakeLanguageServer>,
- Arc<FakeLspAdapter>,
- )>,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
@@ -775,17 +841,9 @@ impl Language {
highlight_map: Default::default(),
})
}),
- adapters: Vec::new(),
-
- #[cfg(any(test, feature = "test-support"))]
- fake_adapter: None,
}
}
- pub fn lsp_adapters(&self) -> &[Arc<CachedLspAdapter>] {
- &self.adapters
- }
-
pub fn with_queries(mut self, queries: LanguageQueries) -> Result<Self> {
if let Some(query) = queries.highlights {
self = self
@@ -1077,76 +1135,10 @@ impl Language {
Arc::get_mut(self.grammar.as_mut().unwrap()).unwrap()
}
- pub async fn with_lsp_adapters(mut self, lsp_adapters: Vec<Arc<dyn LspAdapter>>) -> Self {
- for adapter in lsp_adapters {
- self.adapters.push(CachedLspAdapter::new(adapter).await);
- }
- self
- }
-
- #[cfg(any(test, feature = "test-support"))]
- pub async fn set_fake_lsp_adapter(
- &mut self,
- fake_lsp_adapter: Arc<FakeLspAdapter>,
- ) -> futures::channel::mpsc::UnboundedReceiver<lsp::FakeLanguageServer> {
- let (servers_tx, servers_rx) = futures::channel::mpsc::unbounded();
- self.fake_adapter = Some((servers_tx, fake_lsp_adapter.clone()));
- let adapter = CachedLspAdapter::new(Arc::new(fake_lsp_adapter)).await;
- self.adapters = vec![adapter];
- servers_rx
- }
-
pub fn name(&self) -> Arc<str> {
self.config.name.clone()
}
- pub async fn disk_based_diagnostic_sources(&self) -> &[String] {
- match self.adapters.first().as_ref() {
- Some(adapter) => &adapter.disk_based_diagnostic_sources,
- None => &[],
- }
- }
-
- pub async fn disk_based_diagnostics_progress_token(&self) -> Option<&str> {
- for adapter in &self.adapters {
- let token = adapter.disk_based_diagnostics_progress_token.as_deref();
- if token.is_some() {
- return token;
- }
- }
-
- None
- }
-
- pub async fn process_completion(self: &Arc<Self>, completion: &mut lsp::CompletionItem) {
- for adapter in &self.adapters {
- adapter.process_completion(completion).await;
- }
- }
-
- pub async fn label_for_completion(
- self: &Arc<Self>,
- completion: &lsp::CompletionItem,
- ) -> Option<CodeLabel> {
- self.adapters
- .first()
- .as_ref()?
- .label_for_completion(completion, self)
- .await
- }
-
- pub async fn label_for_symbol(
- self: &Arc<Self>,
- name: &str,
- kind: lsp::SymbolKind,
- ) -> Option<CodeLabel> {
- self.adapters
- .first()
- .as_ref()?
- .label_for_symbol(name, kind, self)
- .await
- }
-
pub fn highlight_text<'a>(
self: &'a Arc<Self>,
text: &'a Rope,
@@ -1404,19 +1396,31 @@ impl Default for FakeLspAdapter {
initialization_options: None,
disk_based_diagnostics_sources: Vec::new(),
prettier_plugins: Vec::new(),
+ language_server_binary: LanguageServerBinary {
+ path: "/the/fake/lsp/path".into(),
+ arguments: vec![],
+ env: Default::default(),
+ },
}
}
}
#[cfg(any(test, feature = "test-support"))]
#[async_trait]
-impl LspAdapter for Arc<FakeLspAdapter> {
+impl LspAdapter for FakeLspAdapter {
fn name(&self) -> LanguageServerName {
LanguageServerName(self.name.into())
}
- fn short_name(&self) -> &'static str {
- "FakeLspAdapter"
+ fn get_language_server_command<'a>(
+ self: Arc<Self>,
+ _: Arc<Language>,
+ _: Arc<Path>,
+ _: Arc<dyn LspAdapterDelegate>,
+ _: futures::lock::MutexGuard<'a, Option<LanguageServerBinary>>,
+ _: &'a mut AsyncAppContext,
+ ) -> Pin<Box<dyn 'a + Future<Output = Result<LanguageServerBinary>>>> {
+ async move { Ok(self.language_server_binary.clone()) }.boxed_local()
}
async fn fetch_latest_server_version(
@@ -1464,6 +1468,10 @@ impl LspAdapter for Arc<FakeLspAdapter> {
fn prettier_plugins(&self) -> &[&'static str] {
&self.prettier_plugins
}
+
+ fn as_fake(&self) -> Option<&FakeLspAdapter> {
+ Some(self)
+ }
}
fn get_capture_indices(query: &Query, captures: &mut [(&str, &mut Option<u32>)]) {
@@ -7,9 +7,9 @@ use collections::{hash_map, HashMap};
use futures::{
channel::{mpsc, oneshot},
future::Shared,
- Future, FutureExt as _, TryFutureExt as _,
+ Future, FutureExt as _,
};
-use gpui::{AppContext, AsyncAppContext, BackgroundExecutor, Task};
+use gpui::{AppContext, BackgroundExecutor, Task};
use lsp::{LanguageServerBinary, LanguageServerId};
use parking_lot::{Mutex, RwLock};
use postage::watch;
@@ -43,14 +43,19 @@ struct LanguageRegistryState {
languages: Vec<Arc<Language>>,
available_languages: Vec<AvailableLanguage>,
grammars: HashMap<Arc<str>, AvailableGrammar>,
+ lsp_adapters: HashMap<Arc<str>, Vec<Arc<CachedLspAdapter>>>,
loading_languages: HashMap<LanguageId, Vec<oneshot::Sender<Result<Arc<Language>>>>>,
subscription: (watch::Sender<()>, watch::Receiver<()>),
theme: Option<Arc<Theme>>,
version: usize,
reload_count: usize,
+
+ #[cfg(any(test, feature = "test-support"))]
+ fake_server_txs:
+ HashMap<Arc<str>, Vec<futures::channel::mpsc::UnboundedSender<lsp::FakeLanguageServer>>>,
}
-#[derive(Clone)]
+#[derive(Clone, Debug, PartialEq, Eq)]
pub enum LanguageServerBinaryStatus {
CheckingForUpdate,
Downloading,
@@ -72,7 +77,6 @@ struct AvailableLanguage {
grammar: Option<Arc<str>>,
matcher: LanguageMatcher,
load: Arc<dyn Fn() -> Result<(LanguageConfig, LanguageQueries)> + 'static + Send + Sync>,
- lsp_adapters: Vec<Arc<dyn LspAdapter>>,
loaded: bool,
}
@@ -112,7 +116,7 @@ pub struct LanguageQueries {
#[derive(Clone, Default)]
struct LspBinaryStatusSender {
- txs: Arc<Mutex<Vec<mpsc::UnboundedSender<(Arc<Language>, LanguageServerBinaryStatus)>>>>,
+ txs: Arc<Mutex<Vec<mpsc::UnboundedSender<(LanguageServerName, LanguageServerBinaryStatus)>>>>,
}
impl LanguageRegistry {
@@ -124,10 +128,14 @@ impl LanguageRegistry {
available_languages: Default::default(),
grammars: Default::default(),
loading_languages: Default::default(),
+ lsp_adapters: Default::default(),
subscription: watch::channel(),
theme: Default::default(),
version: 0,
reload_count: 0,
+
+ #[cfg(any(test, feature = "test-support"))]
+ fake_server_txs: Default::default(),
}),
language_server_download_dir: None,
login_shell_env_loaded: login_shell_env_loaded.shared(),
@@ -139,7 +147,9 @@ impl LanguageRegistry {
#[cfg(any(test, feature = "test-support"))]
pub fn test() -> Self {
- Self::new(Task::ready(()))
+ let mut this = Self::new(Task::ready(()));
+ this.language_server_download_dir = Some(Path::new("/the-download-dir").into());
+ this
}
pub fn set_executor(&mut self, executor: BackgroundExecutor) {
@@ -162,24 +172,71 @@ impl LanguageRegistry {
.remove_languages(languages_to_remove, grammars_to_remove)
}
+ pub fn remove_lsp_adapter(&self, language_name: &str, name: &LanguageServerName) {
+ let mut state = self.state.write();
+ if let Some(adapters) = state.lsp_adapters.get_mut(language_name) {
+ adapters.retain(|adapter| &adapter.name != name)
+ }
+ state.version += 1;
+ state.reload_count += 1;
+ *state.subscription.0.borrow_mut() = ();
+ }
+
#[cfg(any(feature = "test-support", test))]
pub fn register_test_language(&self, config: LanguageConfig) {
self.register_language(
config.name.clone(),
config.grammar.clone(),
config.matcher.clone(),
- vec![],
move || Ok((config.clone(), Default::default())),
)
}
+ pub fn register_lsp_adapter(&self, language_name: Arc<str>, adapter: Arc<dyn LspAdapter>) {
+ self.state
+ .write()
+ .lsp_adapters
+ .entry(language_name)
+ .or_default()
+ .push(CachedLspAdapter::new(adapter));
+ }
+
+ #[cfg(any(feature = "test-support", test))]
+ pub fn register_fake_lsp_adapter(
+ &self,
+ language_name: &str,
+ adapter: crate::FakeLspAdapter,
+ ) -> futures::channel::mpsc::UnboundedReceiver<lsp::FakeLanguageServer> {
+ self.state
+ .write()
+ .lsp_adapters
+ .entry(language_name.into())
+ .or_default()
+ .push(CachedLspAdapter::new(Arc::new(adapter)));
+ self.fake_language_servers(language_name)
+ }
+
+ #[cfg(any(feature = "test-support", test))]
+ pub fn fake_language_servers(
+ &self,
+ language_name: &str,
+ ) -> futures::channel::mpsc::UnboundedReceiver<lsp::FakeLanguageServer> {
+ let (servers_tx, servers_rx) = futures::channel::mpsc::unbounded();
+ self.state
+ .write()
+ .fake_server_txs
+ .entry(language_name.into())
+ .or_default()
+ .push(servers_tx);
+ servers_rx
+ }
+
/// Adds a language to the registry, which can be loaded if needed.
pub fn register_language(
&self,
name: Arc<str>,
grammar_name: Option<Arc<str>>,
matcher: LanguageMatcher,
- lsp_adapters: Vec<Arc<dyn LspAdapter>>,
load: impl Fn() -> Result<(LanguageConfig, LanguageQueries)> + 'static + Send + Sync,
) {
let load = Arc::new(load);
@@ -189,7 +246,6 @@ impl LanguageRegistry {
if existing_language.name == name {
existing_language.grammar = grammar_name;
existing_language.matcher = matcher;
- existing_language.lsp_adapters = lsp_adapters;
existing_language.load = load;
return;
}
@@ -201,7 +257,6 @@ impl LanguageRegistry {
grammar: grammar_name,
matcher,
load,
- lsp_adapters,
loaded: false,
});
state.version += 1;
@@ -376,10 +431,7 @@ impl LanguageRegistry {
None
};
- Language::new_with_id(id, config, grammar)
- .with_lsp_adapters(language.lsp_adapters)
- .await
- .with_queries(queries)
+ Language::new_with_id(id, config, grammar).with_queries(queries)
}
.await;
@@ -492,6 +544,23 @@ impl LanguageRegistry {
self.state.read().languages.iter().cloned().collect()
}
+ pub fn lsp_adapters(&self, language: &Arc<Language>) -> Vec<Arc<CachedLspAdapter>> {
+ self.state
+ .read()
+ .lsp_adapters
+ .get(&language.config.name)
+ .cloned()
+ .unwrap_or_default()
+ }
+
+ pub fn update_lsp_status(
+ &self,
+ server_name: LanguageServerName,
+ status: LanguageServerBinaryStatus,
+ ) {
+ self.lsp_binary_status_tx.send(server_name, status);
+ }
+
pub fn create_pending_language_server(
self: &Arc<Self>,
stderr_capture: Arc<Mutex<Option<String>>>,
@@ -507,100 +576,85 @@ impl LanguageRegistry {
adapter.name.0
);
- #[cfg(any(test, feature = "test-support"))]
- if language.fake_adapter.is_some() {
- let task = cx.spawn(|cx| async move {
- let (servers_tx, fake_adapter) = language.fake_adapter.as_ref().unwrap();
- let (server, mut fake_server) = lsp::FakeLanguageServer::new(
- fake_adapter.name.to_string(),
- fake_adapter.capabilities.clone(),
- cx.clone(),
- );
-
- if let Some(initializer) = &fake_adapter.initializer {
- initializer(&mut fake_server);
- }
-
- let servers_tx = servers_tx.clone();
- cx.background_executor()
- .spawn(async move {
- if fake_server
- .try_receive_notification::<lsp::notification::Initialized>()
- .await
- .is_some()
- {
- servers_tx.unbounded_send(fake_server).ok();
- }
- })
- .detach();
-
- Ok(server)
- });
-
- return Some(PendingLanguageServer {
- server_id,
- task,
- container_dir: None,
- });
- }
-
let download_dir = self
.language_server_download_dir
.clone()
.ok_or_else(|| anyhow!("language server download directory has not been assigned before starting server"))
.log_err()?;
- let this = self.clone();
let language = language.clone();
let container_dir: Arc<Path> = Arc::from(download_dir.join(adapter.name.0.as_ref()));
let root_path = root_path.clone();
- let adapter = adapter.clone();
let login_shell_env_loaded = self.login_shell_env_loaded.clone();
- let lsp_binary_statuses = self.lsp_binary_status_tx.clone();
+ let this = Arc::downgrade(self);
- let task = {
+ let task = cx.spawn({
let container_dir = container_dir.clone();
- cx.spawn(move |mut cx| async move {
- // First we check whether the adapter can give us a user-installed binary.
- // If so, we do *not* want to cache that, because each worktree might give us a different
- // binary:
- //
- // worktree 1: user-installed at `.bin/gopls`
- // worktree 2: user-installed at `~/bin/gopls`
- // worktree 3: no gopls found in PATH -> fallback to Zed installation
- //
- // We only want to cache when we fall back to the global one,
- // because we don't want to download and overwrite our global one
- // for each worktree we might have open.
-
- let user_binary_task = check_user_installed_binary(
- adapter.clone(),
- language.clone(),
- delegate.clone(),
- &mut cx,
- );
- let binary = if let Some(user_binary) = user_binary_task.await {
- user_binary
- } else {
- // If we want to install a binary globally, we need to wait for
- // the login shell to be set on our process.
- login_shell_env_loaded.await;
-
- get_or_install_binary(
- this,
- &adapter,
- language,
- &delegate,
- &cx,
+ move |mut cx| async move {
+ // If we want to install a binary globally, we need to wait for
+ // the login shell to be set on our process.
+ login_shell_env_loaded.await;
+
+ let binary = adapter
+ .clone()
+ .get_language_server_command(
+ language.clone(),
container_dir,
- lsp_binary_statuses,
+ delegate.clone(),
+ &mut cx,
)
- .await?
- };
+ .await?;
if let Some(task) = adapter.will_start_server(&delegate, &mut cx) {
task.await?;
}
+ #[cfg(any(test, feature = "test-support"))]
+ if true {
+ let capabilities = adapter
+ .as_fake()
+ .map(|fake_adapter| fake_adapter.capabilities.clone())
+ .unwrap_or_default();
+
+ let (server, mut fake_server) = lsp::FakeLanguageServer::new(
+ binary,
+ adapter.name.0.to_string(),
+ capabilities,
+ cx.clone(),
+ );
+
+ if let Some(fake_adapter) = adapter.as_fake() {
+ if let Some(initializer) = &fake_adapter.initializer {
+ initializer(&mut fake_server);
+ }
+ }
+
+ cx.background_executor()
+ .spawn(async move {
+ if fake_server
+ .try_receive_notification::<lsp::notification::Initialized>()
+ .await
+ .is_some()
+ {
+ if let Some(this) = this.upgrade() {
+ if let Some(txs) = this
+ .state
+ .write()
+ .fake_server_txs
+ .get_mut(language.name().as_ref())
+ {
+ for tx in txs {
+ tx.unbounded_send(fake_server.clone()).ok();
+ }
+ }
+ }
+ }
+ })
+ .detach();
+
+ return Ok(server);
+ }
+
+ drop(this);
lsp::LanguageServer::new(
stderr_capture,
server_id,
@@ -609,8 +663,8 @@ impl LanguageRegistry {
adapter.code_action_kinds(),
cx,
)
- })
- };
+ }
+ });
Some(PendingLanguageServer {
server_id,
@@ -621,7 +675,7 @@ impl LanguageRegistry {
pub fn language_server_binary_statuses(
&self,
- ) -> mpsc::UnboundedReceiver<(Arc<Language>, LanguageServerBinaryStatus)> {
+ ) -> mpsc::UnboundedReceiver<(LanguageServerName, LanguageServerBinaryStatus)> {
self.lsp_binary_status_tx.subscribe()
}
@@ -718,158 +772,16 @@ impl LanguageRegistryState {
}
impl LspBinaryStatusSender {
- fn subscribe(&self) -> mpsc::UnboundedReceiver<(Arc<Language>, LanguageServerBinaryStatus)> {
+ fn subscribe(
+ &self,
+ ) -> mpsc::UnboundedReceiver<(LanguageServerName, LanguageServerBinaryStatus)> {
let (tx, rx) = mpsc::unbounded();
self.txs.lock().push(tx);
rx
}
- fn send(&self, language: Arc<Language>, status: LanguageServerBinaryStatus) {
+ fn send(&self, name: LanguageServerName, status: LanguageServerBinaryStatus) {
let mut txs = self.txs.lock();
- txs.retain(|tx| {
- tx.unbounded_send((language.clone(), status.clone()))
- .is_ok()
- });
- }
-}
-
-async fn check_user_installed_binary(
- adapter: Arc<CachedLspAdapter>,
- language: Arc<Language>,
- delegate: Arc<dyn LspAdapterDelegate>,
- cx: &mut AsyncAppContext,
-) -> Option<LanguageServerBinary> {
- let Some(task) = adapter.check_if_user_installed(&delegate, cx) else {
- return None;
- };
-
- task.await.and_then(|binary| {
- log::info!(
- "found user-installed language server for {}. path: {:?}, arguments: {:?}",
- language.name(),
- binary.path,
- binary.arguments
- );
- Some(binary)
- })
-}
-
-async fn get_or_install_binary(
- registry: Arc<LanguageRegistry>,
- adapter: &Arc<CachedLspAdapter>,
- language: Arc<Language>,
- delegate: &Arc<dyn LspAdapterDelegate>,
- cx: &AsyncAppContext,
- container_dir: Arc<Path>,
- lsp_binary_statuses: LspBinaryStatusSender,
-) -> Result<LanguageServerBinary> {
- let entry = registry
- .lsp_binary_paths
- .lock()
- .entry(adapter.name.clone())
- .or_insert_with(|| {
- let adapter = adapter.clone();
- let language = language.clone();
- let delegate = delegate.clone();
- cx.spawn(|cx| {
- get_binary(
- adapter,
- language,
- delegate,
- container_dir,
- lsp_binary_statuses,
- cx,
- )
- .map_err(Arc::new)
- })
- .shared()
- })
- .clone();
-
- entry.await.map_err(|err| anyhow!("{:?}", err))
-}
-
-async fn get_binary(
- adapter: Arc<CachedLspAdapter>,
- language: Arc<Language>,
- delegate: Arc<dyn LspAdapterDelegate>,
- container_dir: Arc<Path>,
- statuses: LspBinaryStatusSender,
- mut cx: AsyncAppContext,
-) -> Result<LanguageServerBinary> {
- if !container_dir.exists() {
- smol::fs::create_dir_all(&container_dir)
- .await
- .context("failed to create container directory")?;
- }
-
- if let Some(task) = adapter.will_fetch_server(&delegate, &mut cx) {
- task.await?;
- }
-
- let binary = fetch_latest_binary(
- adapter.clone(),
- language.clone(),
- delegate.as_ref(),
- &container_dir,
- statuses.clone(),
- )
- .await;
-
- if let Err(error) = binary.as_ref() {
- if let Some(binary) = adapter
- .cached_server_binary(container_dir.to_path_buf(), delegate.as_ref())
- .await
- {
- statuses.send(language.clone(), LanguageServerBinaryStatus::Cached);
- log::info!(
- "failed to fetch newest version of language server {:?}. falling back to using {:?}",
- adapter.name,
- binary.path.display()
- );
- return Ok(binary);
- }
-
- statuses.send(
- language.clone(),
- LanguageServerBinaryStatus::Failed {
- error: format!("{:?}", error),
- },
- );
+ txs.retain(|tx| tx.unbounded_send((name.clone(), status.clone())).is_ok());
}
-
- binary
-}
-
-async fn fetch_latest_binary(
- adapter: Arc<CachedLspAdapter>,
- language: Arc<Language>,
- delegate: &dyn LspAdapterDelegate,
- container_dir: &Path,
- lsp_binary_statuses_tx: LspBinaryStatusSender,
-) -> Result<LanguageServerBinary> {
- let container_dir: Arc<Path> = container_dir.into();
-
- lsp_binary_statuses_tx.send(
- language.clone(),
- LanguageServerBinaryStatus::CheckingForUpdate,
- );
-
- log::info!(
- "querying GitHub for latest version of language server {:?}",
- adapter.name.0
- );
- let version_info = adapter.fetch_latest_server_version(delegate).await?;
- lsp_binary_statuses_tx.send(language.clone(), LanguageServerBinaryStatus::Downloading);
-
- log::info!(
- "checking if Zed already installed or fetching version for language server {:?}",
- adapter.name.0
- );
- let binary = adapter
- .fetch_server_binary(version_info, container_dir.to_path_buf(), delegate)
- .await?;
- lsp_binary_statuses_tx.send(language.clone(), LanguageServerBinaryStatus::Downloaded);
-
- Ok(binary)
}
@@ -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 {
@@ -20,7 +20,20 @@ async fn test_lsp_logs(cx: &mut TestAppContext) {
init_test(cx);
- let mut rust_language = Language::new(
+ let fs = FakeFs::new(cx.background_executor.clone());
+ fs.insert_tree(
+ "/the-root",
+ json!({
+ "test.rs": "",
+ "package.json": "",
+ }),
+ )
+ .await;
+
+ let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
+
+ let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+ language_registry.add(Arc::new(Language::new(
LanguageConfig {
name: "Rust".into(),
matcher: LanguageMatcher {
@@ -30,27 +43,14 @@ async fn test_lsp_logs(cx: &mut TestAppContext) {
..Default::default()
},
Some(tree_sitter_rust::language()),
- );
- let mut fake_rust_servers = rust_language
- .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+ )));
+ let mut fake_rust_servers = language_registry.register_fake_lsp_adapter(
+ "Rust",
+ FakeLspAdapter {
name: "the-rust-language-server",
..Default::default()
- }))
- .await;
-
- let fs = FakeFs::new(cx.background_executor.clone());
- fs.insert_tree(
- "/the-root",
- json!({
- "test.rs": "",
- "package.json": "",
- }),
- )
- .await;
- let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
- project.update(cx, |project, _| {
- project.languages().add(Arc::new(rust_language));
- });
+ },
+ );
let log_store = cx.new_model(|cx| LogStore::new(cx));
log_store.update(cx, |store, cx| store.add_project(&project, cx));
@@ -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,
@@ -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(), "")
@@ -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,
@@ -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,
@@ -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,
@@ -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,
@@ -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,
@@ -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,
@@ -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,
@@ -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,
@@ -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,
@@ -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,
@@ -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(),
@@ -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,
@@ -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,
@@ -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,
@@ -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(),
)
@@ -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,
@@ -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,
@@ -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,
@@ -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,
@@ -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,
@@ -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,
@@ -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);
@@ -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,
@@ -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(), "")
@@ -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,
@@ -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,
@@ -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,
@@ -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,
@@ -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() {
@@ -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,
@@ -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,
@@ -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,
@@ -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(
@@ -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();
@@ -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
@@ -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
}
@@ -14,7 +14,7 @@ use futures::{
use gpui::{AsyncAppContext, Model, ModelContext, Task, WeakModel};
use language::{
language_settings::{Formatter, LanguageSettings},
- Buffer, Language, LanguageServerName, LocalFile,
+ Buffer, Language, LanguageRegistry, LanguageServerName, LocalFile,
};
use lsp::{LanguageServer, LanguageServerId};
use node_runtime::NodeRuntime;
@@ -26,7 +26,8 @@ use crate::{
};
pub fn prettier_plugins_for_language(
- language: &Language,
+ language_registry: &Arc<LanguageRegistry>,
+ language: &Arc<Language>,
language_settings: &LanguageSettings,
) -> Option<HashSet<&'static str>> {
match &language_settings.formatter {
@@ -38,8 +39,8 @@ pub fn prettier_plugins_for_language(
prettier_plugins
.get_or_insert_with(|| HashSet::default())
.extend(
- language
- .lsp_adapters()
+ language_registry
+ .lsp_adapters(language)
.iter()
.flat_map(|adapter| adapter.prettier_plugins()),
)
@@ -303,15 +304,20 @@ fn start_prettier(
) -> PrettierTask {
cx.spawn(|project, mut cx| async move {
log::info!("Starting prettier at path {prettier_dir:?}");
- let new_server_id = project.update(&mut cx, |project, _| {
- project.languages.next_language_server_id()
- })?;
-
- let new_prettier = Prettier::start(new_server_id, prettier_dir, node, cx.clone())
- .await
- .context("default prettier spawn")
- .map(Arc::new)
- .map_err(Arc::new)?;
+ let language_registry = project.update(&mut cx, |project, _| project.languages.clone())?;
+ let new_server_id = language_registry.next_language_server_id();
+
+ let new_prettier = Prettier::start(
+ new_server_id,
+ prettier_dir,
+ node,
+ language_registry,
+ cx.clone(),
+ )
+ .await
+ .context("default prettier spawn")
+ .map(Arc::new)
+ .map_err(Arc::new)?;
register_new_prettier(&project, &new_prettier, worktree_id, new_server_id, &mut cx);
Ok(new_prettier)
})
@@ -10,6 +10,7 @@ pub mod terminals;
mod project_tests;
use anyhow::{anyhow, bail, Context as _, Result};
+use async_trait::async_trait;
use client::{proto, Client, Collaborator, TypedEnvelope, UserStore};
use clock::ReplicaId;
use collections::{hash_map, BTreeMap, HashMap, HashSet, VecDeque};
@@ -847,10 +848,12 @@ impl Project {
let current_lsp_settings = &self.current_lsp_settings;
for (worktree_id, started_lsp_name) in self.language_server_ids.keys() {
let language = languages.iter().find_map(|l| {
- let adapter = l
- .lsp_adapters()
+ let adapter = self
+ .languages
+ .lsp_adapters(l)
.iter()
- .find(|adapter| &adapter.name == started_lsp_name)?;
+ .find(|adapter| &adapter.name == started_lsp_name)?
+ .clone();
Some((l, adapter))
});
if let Some((language, adapter)) = language {
@@ -889,9 +892,11 @@ impl Project {
let mut prettier_plugins_by_worktree = HashMap::default();
for (worktree, language, settings) in language_formatters_to_check {
- if let Some(plugins) =
- prettier_support::prettier_plugins_for_language(&language, &settings)
- {
+ if let Some(plugins) = prettier_support::prettier_plugins_for_language(
+ &self.languages,
+ &language,
+ &settings,
+ ) {
prettier_plugins_by_worktree
.entry(worktree)
.or_insert_with(|| HashSet::default())
@@ -2047,7 +2052,7 @@ impl Project {
}
if let Some(language) = language {
- for adapter in language.lsp_adapters() {
+ for adapter in self.languages.lsp_adapters(&language) {
let language_id = adapter.language_ids.get(language.name().as_ref()).cloned();
let server = self
.language_server_ids
@@ -2118,10 +2123,12 @@ impl Project {
let worktree_id = old_file.worktree_id(cx);
let ids = &self.language_server_ids;
- let language = buffer.language().cloned();
- let adapters = language.iter().flat_map(|language| language.lsp_adapters());
- for &server_id in adapters.flat_map(|a| ids.get(&(worktree_id, a.name.clone()))) {
- buffer.update_diagnostics(server_id, Default::default(), cx);
+ if let Some(language) = buffer.language().cloned() {
+ for adapter in self.languages.lsp_adapters(&language) {
+ if let Some(server_id) = ids.get(&(worktree_id, adapter.name.clone())) {
+ buffer.update_diagnostics(*server_id, Default::default(), cx);
+ }
+ }
}
self.buffer_snapshots.remove(&buffer.remote_id());
@@ -2701,9 +2708,11 @@ impl Project {
let settings = language_settings(Some(&new_language), buffer_file.as_ref(), cx).clone();
let buffer_file = File::from_dyn(buffer_file.as_ref());
let worktree = buffer_file.as_ref().map(|f| f.worktree_id(cx));
- if let Some(prettier_plugins) =
- prettier_support::prettier_plugins_for_language(&new_language, &settings)
- {
+ if let Some(prettier_plugins) = prettier_support::prettier_plugins_for_language(
+ &self.languages,
+ &new_language,
+ &settings,
+ ) {
self.install_default_prettier(worktree, prettier_plugins, cx);
};
if let Some(file) = buffer_file {
@@ -2726,7 +2735,7 @@ impl Project {
return;
}
- for adapter in language.lsp_adapters() {
+ for adapter in self.languages.clone().lsp_adapters(&language) {
self.start_language_server(worktree, adapter.clone(), language.clone(), cx);
}
}
@@ -3240,7 +3249,11 @@ impl Project {
};
if file.worktree.read(cx).id() != key.0
- || !language.lsp_adapters().iter().any(|a| a.name == key.1)
+ || !self
+ .languages
+ .lsp_adapters(&language)
+ .iter()
+ .any(|a| a.name == key.1)
{
continue;
}
@@ -3433,8 +3446,10 @@ impl Project {
) {
let worktree_id = worktree.read(cx).id();
- let stop_tasks = language
- .lsp_adapters()
+ let stop_tasks = self
+ .languages
+ .clone()
+ .lsp_adapters(&language)
.iter()
.map(|adapter| {
let stop_task = self.stop_language_server(worktree_id, adapter.name.clone(), cx);
@@ -4785,14 +4800,15 @@ impl Project {
.languages
.language_for_file(&project_path.path, None)
.unwrap_or_else(move |_| adapter_language);
- let language_server_name = adapter.name.clone();
+ let adapter = adapter.clone();
Some(async move {
let language = language.await;
- let label =
- language.label_for_symbol(&symbol_name, symbol_kind).await;
+ let label = adapter
+ .label_for_symbol(&symbol_name, symbol_kind, &language)
+ .await;
Symbol {
- language_server_name,
+ language_server_name: adapter.name.clone(),
source_worktree_id,
path: project_path,
label: label.unwrap_or_else(|| {
@@ -7972,6 +7988,7 @@ impl Project {
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<proto::ApplyCompletionAdditionalEditsResponse> {
+ let languages = this.update(&mut cx, |this, _| this.languages.clone())?;
let (buffer, completion) = this.update(&mut cx, |this, cx| {
let buffer_id = BufferId::new(envelope.payload.buffer_id)?;
let buffer = this
@@ -7986,6 +8003,7 @@ impl Project {
.completion
.ok_or_else(|| anyhow!("invalid completion"))?,
language.cloned(),
+ &languages,
);
Ok::<_, anyhow::Error>((buffer, completion))
})??;
@@ -8713,6 +8731,9 @@ impl Project {
.language_for_file(&path.path, None)
.await
.log_err();
+ let adapter = language
+ .as_ref()
+ .and_then(|language| languages.lsp_adapters(language).first().cloned());
Ok(Symbol {
language_server_name: LanguageServerName(
serialized_symbol.language_server_name.into(),
@@ -8720,10 +8741,10 @@ impl Project {
source_worktree_id,
path,
label: {
- match language {
- Some(language) => {
- language
- .label_for_symbol(&serialized_symbol.name, kind)
+ match language.as_ref().zip(adapter.as_ref()) {
+ Some((language, adapter)) => {
+ adapter
+ .label_for_symbol(&serialized_symbol.name, kind, language)
.await
}
None => None,
@@ -8975,6 +8996,17 @@ impl Project {
self.supplementary_language_servers.iter()
}
+ pub fn language_server_adapter_for_id(
+ &self,
+ id: LanguageServerId,
+ ) -> Option<Arc<CachedLspAdapter>> {
+ if let Some(LanguageServerState::Running { adapter, .. }) = self.language_servers.get(&id) {
+ Some(adapter.clone())
+ } else {
+ None
+ }
+ }
+
pub fn language_server_for_id(&self, id: LanguageServerId) -> Option<Arc<LanguageServer>> {
if let Some(LanguageServerState::Running { server, .. }) = self.language_servers.get(&id) {
Some(server.clone())
@@ -9025,8 +9057,8 @@ impl Project {
) -> Vec<LanguageServerId> {
if let Some((file, language)) = File::from_dyn(buffer.file()).zip(buffer.language()) {
let worktree_id = file.worktree_id(cx);
- language
- .lsp_adapters()
+ self.languages
+ .lsp_adapters(&language)
.iter()
.flat_map(|adapter| {
let key = (worktree_id, adapter.name.clone());
@@ -9190,20 +9222,25 @@ impl<P: AsRef<Path>> From<(WorktreeId, P)> for ProjectPath {
struct ProjectLspAdapterDelegate {
project: Model<Project>,
- worktree: Model<Worktree>,
+ worktree: worktree::Snapshot,
+ fs: Arc<dyn Fs>,
http_client: Arc<dyn HttpClient>,
+ language_registry: Arc<LanguageRegistry>,
}
impl ProjectLspAdapterDelegate {
fn new(project: &Project, worktree: &Model<Worktree>, cx: &ModelContext<Project>) -> Arc<Self> {
Arc::new(Self {
project: cx.handle(),
- worktree: worktree.clone(),
+ worktree: worktree.read(cx).snapshot(),
+ fs: project.fs.clone(),
http_client: project.client.http_client(),
+ language_registry: project.languages.clone(),
})
}
}
+#[async_trait]
impl LspAdapterDelegate for ProjectLspAdapterDelegate {
fn show_notification(&self, message: &str, cx: &mut AppContext) {
self.project
@@ -9214,41 +9251,50 @@ impl LspAdapterDelegate for ProjectLspAdapterDelegate {
self.http_client.clone()
}
- fn which_command(
- &self,
- command: OsString,
- cx: &AppContext,
- ) -> Task<Option<(PathBuf, HashMap<String, String>)>> {
- let worktree_abs_path = self.worktree.read(cx).abs_path();
- let command = command.to_owned();
+ async fn which_command(&self, command: OsString) -> Option<(PathBuf, HashMap<String, String>)> {
+ let worktree_abs_path = self.worktree.abs_path();
- cx.background_executor().spawn(async move {
- let shell_env = load_shell_environment(&worktree_abs_path)
- .await
- .with_context(|| {
- format!(
- "failed to determine load login shell environment in {worktree_abs_path:?}"
- )
- })
- .log_err();
-
- if let Some(shell_env) = shell_env {
- let shell_path = shell_env.get("PATH");
- match which::which_in(&command, shell_path, &worktree_abs_path) {
- Ok(command_path) => Some((command_path, shell_env)),
- Err(error) => {
- log::warn!(
- "failed to determine path for command {:?} in shell PATH {:?}: {error}",
- command.to_string_lossy(),
- shell_path.map(String::as_str).unwrap_or("")
- );
- None
- }
+ let shell_env = load_shell_environment(&worktree_abs_path)
+ .await
+ .with_context(|| {
+ format!("failed to determine load login shell environment in {worktree_abs_path:?}")
+ })
+ .log_err();
+
+ if let Some(shell_env) = shell_env {
+ let shell_path = shell_env.get("PATH");
+ match which::which_in(&command, shell_path, &worktree_abs_path) {
+ Ok(command_path) => Some((command_path, shell_env)),
+ Err(error) => {
+ log::warn!(
+ "failed to determine path for command {:?} in shell PATH {:?}: {error}",
+ command.to_string_lossy(),
+ shell_path.map(String::as_str).unwrap_or("")
+ );
+ None
}
- } else {
- None
}
- })
+ } else {
+ None
+ }
+ }
+
+ fn update_status(
+ &self,
+ server_name: LanguageServerName,
+ status: language::LanguageServerBinaryStatus,
+ ) {
+ self.language_registry
+ .update_lsp_status(server_name, status);
+ }
+
+ async fn read_text_file(&self, path: PathBuf) -> Result<String> {
+ if self.worktree.entry_for_path(&path).is_none() {
+ return Err(anyhow!("no such path {path:?}"));
+ }
+ let path = self.worktree.absolutize(path.as_ref())?;
+ let content = self.fs.load(&path).await?;
+ Ok(content)
}
}
@@ -189,30 +189,24 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
init_test(cx);
- let mut rust_language = Language::new(
- LanguageConfig {
- name: "Rust".into(),
- matcher: LanguageMatcher {
- path_suffixes: vec!["rs".to_string()],
- ..Default::default()
- },
- ..Default::default()
- },
- Some(tree_sitter_rust::language()),
- );
- let mut json_language = Language::new(
- LanguageConfig {
- name: "JSON".into(),
- matcher: LanguageMatcher {
- path_suffixes: vec!["json".to_string()],
- ..Default::default()
- },
- ..Default::default()
- },
- None,
- );
- let mut fake_rust_servers = rust_language
- .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+ let fs = FakeFs::new(cx.executor());
+ fs.insert_tree(
+ "/the-root",
+ json!({
+ "test.rs": "const A: i32 = 1;",
+ "test2.rs": "",
+ "Cargo.toml": "a = 1",
+ "package.json": "{\"a\": 1}",
+ }),
+ )
+ .await;
+
+ let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
+ let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+
+ let mut fake_rust_servers = language_registry.register_fake_lsp_adapter(
+ "Rust",
+ FakeLspAdapter {
name: "the-rust-language-server",
capabilities: lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions {
@@ -222,10 +216,11 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
..Default::default()
},
..Default::default()
- }))
- .await;
- let mut fake_json_servers = json_language
- .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+ },
+ );
+ let mut fake_json_servers = language_registry.register_fake_lsp_adapter(
+ "JSON",
+ FakeLspAdapter {
name: "the-json-language-server",
capabilities: lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions {
@@ -235,22 +230,8 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
..Default::default()
},
..Default::default()
- }))
- .await;
-
- let fs = FakeFs::new(cx.executor());
- fs.insert_tree(
- "/the-root",
- json!({
- "test.rs": "const A: i32 = 1;",
- "test2.rs": "",
- "Cargo.toml": "a = 1",
- "package.json": "{\"a\": 1}",
- }),
- )
- .await;
-
- let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
+ },
+ );
// Open a buffer without an associated language server.
let toml_buffer = project
@@ -273,10 +254,8 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
// Now we add the languages to the project, and ensure they get assigned to all
// the relevant open buffers.
- project.update(cx, |project, _| {
- project.languages.add(Arc::new(json_language));
- project.languages.add(Arc::new(rust_language));
- });
+ language_registry.add(json_lang());
+ language_registry.add(rust_lang());
cx.executor().run_until_parked();
rust_buffer.update(cx, |buffer, _| {
assert_eq!(buffer.language().map(|l| l.name()), Some("Rust".into()));
@@ -581,24 +560,6 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppContext) {
init_test(cx);
- let mut language = Language::new(
- LanguageConfig {
- name: "Rust".into(),
- matcher: LanguageMatcher {
- path_suffixes: vec!["rs".to_string()],
- ..Default::default()
- },
- ..Default::default()
- },
- Some(tree_sitter_rust::language()),
- );
- let mut fake_servers = language
- .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
- name: "the-language-server",
- ..Default::default()
- }))
- .await;
-
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/the-root",
@@ -630,9 +591,16 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
.await;
let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
- project.update(cx, |project, _| {
- project.languages.add(Arc::new(language));
- });
+ let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+ language_registry.add(rust_lang());
+ let mut fake_servers = language_registry.register_fake_lsp_adapter(
+ "Rust",
+ FakeLspAdapter {
+ name: "the-language-server",
+ ..Default::default()
+ },
+ );
+
cx.executor().run_until_parked();
// Start the language server by opening a buffer with a compatible file extension.
@@ -1019,24 +987,6 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
init_test(cx);
let progress_token = "the-progress-token";
- let mut language = Language::new(
- LanguageConfig {
- name: "Rust".into(),
- matcher: LanguageMatcher {
- path_suffixes: vec!["rs".to_string()],
- ..Default::default()
- },
- ..Default::default()
- },
- Some(tree_sitter_rust::language()),
- );
- let mut fake_servers = language
- .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
- disk_based_diagnostics_progress_token: Some(progress_token.into()),
- disk_based_diagnostics_sources: vec!["disk".into()],
- ..Default::default()
- }))
- .await;
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
@@ -1049,7 +999,18 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
.await;
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
- project.update(cx, |project, _| project.languages.add(Arc::new(language)));
+ let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+
+ language_registry.add(rust_lang());
+ let mut fake_servers = language_registry.register_fake_lsp_adapter(
+ "Rust",
+ FakeLspAdapter {
+ disk_based_diagnostics_progress_token: Some(progress_token.into()),
+ disk_based_diagnostics_sources: vec!["disk".into()],
+ ..Default::default()
+ },
+ );
+
let worktree_id = project.update(cx, |p, cx| p.worktrees().next().unwrap().read(cx).id());
// Cause worktree to start the fake language server
@@ -1155,29 +1116,23 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC
init_test(cx);
let progress_token = "the-progress-token";
- let mut language = Language::new(
- LanguageConfig {
- matcher: LanguageMatcher {
- path_suffixes: vec!["rs".to_string()],
- ..Default::default()
- },
- ..Default::default()
- },
- None,
- );
- let mut fake_servers = language
- .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
- disk_based_diagnostics_sources: vec!["disk".into()],
- disk_based_diagnostics_progress_token: Some(progress_token.into()),
- ..Default::default()
- }))
- .await;
let fs = FakeFs::new(cx.executor());
fs.insert_tree("/dir", json!({ "a.rs": "" })).await;
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
- project.update(cx, |project, _| project.languages.add(Arc::new(language)));
+
+ let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+ language_registry.add(rust_lang());
+ let mut fake_servers = language_registry.register_fake_lsp_adapter(
+ "Rust",
+ FakeLspAdapter {
+ name: "the-language-server",
+ disk_based_diagnostics_sources: vec!["disk".into()],
+ disk_based_diagnostics_progress_token: Some(progress_token.into()),
+ ..Default::default()
+ },
+ );
let buffer = project
.update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
@@ -1239,27 +1194,15 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC
async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAppContext) {
init_test(cx);
- let mut language = Language::new(
- LanguageConfig {
- matcher: LanguageMatcher {
- path_suffixes: vec!["rs".to_string()],
- ..Default::default()
- },
- ..Default::default()
- },
- None,
- );
- let mut fake_servers = language
- .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
- ..Default::default()
- }))
- .await;
-
let fs = FakeFs::new(cx.executor());
fs.insert_tree("/dir", json!({ "a.rs": "x" })).await;
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
- project.update(cx, |project, _| project.languages.add(Arc::new(language)));
+
+ let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+ language_registry.add(rust_lang());
+ let mut fake_servers =
+ language_registry.register_fake_lsp_adapter("Rust", FakeLspAdapter::default());
let buffer = project
.update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
@@ -1331,28 +1274,15 @@ async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAp
async fn test_restarted_server_reporting_invalid_buffer_version(cx: &mut gpui::TestAppContext) {
init_test(cx);
- let mut language = Language::new(
- LanguageConfig {
- matcher: LanguageMatcher {
- path_suffixes: vec!["rs".to_string()],
- ..Default::default()
- },
- ..Default::default()
- },
- None,
- );
- let mut fake_servers = language
- .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
- name: "the-lsp",
- ..Default::default()
- }))
- .await;
-
let fs = FakeFs::new(cx.executor());
fs.insert_tree("/dir", json!({ "a.rs": "" })).await;
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
- project.update(cx, |project, _| project.languages.add(Arc::new(language)));
+ let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+
+ language_registry.add(rust_lang());
+ let mut fake_servers =
+ language_registry.register_fake_lsp_adapter("Rust", FakeLspAdapter::default());
let buffer = project
.update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
@@ -1383,50 +1313,29 @@ async fn test_restarted_server_reporting_invalid_buffer_version(cx: &mut gpui::T
async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) {
init_test(cx);
- let mut rust = Language::new(
- LanguageConfig {
- name: Arc::from("Rust"),
- matcher: LanguageMatcher {
- path_suffixes: vec!["rs".to_string()],
- ..Default::default()
- },
- ..Default::default()
- },
- None,
- );
- let mut fake_rust_servers = rust
- .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
- name: "rust-lsp",
- ..Default::default()
- }))
+ let fs = FakeFs::new(cx.executor());
+ fs.insert_tree("/dir", json!({ "a.rs": "", "b.js": "" }))
.await;
- let mut js = Language::new(
- LanguageConfig {
- name: Arc::from("JavaScript"),
- matcher: LanguageMatcher {
- path_suffixes: vec!["js".to_string()],
- ..Default::default()
- },
+
+ let project = Project::test(fs, ["/dir".as_ref()], cx).await;
+ let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+
+ let mut fake_rust_servers = language_registry.register_fake_lsp_adapter(
+ "Rust",
+ FakeLspAdapter {
+ name: "rust-lsp",
..Default::default()
},
- None,
);
- let mut fake_js_servers = js
- .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+ let mut fake_js_servers = language_registry.register_fake_lsp_adapter(
+ "JavaScript",
+ FakeLspAdapter {
name: "js-lsp",
..Default::default()
- }))
- .await;
-
- let fs = FakeFs::new(cx.executor());
- fs.insert_tree("/dir", json!({ "a.rs": "", "b.js": "" }))
- .await;
-
- let project = Project::test(fs, ["/dir".as_ref()], cx).await;
- project.update(cx, |project, _| {
- project.languages.add(Arc::new(rust));
- project.languages.add(Arc::new(js));
- });
+ },
+ );
+ language_registry.add(rust_lang());
+ language_registry.add(js_lang());
let _rs_buffer = project
.update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
@@ -1518,24 +1427,6 @@ async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) {
async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
init_test(cx);
- let mut language = Language::new(
- LanguageConfig {
- name: "Rust".into(),
- matcher: LanguageMatcher {
- path_suffixes: vec!["rs".to_string()],
- ..Default::default()
- },
- ..Default::default()
- },
- Some(tree_sitter_rust::language()),
- );
- let mut fake_servers = language
- .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
- disk_based_diagnostics_sources: vec!["disk".into()],
- ..Default::default()
- }))
- .await;
-
let text = "
fn a() { A }
fn b() { BB }
@@ -1547,7 +1438,16 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
fs.insert_tree("/dir", json!({ "a.rs": text })).await;
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
- project.update(cx, |project, _| project.languages.add(Arc::new(language)));
+ let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+
+ language_registry.add(rust_lang());
+ let mut fake_servers = language_registry.register_fake_lsp_adapter(
+ "Rust",
+ FakeLspAdapter {
+ disk_based_diagnostics_sources: vec!["disk".into()],
+ ..Default::default()
+ },
+ );
let buffer = project
.update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
@@ -1932,19 +1832,6 @@ async fn test_diagnostics_from_multiple_language_servers(cx: &mut gpui::TestAppC
async fn test_edits_from_lsp2_with_past_version(cx: &mut gpui::TestAppContext) {
init_test(cx);
- let mut language = Language::new(
- LanguageConfig {
- name: "Rust".into(),
- matcher: LanguageMatcher {
- path_suffixes: vec!["rs".to_string()],
- ..Default::default()
- },
- ..Default::default()
- },
- Some(tree_sitter_rust::language()),
- );
- let mut fake_servers = language.set_fake_lsp_adapter(Default::default()).await;
-
let text = "
fn a() {
f1();
@@ -1968,7 +1855,12 @@ async fn test_edits_from_lsp2_with_past_version(cx: &mut gpui::TestAppContext) {
.await;
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
- project.update(cx, |project, _| project.languages.add(Arc::new(language)));
+
+ let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+ language_registry.add(rust_lang());
+ let mut fake_servers =
+ language_registry.register_fake_lsp_adapter("Rust", FakeLspAdapter::default());
+
let buffer = project
.update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
.await
@@ -2322,19 +2214,6 @@ fn chunks_with_diagnostics<T: ToOffset + ToPoint>(
async fn test_definition(cx: &mut gpui::TestAppContext) {
init_test(cx);
- let mut language = Language::new(
- LanguageConfig {
- name: "Rust".into(),
- matcher: LanguageMatcher {
- path_suffixes: vec!["rs".to_string()],
- ..Default::default()
- },
- ..Default::default()
- },
- Some(tree_sitter_rust::language()),
- );
- let mut fake_servers = language.set_fake_lsp_adapter(Default::default()).await;
-
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/dir",
@@ -2346,7 +2225,11 @@ async fn test_definition(cx: &mut gpui::TestAppContext) {
.await;
let project = Project::test(fs, ["/dir/b.rs".as_ref()], cx).await;
- project.update(cx, |project, _| project.languages.add(Arc::new(language)));
+
+ let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+ language_registry.add(rust_lang());
+ let mut fake_servers =
+ language_registry.register_fake_lsp_adapter("Rust", FakeLspAdapter::default());
let buffer = project
.update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx))
@@ -2426,30 +2309,6 @@ async fn test_definition(cx: &mut gpui::TestAppContext) {
async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) {
init_test(cx);
- let mut language = Language::new(
- LanguageConfig {
- name: "TypeScript".into(),
- matcher: LanguageMatcher {
- path_suffixes: vec!["ts".to_string()],
- ..Default::default()
- },
- ..Default::default()
- },
- Some(tree_sitter_typescript::language_typescript()),
- );
- let mut fake_language_servers = language
- .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
- capabilities: lsp::ServerCapabilities {
- completion_provider: Some(lsp::CompletionOptions {
- trigger_characters: Some(vec![":".to_string()]),
- ..Default::default()
- }),
- ..Default::default()
- },
- ..Default::default()
- }))
- .await;
-
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/dir",
@@ -2460,7 +2319,23 @@ async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) {
.await;
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
- project.update(cx, |project, _| project.languages.add(Arc::new(language)));
+
+ let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+ language_registry.add(typescript_lang());
+ let mut fake_language_servers = language_registry.register_fake_lsp_adapter(
+ "TypeScript",
+ FakeLspAdapter {
+ capabilities: lsp::ServerCapabilities {
+ completion_provider: Some(lsp::CompletionOptions {
+ trigger_characters: Some(vec![":".to_string()]),
+ ..Default::default()
+ }),
+ ..Default::default()
+ },
+ ..Default::default()
+ },
+ );
+
let buffer = project
.update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
.await
@@ -2526,30 +2401,6 @@ async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) {
async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) {
init_test(cx);
- let mut language = Language::new(
- LanguageConfig {
- name: "TypeScript".into(),
- matcher: LanguageMatcher {
- path_suffixes: vec!["ts".to_string()],
- ..Default::default()
- },
- ..Default::default()
- },
- Some(tree_sitter_typescript::language_typescript()),
- );
- let mut fake_language_servers = language
- .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
- capabilities: lsp::ServerCapabilities {
- completion_provider: Some(lsp::CompletionOptions {
- trigger_characters: Some(vec![":".to_string()]),
- ..Default::default()
- }),
- ..Default::default()
- },
- ..Default::default()
- }))
- .await;
-
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/dir",
@@ -2560,7 +2411,23 @@ async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) {
.await;
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
- project.update(cx, |project, _| project.languages.add(Arc::new(language)));
+
+ let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+ language_registry.add(typescript_lang());
+ let mut fake_language_servers = language_registry.register_fake_lsp_adapter(
+ "TypeScript",
+ FakeLspAdapter {
+ capabilities: lsp::ServerCapabilities {
+ completion_provider: Some(lsp::CompletionOptions {
+ trigger_characters: Some(vec![":".to_string()]),
+ ..Default::default()
+ }),
+ ..Default::default()
+ },
+ ..Default::default()
+ },
+ );
+
let buffer = project
.update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
.await
@@ -2595,19 +2462,6 @@ async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) {
async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) {
init_test(cx);
- let mut language = Language::new(
- LanguageConfig {
- name: "TypeScript".into(),
- matcher: LanguageMatcher {
- path_suffixes: vec!["ts".to_string()],
- ..Default::default()
- },
- ..Default::default()
- },
- None,
- );
- let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
-
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/dir",
@@ -2618,7 +2472,12 @@ async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) {
.await;
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
- project.update(cx, |project, _| project.languages.add(Arc::new(language)));
+
+ let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+ language_registry.add(typescript_lang());
+ let mut fake_language_servers =
+ language_registry.register_fake_lsp_adapter("TypeScript", Default::default());
+
let buffer = project
.update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
.await
@@ -2904,16 +2763,7 @@ async fn test_save_as(cx: &mut gpui::TestAppContext) {
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
let languages = project.update(cx, |project, _| project.languages().clone());
- languages.register_native_grammars([("rust", tree_sitter_rust::language())]);
- languages.register_test_language(LanguageConfig {
- name: "Rust".into(),
- grammar: Some("rust".into()),
- matcher: LanguageMatcher {
- path_suffixes: vec!["rs".into()],
- ..Default::default()
- },
- ..Default::default()
- });
+ languages.add(rust_lang());
let buffer = project.update(cx, |project, cx| {
project.create_buffer("", None, cx).unwrap()
@@ -3733,30 +3583,6 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
async fn test_rename(cx: &mut gpui::TestAppContext) {
init_test(cx);
- let mut language = Language::new(
- LanguageConfig {
- name: "Rust".into(),
- matcher: LanguageMatcher {
- path_suffixes: vec!["rs".to_string()],
- ..Default::default()
- },
- ..Default::default()
- },
- Some(tree_sitter_rust::language()),
- );
- let mut fake_servers = language
- .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
- capabilities: lsp::ServerCapabilities {
- rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
- prepare_provider: Some(true),
- work_done_progress_options: Default::default(),
- })),
- ..Default::default()
- },
- ..Default::default()
- }))
- .await;
-
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/dir",
@@ -3768,7 +3594,23 @@ async fn test_rename(cx: &mut gpui::TestAppContext) {
.await;
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
- project.update(cx, |project, _| project.languages.add(Arc::new(language)));
+
+ let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+ language_registry.add(rust_lang());
+ let mut fake_servers = language_registry.register_fake_lsp_adapter(
+ "Rust",
+ FakeLspAdapter {
+ capabilities: lsp::ServerCapabilities {
+ rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
+ prepare_provider: Some(true),
+ work_done_progress_options: Default::default(),
+ })),
+ ..Default::default()
+ },
+ ..Default::default()
+ },
+ );
+
let buffer = project
.update(cx, |project, cx| {
project.open_local_buffer("/dir/one.rs", cx)
@@ -4475,3 +4317,59 @@ fn init_test(cx: &mut gpui::TestAppContext) {
Project::init_settings(cx);
});
}
+
+fn json_lang() -> Arc<Language> {
+ Arc::new(Language::new(
+ LanguageConfig {
+ name: "JSON".into(),
+ matcher: LanguageMatcher {
+ path_suffixes: vec!["json".to_string()],
+ ..Default::default()
+ },
+ ..Default::default()
+ },
+ None,
+ ))
+}
+
+fn js_lang() -> Arc<Language> {
+ Arc::new(Language::new(
+ LanguageConfig {
+ name: Arc::from("JavaScript"),
+ matcher: LanguageMatcher {
+ path_suffixes: vec!["js".to_string()],
+ ..Default::default()
+ },
+ ..Default::default()
+ },
+ None,
+ ))
+}
+
+fn rust_lang() -> Arc<Language> {
+ Arc::new(Language::new(
+ LanguageConfig {
+ name: "Rust".into(),
+ matcher: LanguageMatcher {
+ path_suffixes: vec!["rs".to_string()],
+ ..Default::default()
+ },
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::language()),
+ ))
+}
+
+fn typescript_lang() -> Arc<Language> {
+ Arc::new(Language::new(
+ LanguageConfig {
+ name: "TypeScript".into(),
+ matcher: LanguageMatcher {
+ path_suffixes: vec!["ts".to_string()],
+ ..Default::default()
+ },
+ ..Default::default()
+ },
+ Some(tree_sitter_typescript::language_typescript()),
+ ))
+}
@@ -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},
@@ -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| {
@@ -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"]);
@@ -178,6 +178,7 @@ fn main() {
extension::init(
fs.clone(),
http.clone(),
+ node_runtime.clone(),
languages.clone(),
ThemeRegistry::global(cx),
cx,
@@ -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();
@@ -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]
@@ -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"
@@ -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"] },
+]
@@ -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
@@ -0,0 +1,3 @@
+(_ "[" "]" @end) @indent
+(_ "{" "}" @end) @indent
+(_ "(" ")" @end) @indent
@@ -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
@@ -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() {}
@@ -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);