From 80bd40cfa3185b20b513d8a255b98f1460f25e6c Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 22 May 2024 19:07:52 -0400 Subject: [PATCH] zed_extension_api: Fork new version (#12160) This PR forks a new version of the `zed_extension_api` in preparation for some upcoming changes that require breaking changes to the WIT. Release Notes: - N/A --------- Co-authored-by: Max --- Cargo.lock | 38 +- crates/extension/src/wasm_host/wit.rs | 62 ++- .../src/wasm_host/wit/since_v0_0_6.rs | 409 +++++------------- .../src/wasm_host/wit/since_v0_0_7.rs | 385 +++++++++++++++++ crates/extension_api/Cargo.toml | 2 +- crates/extension_api/src/extension_api.rs | 2 +- crates/extension_api/src/settings.rs | 2 +- .../wit/since_v0.0.7/extension.wit | 130 ++++++ .../extension_api/wit/since_v0.0.7/github.wit | 28 ++ crates/extension_api/wit/since_v0.0.7/lsp.wit | 83 ++++ .../extension_api/wit/since_v0.0.7/nodejs.wit | 13 + .../wit/since_v0.0.7/platform.wit | 24 + .../wit/since_v0.0.7/settings.rs | 29 ++ 13 files changed, 886 insertions(+), 321 deletions(-) create mode 100644 crates/extension/src/wasm_host/wit/since_v0_0_7.rs create mode 100644 crates/extension_api/wit/since_v0.0.7/extension.wit create mode 100644 crates/extension_api/wit/since_v0.0.7/github.wit create mode 100644 crates/extension_api/wit/since_v0.0.7/lsp.wit create mode 100644 crates/extension_api/wit/since_v0.0.7/nodejs.wit create mode 100644 crates/extension_api/wit/since_v0.0.7/platform.wit create mode 100644 crates/extension_api/wit/since_v0.0.7/settings.rs diff --git a/Cargo.lock b/Cargo.lock index 938b19e658..a6f2c7073b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13160,28 +13160,28 @@ dependencies = [ name = "zed_dart" version = "0.0.2" dependencies = [ - "zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "zed_extension_api 0.0.6", ] [[package]] name = "zed_deno" version = "0.0.1" dependencies = [ - "zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "zed_extension_api 0.0.6", ] [[package]] name = "zed_elixir" version = "0.0.4" dependencies = [ - "zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "zed_extension_api 0.0.6", ] [[package]] name = "zed_elm" version = "0.0.1" dependencies = [ - "zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "zed_extension_api 0.0.6", ] [[package]] @@ -13210,6 +13210,8 @@ dependencies = [ [[package]] name = "zed_extension_api" version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ca8bcaea3feb2d2ce9dbeb061ee48365312a351faa7014c417b0365fe9e459" dependencies = [ "serde", "serde_json", @@ -13218,9 +13220,7 @@ dependencies = [ [[package]] name = "zed_extension_api" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ca8bcaea3feb2d2ce9dbeb061ee48365312a351faa7014c417b0365fe9e459" +version = "0.0.7" dependencies = [ "serde", "serde_json", @@ -13231,42 +13231,42 @@ dependencies = [ name = "zed_gleam" version = "0.1.3" dependencies = [ - "zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "zed_extension_api 0.0.6", ] [[package]] name = "zed_glsl" version = "0.1.0" dependencies = [ - "zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "zed_extension_api 0.0.6", ] [[package]] name = "zed_haskell" version = "0.1.0" dependencies = [ - "zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "zed_extension_api 0.0.6", ] [[package]] name = "zed_html" version = "0.1.1" dependencies = [ - "zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "zed_extension_api 0.0.6", ] [[package]] name = "zed_lua" version = "0.0.2" dependencies = [ - "zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "zed_extension_api 0.0.6", ] [[package]] name = "zed_ocaml" version = "0.0.1" dependencies = [ - "zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "zed_extension_api 0.0.6", ] [[package]] @@ -13294,28 +13294,28 @@ dependencies = [ name = "zed_ruby" version = "0.0.4" dependencies = [ - "zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "zed_extension_api 0.0.6", ] [[package]] name = "zed_svelte" version = "0.0.1" dependencies = [ - "zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "zed_extension_api 0.0.6", ] [[package]] name = "zed_terraform" version = "0.0.3" dependencies = [ - "zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "zed_extension_api 0.0.6", ] [[package]] name = "zed_toml" version = "0.1.1" dependencies = [ - "zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "zed_extension_api 0.0.6", ] [[package]] @@ -13329,14 +13329,14 @@ dependencies = [ name = "zed_vue" version = "0.0.2" dependencies = [ - "zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "zed_extension_api 0.0.6", ] [[package]] name = "zed_zig" version = "0.1.2" dependencies = [ - "zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "zed_extension_api 0.0.6", ] [[package]] diff --git a/crates/extension/src/wasm_host/wit.rs b/crates/extension/src/wasm_host/wit.rs index 35c5a6ce13..e1c280abed 100644 --- a/crates/extension/src/wasm_host/wit.rs +++ b/crates/extension/src/wasm_host/wit.rs @@ -1,7 +1,8 @@ mod since_v0_0_1; mod since_v0_0_4; mod since_v0_0_6; -use since_v0_0_6 as latest; +mod since_v0_0_7; +use since_v0_0_7 as latest; use super::{wasm_engine, WasmState}; use anyhow::{Context, Result}; @@ -46,6 +47,7 @@ pub fn wasm_api_version_range() -> RangeInclusive { } pub enum Extension { + V007(since_v0_0_7::Extension), V006(since_v0_0_6::Extension), V004(since_v0_0_4::Extension), V001(since_v0_0_1::Extension), @@ -62,6 +64,15 @@ impl Extension { latest::Extension::instantiate_async(store, &component, latest::linker()) .await .context("failed to instantiate wasm extension")?; + Ok((Self::V007(extension), instance)) + } else if version >= since_v0_0_6::MIN_VERSION { + let (extension, instance) = since_v0_0_6::Extension::instantiate_async( + store, + &component, + since_v0_0_6::linker(), + ) + .await + .context("failed to instantiate wasm extension")?; Ok((Self::V006(extension), instance)) } else if version >= since_v0_0_4::MIN_VERSION { let (extension, instance) = since_v0_0_4::Extension::instantiate_async( @@ -86,6 +97,7 @@ impl Extension { pub async fn call_init_extension(&self, store: &mut Store) -> Result<()> { match self { + Extension::V007(ext) => ext.call_init_extension(store).await, Extension::V006(ext) => ext.call_init_extension(store).await, Extension::V004(ext) => ext.call_init_extension(store).await, Extension::V001(ext) => ext.call_init_extension(store).await, @@ -100,10 +112,14 @@ impl Extension { resource: Resource>, ) -> Result> { match self { - Extension::V006(ext) => { + Extension::V007(ext) => { ext.call_language_server_command(store, &language_server_id.0, resource) .await } + Extension::V006(ext) => Ok(ext + .call_language_server_command(store, &language_server_id.0, resource) + .await? + .map(|command| command.into())), Extension::V004(ext) => Ok(ext .call_language_server_command(store, config, resource) .await? @@ -123,6 +139,14 @@ impl Extension { resource: Resource>, ) -> Result, String>> { match self { + Extension::V007(ext) => { + ext.call_language_server_initialization_options( + store, + &language_server_id.0, + resource, + ) + .await + } Extension::V006(ext) => { ext.call_language_server_initialization_options( store, @@ -153,6 +177,14 @@ impl Extension { resource: Resource>, ) -> Result, String>> { match self { + Extension::V007(ext) => { + ext.call_language_server_workspace_configuration( + store, + &language_server_id.0, + resource, + ) + .await + } Extension::V006(ext) => { ext.call_language_server_workspace_configuration( store, @@ -172,11 +204,20 @@ impl Extension { completions: Vec, ) -> Result>, String>> { match self { - Extension::V001(_) | Extension::V004(_) => Ok(Ok(Vec::new())), - Extension::V006(ext) => { + Extension::V007(ext) => { ext.call_labels_for_completions(store, &language_server_id.0, &completions) .await } + Extension::V006(ext) => Ok(ext + .call_labels_for_completions(store, &language_server_id.0, &completions) + .await? + .map(|labels| { + labels + .into_iter() + .map(|label| label.map(Into::into)) + .collect() + })), + Extension::V001(_) | Extension::V004(_) => Ok(Ok(Vec::new())), } } @@ -187,11 +228,20 @@ impl Extension { symbols: Vec, ) -> Result>, String>> { match self { - Extension::V001(_) | Extension::V004(_) => Ok(Ok(Vec::new())), - Extension::V006(ext) => { + Extension::V007(ext) => { ext.call_labels_for_symbols(store, &language_server_id.0, &symbols) .await } + Extension::V006(ext) => Ok(ext + .call_labels_for_symbols(store, &language_server_id.0, &symbols) + .await? + .map(|labels| { + labels + .into_iter() + .map(|label| label.map(Into::into)) + .collect() + })), + Extension::V001(_) | Extension::V004(_) => Ok(Ok(Vec::new())), } } } diff --git a/crates/extension/src/wasm_host/wit/since_v0_0_6.rs b/crates/extension/src/wasm_host/wit/since_v0_0_6.rs index 3f3b118e2f..5814d2af24 100644 --- a/crates/extension/src/wasm_host/wit/since_v0_0_6.rs +++ b/crates/extension/src/wasm_host/wit/since_v0_0_6.rs @@ -1,36 +1,26 @@ -use crate::wasm_host::{wit::ToWasmtimeResult, WasmState}; -use ::settings::Settings; -use anyhow::{anyhow, bail, Result}; -use async_compression::futures::bufread::GzipDecoder; -use async_tar::Archive; +use super::latest; +use crate::wasm_host::WasmState; +use anyhow::Result; use async_trait::async_trait; -use futures::{io::BufReader, FutureExt as _}; -use language::{ - language_settings::AllLanguageSettings, LanguageServerBinaryStatus, LspAdapterDelegate, -}; -use project::project_settings::ProjectSettings; +use language::LspAdapterDelegate; use semantic_version::SemanticVersion; -use std::{ - env, - path::{Path, PathBuf}, - sync::{Arc, OnceLock}, -}; -use util::maybe; +use std::sync::{Arc, OnceLock}; use wasmtime::component::{Linker, Resource}; pub const MIN_VERSION: SemanticVersion = SemanticVersion::new(0, 0, 6); -pub const MAX_VERSION: SemanticVersion = SemanticVersion::new(0, 0, 6); wasmtime::component::bindgen!({ async: true, path: "../extension_api/wit/since_v0.0.6", with: { "worktree": ExtensionWorktree, + "zed:extension/github": latest::zed::extension::github, + "zed:extension/lsp": latest::zed::extension::lsp, + "zed:extension/nodejs": latest::zed::extension::nodejs, + "zed:extension/platform": latest::zed::extension::platform, }, }); -pub use self::zed::extension::*; - mod settings { include!("../../../../extension_api/wit/since_v0.0.6/settings.rs"); } @@ -39,7 +29,93 @@ pub type ExtensionWorktree = Arc; pub fn linker() -> &'static Linker { static LINKER: OnceLock> = OnceLock::new(); - LINKER.get_or_init(|| super::new_linker(Extension::add_to_linker)) + LINKER.get_or_init(|| { + super::new_linker(|linker, f| { + Extension::add_to_linker(linker, f)?; + latest::zed::extension::github::add_to_linker(linker, f)?; + latest::zed::extension::nodejs::add_to_linker(linker, f)?; + latest::zed::extension::platform::add_to_linker(linker, f)?; + Ok(()) + }) + }) +} + +impl From for latest::Command { + fn from(value: Command) -> Self { + Self { + command: value.command, + args: value.args, + env: value.env, + } + } +} + +impl From for latest::SettingsLocation { + fn from(value: SettingsLocation) -> Self { + Self { + worktree_id: value.worktree_id, + path: value.path, + } + } +} + +impl From for latest::LanguageServerInstallationStatus { + fn from(value: LanguageServerInstallationStatus) -> Self { + match value { + LanguageServerInstallationStatus::None => Self::None, + LanguageServerInstallationStatus::Downloading => Self::Downloading, + LanguageServerInstallationStatus::CheckingForUpdate => Self::CheckingForUpdate, + LanguageServerInstallationStatus::Failed(message) => Self::Failed(message), + } + } +} + +impl From for latest::DownloadedFileType { + fn from(value: DownloadedFileType) -> Self { + match value { + DownloadedFileType::Gzip => Self::Gzip, + DownloadedFileType::GzipTar => Self::GzipTar, + DownloadedFileType::Zip => Self::Zip, + DownloadedFileType::Uncompressed => Self::Uncompressed, + } + } +} + +impl From for latest::Range { + fn from(value: Range) -> Self { + Self { + start: value.start, + end: value.end, + } + } +} + +impl From for latest::CodeLabelSpan { + fn from(value: CodeLabelSpan) -> Self { + match value { + CodeLabelSpan::CodeRange(range) => Self::CodeRange(range.into()), + CodeLabelSpan::Literal(literal) => Self::Literal(literal.into()), + } + } +} + +impl From for latest::CodeLabelSpanLiteral { + fn from(value: CodeLabelSpanLiteral) -> Self { + Self { + text: value.text, + highlight_name: value.highlight_name, + } + } +} + +impl From for latest::CodeLabel { + fn from(value: CodeLabel) -> Self { + Self { + code: value.code, + spans: value.spans.into_iter().map(Into::into).collect(), + filter_range: value.filter_range.into(), + } + } } #[async_trait] @@ -48,16 +124,14 @@ impl HostWorktree for WasmState { &mut self, delegate: Resource>, ) -> wasmtime::Result { - let delegate = self.table.get(&delegate)?; - Ok(delegate.worktree_id()) + latest::HostWorktree::id(self, delegate).await } async fn root_path( &mut self, delegate: Resource>, ) -> wasmtime::Result { - let delegate = self.table.get(&delegate)?; - Ok(delegate.worktree_root_path().to_string_lossy().to_string()) + latest::HostWorktree::root_path(self, delegate).await } async fn read_text_file( @@ -65,19 +139,14 @@ impl HostWorktree for WasmState { delegate: Resource>, path: String, ) -> wasmtime::Result> { - let delegate = self.table.get(&delegate)?; - Ok(delegate - .read_text_file(path.into()) - .await - .map_err(|error| error.to_string())) + latest::HostWorktree::read_text_file(self, delegate, path).await } async fn shell_env( &mut self, delegate: Resource>, ) -> wasmtime::Result { - let delegate = self.table.get(&delegate)?; - Ok(delegate.shell_env().await.into_iter().collect()) + latest::HostWorktree::shell_env(self, delegate).await } async fn which( @@ -85,11 +154,7 @@ impl HostWorktree for WasmState { delegate: Resource>, binary_name: String, ) -> wasmtime::Result> { - let delegate = self.table.get(&delegate)?; - Ok(delegate - .which(binary_name.as_ref()) - .await - .map(|path| path.to_string_lossy().to_string())) + latest::HostWorktree::which(self, delegate, binary_name).await } fn drop(&mut self, _worktree: Resource) -> Result<()> { @@ -98,107 +163,6 @@ impl HostWorktree for WasmState { } } -#[async_trait] -impl nodejs::Host for WasmState { - async fn node_binary_path(&mut self) -> wasmtime::Result> { - self.host - .node_runtime - .binary_path() - .await - .map(|path| path.to_string_lossy().to_string()) - .to_wasmtime_result() - } - - async fn npm_package_latest_version( - &mut self, - package_name: String, - ) -> wasmtime::Result> { - self.host - .node_runtime - .npm_package_latest_version(&package_name) - .await - .to_wasmtime_result() - } - - async fn npm_package_installed_version( - &mut self, - package_name: String, - ) -> wasmtime::Result, String>> { - self.host - .node_runtime - .npm_package_installed_version(&self.work_dir(), &package_name) - .await - .to_wasmtime_result() - } - - async fn npm_install_package( - &mut self, - package_name: String, - version: String, - ) -> wasmtime::Result> { - self.host - .node_runtime - .npm_install_packages(&self.work_dir(), &[(&package_name, &version)]) - .await - .to_wasmtime_result() - } -} - -#[async_trait] -impl lsp::Host for WasmState {} - -#[async_trait] -impl github::Host for WasmState { - async fn latest_github_release( - &mut self, - repo: String, - options: github::GithubReleaseOptions, - ) -> wasmtime::Result> { - maybe!(async { - let release = http::github::latest_github_release( - &repo, - options.require_assets, - options.pre_release, - self.host.http_client.clone(), - ) - .await?; - Ok(github::GithubRelease { - version: release.tag_name, - assets: release - .assets - .into_iter() - .map(|asset| github::GithubReleaseAsset { - name: asset.name, - download_url: asset.browser_download_url, - }) - .collect(), - }) - }) - .await - .to_wasmtime_result() - } -} - -#[async_trait] -impl platform::Host for WasmState { - async fn current_platform(&mut self) -> Result<(platform::Os, platform::Architecture)> { - Ok(( - match env::consts::OS { - "macos" => platform::Os::Mac, - "linux" => platform::Os::Linux, - "windows" => platform::Os::Windows, - _ => panic!("unsupported os"), - }, - match env::consts::ARCH { - "aarch64" => platform::Architecture::Aarch64, - "x86" => platform::Architecture::X86, - "x86_64" => platform::Architecture::X8664, - _ => panic!("unsupported architecture"), - }, - )) - } -} - #[async_trait] impl ExtensionImports for WasmState { async fn get_settings( @@ -207,50 +171,13 @@ impl ExtensionImports for WasmState { category: String, key: Option, ) -> wasmtime::Result> { - self.on_main_thread(|cx| { - async move { - let location = location - .as_ref() - .map(|location| ::settings::SettingsLocation { - worktree_id: location.worktree_id as usize, - path: Path::new(&location.path), - }); - - cx.update(|cx| match category.as_str() { - "language" => { - let settings = - AllLanguageSettings::get(location, cx).language(key.as_deref()); - Ok(serde_json::to_string(&settings::LanguageSettings { - tab_size: settings.tab_size, - })?) - } - "lsp" => { - let settings = key - .and_then(|key| { - ProjectSettings::get(location, cx) - .lsp - .get(&Arc::::from(key)) - }) - .cloned() - .unwrap_or_default(); - Ok(serde_json::to_string(&settings::LspSettings { - binary: settings.binary.map(|binary| settings::BinarySettings { - path: binary.path, - arguments: binary.arguments, - }), - settings: settings.settings, - initialization_options: settings.initialization_options, - })?) - } - _ => { - bail!("Unknown settings category: {}", category); - } - }) - } - .boxed_local() - }) - .await? - .to_wasmtime_result() + latest::ExtensionImports::get_settings( + self, + location.map(|location| location.into()), + category, + key, + ) + .await } async fn set_language_server_installation_status( @@ -258,23 +185,12 @@ impl ExtensionImports for WasmState { server_name: String, status: LanguageServerInstallationStatus, ) -> wasmtime::Result<()> { - let status = match status { - LanguageServerInstallationStatus::CheckingForUpdate => { - LanguageServerBinaryStatus::CheckingForUpdate - } - LanguageServerInstallationStatus::Downloading => { - LanguageServerBinaryStatus::Downloading - } - LanguageServerInstallationStatus::None => LanguageServerBinaryStatus::None, - LanguageServerInstallationStatus::Failed(error) => { - LanguageServerBinaryStatus::Failed { error } - } - }; - - self.host - .language_registry - .update_lsp_status(language::LanguageServerName(server_name.into()), status); - Ok(()) + latest::ExtensionImports::set_language_server_installation_status( + self, + server_name, + status.into(), + ) + .await } async fn download_file( @@ -283,103 +199,10 @@ impl ExtensionImports for WasmState { path: String, file_type: DownloadedFileType, ) -> wasmtime::Result> { - maybe!(async { - let path = PathBuf::from(path); - let extension_work_dir = self.host.work_dir.join(self.manifest.id.as_ref()); - - self.host.fs.create_dir(&extension_work_dir).await?; - - let destination_path = self - .host - .writeable_path_from_extension(&self.manifest.id, &path)?; - - let mut response = self - .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 { - DownloadedFileType::Uncompressed => { - futures::pin_mut!(body); - self.host - .fs - .create_file_with(&destination_path, body) - .await?; - } - DownloadedFileType::Gzip => { - let body = GzipDecoder::new(body); - futures::pin_mut!(body); - self.host - .fs - .create_file_with(&destination_path, body) - .await?; - } - DownloadedFileType::GzipTar => { - let body = GzipDecoder::new(body); - futures::pin_mut!(body); - self.host - .fs - .extract_tar_file(&destination_path, Archive::new(body)) - .await?; - } - DownloadedFileType::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(); - zip_path.set_file_name(zip_filename); - - futures::pin_mut!(body); - self.host.fs.create_file_with(&zip_path, body).await?; - - let unzip_status = std::process::Command::new("unzip") - .current_dir(&extension_work_dir) - .arg("-d") - .arg(&destination_path) - .arg(&zip_path) - .output()? - .status; - if !unzip_status.success() { - Err(anyhow!("failed to unzip {} archive", path.display()))?; - } - } - } - - Ok(()) - }) - .await - .to_wasmtime_result() + latest::ExtensionImports::download_file(self, url, path, file_type.into()).await } async fn make_file_executable(&mut self, path: String) -> wasmtime::Result> { - #[allow(unused)] - let path = self - .host - .writeable_path_from_extension(&self.manifest.id, Path::new(&path))?; - - #[cfg(unix)] - { - use std::fs::{self, Permissions}; - use std::os::unix::fs::PermissionsExt; - - return fs::set_permissions(&path, Permissions::from_mode(0o755)) - .map_err(|error| anyhow!("failed to set permissions for path {path:?}: {error}")) - .to_wasmtime_result(); - } - - #[cfg(not(unix))] - Ok(Ok(())) + latest::ExtensionImports::make_file_executable(self, path).await } } diff --git a/crates/extension/src/wasm_host/wit/since_v0_0_7.rs b/crates/extension/src/wasm_host/wit/since_v0_0_7.rs new file mode 100644 index 0000000000..d962d5f7a9 --- /dev/null +++ b/crates/extension/src/wasm_host/wit/since_v0_0_7.rs @@ -0,0 +1,385 @@ +use crate::wasm_host::{wit::ToWasmtimeResult, WasmState}; +use ::settings::Settings; +use anyhow::{anyhow, bail, Result}; +use async_compression::futures::bufread::GzipDecoder; +use async_tar::Archive; +use async_trait::async_trait; +use futures::{io::BufReader, FutureExt as _}; +use language::{ + language_settings::AllLanguageSettings, LanguageServerBinaryStatus, LspAdapterDelegate, +}; +use project::project_settings::ProjectSettings; +use semantic_version::SemanticVersion; +use std::{ + env, + path::{Path, PathBuf}, + sync::{Arc, OnceLock}, +}; +use util::maybe; +use wasmtime::component::{Linker, Resource}; + +pub const MIN_VERSION: SemanticVersion = SemanticVersion::new(0, 0, 7); +pub const MAX_VERSION: SemanticVersion = SemanticVersion::new(0, 0, 7); + +wasmtime::component::bindgen!({ + async: true, + path: "../extension_api/wit/since_v0.0.7", + with: { + "worktree": ExtensionWorktree, + }, +}); + +pub use self::zed::extension::*; + +mod settings { + include!("../../../../extension_api/wit/since_v0.0.7/settings.rs"); +} + +pub type ExtensionWorktree = Arc; + +pub fn linker() -> &'static Linker { + static LINKER: OnceLock> = OnceLock::new(); + LINKER.get_or_init(|| super::new_linker(Extension::add_to_linker)) +} + +#[async_trait] +impl HostWorktree for WasmState { + async fn id( + &mut self, + delegate: Resource>, + ) -> wasmtime::Result { + let delegate = self.table.get(&delegate)?; + Ok(delegate.worktree_id()) + } + + async fn root_path( + &mut self, + delegate: Resource>, + ) -> wasmtime::Result { + let delegate = self.table.get(&delegate)?; + Ok(delegate.worktree_root_path().to_string_lossy().to_string()) + } + + async fn read_text_file( + &mut self, + delegate: Resource>, + path: String, + ) -> wasmtime::Result> { + let delegate = self.table.get(&delegate)?; + Ok(delegate + .read_text_file(path.into()) + .await + .map_err(|error| error.to_string())) + } + + async fn shell_env( + &mut self, + delegate: Resource>, + ) -> wasmtime::Result { + let delegate = self.table.get(&delegate)?; + Ok(delegate.shell_env().await.into_iter().collect()) + } + + async fn which( + &mut self, + delegate: Resource>, + binary_name: String, + ) -> wasmtime::Result> { + 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) -> Result<()> { + // We only ever hand out borrows of worktrees. + Ok(()) + } +} + +#[async_trait] +impl nodejs::Host for WasmState { + async fn node_binary_path(&mut self) -> wasmtime::Result> { + self.host + .node_runtime + .binary_path() + .await + .map(|path| path.to_string_lossy().to_string()) + .to_wasmtime_result() + } + + async fn npm_package_latest_version( + &mut self, + package_name: String, + ) -> wasmtime::Result> { + self.host + .node_runtime + .npm_package_latest_version(&package_name) + .await + .to_wasmtime_result() + } + + async fn npm_package_installed_version( + &mut self, + package_name: String, + ) -> wasmtime::Result, String>> { + self.host + .node_runtime + .npm_package_installed_version(&self.work_dir(), &package_name) + .await + .to_wasmtime_result() + } + + async fn npm_install_package( + &mut self, + package_name: String, + version: String, + ) -> wasmtime::Result> { + self.host + .node_runtime + .npm_install_packages(&self.work_dir(), &[(&package_name, &version)]) + .await + .to_wasmtime_result() + } +} + +#[async_trait] +impl lsp::Host for WasmState {} + +#[async_trait] +impl github::Host for WasmState { + async fn latest_github_release( + &mut self, + repo: String, + options: github::GithubReleaseOptions, + ) -> wasmtime::Result> { + maybe!(async { + let release = http::github::latest_github_release( + &repo, + options.require_assets, + options.pre_release, + self.host.http_client.clone(), + ) + .await?; + Ok(github::GithubRelease { + version: release.tag_name, + assets: release + .assets + .into_iter() + .map(|asset| github::GithubReleaseAsset { + name: asset.name, + download_url: asset.browser_download_url, + }) + .collect(), + }) + }) + .await + .to_wasmtime_result() + } +} + +#[async_trait] +impl platform::Host for WasmState { + async fn current_platform(&mut self) -> Result<(platform::Os, platform::Architecture)> { + Ok(( + match env::consts::OS { + "macos" => platform::Os::Mac, + "linux" => platform::Os::Linux, + "windows" => platform::Os::Windows, + _ => panic!("unsupported os"), + }, + match env::consts::ARCH { + "aarch64" => platform::Architecture::Aarch64, + "x86" => platform::Architecture::X86, + "x86_64" => platform::Architecture::X8664, + _ => panic!("unsupported architecture"), + }, + )) + } +} + +#[async_trait] +impl ExtensionImports for WasmState { + async fn get_settings( + &mut self, + location: Option, + category: String, + key: Option, + ) -> wasmtime::Result> { + self.on_main_thread(|cx| { + async move { + let location = location + .as_ref() + .map(|location| ::settings::SettingsLocation { + worktree_id: location.worktree_id as usize, + path: Path::new(&location.path), + }); + + cx.update(|cx| match category.as_str() { + "language" => { + let settings = + AllLanguageSettings::get(location, cx).language(key.as_deref()); + Ok(serde_json::to_string(&settings::LanguageSettings { + tab_size: settings.tab_size, + })?) + } + "lsp" => { + let settings = key + .and_then(|key| { + ProjectSettings::get(location, cx) + .lsp + .get(&Arc::::from(key)) + }) + .cloned() + .unwrap_or_default(); + Ok(serde_json::to_string(&settings::LspSettings { + binary: settings.binary.map(|binary| settings::BinarySettings { + path: binary.path, + arguments: binary.arguments, + }), + settings: settings.settings, + initialization_options: settings.initialization_options, + })?) + } + _ => { + bail!("Unknown settings category: {}", category); + } + }) + } + .boxed_local() + }) + .await? + .to_wasmtime_result() + } + + async fn set_language_server_installation_status( + &mut self, + server_name: String, + status: LanguageServerInstallationStatus, + ) -> wasmtime::Result<()> { + let status = match status { + LanguageServerInstallationStatus::CheckingForUpdate => { + LanguageServerBinaryStatus::CheckingForUpdate + } + LanguageServerInstallationStatus::Downloading => { + LanguageServerBinaryStatus::Downloading + } + LanguageServerInstallationStatus::None => LanguageServerBinaryStatus::None, + 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, + path: String, + file_type: DownloadedFileType, + ) -> wasmtime::Result> { + maybe!(async { + let path = PathBuf::from(path); + let extension_work_dir = self.host.work_dir.join(self.manifest.id.as_ref()); + + self.host.fs.create_dir(&extension_work_dir).await?; + + let destination_path = self + .host + .writeable_path_from_extension(&self.manifest.id, &path)?; + + let mut response = self + .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 { + DownloadedFileType::Uncompressed => { + futures::pin_mut!(body); + self.host + .fs + .create_file_with(&destination_path, body) + .await?; + } + DownloadedFileType::Gzip => { + let body = GzipDecoder::new(body); + futures::pin_mut!(body); + self.host + .fs + .create_file_with(&destination_path, body) + .await?; + } + DownloadedFileType::GzipTar => { + let body = GzipDecoder::new(body); + futures::pin_mut!(body); + self.host + .fs + .extract_tar_file(&destination_path, Archive::new(body)) + .await?; + } + DownloadedFileType::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(); + zip_path.set_file_name(zip_filename); + + futures::pin_mut!(body); + self.host.fs.create_file_with(&zip_path, body).await?; + + let unzip_status = std::process::Command::new("unzip") + .current_dir(&extension_work_dir) + .arg("-d") + .arg(&destination_path) + .arg(&zip_path) + .output()? + .status; + if !unzip_status.success() { + Err(anyhow!("failed to unzip {} archive", path.display()))?; + } + } + } + + Ok(()) + }) + .await + .to_wasmtime_result() + } + + async fn make_file_executable(&mut self, path: String) -> wasmtime::Result> { + #[allow(unused)] + let path = self + .host + .writeable_path_from_extension(&self.manifest.id, Path::new(&path))?; + + #[cfg(unix)] + { + use std::fs::{self, Permissions}; + use std::os::unix::fs::PermissionsExt; + + return fs::set_permissions(&path, Permissions::from_mode(0o755)) + .map_err(|error| anyhow!("failed to set permissions for path {path:?}: {error}")) + .to_wasmtime_result(); + } + + #[cfg(not(unix))] + Ok(Ok(())) + } +} diff --git a/crates/extension_api/Cargo.toml b/crates/extension_api/Cargo.toml index 1fcb8d0453..f771431990 100644 --- a/crates/extension_api/Cargo.toml +++ b/crates/extension_api/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zed_extension_api" -version = "0.0.6" +version = "0.0.7" description = "APIs for creating Zed extensions in Rust" repository = "https://github.com/zed-industries/zed" documentation = "https://docs.rs/zed_extension_api" diff --git a/crates/extension_api/src/extension_api.rs b/crates/extension_api/src/extension_api.rs index be3dcfc0eb..21d41d1001 100644 --- a/crates/extension_api/src/extension_api.rs +++ b/crates/extension_api/src/extension_api.rs @@ -140,7 +140,7 @@ pub static ZED_API_VERSION: [u8; 6] = *include_bytes!(concat!(env!("OUT_DIR"), " mod wit { wit_bindgen::generate!({ skip: ["init-extension"], - path: "./wit/since_v0.0.6", + path: "./wit/since_v0.0.7", }); } diff --git a/crates/extension_api/src/settings.rs b/crates/extension_api/src/settings.rs index ffed133c4c..067e8a60db 100644 --- a/crates/extension_api/src/settings.rs +++ b/crates/extension_api/src/settings.rs @@ -1,4 +1,4 @@ -#[path = "../wit/since_v0.0.6/settings.rs"] +#[path = "../wit/since_v0.0.7/settings.rs"] mod types; use crate::{wit, Result, SettingsLocation, Worktree}; diff --git a/crates/extension_api/wit/since_v0.0.7/extension.wit b/crates/extension_api/wit/since_v0.0.7/extension.wit new file mode 100644 index 0000000000..2f42cc0365 --- /dev/null +++ b/crates/extension_api/wit/since_v0.0.7/extension.wit @@ -0,0 +1,130 @@ +package zed:extension; + +world extension { + import github; + import platform; + import nodejs; + + use lsp.{completion, symbol}; + + /// Initializes the extension. + export init-extension: func(); + + /// The type of a downloaded file. + enum downloaded-file-type { + /// A gzipped file (`.gz`). + gzip, + /// A gzipped tar archive (`.tar.gz`). + gzip-tar, + /// A ZIP file (`.zip`). + zip, + /// An uncompressed file. + uncompressed, + } + + /// The installation status for a language server. + variant language-server-installation-status { + /// The language server has no installation status. + none, + /// The language server is being downloaded. + downloading, + /// The language server is checking for updates. + checking-for-update, + /// The language server installation failed for specified reason. + failed(string), + } + + record settings-location { + worktree-id: u64, + path: string, + } + + import get-settings: func(path: option, category: string, key: option) -> result; + + /// Downloads a file from the given URL and saves it to the given path within the extension's + /// working directory. + /// + /// The file will be extracted according to the given file type. + import download-file: func(url: string, file-path: string, file-type: downloaded-file-type) -> result<_, string>; + + /// Makes the file at the given path executable. + import make-file-executable: func(filepath: string) -> 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); + + /// A list of environment variables. + type env-vars = list>; + + /// A command. + record command { + /// The command to execute. + command: string, + /// The arguments to pass to the command. + args: list, + /// The environment variables to set for the command. + env: env-vars, + } + + /// A Zed worktree. + resource worktree { + /// Returns the ID of the worktree. + id: func() -> u64; + /// Returns the root path of the worktree. + root-path: func() -> string; + /// Returns the textual contents of the specified file in the worktree. + read-text-file: func(path: string) -> result; + /// Returns the path to the given binary name, if one is present on the `$PATH`. + which: func(binary-name: string) -> option; + /// Returns the current shell environment. + shell-env: func() -> env-vars; + } + + /// Returns the command used to start up the language server. + export language-server-command: func(language-server-id: string, worktree: borrow) -> result; + + /// Returns the initialization options to pass to the language server on startup. + /// + /// The initialization options are represented as a JSON string. + export language-server-initialization-options: func(language-server-id: string, worktree: borrow) -> result, string>; + + /// Returns the workspace configuration options to pass to the language server. + export language-server-workspace-configuration: func(language-server-id: string, worktree: borrow) -> result, string>; + + /// A label containing some code. + record code-label { + /// The source code to parse with Tree-sitter. + code: string, + /// The spans to display in the label. + spans: list, + /// The range of the displayed label to include when filtering. + filter-range: range, + } + + /// A span within a code label. + variant code-label-span { + /// A range into the parsed code. + code-range(range), + /// A span containing a code literal. + literal(code-label-span-literal), + } + + /// A span containing a code literal. + record code-label-span-literal { + /// The literal text. + text: string, + /// The name of the highlight to use for this literal. + highlight-name: option, + } + + /// A (half-open) range (`[start, end)`). + record range { + /// The start of the range (inclusive). + start: u32, + /// The end of the range (exclusive). + end: u32, + } + + export labels-for-completions: func(language-server-id: string, completions: list) -> result>, string>; + export labels-for-symbols: func(language-server-id: string, symbols: list) -> result>, string>; +} diff --git a/crates/extension_api/wit/since_v0.0.7/github.wit b/crates/extension_api/wit/since_v0.0.7/github.wit new file mode 100644 index 0000000000..53ecacb720 --- /dev/null +++ b/crates/extension_api/wit/since_v0.0.7/github.wit @@ -0,0 +1,28 @@ +interface github { + /// A GitHub release. + record github-release { + /// The version of the release. + version: string, + /// The list of assets attached to the release. + assets: list, + } + + /// An asset from a GitHub release. + record github-release-asset { + /// The name of the asset. + name: string, + /// The download URL for the asset. + download-url: string, + } + + /// The options used to filter down GitHub releases. + record github-release-options { + /// Whether releases without assets should be included. + require-assets: bool, + /// Whether pre-releases should be included. + pre-release: bool, + } + + /// Returns the latest release for the given GitHub repository. + latest-github-release: func(repo: string, options: github-release-options) -> result; +} diff --git a/crates/extension_api/wit/since_v0.0.7/lsp.wit b/crates/extension_api/wit/since_v0.0.7/lsp.wit new file mode 100644 index 0000000000..19e81b6b14 --- /dev/null +++ b/crates/extension_api/wit/since_v0.0.7/lsp.wit @@ -0,0 +1,83 @@ +interface lsp { + /// An LSP completion. + record completion { + label: string, + detail: option, + kind: option, + insert-text-format: option, + } + + /// The kind of an LSP completion. + variant completion-kind { + text, + method, + function, + %constructor, + field, + variable, + class, + %interface, + module, + property, + unit, + value, + %enum, + keyword, + snippet, + color, + file, + reference, + folder, + enum-member, + constant, + struct, + event, + operator, + type-parameter, + other(s32), + } + + /// Defines how to interpret the insert text in a completion item. + variant insert-text-format { + plain-text, + snippet, + other(s32), + } + + /// An LSP symbol. + record symbol { + kind: symbol-kind, + name: string, + } + + /// The kind of an LSP symbol. + variant symbol-kind { + file, + module, + namespace, + %package, + class, + method, + property, + field, + %constructor, + %enum, + %interface, + function, + variable, + constant, + %string, + number, + boolean, + array, + object, + key, + null, + enum-member, + struct, + event, + operator, + type-parameter, + other(s32), + } +} diff --git a/crates/extension_api/wit/since_v0.0.7/nodejs.wit b/crates/extension_api/wit/since_v0.0.7/nodejs.wit new file mode 100644 index 0000000000..c814548314 --- /dev/null +++ b/crates/extension_api/wit/since_v0.0.7/nodejs.wit @@ -0,0 +1,13 @@ +interface nodejs { + /// Returns the path to the Node binary used by Zed. + node-binary-path: func() -> result; + + /// Returns the latest version of the given NPM package. + npm-package-latest-version: func(package-name: string) -> result; + + /// Returns the installed version of the given NPM package, if it exists. + npm-package-installed-version: func(package-name: string) -> result, string>; + + /// Installs the specified NPM package. + npm-install-package: func(package-name: string, version: string) -> result<_, string>; +} diff --git a/crates/extension_api/wit/since_v0.0.7/platform.wit b/crates/extension_api/wit/since_v0.0.7/platform.wit new file mode 100644 index 0000000000..48472a99bc --- /dev/null +++ b/crates/extension_api/wit/since_v0.0.7/platform.wit @@ -0,0 +1,24 @@ +interface platform { + /// An operating system. + enum os { + /// macOS. + mac, + /// Linux. + linux, + /// Windows. + windows, + } + + /// A platform architecture. + enum architecture { + /// AArch64 (e.g., Apple Silicon). + aarch64, + /// x86. + x86, + /// x86-64. + x8664, + } + + /// Gets the current operating system and architecture. + current-platform: func() -> tuple; +} diff --git a/crates/extension_api/wit/since_v0.0.7/settings.rs b/crates/extension_api/wit/since_v0.0.7/settings.rs new file mode 100644 index 0000000000..5c6cae7064 --- /dev/null +++ b/crates/extension_api/wit/since_v0.0.7/settings.rs @@ -0,0 +1,29 @@ +use serde::{Deserialize, Serialize}; +use std::num::NonZeroU32; + +/// The settings for a particular language. +#[derive(Debug, Serialize, Deserialize)] +pub struct LanguageSettings { + /// How many columns a tab should occupy. + pub tab_size: NonZeroU32, +} + +/// The settings for a particular language server. +#[derive(Default, Debug, Serialize, Deserialize)] +pub struct LspSettings { + /// The settings for the language server binary. + pub binary: Option, + /// The initialization options to pass to the language server. + pub initialization_options: Option, + /// The settings to pass to language server. + pub settings: Option, +} + +/// The settings for a language server binary. +#[derive(Debug, Serialize, Deserialize)] +pub struct BinarySettings { + /// The path to the binary. + pub path: Option, + /// The arguments to pass to the binary. + pub arguments: Option>, +}