Provide wasm extensions with APIs needed for using pre-installed LSP binaries (#9085)

Max Brunsfeld and Marshall created

In this PR, we've added two new methods that LSP extensions can call:
* `shell_env()`, for retrieving the environment variables set in the
user's default shell in the worktree
* `which(command)`, for looking up paths to an executable (accounting
for the user's shell env in the worktree)

To test this out, we moved the `uiua` language support into an
extension. We went ahead and removed the built-in support, since this
language is extremely obscure. Sorry @mikayla-maki. To continue coding
in Uiua in Zed, for now you can `Add Dev Extension` from the extensions
pane, and select the `extensions/uiua` directory in the Zed repo. Very
soon, we'll support publishing these extensions so that you'll be able
to just install it normally.

Release Notes:

- N/A

---------

Co-authored-by: Marshall <marshall@zed.dev>

Change summary

.github/workflows/ci.yml                      |   3 
Cargo.lock                                    | 163 +-------------------
Cargo.toml                                    |   7 
crates/extension/Cargo.toml                   |   3 
crates/extension/src/build_extension.rs       |  84 ++++++++++
crates/extension/src/extension_store.rs       |   4 
crates/extension/src/extension_store_test.rs  |  18 --
crates/extension/src/wasm_host.rs             |  62 ++++---
crates/extension_api/wit/extension.wit        |   6 
crates/language/src/language.rs               |   5 
crates/languages/Cargo.toml                   |   1 
crates/languages/src/go.rs                    |   3 
crates/languages/src/lib.rs                   |   3 
crates/languages/src/zig.rs                   |   4 
crates/project/src/project.rs                 |  54 +++---
extensions/.gitignore                         |   0 
extensions/uiua/Cargo.toml                    |  16 ++
extensions/uiua/extension.toml                |  13 +
extensions/uiua/languages/uiua/config.toml    |   0 
extensions/uiua/languages/uiua/highlights.scm |   0 
extensions/uiua/languages/uiua/indents.scm    |   0 
extensions/uiua/src/uiua.rs                   |  27 +++
script/setup-wasm                             |  15 -
23 files changed, 235 insertions(+), 256 deletions(-)

Detailed changes

.github/workflows/ci.yml 🔗

@@ -89,9 +89,6 @@ jobs:
       - name: cargo clippy
         run: cargo xtask clippy
 
-      - name: Install WASI dependencies
-        run: script/setup-wasm
-
       - name: Run tests
         uses: ./.github/actions/run_tests
 

Cargo.lock 🔗

@@ -3011,15 +3011,6 @@ 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"
@@ -3159,16 +3150,6 @@ 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"
@@ -3556,6 +3537,7 @@ dependencies = [
  "theme",
  "toml 0.8.10",
  "util",
+ "wasm-encoder",
  "wasmparser",
  "wasmtime",
  "wasmtime-wasi",
@@ -4165,28 +4147,6 @@ 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.2",
- "debugid",
- "fxhash",
- "serde",
- "serde_json",
-]
-
 [[package]]
 name = "generic-array"
 version = "0.14.7"
@@ -5060,26 +5020,6 @@ 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"
@@ -5385,7 +5325,6 @@ dependencies = [
  "tree-sitter-svelte",
  "tree-sitter-toml",
  "tree-sitter-typescript",
- "tree-sitter-uiua",
  "tree-sitter-vue",
  "tree-sitter-yaml",
  "tree-sitter-zig",
@@ -10816,15 +10755,6 @@ dependencies = [
  "tree-sitter",
 ]
 
-[[package]]
-name = "tree-sitter-uiua"
-version = "0.10.0"
-source = "git+https://github.com/shnarazk/tree-sitter-uiua?rev=21dc2db39494585bf29a3f86d5add6e9d11a22ba#21dc2db39494585bf29a3f86d5add6e9d11a22ba"
-dependencies = [
- "cc",
- "tree-sitter",
-]
-
 [[package]]
 name = "tree-sitter-vue"
 version = "0.0.1"
@@ -11440,15 +11370,6 @@ dependencies = [
  "leb128",
 ]
 
-[[package]]
-name = "wasm-encoder"
-version = "0.201.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b9c7d2731df60006819b013f64ccc2019691deccf6e11a1804bc850cd6748f1a"
-dependencies = [
- "leb128",
-]
-
 [[package]]
 name = "wasm-metadata"
 version = "0.10.20"
@@ -11461,7 +11382,7 @@ dependencies = [
  "serde_derive",
  "serde_json",
  "spdx",
- "wasm-encoder 0.41.2",
+ "wasm-encoder",
  "wasmparser",
 ]
 
@@ -11492,41 +11413,33 @@ version = "18.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4c843b8bc4dd4f3a76173ba93405c71111d570af0d90ea5f6299c705d0c2add2"
 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",
 ]
 
@@ -11563,26 +11476,6 @@ dependencies = [
  "quote",
 ]
 
-[[package]]
-name = "wasmtime-cache"
-version = "18.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6fb4fc2bbf9c790a57875eba65588fa97acf57a7d784dc86d057e648d9a1ed91"
-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.2"
@@ -11664,7 +11557,7 @@ dependencies = [
  "serde_derive",
  "target-lexicon",
  "thiserror",
- "wasm-encoder 0.41.2",
+ "wasm-encoder",
  "wasmparser",
  "wasmprinter",
  "wasmtime-component-util",
@@ -11686,18 +11579,6 @@ dependencies = [
  "windows-sys 0.52.0",
 ]
 
-[[package]]
-name = "wasmtime-jit-debug"
-version = "18.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "833dae95bc7a4f9177bf93f9497419763535b74e37eb8c37be53937d3281e287"
-dependencies = [
- "object",
- "once_cell",
- "rustix 0.38.30",
- "wasmtime-versioned-export-macros",
-]
-
 [[package]]
 name = "wasmtime-jit-icache-coherence"
 version = "18.0.2"
@@ -11729,11 +11610,10 @@ dependencies = [
  "psm",
  "rustix 0.38.30",
  "sptr",
- "wasm-encoder 0.41.2",
+ "wasm-encoder",
  "wasmtime-asm-macros",
  "wasmtime-environ",
  "wasmtime-fiber",
- "wasmtime-jit-debug",
  "wasmtime-versioned-export-macros",
  "wasmtime-wmemcheck",
  "windows-sys 0.52.0",
@@ -11840,28 +11720,6 @@ dependencies = [
  "leb128",
 ]
 
-[[package]]
-name = "wast"
-version = "201.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ef6e1ef34d7da3e2b374fd2b1a9c0227aff6cad596e1b24df9b58d0f6222faa"
-dependencies = [
- "bumpalo",
- "leb128",
- "memchr",
- "unicode-width",
- "wasm-encoder 0.201.0",
-]
-
-[[package]]
-name = "wat"
-version = "1.201.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "453d5b37a45b98dee4f4cb68015fc73634d7883bbef1c65e6e9c78d454cf3f32"
-dependencies = [
- "wast 201.0.0",
-]
-
 [[package]]
 name = "wayland-backend"
 version = "0.3.3"
@@ -12515,7 +12373,7 @@ dependencies = [
  "serde",
  "serde_derive",
  "serde_json",
- "wasm-encoder 0.41.2",
+ "wasm-encoder",
  "wasm-metadata",
  "wasmparser",
  "wit-parser 0.13.2",
@@ -12534,7 +12392,7 @@ dependencies = [
  "serde",
  "serde_derive",
  "serde_json",
- "wasm-encoder 0.41.2",
+ "wasm-encoder",
  "wasm-metadata",
  "wasmparser",
  "wit-parser 0.14.0",
@@ -12584,7 +12442,7 @@ dependencies = [
  "anyhow",
  "log",
  "thiserror",
- "wast 35.0.2",
+ "wast",
 ]
 
 [[package]]
@@ -13003,6 +12861,13 @@ dependencies = [
  "zed_extension_api",
 ]
 
+[[package]]
+name = "zed_uiua"
+version = "0.0.1"
+dependencies = [
+ "zed_extension_api",
+]
+
 [[package]]
 name = "zeno"
 version = "0.2.3"

Cargo.toml 🔗

@@ -92,7 +92,10 @@ members = [
     "crates/workspace",
     "crates/zed",
     "crates/zed_actions",
+
     "extensions/gleam",
+    "extensions/uiua",
+
     "tooling/xtask",
 ]
 default-members = ["crates/zed"]
@@ -308,7 +311,6 @@ tree-sitter-scheme = { git = "https://github.com/6cdh/tree-sitter-scheme", rev =
 tree-sitter-svelte = { git = "https://github.com/Himujjal/tree-sitter-svelte", rev = "bd60db7d3d06f89b6ec3b287c9a6e9190b5564bd" }
 tree-sitter-toml = { git = "https://github.com/tree-sitter/tree-sitter-toml", rev = "342d9be207c2dba869b9967124c679b5e6fd0ebe" }
 tree-sitter-typescript = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "5d20856f34315b068c41edaee2ac8a100081d259" }
-tree-sitter-uiua = { git = "https://github.com/shnarazk/tree-sitter-uiua", rev = "21dc2db39494585bf29a3f86d5add6e9d11a22ba" }
 tree-sitter-vue = { git = "https://github.com/zed-industries/tree-sitter-vue", rev = "6608d9d60c386f19d80af7d8132322fa11199c42" }
 tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml", rev = "f545a41f57502e1b5ddf2a6668896c1b0620f930" }
 tree-sitter-zig = { git = "https://github.com/maxxnino/tree-sitter-zig", rev = "0d08703e4c3f426ec61695d7617415fff97029bd" }
@@ -317,7 +319,8 @@ unicase = "2.6"
 url = "2.2"
 uuid = { version = "1.1.2", features = ["v4"] }
 wasmparser = "0.121"
-wasmtime = "18.0"
+wasm-encoder = "0.41"
+wasmtime = { version = "18.0", default-features = false, features = ["async", "demangle", "runtime", "cranelift", "component-model"] }
 wasmtime-wasi = "18.0"
 which = "6.0.0"
 wit-component = "0.20"

crates/extension/Cargo.toml 🔗

@@ -37,7 +37,8 @@ settings.workspace = true
 theme.workspace = true
 toml.workspace = true
 util.workspace = true
-wasmtime = { workspace = true, features = ["async"] }
+wasm-encoder.workspace = true
+wasmtime.workspace = true
 wasmtime-wasi.workspace = true
 wasmparser.workspace = true
 wit-component.workspace = true

crates/extension/src/build_extension.rs 🔗

@@ -6,13 +6,16 @@ use async_tar::Archive;
 use futures::io::BufReader;
 use futures::AsyncReadExt;
 use serde::Deserialize;
+use std::mem;
 use std::{
     env, fs,
     path::{Path, PathBuf},
     process::{Command, Stdio},
     sync::Arc,
 };
-use util::http::{AsyncBody, HttpClient};
+use util::http::{self, AsyncBody, HttpClient};
+use wasm_encoder::{ComponentSectionId, Encode as _, RawSection, Section as _};
+use wasmparser::Parser;
 use wit_component::ComponentEncoder;
 
 /// Currently, we compile with Rust's `wasm32-wasi` target, which works with WASI `preview1`.
@@ -59,8 +62,11 @@ struct CargoTomlPackage {
 }
 
 impl ExtensionBuilder {
-    pub fn new(cache_dir: PathBuf, http: Arc<dyn HttpClient>) -> Self {
-        Self { cache_dir, http }
+    pub fn new(cache_dir: PathBuf) -> Self {
+        Self {
+            cache_dir,
+            http: http::client(),
+        }
     }
 
     pub async fn compile_extension(
@@ -138,6 +144,10 @@ impl ExtensionBuilder {
             .encode()
             .context("failed to encode wasm component")?;
 
+        let component_bytes = self
+            .strip_custom_sections(&component_bytes)
+            .context("failed to strip debug sections from wasm component")?;
+
         fs::write(extension_dir.join("extension.wasm"), &component_bytes)
             .context("failed to write extension.wasm")?;
 
@@ -310,7 +320,7 @@ impl ExtensionBuilder {
     async fn install_wasi_preview1_adapter_if_needed(&self) -> Result<Vec<u8>> {
         let cache_path = self.cache_dir.join("wasi_snapshot_preview1.reactor.wasm");
         if let Ok(content) = fs::read(&cache_path) {
-            if wasmparser::Parser::is_core_wasm(&content) {
+            if Parser::is_core_wasm(&content) {
                 return Ok(content);
             }
         }
@@ -333,7 +343,7 @@ impl ExtensionBuilder {
         fs::write(&cache_path, &content)
             .with_context(|| format!("failed to save file {}", cache_path.display()))?;
 
-        if !wasmparser::Parser::is_core_wasm(&content) {
+        if !Parser::is_core_wasm(&content) {
             bail!("downloaded wasi adapter is invalid");
         }
         Ok(content)
@@ -379,4 +389,68 @@ impl ExtensionBuilder {
 
         Ok(clang_path)
     }
+
+    // This was adapted from:
+    // https://github.com/bytecodealliance/wasm-tools/1791a8f139722e9f8679a2bd3d8e423e55132b22/src/bin/wasm-tools/strip.rs
+    fn strip_custom_sections(&self, input: &Vec<u8>) -> Result<Vec<u8>> {
+        use wasmparser::Payload::*;
+
+        let strip_custom_section = |name: &str| name.starts_with(".debug");
+
+        let mut output = Vec::new();
+        let mut stack = Vec::new();
+
+        for payload in Parser::new(0).parse_all(input) {
+            let payload = payload?;
+
+            // Track nesting depth, so that we don't mess with inner producer sections:
+            match payload {
+                Version { encoding, .. } => {
+                    output.extend_from_slice(match encoding {
+                        wasmparser::Encoding::Component => &wasm_encoder::Component::HEADER,
+                        wasmparser::Encoding::Module => &wasm_encoder::Module::HEADER,
+                    });
+                }
+                ModuleSection { .. } | ComponentSection { .. } => {
+                    stack.push(mem::take(&mut output));
+                    continue;
+                }
+                End { .. } => {
+                    let mut parent = match stack.pop() {
+                        Some(c) => c,
+                        None => break,
+                    };
+                    if output.starts_with(&wasm_encoder::Component::HEADER) {
+                        parent.push(ComponentSectionId::Component as u8);
+                        output.encode(&mut parent);
+                    } else {
+                        parent.push(ComponentSectionId::CoreModule as u8);
+                        output.encode(&mut parent);
+                    }
+                    output = parent;
+                }
+                _ => {}
+            }
+
+            match &payload {
+                CustomSection(c) => {
+                    if strip_custom_section(c.name()) {
+                        continue;
+                    }
+                }
+
+                _ => {}
+            }
+
+            if let Some((id, range)) = payload.as_section() {
+                RawSection {
+                    id,
+                    data: &input[range],
+                }
+                .append_to(&mut output);
+            }
+        }
+
+        Ok(output)
+    }
 }

crates/extension/src/extension_store.rs 🔗

@@ -193,7 +193,7 @@ impl ExtensionStore {
             extension_index: Default::default(),
             installed_dir,
             index_path,
-            builder: Arc::new(ExtensionBuilder::new(build_dir, http_client.clone())),
+            builder: Arc::new(ExtensionBuilder::new(build_dir)),
             outstanding_operations: Default::default(),
             modified_extensions: Default::default(),
             reload_complete_senders: Vec::new(),
@@ -545,7 +545,7 @@ impl ExtensionStore {
                         builder
                             .compile_extension(
                                 &extension_source_path,
-                                CompileExtensionOptions { release: true },
+                                CompileExtensionOptions { release: false },
                             )
                             .await
                     }

crates/extension/src/extension_store_test.rs 🔗

@@ -7,10 +7,7 @@ use collections::BTreeMap;
 use fs::{FakeFs, Fs, RealFs};
 use futures::{io::BufReader, AsyncReadExt, StreamExt};
 use gpui::{Context, TestAppContext};
-use language::{
-    Language, LanguageConfig, LanguageMatcher, LanguageRegistry, LanguageServerBinaryStatus,
-    LanguageServerName,
-};
+use language::{LanguageMatcher, LanguageRegistry, LanguageServerBinaryStatus, LanguageServerName};
 use node_runtime::FakeNodeRuntime;
 use parking_lot::Mutex;
 use project::Project;
@@ -573,19 +570,6 @@ async fn test_extension_store_with_gleam_extension(cx: &mut TestAppContext) {
         })
         .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();
     let expected_server_path = extensions_dir.join("work/gleam/gleam-v1.2.3/gleam");

crates/extension/src/wasm_host.rs 🔗

@@ -3,7 +3,7 @@ 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 fs::{normalize_path, Fs};
 use futures::{
     channel::{
         mpsc::{self, UnboundedSender},
@@ -72,8 +72,6 @@ type ExtensionCall = Box<
 
 static WASM_ENGINE: OnceLock<wasmtime::Engine> = OnceLock::new();
 
-const EXTENSION_WORK_DIR_PATH: &str = "/zed/work";
-
 impl WasmHost {
     pub fn new(
         fs: Arc<dyn Fs>,
@@ -181,11 +179,12 @@ impl WasmHost {
             .await
             .context("failed to create extension work dir")?;
 
-        let work_dir_preopen = Dir::open_ambient_dir(extension_work_dir, ambient_authority())
+        let work_dir_preopen = Dir::open_ambient_dir(&extension_work_dir, ambient_authority())
             .context("failed to preopen extension work directory")?;
         let current_dir_preopen = work_dir_preopen
             .try_clone()
             .context("failed to preopen extension current directory")?;
+        let extension_work_dir = extension_work_dir.to_string_lossy();
 
         let perms = wasi::FilePerms::all();
         let dir_perms = wasi::DirPerms::all();
@@ -193,26 +192,24 @@ impl WasmHost {
         Ok(wasi::WasiCtxBuilder::new()
             .inherit_stdio()
             .preopened_dir(current_dir_preopen, dir_perms, perms, ".")
-            .preopened_dir(work_dir_preopen, dir_perms, perms, EXTENSION_WORK_DIR_PATH)
-            .env("PWD", EXTENSION_WORK_DIR_PATH)
-            .env("RUST_BACKTRACE", "1")
+            .preopened_dir(work_dir_preopen, dir_perms, perms, &extension_work_dir)
+            .env("PWD", &extension_work_dir)
+            .env("RUST_BACKTRACE", "full")
             .build())
     }
 
     pub fn path_from_extension(&self, id: &Arc<str>, path: &Path) -> PathBuf {
-        self.writeable_path_from_extension(id, path)
-            .unwrap_or_else(|| path.to_path_buf())
+        let extension_work_dir = self.work_dir.join(id.as_ref());
+        normalize_path(&extension_work_dir.join(path))
     }
 
-    pub fn writeable_path_from_extension(&self, id: &Arc<str>, path: &Path) -> Option<PathBuf> {
-        let path = path.strip_prefix(EXTENSION_WORK_DIR_PATH).unwrap_or(path);
-        if path.is_relative() {
-            let mut result = self.work_dir.clone();
-            result.push(id.as_ref());
-            result.extend(path);
-            Some(result)
+    pub fn writeable_path_from_extension(&self, id: &Arc<str>, path: &Path) -> Result<PathBuf> {
+        let extension_work_dir = self.work_dir.join(id.as_ref());
+        let path = normalize_path(&extension_work_dir.join(path));
+        if path.starts_with(&extension_work_dir) {
+            Ok(path)
         } else {
-            None
+            Err(anyhow!("cannot write to path {}", path.display()))
         }
     }
 }
@@ -252,13 +249,6 @@ impl WasmExtension {
     }
 }
 
-impl WasmState {
-    pub fn writeable_path_from_extension(&self, path: &Path) -> Option<PathBuf> {
-        self.host
-            .writeable_path_from_extension(&self.manifest.id, path)
-    }
-}
-
 #[async_trait]
 impl wit::HostWorktree for WasmState {
     async fn read_text_file(
@@ -273,6 +263,26 @@ impl wit::HostWorktree for WasmState {
             .map_err(|error| error.to_string()))
     }
 
+    async fn shell_env(
+        &mut self,
+        delegate: Resource<Arc<dyn LspAdapterDelegate>>,
+    ) -> wasmtime::Result<wit::EnvVars> {
+        let delegate = self.table.get(&delegate)?;
+        Ok(delegate.shell_env().await.into_iter().collect())
+    }
+
+    async fn which(
+        &mut self,
+        delegate: Resource<Arc<dyn LspAdapterDelegate>>,
+        binary_name: String,
+    ) -> wasmtime::Result<Option<String>> {
+        let delegate = self.table.get(&delegate)?;
+        Ok(delegate
+            .which(binary_name.as_ref())
+            .await
+            .map(|path| path.to_string_lossy().to_string()))
+    }
+
     fn drop(&mut self, _worktree: Resource<wit::Worktree>) -> Result<()> {
         // we only ever hand out borrows of worktrees
         Ok(())
@@ -395,8 +405,8 @@ impl wit::ExtensionImports for WasmState {
             this.host.fs.create_dir(&extension_work_dir).await?;
 
             let destination_path = this
-                .writeable_path_from_extension(&path)
-                .ok_or_else(|| anyhow!("cannot write to path {:?}", path))?;
+                .host
+                .writeable_path_from_extension(&this.manifest.id, &path)?;
 
             let mut response = this
                 .host

crates/extension_api/wit/extension.wit 🔗

@@ -61,14 +61,18 @@ world extension {
     /// 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);
 
+    type env-vars = list<tuple<string, string>>;
+
     record command {
         command: string,
         args: list<string>,
-        env: list<tuple<string, string>>,
+        env: env-vars,
     }
 
     resource worktree {
         read-text-file: func(path: string) -> result<string, string>;
+        which: func(binary-name: string) -> option<string>;
+        shell-env: func() -> env-vars;
     }
 
     record language-server-config {

crates/language/src/language.rs 🔗

@@ -40,7 +40,7 @@ use smol::future::FutureExt as _;
 use std::{
     any::Any,
     cell::RefCell,
-    ffi::OsString,
+    ffi::OsStr,
     fmt::Debug,
     hash::Hash,
     mem,
@@ -277,7 +277,8 @@ pub trait LspAdapterDelegate: Send + Sync {
     fn http_client(&self) -> Arc<dyn HttpClient>;
     fn update_status(&self, language: LanguageServerName, status: LanguageServerBinaryStatus);
 
-    async fn which_command(&self, command: OsString) -> Option<(PathBuf, HashMap<String, String>)>;
+    async fn which(&self, command: &OsStr) -> Option<PathBuf>;
+    async fn shell_env(&self) -> HashMap<String, String>;
     async fn read_text_file(&self, path: PathBuf) -> Result<String>;
 }
 

crates/languages/Cargo.toml 🔗

@@ -76,7 +76,6 @@ tree-sitter-scheme.workspace = true
 tree-sitter-svelte.workspace = true
 tree-sitter-toml.workspace = true
 tree-sitter-typescript.workspace = true
-tree-sitter-uiua.workspace = true
 tree-sitter-vue.workspace = true
 tree-sitter-yaml.workspace = true
 tree-sitter-zig.workspace = true

crates/languages/src/go.rs 🔗

@@ -58,7 +58,8 @@ impl super::LspAdapter for GoLspAdapter {
         &self,
         delegate: &dyn LspAdapterDelegate,
     ) -> Option<LanguageServerBinary> {
-        let (path, env) = delegate.which_command(OsString::from("gopls")).await?;
+        let env = delegate.shell_env().await;
+        let path = delegate.which("gopls".as_ref()).await?;
         Some(LanguageServerBinary {
             path,
             arguments: server_binary_arguments(),

crates/languages/src/lib.rs 🔗

@@ -39,7 +39,6 @@ mod tailwind;
 mod terraform;
 mod toml;
 mod typescript;
-mod uiua;
 mod vue;
 mod yaml;
 mod zig;
@@ -114,7 +113,6 @@ pub fn init(
         ("toml", tree_sitter_toml::language()),
         ("tsx", tree_sitter_typescript::language_tsx()),
         ("typescript", tree_sitter_typescript::language_typescript()),
-        ("uiua", tree_sitter_uiua::language()),
         ("vue", tree_sitter_vue::language()),
         ("yaml", tree_sitter_yaml::language()),
         ("zig", tree_sitter_zig::language()),
@@ -344,7 +342,6 @@ pub fn init(
         "vue",
         vec![Arc::new(vue::VueLspAdapter::new(node_runtime.clone()))]
     );
-    language!("uiua", vec![Arc::new(uiua::UiuaLanguageServer {})]);
     language!("proto");
     language!("terraform", vec![Arc::new(terraform::TerraformLspAdapter)]);
     language!(

crates/languages/src/zig.rs 🔗

@@ -7,7 +7,6 @@ use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
 use lsp::LanguageServerBinary;
 use smol::fs;
 use std::env::consts::{ARCH, OS};
-use std::ffi::OsString;
 use std::{any::Any, path::PathBuf};
 use util::async_maybe;
 use util::github::latest_github_release;
@@ -45,7 +44,8 @@ impl LspAdapter for ZlsAdapter {
         &self,
         delegate: &dyn LspAdapterDelegate,
     ) -> Option<LanguageServerBinary> {
-        let (path, env) = delegate.which_command(OsString::from("zls")).await?;
+        let env = delegate.shell_env().await;
+        let path = delegate.which("zls".as_ref()).await?;
         Some(LanguageServerBinary {
             path,
             arguments: vec![],

crates/project/src/project.rs 🔗

@@ -72,7 +72,7 @@ use std::{
     cmp::{self, Ordering},
     convert::TryInto,
     env,
-    ffi::OsString,
+    ffi::OsStr,
     hash::Hash,
     mem,
     num::NonZeroU32,
@@ -9390,6 +9390,7 @@ struct ProjectLspAdapterDelegate {
     fs: Arc<dyn Fs>,
     http_client: Arc<dyn HttpClient>,
     language_registry: Arc<LanguageRegistry>,
+    shell_env: Mutex<Option<HashMap<String, String>>>,
 }
 
 impl ProjectLspAdapterDelegate {
@@ -9400,8 +9401,21 @@ impl ProjectLspAdapterDelegate {
             fs: project.fs.clone(),
             http_client: project.client.http_client(),
             language_registry: project.languages.clone(),
+            shell_env: Default::default(),
         })
     }
+
+    async fn load_shell_env(&self) {
+        let worktree_abs_path = self.worktree.abs_path();
+        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()
+            .unwrap_or_default();
+        *self.shell_env.lock() = Some(shell_env);
+    }
 }
 
 #[async_trait]
@@ -9416,32 +9430,20 @@ impl LspAdapterDelegate for ProjectLspAdapterDelegate {
         self.http_client.clone()
     }
 
-    async fn which_command(&self, command: OsString) -> Option<(PathBuf, HashMap<String, String>)> {
-        let worktree_abs_path = self.worktree.abs_path();
+    async fn shell_env(&self) -> HashMap<String, String> {
+        self.load_shell_env().await;
+        self.shell_env.lock().as_ref().cloned().unwrap_or_default()
+    }
 
-        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
-        }
+    async fn which(&self, command: &OsStr) -> Option<PathBuf> {
+        let worktree_abs_path = self.worktree.abs_path();
+        self.load_shell_env().await;
+        let shell_path = self
+            .shell_env
+            .lock()
+            .as_ref()
+            .and_then(|shell_env| shell_env.get("PATH").cloned());
+        which::which_in(command, shell_path.as_ref(), &worktree_abs_path).ok()
     }
 
     fn update_status(

extensions/uiua/Cargo.toml 🔗

@@ -0,0 +1,16 @@
+[package]
+name = "zed_uiua"
+version = "0.0.1"
+edition = "2021"
+publish = false
+license = "Apache-2.0"
+
+[lints]
+workspace = true
+
+[dependencies]
+zed_extension_api = { path = "../../crates/extension_api" }
+
+[lib]
+path = "src/uiua.rs"
+crate-type = ["cdylib"]

extensions/uiua/extension.toml 🔗

@@ -0,0 +1,13 @@
+id = "uiua"
+name = "Uiua"
+description = "Uiua support for Zed"
+version = "0.0.1"
+authors = ["Max Brunsfeld <max@zed.dev>"]
+
+[language_servers.uiua]
+name = "Uiua LSP"
+language = "Uiua"
+
+[grammars.uiua]
+repository = "https://github.com/shnarazk/tree-sitter-uiua"
+commit = "21dc2db39494585bf29a3f86d5add6e9d11a22ba"

extensions/uiua/src/uiua.rs 🔗

@@ -0,0 +1,27 @@
+use zed_extension_api::{self as zed, Result};
+
+struct UiuaExtension;
+
+impl zed::Extension for UiuaExtension {
+    fn new() -> Self {
+        Self
+    }
+
+    fn language_server_command(
+        &mut self,
+        _config: zed::LanguageServerConfig,
+        worktree: &zed::Worktree,
+    ) -> Result<zed::Command> {
+        let path = worktree
+            .which("uiua")
+            .ok_or_else(|| "uiua is not installed".to_string())?;
+
+        Ok(zed::Command {
+            command: path,
+            args: vec!["lsp".to_string()],
+            env: Default::default(),
+        })
+    }
+}
+
+zed::register_extension!(UiuaExtension);

script/setup-wasm 🔗

@@ -1,15 +0,0 @@
-#!/bin/bash
-
-set -eu
-
-WASI_ADAPTER_URL="https://github.com/bytecodealliance/wasmtime/releases/download/v18.0.2/wasi_snapshot_preview1.reactor.wasm"
-WASI_SDK_URL="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-21/wasi-sdk-21.0-macos.tar.gz"
-
-echo "Downloading WASI adapter: $WASI_ADAPTER_URL"
-curl -L $WASI_ADAPTER_URL -o target/wasi_snapshot_preview1.reactor.wasm
-
-echo "Downloading WASI SDK: $WASI_SDK_URL"
-mkdir -p target/wasi-sdk.archive
-curl -L $WASI_SDK_URL | tar -xz - -C target/wasi-sdk.archive
-rm -rf target/wasi-sdk/
-mv -f target/wasi-sdk.archive/wasi-sdk-21.0/ target/wasi-sdk