Allow wasm extensions to do arbitrary file I/O in their own directory to install language servers (#9043)

This PR provides WASM extensions with write access to their own specific
working directory under the Zed `extensions` dir. This directory is set
as the extensions `current_dir` when they run. Extensions can return
relative paths from the `Extension::language_server_command` method, and
those relative paths will be interpreted relative to this working dir.

With this functionality, most language servers that we currently build
into zed can be installed using extensions.

Release Notes:

- N/A
This commit is contained in:
Max Brunsfeld 2024-03-08 08:49:27 -08:00 committed by GitHub
parent a550b9cecf
commit 51ebe0eb01
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 421 additions and 215 deletions

View file

@ -86,12 +86,6 @@ jobs:
clean: false clean: false
submodules: "recursive" submodules: "recursive"
- name: Install cargo-component
run: |
if ! which cargo-component > /dev/null; then
cargo install cargo-component
fi
- name: cargo clippy - name: cargo clippy
run: cargo xtask clippy run: cargo xtask clippy

4
Cargo.lock generated
View file

@ -3536,7 +3536,10 @@ dependencies = [
"async-compression", "async-compression",
"async-tar", "async-tar",
"async-trait", "async-trait",
"cap-std",
"collections", "collections",
"ctor",
"env_logger",
"fs", "fs",
"futures 0.3.28", "futures 0.3.28",
"gpui", "gpui",
@ -3544,6 +3547,7 @@ dependencies = [
"log", "log",
"lsp", "lsp",
"node_runtime", "node_runtime",
"parking_lot 0.11.2",
"project", "project",
"schemars", "schemars",
"serde", "serde",

View file

@ -201,6 +201,7 @@ bitflags = "2.4.2"
blade-graphics = { git = "https://github.com/kvark/blade", rev = "43721bf42d298b7cbee2195ee66f73a5f1c7b2fc" } blade-graphics = { git = "https://github.com/kvark/blade", rev = "43721bf42d298b7cbee2195ee66f73a5f1c7b2fc" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "43721bf42d298b7cbee2195ee66f73a5f1c7b2fc" } blade-macros = { git = "https://github.com/kvark/blade", rev = "43721bf42d298b7cbee2195ee66f73a5f1c7b2fc" }
blade-rwh = { package = "raw-window-handle", version = "0.5" } blade-rwh = { package = "raw-window-handle", version = "0.5" }
cap-std = "2.0"
chrono = { version = "0.4", features = ["serde"] } chrono = { version = "0.4", features = ["serde"] }
clap = "4.4" clap = "4.4"
clickhouse = { version = "0.11.6" } clickhouse = { version = "0.11.6" }

View file

@ -20,6 +20,7 @@ anyhow.workspace = true
async-compression.workspace = true async-compression.workspace = true
async-tar.workspace = true async-tar.workspace = true
async-trait.workspace = true async-trait.workspace = true
cap-std.workspace = true
collections.workspace = true collections.workspace = true
fs.workspace = true fs.workspace = true
futures.workspace = true futures.workspace = true
@ -42,6 +43,10 @@ wasmparser.workspace = true
wit-component.workspace = true wit-component.workspace = true
[dev-dependencies] [dev-dependencies]
ctor.workspace = true
env_logger.workspace = true
parking_lot.workspace = true
fs = { workspace = true, features = ["test-support"] } fs = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] } gpui = { workspace = true, features = ["test-support"] }
language = { workspace = true, features = ["test-support"] } language = { workspace = true, features = ["test-support"] }

View file

@ -317,7 +317,10 @@ impl ExtensionBuilder {
fs::remove_file(&cache_path).ok(); fs::remove_file(&cache_path).ok();
log::info!("downloading wasi adapter module"); log::info!(
"downloading wasi adapter module to {}",
cache_path.display()
);
let mut response = self let mut response = self
.http .http
.get(WASI_ADAPTER_URL, AsyncBody::default(), true) .get(WASI_ADAPTER_URL, AsyncBody::default(), true)
@ -357,6 +360,7 @@ impl ExtensionBuilder {
fs::remove_dir_all(&wasi_sdk_dir).ok(); fs::remove_dir_all(&wasi_sdk_dir).ok();
fs::remove_dir_all(&tar_out_dir).ok(); fs::remove_dir_all(&tar_out_dir).ok();
log::info!("downloading wasi-sdk to {}", wasi_sdk_dir.display());
let mut response = self.http.get(&url, AsyncBody::default(), true).await?; let mut response = self.http.get(&url, AsyncBody::default(), true).await?;
let body = BufReader::new(response.body_mut()); let body = BufReader::new(response.body_mut());
let body = GzipDecoder::new(body); let body = GzipDecoder::new(body);

View file

@ -1,4 +1,4 @@
use crate::wasm_host::{wit::LanguageServerConfig, WasmExtension}; use crate::wasm_host::{wit::LanguageServerConfig, WasmExtension, WasmHost};
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use async_trait::async_trait; use async_trait::async_trait;
use futures::{Future, FutureExt}; use futures::{Future, FutureExt};
@ -16,7 +16,7 @@ use wasmtime_wasi::preview2::WasiView as _;
pub struct ExtensionLspAdapter { pub struct ExtensionLspAdapter {
pub(crate) extension: WasmExtension, pub(crate) extension: WasmExtension,
pub(crate) config: LanguageServerConfig, pub(crate) config: LanguageServerConfig,
pub(crate) work_dir: PathBuf, pub(crate) host: Arc<WasmHost>,
} }
#[async_trait] #[async_trait]
@ -41,18 +41,23 @@ impl LspAdapter for ExtensionLspAdapter {
|extension, store| { |extension, store| {
async move { async move {
let resource = store.data_mut().table().push(delegate)?; let resource = store.data_mut().table().push(delegate)?;
extension let command = extension
.call_language_server_command(store, &this.config, resource) .call_language_server_command(store, &this.config, resource)
.await .await?
.map_err(|e| anyhow!("{}", e))?;
anyhow::Ok(command)
} }
.boxed() .boxed()
} }
}) })
.await? .await?;
.map_err(|e| anyhow!("{}", e))?;
let path = self
.host
.path_from_extension(&self.extension.manifest.id, command.command.as_ref());
Ok(LanguageServerBinary { Ok(LanguageServerBinary {
path: self.work_dir.join(&command.command), path,
arguments: command.args.into_iter().map(|arg| arg.into()).collect(), arguments: command.args.into_iter().map(|arg| arg.into()).collect(),
env: Some(command.env.into_iter().collect()), env: Some(command.env.into_iter().collect()),
}) })

