Skip to content

Commit

Permalink
Allow wasm extensions to do arbitrary file I/O in their own directory…
Browse files Browse the repository at this point in the history
… 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
  • Loading branch information
maxbrunsfeld authored Mar 8, 2024
1 parent a550b9c commit 51ebe0e
Show file tree
Hide file tree
Showing 13 changed files with 409 additions and 203 deletions.
6 changes: 0 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,6 @@ 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
run: cargo xtask clippy

Expand Down
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ bitflags = "2.4.2"
blade-graphics = { 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" }
cap-std = "2.0"
chrono = { version = "0.4", features = ["serde"] }
clap = "4.4"
clickhouse = { version = "0.11.6" }
Expand Down
5 changes: 5 additions & 0 deletions crates/extension/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ anyhow.workspace = true
async-compression.workspace = true
async-tar.workspace = true
async-trait.workspace = true
cap-std.workspace = true
collections.workspace = true
fs.workspace = true
futures.workspace = true
Expand All @@ -42,6 +43,10 @@ wasmparser.workspace = true
wit-component.workspace = true

[dev-dependencies]
ctor.workspace = true
env_logger.workspace = true
parking_lot.workspace = true

fs = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] }
language = { workspace = true, features = ["test-support"] }
Expand Down
6 changes: 5 additions & 1 deletion crates/extension/src/build_extension.rs
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,10 @@ impl ExtensionBuilder {

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
.http
.get(WASI_ADAPTER_URL, AsyncBody::default(), true)
Expand Down Expand Up @@ -357,6 +360,7 @@ impl ExtensionBuilder {
fs::remove_dir_all(&wasi_sdk_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 body = BufReader::new(response.body_mut());
let body = GzipDecoder::new(body);
Expand Down
19 changes: 12 additions & 7 deletions crates/extension/src/extension_lsp_adapter.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::wasm_host::{wit::LanguageServerConfig, WasmExtension};
use crate::wasm_host::{wit::LanguageServerConfig, WasmExtension, WasmHost};
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use futures::{Future, FutureExt};
Expand All @@ -16,7 +16,7 @@ use wasmtime_wasi::preview2::WasiView as _;
pub struct ExtensionLspAdapter {
pub(crate) extension: WasmExtension,
pub(crate) config: LanguageServerConfig,
pub(crate) work_dir: PathBuf,
pub(crate) host: Arc<WasmHost>,
}

#[async_trait]
Expand All @@ -41,18 +41,23 @@ impl LspAdapter for ExtensionLspAdapter {
|extension, store| {
async move {
let resource = store.data_mut().table().push(delegate)?;
extension
let command = extension
.call_language_server_command(store, &this.config, resource)
.await
.await?
.map_err(|e| anyhow!("{}", e))?;
anyhow::Ok(command)
}
.boxed()
}
})
.await?
.map_err(|e| anyhow!("{}", e))?;
.await?;

let path = self
.host
.path_from_extension(&self.extension.manifest.id, command.command.as_ref());

Ok(LanguageServerBinary {
path: self.work_dir.join(&command.command),
path,
arguments: command.args.into_iter().map(|arg| arg.into()).collect(),
env: Some(command.env.into_iter().collect()),
})
Expand Down
42 changes: 26 additions & 16 deletions crates/extension/src/extension_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ enum ExtensionOperation {
#[derive(Copy, Clone)]
pub enum Event {
ExtensionsUpdated,
StartedReloading,
}

impl EventEmitter<Event> for ExtensionStore {}
Expand Down Expand Up @@ -148,6 +149,7 @@ pub fn init(
let store = cx.new_model(move |cx| {
ExtensionStore::new(
EXTENSIONS_DIR.clone(),
None,
fs,
http_client,
node_runtime,
Expand All @@ -159,7 +161,7 @@ pub fn init(

cx.on_action(|_: &ReloadExtensions, cx| {
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));
Expand All @@ -170,8 +172,10 @@ impl ExtensionStore {
cx.global::<GlobalExtensionStore>().0.clone()
}

#[allow(clippy::too_many_arguments)]
pub fn new(
extensions_dir: PathBuf,
build_dir: Option<PathBuf>,
fs: Arc<dyn Fs>,
http_client: Arc<HttpClientWithUrl>,
node_runtime: Arc<dyn NodeRuntime>,
Expand All @@ -180,7 +184,7 @@ impl ExtensionStore {
cx: &mut ModelContext<Self>,
) -> Self {
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 index_path = extensions_dir.join("index.json");

Expand Down Expand Up @@ -226,7 +230,7 @@ impl ExtensionStore {
// it must be asynchronously rebuilt.
let mut extension_index = ExtensionIndex::default();
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() {
extension_index = index;
if let (Ok(Some(index_metadata)), Ok(Some(extensions_metadata))) =
Expand All @@ -243,7 +247,7 @@ impl ExtensionStore {
// index needs to be rebuild, then enqueue
let load_initial_extensions = this.extensions_updated(extension_index, cx);
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
Expand All @@ -255,7 +259,7 @@ impl ExtensionStore {

let mut debounce_timer = cx
.background_executor()
.timer(RELOAD_DEBOUNCE_DURATION)
.spawn(futures::future::pending())
.fuse();
loop {
select_biased! {
Expand All @@ -271,7 +275,8 @@ impl ExtensionStore {
this.update(&mut cx, |this, _| {
this.modified_extensions.extend(extension_id);
})?;
debounce_timer = cx.background_executor()
debounce_timer = cx
.background_executor()
.timer(RELOAD_DEBOUNCE_DURATION)
.fuse();
}
Expand Down Expand Up @@ -313,12 +318,17 @@ impl ExtensionStore {
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();
self.reload_complete_senders.push(tx);
self.reload_tx
.unbounded_send(modified_extension)
.expect("reload task exited");
cx.emit(Event::StartedReloading);
async move {
rx.await.ok();
}
Expand Down Expand Up @@ -444,7 +454,7 @@ impl ExtensionStore {
archive
.unpack(extensions_dir.join(extension_id.as_ref()))
.await?;
this.update(&mut cx, |this, _| this.reload(Some(extension_id)))?
this.update(&mut cx, |this, cx| this.reload(Some(extension_id), cx))?
.await;
anyhow::Ok(())
})
Expand Down Expand Up @@ -483,7 +493,8 @@ impl ExtensionStore {
)
.await?;

this.update(&mut cx, |this, _| this.reload(None))?.await;
this.update(&mut cx, |this, cx| this.reload(None, cx))?
.await;
anyhow::Ok(())
})
.detach_and_log_err(cx)
Expand All @@ -493,7 +504,7 @@ impl ExtensionStore {
&mut self,
extension_source_path: PathBuf,
cx: &mut ModelContext<Self>,
) {
) -> Task<Result<()>> {
let extensions_dir = self.extensions_dir();
let fs = self.fs.clone();
let builder = self.builder.clone();
Expand Down Expand Up @@ -560,11 +571,10 @@ impl ExtensionStore {
fs.create_symlink(output_path, extension_source_path)
.await?;

this.update(&mut cx, |this, _| this.reload(Some(extension_id)))?
this.update(&mut cx, |this, cx| this.reload(None, cx))?
.await;
Ok(())
})
.detach_and_log_err(cx)
}

pub fn rebuild_dev_extension(&mut self, extension_id: Arc<str>, cx: &mut ModelContext<Self>) {
Expand Down Expand Up @@ -592,7 +602,7 @@ impl ExtensionStore {
})?;

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;
}

Expand Down Expand Up @@ -664,9 +674,9 @@ impl ExtensionStore {

log::info!(
"extensions updated. loading {}, reloading {}, unloading {}",
extensions_to_unload.len() - reload_count,
extensions_to_load.len() - reload_count,
reload_count,
extensions_to_load.len() - reload_count
extensions_to_unload.len() - reload_count
);

let themes_to_remove = old_index
Expand Down Expand Up @@ -839,7 +849,7 @@ impl ExtensionStore {
language_server_config.language.clone(),
Arc::new(ExtensionLspAdapter {
extension: wasm_extension.clone(),
work_dir: this.wasm_host.work_dir.join(manifest.id.as_ref()),
host: this.wasm_host.clone(),
config: wit::LanguageServerConfig {
name: language_server_name.0.to_string(),
language_name: language_server_config.language.to_string(),
Expand Down
Loading

0 comments on commit 51ebe0e

Please sign in to comment.