View file

@ -100,6 +100,7 @@ enum ExtensionOperation {
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
pub enum Event { pub enum Event {
ExtensionsUpdated, ExtensionsUpdated,
StartedReloading,
} }
impl EventEmitter<Event> for ExtensionStore {} impl EventEmitter<Event> for ExtensionStore {}
@ -148,6 +149,7 @@ pub fn init(
let store = cx.new_model(move |cx| { let store = cx.new_model(move |cx| {
ExtensionStore::new( ExtensionStore::new(
EXTENSIONS_DIR.clone(), EXTENSIONS_DIR.clone(),
None,
fs, fs,
http_client, http_client,
node_runtime, node_runtime,
@ -159,7 +161,7 @@ pub fn init(
cx.on_action(|_: &ReloadExtensions, cx| { cx.on_action(|_: &ReloadExtensions, cx| {
let store = cx.global::<GlobalExtensionStore>().0.clone(); let store = cx.global::<GlobalExtensionStore>().0.clone();
store.update(cx, |store, _| drop(store.reload(None))); store.update(cx, |store, cx| drop(store.reload(None, cx)));
}); });
cx.set_global(GlobalExtensionStore(store)); cx.set_global(GlobalExtensionStore(store));
@ -170,8 +172,10 @@ impl ExtensionStore {
cx.global::<GlobalExtensionStore>().0.clone() cx.global::<GlobalExtensionStore>().0.clone()
} }
#[allow(clippy::too_many_arguments)]
pub fn new( pub fn new(
extensions_dir: PathBuf, extensions_dir: PathBuf,
build_dir: Option<PathBuf>,
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
http_client: Arc<HttpClientWithUrl>, http_client: Arc<HttpClientWithUrl>,
node_runtime: Arc<dyn NodeRuntime>, node_runtime: Arc<dyn NodeRuntime>,
@ -180,7 +184,7 @@ impl ExtensionStore {
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Self { ) -> Self {
let work_dir = extensions_dir.join("work"); let work_dir = extensions_dir.join("work");
let build_dir = extensions_dir.join("build"); let build_dir = build_dir.unwrap_or_else(|| extensions_dir.join("build"));
let installed_dir = extensions_dir.join("installed"); let installed_dir = extensions_dir.join("installed");
let index_path = extensions_dir.join("index.json"); let index_path = extensions_dir.join("index.json");
@ -226,7 +230,7 @@ impl ExtensionStore {
// it must be asynchronously rebuilt. // it must be asynchronously rebuilt.
let mut extension_index = ExtensionIndex::default(); let mut extension_index = ExtensionIndex::default();
let mut extension_index_needs_rebuild = true; let mut extension_index_needs_rebuild = true;
if let Some(index_content) = index_content.log_err() { if let Some(index_content) = index_content.ok() {
if let Some(index) = serde_json::from_str(&index_content).log_err() { if let Some(index) = serde_json::from_str(&index_content).log_err() {
extension_index = index; extension_index = index;
if let (Ok(Some(index_metadata)), Ok(Some(extensions_metadata))) = if let (Ok(Some(index_metadata)), Ok(Some(extensions_metadata))) =
@ -243,7 +247,7 @@ impl ExtensionStore {
// index needs to be rebuild, then enqueue // index needs to be rebuild, then enqueue
let load_initial_extensions = this.extensions_updated(extension_index, cx); let load_initial_extensions = this.extensions_updated(extension_index, cx);
if extension_index_needs_rebuild { if extension_index_needs_rebuild {
let _ = this.reload(None); let _ = this.reload(None, cx);
} }
// Perform all extension loading in a single task to ensure that we // Perform all extension loading in a single task to ensure that we
@ -255,7 +259,7 @@ impl ExtensionStore {
let mut debounce_timer = cx let mut debounce_timer = cx
.background_executor() .background_executor()
.timer(RELOAD_DEBOUNCE_DURATION) .spawn(futures::future::pending())
.fuse(); .fuse();
loop { loop {
select_biased! { select_biased! {
@ -271,7 +275,8 @@ impl ExtensionStore {
this.update(&mut cx, |this, _| { this.update(&mut cx, |this, _| {
this.modified_extensions.extend(extension_id); this.modified_extensions.extend(extension_id);
})?; })?;
debounce_timer = cx.background_executor() debounce_timer = cx
.background_executor()
.timer(RELOAD_DEBOUNCE_DURATION) .timer(RELOAD_DEBOUNCE_DURATION)
.fuse(); .fuse();
} }
@ -313,12 +318,17 @@ impl ExtensionStore {
this this
} }
fn reload(&mut self, modified_extension: Option<Arc<str>>) -> impl Future<Output = ()> { fn reload(
&mut self,
modified_extension: Option<Arc<str>>,
cx: &mut ModelContext<Self>,
) -> impl Future<Output = ()> {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
self.reload_complete_senders.push(tx); self.reload_complete_senders.push(tx);
self.reload_tx self.reload_tx
.unbounded_send(modified_extension) .unbounded_send(modified_extension)
.expect("reload task exited"); .expect("reload task exited");
cx.emit(Event::StartedReloading);
async move { async move {
rx.await.ok(); rx.await.ok();
} }
@ -444,7 +454,7 @@ impl ExtensionStore {
archive archive
.unpack(extensions_dir.join(extension_id.as_ref())) .unpack(extensions_dir.join(extension_id.as_ref()))
.await?; .await?;
this.update(&mut cx, |this, _| this.reload(Some(extension_id)))? this.update(&mut cx, |this, cx| this.reload(Some(extension_id), cx))?
.await; .await;
anyhow::Ok(()) anyhow::Ok(())
}) })
@ -483,7 +493,8 @@ impl ExtensionStore {
) )
.await?; .await?;
this.update(&mut cx, |this, _| this.reload(None))?.await; this.update(&mut cx, |this, cx| this.reload(None, cx))?
.await;
anyhow::Ok(()) anyhow::Ok(())
}) })
.detach_and_log_err(cx) .detach_and_log_err(cx)
@ -493,7 +504,7 @@ impl ExtensionStore {
&mut self, &mut self,
extension_source_path: PathBuf, extension_source_path: PathBuf,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) { ) -> Task<Result<()>> {
let extensions_dir = self.extensions_dir(); let extensions_dir = self.extensions_dir();
let fs = self.fs.clone(); let fs = self.fs.clone();
let builder = self.builder.clone(); let builder = self.builder.clone();
@ -560,11 +571,10 @@ impl ExtensionStore {
fs.create_symlink(output_path, extension_source_path) fs.create_symlink(output_path, extension_source_path)
.await?; .await?;
this.update(&mut cx, |this, _| this.reload(Some(extension_id)))? this.update(&mut cx, |this, cx| this.reload(None, cx))?
.await; .await;
Ok(()) Ok(())
}) })
.detach_and_log_err(cx)
} }
pub fn rebuild_dev_extension(&mut self, extension_id: Arc<str>, cx: &mut ModelContext<Self>) { pub fn rebuild_dev_extension(&mut self, extension_id: Arc<str>, cx: &mut ModelContext<Self>) {
@ -592,7 +602,7 @@ impl ExtensionStore {
})?; })?;
if result.is_ok() { if result.is_ok() {
this.update(&mut cx, |this, _| this.reload(Some(extension_id)))? this.update(&mut cx, |this, cx| this.reload(Some(extension_id), cx))?
.await; .await;
} }
@ -664,9 +674,9 @@ impl ExtensionStore {
log::info!( log::info!(
"extensions updated. loading {}, reloading {}, unloading {}", "extensions updated. loading {}, reloading {}, unloading {}",
extensions_to_unload.len() - reload_count, extensions_to_load.len() - reload_count,
reload_count, reload_count,
extensions_to_load.len() - reload_count extensions_to_unload.len() - reload_count
); );
let themes_to_remove = old_index let themes_to_remove = old_index
@ -839,7 +849,7 @@ impl ExtensionStore {
language_server_config.language.clone(), language_server_config.language.clone(),
Arc::new(ExtensionLspAdapter { Arc::new(ExtensionLspAdapter {
extension: wasm_extension.clone(), extension: wasm_extension.clone(),
work_dir: this.wasm_host.work_dir.join(manifest.id.as_ref()), host: this.wasm_host.clone(),
config: wit::LanguageServerConfig { config: wit::LanguageServerConfig {
name: language_server_name.0.to_string(), name: language_server_name.0.to_string(),
language_name: language_server_config.language.to_string(), language_name: language_server_config.language.to_string(),

View file

@ -1,11 +1,10 @@
use crate::{ use crate::{
build_extension::{CompileExtensionOptions, ExtensionBuilder},
ExtensionIndex, ExtensionIndexEntry, ExtensionIndexLanguageEntry, ExtensionIndexThemeEntry, ExtensionIndex, ExtensionIndexEntry, ExtensionIndexLanguageEntry, ExtensionIndexThemeEntry,
ExtensionManifest, ExtensionStore, GrammarManifestEntry, RELOAD_DEBOUNCE_DURATION, ExtensionManifest, ExtensionStore, GrammarManifestEntry, RELOAD_DEBOUNCE_DURATION,
}; };
use async_compression::futures::bufread::GzipEncoder; use async_compression::futures::bufread::GzipEncoder;
use collections::BTreeMap; use collections::BTreeMap;
use fs::{FakeFs, Fs}; use fs::{FakeFs, Fs, RealFs};
use futures::{io::BufReader, AsyncReadExt, StreamExt}; use futures::{io::BufReader, AsyncReadExt, StreamExt};
use gpui::{Context, TestAppContext}; use gpui::{Context, TestAppContext};
use language::{ use language::{
@ -13,6 +12,7 @@ use language::{
LanguageServerName, LanguageServerName,
}; };
use node_runtime::FakeNodeRuntime; use node_runtime::FakeNodeRuntime;
use parking_lot::Mutex;
use project::Project; use project::Project;
use serde_json::json; use serde_json::json;
use settings::SettingsStore; use settings::SettingsStore;
@ -22,7 +22,18 @@ use std::{
sync::Arc, sync::Arc,
}; };
use theme::ThemeRegistry; use theme::ThemeRegistry;
use util::http::{self, FakeHttpClient, Response}; use util::{
http::{FakeHttpClient, Response},
test::temp_tree,
};
#[cfg(test)]
#[ctor::ctor]
fn init_logger() {
if std::env::var("RUST_LOG").is_ok() {
env_logger::init();
}
}
#[gpui::test] #[gpui::test]
async fn test_extension_store(cx: &mut TestAppContext) { async fn test_extension_store(cx: &mut TestAppContext) {
@ -248,6 +259,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
let store = cx.new_model(|cx| { let store = cx.new_model(|cx| {
ExtensionStore::new( ExtensionStore::new(
PathBuf::from("/the-extension-dir"), PathBuf::from("/the-extension-dir"),
None,
fs.clone(), fs.clone(),
http_client.clone(), http_client.clone(),
node_runtime.clone(), node_runtime.clone(),
@ -335,7 +347,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
}, },
); );
let _ = store.update(cx, |store, _| store.reload(None)); let _ = store.update(cx, |store, cx| store.reload(None, cx));
cx.executor().advance_clock(RELOAD_DEBOUNCE_DURATION); cx.executor().advance_clock(RELOAD_DEBOUNCE_DURATION);
store.read_with(cx, |store, _| { store.read_with(cx, |store, _| {
@ -365,6 +377,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
let store = cx.new_model(|cx| { let store = cx.new_model(|cx| {
ExtensionStore::new( ExtensionStore::new(
PathBuf::from("/the-extension-dir"), PathBuf::from("/the-extension-dir"),
None,
fs.clone(), fs.clone(),
http_client.clone(), http_client.clone(),
node_runtime.clone(), node_runtime.clone(),
@ -422,6 +435,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
#[gpui::test] #[gpui::test]
async fn test_extension_store_with_gleam_extension(cx: &mut TestAppContext) { async fn test_extension_store_with_gleam_extension(cx: &mut TestAppContext) {
init_test(cx); init_test(cx);
cx.executor().allow_parking();
let root_dir = Path::new(env!("CARGO_MANIFEST_DIR")) let root_dir = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent() .parent()
@ -431,32 +445,19 @@ async fn test_extension_store_with_gleam_extension(cx: &mut TestAppContext) {
let cache_dir = root_dir.join("target"); let cache_dir = root_dir.join("target");
let gleam_extension_dir = root_dir.join("extensions").join("gleam"); let gleam_extension_dir = root_dir.join("extensions").join("gleam");
cx.executor().allow_parking(); let fs = Arc::new(RealFs);
ExtensionBuilder::new(cache_dir, http::client()) let extensions_dir = temp_tree(json!({
.compile_extension( "installed": {},
&gleam_extension_dir, "work": {}
CompileExtensionOptions { release: false }, }));
) let project_dir = temp_tree(json!({
.await "test.gleam": ""
.unwrap(); }));
cx.executor().forbid_parking();
let fs = FakeFs::new(cx.executor()); let extensions_dir = extensions_dir.path().canonicalize().unwrap();
fs.insert_tree("/the-extension-dir", json!({ "installed": {} })) let project_dir = project_dir.path().canonicalize().unwrap();
.await;
fs.insert_tree_from_real_fs("/the-extension-dir/installed/gleam", gleam_extension_dir)
.await;
fs.insert_tree( let project = Project::test(fs.clone(), [project_dir.as_path()], cx).await;
"/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 language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
let theme_registry = Arc::new(ThemeRegistry::new(Box::new(()))); let theme_registry = Arc::new(ThemeRegistry::new(Box::new(())));
@ -464,55 +465,76 @@ async fn test_extension_store_with_gleam_extension(cx: &mut TestAppContext) {
let mut status_updates = language_registry.language_server_binary_statuses(); let mut status_updates = language_registry.language_server_binary_statuses();
let http_client = FakeHttpClient::create({ struct FakeLanguageServerVersion {
move |request| async move { version: String,
match request.uri().to_string().as_str() { binary_contents: String,
"https://api.github.com/repos/gleam-lang/gleam/releases" => Ok(Response::new( http_request_count: usize,
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 language_server_version = Arc::new(Mutex::new(FakeLanguageServerVersion {
version: "v1.2.3".into(),
binary_contents: "the-binary-contents".into(),
http_request_count: 0,
}));
let http_client = FakeHttpClient::create({
let language_server_version = language_server_version.clone();
move |request| {
let language_server_version = language_server_version.clone();
async move {
language_server_version.lock().http_request_count += 1;
let version = language_server_version.lock().version.clone();
let binary_contents = language_server_version.lock().binary_contents.clone();
let github_releases_uri = "https://api.github.com/repos/gleam-lang/gleam/releases";
let asset_download_uri =
format!("https://fake-download.example.com/gleam-{version}");
let uri = request.uri().to_string();
if uri == github_releases_uri {
Ok(Response::new(
json!([
{
"tag_name": version,
"prerelease": false,
"tarball_url": "",
"zipball_url": "",
"assets": [
{
"name": format!("gleam-{version}-aarch64-apple-darwin.tar.gz"),
"browser_download_url": asset_download_uri
}
]
}
])
.to_string()
.into(),
))
} else if uri == asset_download_uri {
let mut bytes = Vec::<u8>::new(); let mut bytes = Vec::<u8>::new();
let mut archive = async_tar::Builder::new(&mut bytes); let mut archive = async_tar::Builder::new(&mut bytes);
let mut header = async_tar::Header::new_gnu(); let mut header = async_tar::Header::new_gnu();
let content = "the-gleam-binary-contents".as_bytes(); header.set_size(binary_contents.len() as u64);
header.set_size(content.len() as u64);
archive archive
.append_data(&mut header, "gleam", content) .append_data(&mut header, "gleam", binary_contents.as_bytes())
.await .await
.unwrap(); .unwrap();
archive.into_inner().await.unwrap(); archive.into_inner().await.unwrap();
let mut gzipped_bytes = Vec::new(); let mut gzipped_bytes = Vec::new();
let mut encoder = GzipEncoder::new(BufReader::new(bytes.as_slice())); let mut encoder = GzipEncoder::new(BufReader::new(bytes.as_slice()));
encoder.read_to_end(&mut gzipped_bytes).await.unwrap(); encoder.read_to_end(&mut gzipped_bytes).await.unwrap();
Ok(Response::new(gzipped_bytes.into())) Ok(Response::new(gzipped_bytes.into()))
} else {
Ok(Response::builder().status(404).body("not found".into())?)
} }
_ => Ok(Response::builder().status(404).body("not found".into())?),
} }
} }
}); });
let _store = cx.new_model(|cx| { let extension_store = cx.new_model(|cx| {
ExtensionStore::new( ExtensionStore::new(
PathBuf::from("/the-extension-dir"), extensions_dir.clone(),
Some(cache_dir),
fs.clone(), fs.clone(),
http_client.clone(), http_client.clone(),
node_runtime, node_runtime,
@ -522,17 +544,35 @@ async fn test_extension_store_with_gleam_extension(cx: &mut TestAppContext) {
) )
}); });
cx.executor().advance_clock(RELOAD_DEBOUNCE_DURATION); // Ensure that debounces fire.
let mut events = cx.events(&extension_store);
let executor = cx.executor();
let _task = cx.executor().spawn(async move {
while let Some(event) = events.next().await {
match event {
crate::Event::StartedReloading => {
executor.advance_clock(RELOAD_DEBOUNCE_DURATION);
}
_ => (),
}
}
});
extension_store
.update(cx, |store, cx| {
store.install_dev_extension(gleam_extension_dir.clone(), cx)
})
.await
.unwrap();
let mut fake_servers = language_registry.fake_language_servers("Gleam"); let mut fake_servers = language_registry.fake_language_servers("Gleam");
let buffer = project let buffer = project
.update(cx, |project, cx| { .update(cx, |project, cx| {
project.open_local_buffer("/the-project-dir/test.gleam", cx) project.open_local_buffer(project_dir.join("test.gleam"), cx)
}) })
.await .await
.unwrap(); .unwrap();
project.update(cx, |project, cx| { project.update(cx, |project, cx| {
project.set_language_for_buffer( project.set_language_for_buffer(
&buffer, &buffer,
@ -548,20 +588,16 @@ async fn test_extension_store_with_gleam_extension(cx: &mut TestAppContext) {
}); });
let fake_server = fake_servers.next().await.unwrap(); let fake_server = fake_servers.next().await.unwrap();
let expected_server_path = extensions_dir.join("work/gleam/gleam-v1.2.3/gleam");
let expected_binary_contents = language_server_version.lock().binary_contents.clone();
assert_eq!( assert_eq!(fake_server.binary.path, expected_server_path);
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!(fake_server.binary.arguments, [OsString::from("lsp")]);
assert_eq!(
fs.load(&expected_server_path).await.unwrap(),
expected_binary_contents
);
assert_eq!(language_server_version.lock().http_request_count, 2);
assert_eq!( assert_eq!(
[ [
status_updates.next().await.unwrap(), status_updates.next().await.unwrap(),
@ -583,6 +619,51 @@ async fn test_extension_store_with_gleam_extension(cx: &mut TestAppContext) {
) )
] ]
); );
// Simulate a new version of the language server being released
language_server_version.lock().version = "v2.0.0".into();
language_server_version.lock().binary_contents = "the-new-binary-contents".into();
language_server_version.lock().http_request_count = 0;
// Start a new instance of the language server.
project.update(cx, |project, cx| {
project.restart_language_servers_for_buffers([buffer.clone()], cx)
});
// The extension has cached the binary path, and does not attempt
// to reinstall it.
let fake_server = fake_servers.next().await.unwrap();
assert_eq!(fake_server.binary.path, expected_server_path);
assert_eq!(
fs.load(&expected_server_path).await.unwrap(),
expected_binary_contents
);
assert_eq!(language_server_version.lock().http_request_count, 0);
// Reload the extension, clearing its cache.
// Start a new instance of the language server.
extension_store
.update(cx, |store, cx| store.reload(Some("gleam".into()), cx))
.await;
cx.executor().run_until_parked();
project.update(cx, |project, cx| {
project.restart_language_servers_for_buffers([buffer.clone()], cx)
});
// The extension re-fetches the latest version of the language server.
let fake_server = fake_servers.next().await.unwrap();
let new_expected_server_path = extensions_dir.join("work/gleam/gleam-v2.0.0/gleam");
let expected_binary_contents = language_server_version.lock().binary_contents.clone();
assert_eq!(fake_server.binary.path, new_expected_server_path);
assert_eq!(fake_server.binary.arguments, [OsString::from("lsp")]);
assert_eq!(
fs.load(&new_expected_server_path).await.unwrap(),
expected_binary_contents
);
// The old language server directory has been cleaned up.
assert!(fs.metadata(&expected_server_path).await.unwrap().is_none());
} }
fn init_test(cx: &mut TestAppContext) { fn init_test(cx: &mut TestAppContext) {

View file

@ -5,7 +5,10 @@ use async_tar::Archive;
use async_trait::async_trait; use async_trait::async_trait;
use fs::Fs; use fs::Fs;
use futures::{ use futures::{
channel::{mpsc::UnboundedSender, oneshot}, channel::{
mpsc::{self, UnboundedSender},
oneshot,
},
future::BoxFuture, future::BoxFuture,
io::BufReader, io::BufReader,
Future, FutureExt, StreamExt as _, Future, FutureExt, StreamExt as _,
@ -14,7 +17,8 @@ use gpui::BackgroundExecutor;
use language::{LanguageRegistry, LanguageServerBinaryStatus, LspAdapterDelegate}; use language::{LanguageRegistry, LanguageServerBinaryStatus, LspAdapterDelegate};
use node_runtime::NodeRuntime; use node_runtime::NodeRuntime;
use std::{ use std::{
path::PathBuf, env,
path::{Path, PathBuf},
sync::{Arc, OnceLock}, sync::{Arc, OnceLock},
}; };
use util::{http::HttpClient, SemanticVersion}; use util::{http::HttpClient, SemanticVersion};
@ -22,7 +26,7 @@ use wasmtime::{
component::{Component, Linker, Resource, ResourceTable}, component::{Component, Linker, Resource, ResourceTable},
Engine, Store, Engine, Store,
}; };
use wasmtime_wasi::preview2::{command as wasi_command, WasiCtx, WasiCtxBuilder, WasiView}; use wasmtime_wasi::preview2::{self as wasi, WasiCtx};
pub mod wit { pub mod wit {
wasmtime::component::bindgen!({ wasmtime::component::bindgen!({
@ -49,6 +53,7 @@ pub(crate) struct WasmHost {
#[derive(Clone)] #[derive(Clone)]
pub struct WasmExtension { pub struct WasmExtension {
tx: UnboundedSender<ExtensionCall>, tx: UnboundedSender<ExtensionCall>,
pub(crate) manifest: Arc<ExtensionManifest>,
#[allow(unused)] #[allow(unused)]
zed_api_version: SemanticVersion, zed_api_version: SemanticVersion,
} }
@ -56,7 +61,7 @@ pub struct WasmExtension {
pub(crate) struct WasmState { pub(crate) struct WasmState {
manifest: Arc<ExtensionManifest>, manifest: Arc<ExtensionManifest>,
table: ResourceTable, table: ResourceTable,
ctx: WasiCtx, ctx: wasi::WasiCtx,
host: Arc<WasmHost>, host: Arc<WasmHost>,
} }
@ -67,6 +72,8 @@ type ExtensionCall = Box<
static WASM_ENGINE: OnceLock<wasmtime::Engine> = OnceLock::new(); static WASM_ENGINE: OnceLock<wasmtime::Engine> = OnceLock::new();
const EXTENSION_WORK_DIR_PATH: &str = "/zed/work";
impl WasmHost { impl WasmHost {
pub fn new( pub fn new(
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
@ -84,8 +91,8 @@ impl WasmHost {
}) })
.clone(); .clone();
let mut linker = Linker::new(&engine); let mut linker = Linker::new(&engine);
wasi_command::add_to_linker(&mut linker).unwrap(); wasi::command::add_to_linker(&mut linker).unwrap();
wit::Extension::add_to_linker(&mut linker, |state: &mut WasmState| state).unwrap(); wit::Extension::add_to_linker(&mut linker, wasi_view).unwrap();
Arc::new(Self { Arc::new(Self {
engine, engine,
linker: Arc::new(linker), linker: Arc::new(linker),
@ -112,22 +119,14 @@ impl WasmHost {
for part in wasmparser::Parser::new(0).parse_all(&wasm_bytes) { for part in wasmparser::Parser::new(0).parse_all(&wasm_bytes) {
if let wasmparser::Payload::CustomSection(s) = part? { if let wasmparser::Payload::CustomSection(s) = part? {
if s.name() == "zed:api-version" { if s.name() == "zed:api-version" {
if s.data().len() != 6 { zed_api_version = parse_extension_version(s.data());
if zed_api_version.is_none() {
bail!( bail!(
"extension {} has invalid zed:api-version section: {:?}", "extension {} has invalid zed:api-version section: {:?}",
manifest.id, manifest.id,
s.data() 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,
})
} }
} }
} }
@ -139,36 +138,95 @@ impl WasmHost {
let mut store = wasmtime::Store::new( let mut store = wasmtime::Store::new(
&this.engine, &this.engine,
WasmState { WasmState {
manifest, ctx: this.build_wasi_ctx(&manifest).await?,
manifest: manifest.clone(),
table: ResourceTable::new(), table: ResourceTable::new(),
ctx: WasiCtxBuilder::new()
.inherit_stdio()
.env("RUST_BACKTRACE", "1")
.build(),
host: this.clone(), host: this.clone(),
}, },
); );
let (mut extension, instance) = let (mut extension, instance) =
wit::Extension::instantiate_async(&mut store, &component, &this.linker) wit::Extension::instantiate_async(&mut store, &component, &this.linker)
.await .await
.context("failed to instantiate wasm component")?; .context("failed to instantiate wasm extension")?;
let (tx, mut rx) = futures::channel::mpsc::unbounded::<ExtensionCall>(); extension
.call_init_extension(&mut store)
.await
.context("failed to initialize wasm extension")?;
let (tx, mut rx) = mpsc::unbounded::<ExtensionCall>();
executor executor
.spawn(async move { .spawn(async move {
extension.call_init_extension(&mut store).await.unwrap();
let _instance = instance; let _instance = instance;
while let Some(call) = rx.next().await { while let Some(call) = rx.next().await {
(call)(&mut extension, &mut store).await; (call)(&mut extension, &mut store).await;
} }
}) })
.detach(); .detach();
Ok(WasmExtension { Ok(WasmExtension {
manifest,
tx, tx,
zed_api_version, zed_api_version,
}) })
} }
} }
async fn build_wasi_ctx(&self, manifest: &Arc<ExtensionManifest>) -> Result<WasiCtx> {
use cap_std::{ambient_authority, fs::Dir};
let extension_work_dir = self.work_dir.join(manifest.id.as_ref());
self.fs
.create_dir(&extension_work_dir)
.await
.context("failed to create extension work dir")?;
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 perms = wasi::FilePerms::all();
let dir_perms = wasi::DirPerms::all();
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")
.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())
}
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)
} else {
None
}
}
}
fn parse_extension_version(data: &[u8]) -> Option<SemanticVersion> {
if data.len() == 6 {
Some(SemanticVersion {
major: u16::from_be_bytes([data[0], data[1]]) as _,
minor: u16::from_be_bytes([data[2], data[3]]) as _,
patch: u16::from_be_bytes([data[4], data[5]]) as _,
})
} else {
None
}
} }
impl WasmExtension { impl WasmExtension {
@ -194,6 +252,13 @@ 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] #[async_trait]
impl wit::HostWorktree for WasmState { impl wit::HostWorktree for WasmState {
async fn read_text_file( async fn read_text_file(
@ -201,7 +266,7 @@ impl wit::HostWorktree for WasmState {
delegate: Resource<Arc<dyn LspAdapterDelegate>>, delegate: Resource<Arc<dyn LspAdapterDelegate>>,
path: String, path: String,
) -> wasmtime::Result<Result<String, String>> { ) -> wasmtime::Result<Result<String, String>> {
let delegate = self.table().get(&delegate)?; let delegate = self.table.get(&delegate)?;
Ok(delegate Ok(delegate
.read_text_file(path.into()) .read_text_file(path.into())
.await .await
@ -269,13 +334,13 @@ impl wit::ExtensionImports for WasmState {
async fn current_platform(&mut self) -> Result<(wit::Os, wit::Architecture)> { async fn current_platform(&mut self) -> Result<(wit::Os, wit::Architecture)> {
Ok(( Ok((
match std::env::consts::OS { match env::consts::OS {
"macos" => wit::Os::Mac, "macos" => wit::Os::Mac,
"linux" => wit::Os::Linux, "linux" => wit::Os::Linux,
"windows" => wit::Os::Windows, "windows" => wit::Os::Windows,
_ => panic!("unsupported os"), _ => panic!("unsupported os"),
}, },
match std::env::consts::ARCH { match env::consts::ARCH {
"aarch64" => wit::Architecture::Aarch64, "aarch64" => wit::Architecture::Aarch64,
"x86" => wit::Architecture::X86, "x86" => wit::Architecture::X86,
"x86_64" => wit::Architecture::X8664, "x86_64" => wit::Architecture::X8664,
@ -314,18 +379,24 @@ impl wit::ExtensionImports for WasmState {
async fn download_file( async fn download_file(
&mut self, &mut self,
url: String, url: String,
filename: String, path: String,
file_type: wit::DownloadedFileType, file_type: wit::DownloadedFileType,
) -> wasmtime::Result<Result<(), String>> { ) -> wasmtime::Result<Result<(), String>> {
let path = PathBuf::from(path);
async fn inner( async fn inner(
this: &mut WasmState, this: &mut WasmState,
url: String, url: String,
filename: String, path: PathBuf,
file_type: wit::DownloadedFileType, file_type: wit::DownloadedFileType,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
this.host.fs.create_dir(&this.host.work_dir).await?; let extension_work_dir = this.host.work_dir.join(this.manifest.id.as_ref());
let container_dir = this.host.work_dir.join(this.manifest.id.as_ref());
let destination_path = container_dir.join(&filename); 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))?;
let mut response = this let mut response = this
.host .host
@ -367,19 +438,24 @@ impl wit::ExtensionImports for WasmState {
.await?; .await?;
} }
wit::DownloadedFileType::Zip => { wit::DownloadedFileType::Zip => {
let zip_filename = format!("{filename}.zip"); let file_name = destination_path
.file_name()
.ok_or_else(|| anyhow!("invalid download path"))?
.to_string_lossy();
let zip_filename = format!("{file_name}.zip");
let mut zip_path = destination_path.clone(); let mut zip_path = destination_path.clone();
zip_path.set_file_name(zip_filename); zip_path.set_file_name(zip_filename);
futures::pin_mut!(body); futures::pin_mut!(body);
this.host.fs.create_file_with(&zip_path, body).await?; this.host.fs.create_file_with(&zip_path, body).await?;
let unzip_status = std::process::Command::new("unzip") let unzip_status = std::process::Command::new("unzip")
.current_dir(&container_dir) .current_dir(&extension_work_dir)
.arg(&zip_path) .arg(&zip_path)
.output()? .output()?
.status; .status;
if !unzip_status.success() { if !unzip_status.success() {
Err(anyhow!("failed to unzip {filename} archive"))?; Err(anyhow!("failed to unzip {} archive", path.display()))?;
} }
} }
} }
@ -387,19 +463,23 @@ impl wit::ExtensionImports for WasmState {
Ok(()) Ok(())
} }
Ok(inner(self, url, filename, file_type) Ok(inner(self, url, path, file_type)
.await .await
.map(|_| ()) .map(|_| ())
.map_err(|err| err.to_string())) .map_err(|err| err.to_string()))
} }
} }
impl WasiView for WasmState { fn wasi_view(state: &mut WasmState) -> &mut WasmState {
state
}
impl wasi::WasiView for WasmState {
fn table(&mut self) -> &mut ResourceTable { fn table(&mut self) -> &mut ResourceTable {
&mut self.table &mut self.table
} }
fn ctx(&mut self) -> &mut WasiCtx { fn ctx(&mut self) -> &mut wasi::WasiCtx {
&mut self.ctx &mut self.ctx
} }
} }

View file

@ -20,6 +20,7 @@ macro_rules! register_extension {
($extension_type:ty) => { ($extension_type:ty) => {
#[export_name = "init-extension"] #[export_name = "init-extension"]
pub extern "C" fn __init_extension() { pub extern "C" fn __init_extension() {
std::env::set_current_dir(std::env::var("PWD").unwrap()).unwrap();
zed_extension_api::register_extension(|| { zed_extension_api::register_extension(|| {
Box::new(<$extension_type as zed_extension_api::Extension>::new()) Box::new(<$extension_type as zed_extension_api::Extension>::new())
}); });

View file

@ -45,7 +45,9 @@ pub fn init(cx: &mut AppContext) {
let extension_path = prompt.await.log_err()??.pop()?; let extension_path = prompt.await.log_err()??.pop()?;
store store
.update(&mut cx, |store, cx| { .update(&mut cx, |store, cx| {
store.install_dev_extension(extension_path, cx); store
.install_dev_extension(extension_path, cx)
.detach_and_log_err(cx)
}) })
.ok()?; .ok()?;
Some(()) Some(())
@ -93,9 +95,8 @@ impl ExtensionsPage {
let subscriptions = [ let subscriptions = [
cx.observe(&store, |_, _, cx| cx.notify()), cx.observe(&store, |_, _, cx| cx.notify()),
cx.subscribe(&store, |this, _, event, cx| match event { cx.subscribe(&store, |this, _, event, cx| match event {
extension::Event::ExtensionsUpdated => { extension::Event::ExtensionsUpdated => this.fetch_extensions_debounced(cx),
this.fetch_extensions_debounced(cx); _ => {}
}
}), }),
]; ];

View file

@ -9385,7 +9385,7 @@ impl<P: AsRef<Path>> From<(WorktreeId, P)> for ProjectPath {
} }
struct ProjectLspAdapterDelegate { struct ProjectLspAdapterDelegate {
project: Model<Project>, project: WeakModel<Project>,
worktree: worktree::Snapshot, worktree: worktree::Snapshot,
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
http_client: Arc<dyn HttpClient>, http_client: Arc<dyn HttpClient>,
@ -9395,7 +9395,7 @@ struct ProjectLspAdapterDelegate {
impl ProjectLspAdapterDelegate { impl ProjectLspAdapterDelegate {
fn new(project: &Project, worktree: &Model<Worktree>, cx: &ModelContext<Project>) -> Arc<Self> { fn new(project: &Project, worktree: &Model<Worktree>, cx: &ModelContext<Project>) -> Arc<Self> {
Arc::new(Self { Arc::new(Self {
project: cx.handle(), project: cx.weak_model(),
worktree: worktree.read(cx).snapshot(), worktree: worktree.read(cx).snapshot(),
fs: project.fs.clone(), fs: project.fs.clone(),
http_client: project.client.http_client(), http_client: project.client.http_client(),
@ -9408,7 +9408,8 @@ impl ProjectLspAdapterDelegate {
impl LspAdapterDelegate for ProjectLspAdapterDelegate { impl LspAdapterDelegate for ProjectLspAdapterDelegate {
fn show_notification(&self, message: &str, cx: &mut AppContext) { fn show_notification(&self, message: &str, cx: &mut AppContext) {
self.project self.project
.update(cx, |_, cx| cx.emit(Event::Notification(message.to_owned()))); .update(cx, |_, cx| cx.emit(Event::Notification(message.to_owned())))
.ok();
} }
fn http_client(&self) -> Arc<dyn HttpClient> { fn http_client(&self) -> Arc<dyn HttpClient> {

View file

@ -1,9 +1,92 @@
use std::fs;
use zed_extension_api::{self as zed, Result}; use zed_extension_api::{self as zed, Result};
struct GleamExtension { struct GleamExtension {
cached_binary_path: Option<String>, cached_binary_path: Option<String>,
} }
impl GleamExtension {
fn language_server_binary_path(&mut self, config: zed::LanguageServerConfig) -> Result<String> {
if let Some(path) = &self.cached_binary_path {
if fs::metadata(path).map_or(false, |stat| stat.is_file()) {
zed::set_language_server_installation_status(
&config.name,
&zed::LanguageServerInstallationStatus::Cached,
);
return Ok(path.clone());
}
}
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))?;
let version_dir = format!("gleam-{}", release.version);
let binary_path = format!("{version_dir}/gleam");
if !fs::metadata(&binary_path).map_or(false, |stat| stat.is_file()) {
zed::set_language_server_installation_status(
&config.name,
&zed::LanguageServerInstallationStatus::Downloading,
);
zed::download_file(
&asset.download_url,
&version_dir,
zed::DownloadedFileType::GzipTar,
)
.map_err(|e| format!("failed to download file: {e}"))?;
let entries =
fs::read_dir(".").map_err(|e| format!("failed to list working directory {e}"))?;
for entry in entries {
let entry = entry.map_err(|e| format!("failed to load directory entry {e}"))?;
if entry.file_name().to_str() != Some(&version_dir) {
fs::remove_dir_all(&entry.path()).ok();
}
}
zed::set_language_server_installation_status(
&config.name,
&zed::LanguageServerInstallationStatus::Downloaded,
);
}
self.cached_binary_path = Some(binary_path.clone());
Ok(binary_path)
}
}
impl zed::Extension for GleamExtension { impl zed::Extension for GleamExtension {
fn new() -> Self { fn new() -> Self {
Self { Self {
@ -16,72 +99,8 @@ impl zed::Extension for GleamExtension {
config: zed::LanguageServerConfig, config: zed::LanguageServerConfig,
_worktree: &zed::Worktree, _worktree: &zed::Worktree,
) -> Result<zed::Command> { ) -> 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 { Ok(zed::Command {
command: binary_path, command: self.language_server_binary_path(config)?,
args: vec!["lsp".to_string()], args: vec!["lsp".to_string()],
env: Default::default(), env: Default::default(),
}) })