Fix issues with extension API that come up when moving Svelte into an extension (#9611)
We're doing it. Svelte support is moving into an extension. This PR fixes some issues that came up along the way. Notes * extensions need to be able to retrieve the path the `node` binary installed by Zed * previously we were silently swallowing any errors that occurred while loading a grammar * npm commands ran by extensions weren't run in the right directory * Tree-sitter's WASM stdlib didn't support a C function (`strncmp`) needed by the Svelte parser's external scanner * the way that LSP installation status was reported was unnecessarily complex Release Notes: - Removed built-in support for the Svelte and Gleam languages, because full support for those languages is now available via extensions. These extensions will be suggested for download when you open a `.svelte` or `.gleam` file. --------- Co-authored-by: Marshall <marshall@zed.dev>
This commit is contained in:
parent
4459eacc98
commit
6ebe599c98
70 changed files with 1278 additions and 1223 deletions
|
@ -85,10 +85,7 @@ impl ExtensionBuilder {
|
|||
|
||||
let cargo_toml_path = extension_dir.join("Cargo.toml");
|
||||
if extension_manifest.lib.kind == Some(ExtensionLibraryKind::Rust)
|
||||
|| fs::metadata(&cargo_toml_path)
|
||||
.ok()
|
||||
.map(|metadata| metadata.is_file())
|
||||
.unwrap_or(false)
|
||||
|| fs::metadata(&cargo_toml_path).map_or(false, |stat| stat.is_file())
|
||||
{
|
||||
log::info!("compiling Rust extension {}", extension_dir.display());
|
||||
self.compile_rust_extension(extension_dir, options)
|
||||
|
|
|
@ -11,7 +11,7 @@ use std::{
|
|||
pin::Pin,
|
||||
sync::Arc,
|
||||
};
|
||||
use wasmtime_wasi::preview2::WasiView as _;
|
||||
use wasmtime_wasi::WasiView as _;
|
||||
|
||||
pub struct ExtensionLspAdapter {
|
||||
pub(crate) extension: WasmExtension,
|
||||
|
|
|
@ -39,6 +39,7 @@ use theme::{ThemeRegistry, ThemeSettings};
|
|||
use url::Url;
|
||||
use util::{
|
||||
http::{AsyncBody, HttpClient, HttpClientWithUrl},
|
||||
maybe,
|
||||
paths::EXTENSIONS_DIR,
|
||||
ResultExt,
|
||||
};
|
||||
|
@ -108,6 +109,7 @@ pub enum Event {
|
|||
ExtensionsUpdated,
|
||||
StartedReloading,
|
||||
ExtensionInstalled(Arc<str>),
|
||||
ExtensionFailedToLoad(Arc<str>),
|
||||
}
|
||||
|
||||
impl EventEmitter<Event> for ExtensionStore {}
|
||||
|
@ -886,41 +888,38 @@ impl ExtensionStore {
|
|||
continue;
|
||||
};
|
||||
|
||||
let mut path = root_dir.clone();
|
||||
path.extend([extension.manifest.id.as_ref(), "extension.wasm"]);
|
||||
let Some(mut wasm_file) = fs
|
||||
.open_sync(&path)
|
||||
.await
|
||||
.context("failed to open wasm file")
|
||||
.log_err()
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let wasm_extension = maybe!(async {
|
||||
let mut path = root_dir.clone();
|
||||
path.extend([extension.manifest.clone().id.as_ref(), "extension.wasm"]);
|
||||
let mut wasm_file = fs
|
||||
.open_sync(&path)
|
||||
.await
|
||||
.context("failed to open wasm file")?;
|
||||
|
||||
let mut wasm_bytes = Vec::new();
|
||||
if wasm_file
|
||||
.read_to_end(&mut wasm_bytes)
|
||||
.context("failed to read wasm")
|
||||
.log_err()
|
||||
.is_none()
|
||||
{
|
||||
continue;
|
||||
let mut wasm_bytes = Vec::new();
|
||||
wasm_file
|
||||
.read_to_end(&mut wasm_bytes)
|
||||
.context("failed to read wasm")?;
|
||||
|
||||
wasm_host
|
||||
.load_extension(
|
||||
wasm_bytes,
|
||||
extension.manifest.clone().clone(),
|
||||
cx.background_executor().clone(),
|
||||
)
|
||||
.await
|
||||
.context("failed to load wasm extension")
|
||||
})
|
||||
.await;
|
||||
|
||||
if let Some(wasm_extension) = wasm_extension.log_err() {
|
||||
wasm_extensions.push((extension.manifest.clone(), wasm_extension));
|
||||
} else {
|
||||
this.update(&mut cx, |_, cx| {
|
||||
cx.emit(Event::ExtensionFailedToLoad(extension.manifest.id.clone()))
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
let Some(wasm_extension) = wasm_host
|
||||
.load_extension(
|
||||
wasm_bytes,
|
||||
extension.manifest.clone(),
|
||||
cx.background_executor().clone(),
|
||||
)
|
||||
.await
|
||||
.context("failed to load wasm extension")
|
||||
.log_err()
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
wasm_extensions.push((extension.manifest.clone(), wasm_extension));
|
||||
}
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use crate::{
|
||||
ExtensionIndex, ExtensionIndexEntry, ExtensionIndexLanguageEntry, ExtensionIndexThemeEntry,
|
||||
ExtensionManifest, ExtensionStore, GrammarManifestEntry, RELOAD_DEBOUNCE_DURATION,
|
||||
Event, ExtensionIndex, ExtensionIndexEntry, ExtensionIndexLanguageEntry,
|
||||
ExtensionIndexThemeEntry, ExtensionManifest, ExtensionStore, GrammarManifestEntry,
|
||||
RELOAD_DEBOUNCE_DURATION,
|
||||
};
|
||||
use async_compression::futures::bufread::GzipEncoder;
|
||||
use collections::BTreeMap;
|
||||
|
@ -558,6 +559,15 @@ async fn test_extension_store_with_gleam_extension(cx: &mut TestAppContext) {
|
|||
}
|
||||
});
|
||||
|
||||
extension_store.update(cx, |_, cx| {
|
||||
cx.subscribe(&extension_store, |_, _, event, _| {
|
||||
if matches!(event, Event::ExtensionFailedToLoad(_)) {
|
||||
panic!("extension failed to load");
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
|
||||
extension_store
|
||||
.update(cx, |store, cx| {
|
||||
store.install_dev_extension(gleam_extension_dir.clone(), cx)
|
||||
|
@ -602,7 +612,7 @@ async fn test_extension_store_with_gleam_extension(cx: &mut TestAppContext) {
|
|||
),
|
||||
(
|
||||
LanguageServerName("gleam".into()),
|
||||
LanguageServerBinaryStatus::Downloaded
|
||||
LanguageServerBinaryStatus::None
|
||||
)
|
||||
]
|
||||
);
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
pub(crate) mod wit;
|
||||
|
||||
use crate::ExtensionManifest;
|
||||
use anyhow::{anyhow, bail, Context as _, Result};
|
||||
use async_compression::futures::bufread::GzipDecoder;
|
||||
use async_tar::Archive;
|
||||
use async_trait::async_trait;
|
||||
use fs::{normalize_path, Fs};
|
||||
use futures::{
|
||||
channel::{
|
||||
|
@ -10,42 +9,28 @@ use futures::{
|
|||
oneshot,
|
||||
},
|
||||
future::BoxFuture,
|
||||
io::BufReader,
|
||||
Future, FutureExt, StreamExt as _,
|
||||
};
|
||||
use gpui::BackgroundExecutor;
|
||||
use language::{LanguageRegistry, LanguageServerBinaryStatus, LspAdapterDelegate};
|
||||
use language::LanguageRegistry;
|
||||
use node_runtime::NodeRuntime;
|
||||
use std::{
|
||||
env,
|
||||
path::{Path, PathBuf},
|
||||
sync::{Arc, OnceLock},
|
||||
};
|
||||
use util::{http::HttpClient, SemanticVersion};
|
||||
use wasmtime::{
|
||||
component::{Component, Linker, Resource, ResourceTable},
|
||||
component::{Component, ResourceTable},
|
||||
Engine, Store,
|
||||
};
|
||||
use wasmtime_wasi::preview2::{self as wasi, WasiCtx};
|
||||
|
||||
pub mod wit {
|
||||
wasmtime::component::bindgen!({
|
||||
async: true,
|
||||
path: "../extension_api/wit",
|
||||
with: {
|
||||
"worktree": super::ExtensionWorktree,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
pub type ExtensionWorktree = Arc<dyn LspAdapterDelegate>;
|
||||
use wasmtime_wasi as wasi;
|
||||
use wit::Extension;
|
||||
|
||||
pub(crate) struct WasmHost {
|
||||
engine: Engine,
|
||||
linker: Arc<wasmtime::component::Linker<WasmState>>,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
node_runtime: Arc<dyn NodeRuntime>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
pub(crate) language_registry: Arc<LanguageRegistry>,
|
||||
fs: Arc<dyn Fs>,
|
||||
pub(crate) work_dir: PathBuf,
|
||||
}
|
||||
|
@ -60,17 +45,27 @@ pub struct WasmExtension {
|
|||
|
||||
pub(crate) struct WasmState {
|
||||
manifest: Arc<ExtensionManifest>,
|
||||
table: ResourceTable,
|
||||
pub(crate) table: ResourceTable,
|
||||
ctx: wasi::WasiCtx,
|
||||
host: Arc<WasmHost>,
|
||||
pub(crate) host: Arc<WasmHost>,
|
||||
}
|
||||
|
||||
type ExtensionCall = Box<
|
||||
dyn Send
|
||||
+ for<'a> FnOnce(&'a mut wit::Extension, &'a mut Store<WasmState>) -> BoxFuture<'a, ()>,
|
||||
dyn Send + for<'a> FnOnce(&'a mut Extension, &'a mut Store<WasmState>) -> BoxFuture<'a, ()>,
|
||||
>;
|
||||
|
||||
static WASM_ENGINE: OnceLock<wasmtime::Engine> = OnceLock::new();
|
||||
fn wasm_engine() -> wasmtime::Engine {
|
||||
static WASM_ENGINE: OnceLock<wasmtime::Engine> = OnceLock::new();
|
||||
|
||||
WASM_ENGINE
|
||||
.get_or_init(|| {
|
||||
let mut config = wasmtime::Config::new();
|
||||
config.wasm_component_model(true);
|
||||
config.async_support(true);
|
||||
wasmtime::Engine::new(&config).unwrap()
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
|
||||
impl WasmHost {
|
||||
pub fn new(
|
||||
|
@ -80,20 +75,8 @@ impl WasmHost {
|
|||
language_registry: Arc<LanguageRegistry>,
|
||||
work_dir: PathBuf,
|
||||
) -> Arc<Self> {
|
||||
let engine = WASM_ENGINE
|
||||
.get_or_init(|| {
|
||||
let mut config = wasmtime::Config::new();
|
||||
config.wasm_component_model(true);
|
||||
config.async_support(true);
|
||||
wasmtime::Engine::new(&config).unwrap()
|
||||
})
|
||||
.clone();
|
||||
let mut linker = Linker::new(&engine);
|
||||
wasi::command::add_to_linker(&mut linker).unwrap();
|
||||
wit::Extension::add_to_linker(&mut linker, wasi_view).unwrap();
|
||||
Arc::new(Self {
|
||||
engine,
|
||||
linker: Arc::new(linker),
|
||||
engine: wasm_engine(),
|
||||
fs,
|
||||
work_dir,
|
||||
http_client,
|
||||
|
@ -144,9 +127,8 @@ impl WasmHost {
|
|||
);
|
||||
|
||||
let (mut extension, instance) =
|
||||
wit::Extension::instantiate_async(&mut store, &component, &this.linker)
|
||||
.await
|
||||
.context("failed to instantiate wasm extension")?;
|
||||
Extension::instantiate_async(&mut store, zed_api_version, &component).await?;
|
||||
|
||||
extension
|
||||
.call_init_extension(&mut store)
|
||||
.await
|
||||
|
@ -170,7 +152,7 @@ impl WasmHost {
|
|||
}
|
||||
}
|
||||
|
||||
async fn build_wasi_ctx(&self, manifest: &Arc<ExtensionManifest>) -> Result<WasiCtx> {
|
||||
async fn build_wasi_ctx(&self, manifest: &Arc<ExtensionManifest>) -> Result<wasi::WasiCtx> {
|
||||
use cap_std::{ambient_authority, fs::Dir};
|
||||
|
||||
let extension_work_dir = self.work_dir.join(manifest.id.as_ref());
|
||||
|
@ -232,7 +214,7 @@ impl WasmExtension {
|
|||
T: 'static + Send,
|
||||
Fn: 'static
|
||||
+ Send
|
||||
+ for<'a> FnOnce(&'a mut wit::Extension, &'a mut Store<WasmState>) -> BoxFuture<'a, T>,
|
||||
+ for<'a> FnOnce(&'a mut Extension, &'a mut Store<WasmState>) -> BoxFuture<'a, T>,
|
||||
{
|
||||
let (return_tx, return_rx) = oneshot::channel();
|
||||
self.tx
|
||||
|
@ -249,279 +231,10 @@ impl WasmExtension {
|
|||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl wit::HostWorktree for WasmState {
|
||||
async fn read_text_file(
|
||||
&mut self,
|
||||
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
|
||||
path: String,
|
||||
) -> wasmtime::Result<Result<String, String>> {
|
||||
let delegate = self.table.get(&delegate)?;
|
||||
Ok(delegate
|
||||
.read_text_file(path.into())
|
||||
.await
|
||||
.map_err(|error| error.to_string()))
|
||||
impl WasmState {
|
||||
fn work_dir(&self) -> PathBuf {
|
||||
self.host.work_dir.join(self.manifest.id.as_ref())
|
||||
}
|
||||
|
||||
async fn shell_env(
|
||||
&mut self,
|
||||
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
|
||||
) -> wasmtime::Result<wit::EnvVars> {
|
||||
let delegate = self.table.get(&delegate)?;
|
||||
Ok(delegate.shell_env().await.into_iter().collect())
|
||||
}
|
||||
|
||||
async fn which(
|
||||
&mut self,
|
||||
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
|
||||
binary_name: String,
|
||||
) -> wasmtime::Result<Option<String>> {
|
||||
let delegate = self.table.get(&delegate)?;
|
||||
Ok(delegate
|
||||
.which(binary_name.as_ref())
|
||||
.await
|
||||
.map(|path| path.to_string_lossy().to_string()))
|
||||
}
|
||||
|
||||
fn drop(&mut self, _worktree: Resource<wit::Worktree>) -> Result<()> {
|
||||
// we only ever hand out borrows of worktrees
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl wit::ExtensionImports for WasmState {
|
||||
async fn npm_package_latest_version(
|
||||
&mut self,
|
||||
package_name: String,
|
||||
) -> wasmtime::Result<Result<String, String>> {
|
||||
async fn inner(this: &mut WasmState, package_name: String) -> anyhow::Result<String> {
|
||||
this.host
|
||||
.node_runtime
|
||||
.npm_package_latest_version(&package_name)
|
||||
.await
|
||||
}
|
||||
|
||||
Ok(inner(self, package_name)
|
||||
.await
|
||||
.map_err(|err| err.to_string()))
|
||||
}
|
||||
|
||||
async fn npm_package_installed_version(
|
||||
&mut self,
|
||||
package_name: String,
|
||||
) -> wasmtime::Result<Result<Option<String>, String>> {
|
||||
async fn inner(
|
||||
this: &mut WasmState,
|
||||
package_name: String,
|
||||
) -> anyhow::Result<Option<String>> {
|
||||
this.host
|
||||
.node_runtime
|
||||
.npm_package_installed_version(&this.host.work_dir, &package_name)
|
||||
.await
|
||||
}
|
||||
|
||||
Ok(inner(self, package_name)
|
||||
.await
|
||||
.map_err(|err| err.to_string()))
|
||||
}
|
||||
|
||||
async fn npm_install_package(
|
||||
&mut self,
|
||||
package_name: String,
|
||||
version: String,
|
||||
) -> wasmtime::Result<Result<(), String>> {
|
||||
async fn inner(
|
||||
this: &mut WasmState,
|
||||
package_name: String,
|
||||
version: String,
|
||||
) -> anyhow::Result<()> {
|
||||
this.host
|
||||
.node_runtime
|
||||
.npm_install_packages(&this.host.work_dir, &[(&package_name, &version)])
|
||||
.await
|
||||
}
|
||||
|
||||
Ok(inner(self, package_name, version)
|
||||
.await
|
||||
.map_err(|err| err.to_string()))
|
||||
}
|
||||
|
||||
async fn latest_github_release(
|
||||
&mut self,
|
||||
repo: String,
|
||||
options: wit::GithubReleaseOptions,
|
||||
) -> wasmtime::Result<Result<wit::GithubRelease, String>> {
|
||||
async fn inner(
|
||||
this: &mut WasmState,
|
||||
repo: String,
|
||||
options: wit::GithubReleaseOptions,
|
||||
) -> anyhow::Result<wit::GithubRelease> {
|
||||
let release = util::github::latest_github_release(
|
||||
&repo,
|
||||
options.require_assets,
|
||||
options.pre_release,
|
||||
this.host.http_client.clone(),
|
||||
)
|
||||
.await?;
|
||||
Ok(wit::GithubRelease {
|
||||
version: release.tag_name,
|
||||
assets: release
|
||||
.assets
|
||||
.into_iter()
|
||||
.map(|asset| wit::GithubReleaseAsset {
|
||||
name: asset.name,
|
||||
download_url: asset.browser_download_url,
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
|
||||
Ok(inner(self, repo, options)
|
||||
.await
|
||||
.map_err(|err| err.to_string()))
|
||||
}
|
||||
|
||||
async fn current_platform(&mut self) -> Result<(wit::Os, wit::Architecture)> {
|
||||
Ok((
|
||||
match env::consts::OS {
|
||||
"macos" => wit::Os::Mac,
|
||||
"linux" => wit::Os::Linux,
|
||||
"windows" => wit::Os::Windows,
|
||||
_ => panic!("unsupported os"),
|
||||
},
|
||||
match env::consts::ARCH {
|
||||
"aarch64" => wit::Architecture::Aarch64,
|
||||
"x86" => wit::Architecture::X86,
|
||||
"x86_64" => wit::Architecture::X8664,
|
||||
_ => panic!("unsupported architecture"),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
async fn set_language_server_installation_status(
|
||||
&mut self,
|
||||
server_name: String,
|
||||
status: wit::LanguageServerInstallationStatus,
|
||||
) -> wasmtime::Result<()> {
|
||||
let status = match status {
|
||||
wit::LanguageServerInstallationStatus::CheckingForUpdate => {
|
||||
LanguageServerBinaryStatus::CheckingForUpdate
|
||||
}
|
||||
wit::LanguageServerInstallationStatus::Downloading => {
|
||||
LanguageServerBinaryStatus::Downloading
|
||||
}
|
||||
wit::LanguageServerInstallationStatus::Downloaded => {
|
||||
LanguageServerBinaryStatus::Downloaded
|
||||
}
|
||||
wit::LanguageServerInstallationStatus::Cached => LanguageServerBinaryStatus::Cached,
|
||||
wit::LanguageServerInstallationStatus::Failed(error) => {
|
||||
LanguageServerBinaryStatus::Failed { error }
|
||||
}
|
||||
};
|
||||
|
||||
self.host
|
||||
.language_registry
|
||||
.update_lsp_status(language::LanguageServerName(server_name.into()), status);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn download_file(
|
||||
&mut self,
|
||||
url: String,
|
||||
path: String,
|
||||
file_type: wit::DownloadedFileType,
|
||||
) -> wasmtime::Result<Result<(), String>> {
|
||||
let path = PathBuf::from(path);
|
||||
|
||||
async fn inner(
|
||||
this: &mut WasmState,
|
||||
url: String,
|
||||
path: PathBuf,
|
||||
file_type: wit::DownloadedFileType,
|
||||
) -> anyhow::Result<()> {
|
||||
let extension_work_dir = this.host.work_dir.join(this.manifest.id.as_ref());
|
||||
|
||||
this.host.fs.create_dir(&extension_work_dir).await?;
|
||||
|
||||
let destination_path = this
|
||||
.host
|
||||
.writeable_path_from_extension(&this.manifest.id, &path)?;
|
||||
|
||||
let mut response = this
|
||||
.host
|
||||
.http_client
|
||||
.get(&url, Default::default(), true)
|
||||
.await
|
||||
.map_err(|err| anyhow!("error downloading release: {}", err))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
Err(anyhow!(
|
||||
"download failed with status {}",
|
||||
response.status().to_string()
|
||||
))?;
|
||||
}
|
||||
let body = BufReader::new(response.body_mut());
|
||||
|
||||
match file_type {
|
||||
wit::DownloadedFileType::Uncompressed => {
|
||||
futures::pin_mut!(body);
|
||||
this.host
|
||||
.fs
|
||||
.create_file_with(&destination_path, body)
|
||||
.await?;
|
||||
}
|
||||
wit::DownloadedFileType::Gzip => {
|
||||
let body = GzipDecoder::new(body);
|
||||
futures::pin_mut!(body);
|
||||
this.host
|
||||
.fs
|
||||
.create_file_with(&destination_path, body)
|
||||
.await?;
|
||||
}
|
||||
wit::DownloadedFileType::GzipTar => {
|
||||
let body = GzipDecoder::new(body);
|
||||
futures::pin_mut!(body);
|
||||
this.host
|
||||
.fs
|
||||
.extract_tar_file(&destination_path, Archive::new(body))
|
||||
.await?;
|
||||
}
|
||||
wit::DownloadedFileType::Zip => {
|
||||
let 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);
|
||||
this.host.fs.create_file_with(&zip_path, body).await?;
|
||||
|
||||
let unzip_status = std::process::Command::new("unzip")
|
||||
.current_dir(&extension_work_dir)
|
||||
.arg(&zip_path)
|
||||
.output()?
|
||||
.status;
|
||||
if !unzip_status.success() {
|
||||
Err(anyhow!("failed to unzip {} archive", path.display()))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Ok(inner(self, url, path, file_type)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|err| err.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
fn wasi_view(state: &mut WasmState) -> &mut WasmState {
|
||||
state
|
||||
}
|
||||
|
||||
impl wasi::WasiView for WasmState {
|
||||
|
|
103
crates/extension/src/wasm_host/wit.rs
Normal file
103
crates/extension/src/wasm_host/wit.rs
Normal file
|
@ -0,0 +1,103 @@
|
|||
mod v0_0_1;
|
||||
mod v0_0_4;
|
||||
|
||||
use super::{wasm_engine, WasmState};
|
||||
use anyhow::{Context, Result};
|
||||
use language::LspAdapterDelegate;
|
||||
use std::sync::Arc;
|
||||
use util::SemanticVersion;
|
||||
use wasmtime::{
|
||||
component::{Component, Instance, Linker, Resource},
|
||||
Store,
|
||||
};
|
||||
|
||||
use v0_0_4 as latest;
|
||||
|
||||
pub use latest::{Command, LanguageServerConfig};
|
||||
|
||||
pub fn new_linker(
|
||||
f: impl Fn(&mut Linker<WasmState>, fn(&mut WasmState) -> &mut WasmState) -> Result<()>,
|
||||
) -> Linker<WasmState> {
|
||||
let mut linker = Linker::new(&wasm_engine());
|
||||
wasmtime_wasi::command::add_to_linker(&mut linker).unwrap();
|
||||
f(&mut linker, wasi_view).unwrap();
|
||||
linker
|
||||
}
|
||||
|
||||
fn wasi_view(state: &mut WasmState) -> &mut WasmState {
|
||||
state
|
||||
}
|
||||
|
||||
pub enum Extension {
|
||||
V004(v0_0_4::Extension),
|
||||
V001(v0_0_1::Extension),
|
||||
}
|
||||
|
||||
impl Extension {
|
||||
pub async fn instantiate_async(
|
||||
store: &mut Store<WasmState>,
|
||||
version: SemanticVersion,
|
||||
component: &Component,
|
||||
) -> Result<(Self, Instance)> {
|
||||
if version < latest::VERSION {
|
||||
let (extension, instance) =
|
||||
v0_0_1::Extension::instantiate_async(store, &component, v0_0_1::linker())
|
||||
.await
|
||||
.context("failed to instantiate wasm extension")?;
|
||||
Ok((Self::V001(extension), instance))
|
||||
} else {
|
||||
let (extension, instance) =
|
||||
v0_0_4::Extension::instantiate_async(store, &component, v0_0_4::linker())
|
||||
.await
|
||||
.context("failed to instantiate wasm extension")?;
|
||||
Ok((Self::V004(extension), instance))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn call_init_extension(&self, store: &mut Store<WasmState>) -> Result<()> {
|
||||
match self {
|
||||
Extension::V004(ext) => ext.call_init_extension(store).await,
|
||||
Extension::V001(ext) => ext.call_init_extension(store).await,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn call_language_server_command(
|
||||
&self,
|
||||
store: &mut Store<WasmState>,
|
||||
config: &LanguageServerConfig,
|
||||
resource: Resource<Arc<dyn LspAdapterDelegate>>,
|
||||
) -> Result<Result<Command, String>> {
|
||||
match self {
|
||||
Extension::V004(ext) => {
|
||||
ext.call_language_server_command(store, config, resource)
|
||||
.await
|
||||
}
|
||||
Extension::V001(ext) => Ok(ext
|
||||
.call_language_server_command(store, &config.clone().into(), resource)
|
||||
.await?
|
||||
.map(|command| command.into())),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn call_language_server_initialization_options(
|
||||
&self,
|
||||
store: &mut Store<WasmState>,
|
||||
config: &LanguageServerConfig,
|
||||
resource: Resource<Arc<dyn LspAdapterDelegate>>,
|
||||
) -> Result<Result<Option<String>, String>> {
|
||||
match self {
|
||||
Extension::V004(ext) => {
|
||||
ext.call_language_server_initialization_options(store, config, resource)
|
||||
.await
|
||||
}
|
||||
Extension::V001(ext) => {
|
||||
ext.call_language_server_initialization_options(
|
||||
store,
|
||||
&config.clone().into(),
|
||||
resource,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
210
crates/extension/src/wasm_host/wit/v0_0_1.rs
Normal file
210
crates/extension/src/wasm_host/wit/v0_0_1.rs
Normal file
|
@ -0,0 +1,210 @@
|
|||
use super::latest;
|
||||
use crate::wasm_host::WasmState;
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use language::{LanguageServerBinaryStatus, LspAdapterDelegate};
|
||||
use std::sync::{Arc, OnceLock};
|
||||
use wasmtime::component::{Linker, Resource};
|
||||
|
||||
wasmtime::component::bindgen!({
|
||||
async: true,
|
||||
path: "../extension_api/wit/0.0.1",
|
||||
with: {
|
||||
"worktree": ExtensionWorktree,
|
||||
},
|
||||
});
|
||||
|
||||
pub type ExtensionWorktree = Arc<dyn LspAdapterDelegate>;
|
||||
|
||||
pub fn linker() -> &'static Linker<WasmState> {
|
||||
static LINKER: OnceLock<Linker<WasmState>> = OnceLock::new();
|
||||
LINKER.get_or_init(|| super::new_linker(Extension::add_to_linker))
|
||||
}
|
||||
|
||||
impl From<latest::Os> for Os {
|
||||
fn from(value: latest::Os) -> Self {
|
||||
match value {
|
||||
latest::Os::Mac => Os::Mac,
|
||||
latest::Os::Linux => Os::Linux,
|
||||
latest::Os::Windows => Os::Windows,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<latest::Architecture> for Architecture {
|
||||
fn from(value: latest::Architecture) -> Self {
|
||||
match value {
|
||||
latest::Architecture::Aarch64 => Self::Aarch64,
|
||||
latest::Architecture::X86 => Self::X86,
|
||||
latest::Architecture::X8664 => Self::X8664,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<latest::GithubRelease> for GithubRelease {
|
||||
fn from(value: latest::GithubRelease) -> Self {
|
||||
Self {
|
||||
version: value.version,
|
||||
assets: value.assets.into_iter().map(|asset| asset.into()).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<latest::GithubReleaseAsset> for GithubReleaseAsset {
|
||||
fn from(value: latest::GithubReleaseAsset) -> Self {
|
||||
Self {
|
||||
name: value.name,
|
||||
download_url: value.download_url,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GithubReleaseOptions> for latest::GithubReleaseOptions {
|
||||
fn from(value: GithubReleaseOptions) -> Self {
|
||||
Self {
|
||||
require_assets: value.require_assets,
|
||||
pre_release: value.pre_release,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DownloadedFileType> for latest::DownloadedFileType {
|
||||
fn from(value: DownloadedFileType) -> Self {
|
||||
match value {
|
||||
DownloadedFileType::Gzip => latest::DownloadedFileType::Gzip,
|
||||
DownloadedFileType::GzipTar => latest::DownloadedFileType::GzipTar,
|
||||
DownloadedFileType::Zip => latest::DownloadedFileType::Zip,
|
||||
DownloadedFileType::Uncompressed => latest::DownloadedFileType::Uncompressed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<latest::LanguageServerConfig> for LanguageServerConfig {
|
||||
fn from(value: latest::LanguageServerConfig) -> Self {
|
||||
Self {
|
||||
name: value.name,
|
||||
language_name: value.language_name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Command> for latest::Command {
|
||||
fn from(value: Command) -> Self {
|
||||
Self {
|
||||
command: value.command,
|
||||
args: value.args,
|
||||
env: value.env,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl HostWorktree for WasmState {
|
||||
async fn read_text_file(
|
||||
&mut self,
|
||||
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
|
||||
path: String,
|
||||
) -> wasmtime::Result<Result<String, String>> {
|
||||
latest::HostWorktree::read_text_file(self, delegate, path).await
|
||||
}
|
||||
|
||||
async fn shell_env(
|
||||
&mut self,
|
||||
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
|
||||
) -> wasmtime::Result<EnvVars> {
|
||||
latest::HostWorktree::shell_env(self, delegate).await
|
||||
}
|
||||
|
||||
async fn which(
|
||||
&mut self,
|
||||
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
|
||||
binary_name: String,
|
||||
) -> wasmtime::Result<Option<String>> {
|
||||
latest::HostWorktree::which(self, delegate, binary_name).await
|
||||
}
|
||||
|
||||
fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ExtensionImports for WasmState {
|
||||
async fn node_binary_path(&mut self) -> wasmtime::Result<Result<String, String>> {
|
||||
latest::ExtensionImports::node_binary_path(self).await
|
||||
}
|
||||
|
||||
async fn npm_package_latest_version(
|
||||
&mut self,
|
||||
package_name: String,
|
||||
) -> wasmtime::Result<Result<String, String>> {
|
||||
latest::ExtensionImports::npm_package_latest_version(self, package_name).await
|
||||
}
|
||||
|
||||
async fn npm_package_installed_version(
|
||||
&mut self,
|
||||
package_name: String,
|
||||
) -> wasmtime::Result<Result<Option<String>, String>> {
|
||||
latest::ExtensionImports::npm_package_installed_version(self, package_name).await
|
||||
}
|
||||
|
||||
async fn npm_install_package(
|
||||
&mut self,
|
||||
package_name: String,
|
||||
version: String,
|
||||
) -> wasmtime::Result<Result<(), String>> {
|
||||
latest::ExtensionImports::npm_install_package(self, package_name, version).await
|
||||
}
|
||||
|
||||
async fn latest_github_release(
|
||||
&mut self,
|
||||
repo: String,
|
||||
options: GithubReleaseOptions,
|
||||
) -> wasmtime::Result<Result<GithubRelease, String>> {
|
||||
Ok(
|
||||
latest::ExtensionImports::latest_github_release(self, repo, options.into())
|
||||
.await?
|
||||
.map(|github| github.into()),
|
||||
)
|
||||
}
|
||||
|
||||
async fn current_platform(&mut self) -> Result<(Os, Architecture)> {
|
||||
latest::ExtensionImports::current_platform(self)
|
||||
.await
|
||||
.map(|(os, arch)| (os.into(), arch.into()))
|
||||
}
|
||||
|
||||
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::Cached
|
||||
| LanguageServerInstallationStatus::Downloaded => 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<Result<(), String>> {
|
||||
latest::ExtensionImports::download_file(self, url, path, file_type.into()).await
|
||||
}
|
||||
}
|
284
crates/extension/src/wasm_host/wit/v0_0_4.rs
Normal file
284
crates/extension/src/wasm_host/wit/v0_0_4.rs
Normal file
|
@ -0,0 +1,284 @@
|
|||
use crate::wasm_host::WasmState;
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_compression::futures::bufread::GzipDecoder;
|
||||
use async_tar::Archive;
|
||||
use async_trait::async_trait;
|
||||
use futures::io::BufReader;
|
||||
use language::{LanguageServerBinaryStatus, LspAdapterDelegate};
|
||||
use std::{
|
||||
env,
|
||||
path::PathBuf,
|
||||
sync::{Arc, OnceLock},
|
||||
};
|
||||
use util::{maybe, SemanticVersion};
|
||||
use wasmtime::component::{Linker, Resource};
|
||||
|
||||
pub const VERSION: SemanticVersion = SemanticVersion {
|
||||
major: 0,
|
||||
minor: 0,
|
||||
patch: 4,
|
||||
};
|
||||
|
||||
wasmtime::component::bindgen!({
|
||||
async: true,
|
||||
path: "../extension_api/wit/0.0.4",
|
||||
with: {
|
||||
"worktree": ExtensionWorktree,
|
||||
},
|
||||
});
|
||||
|
||||
pub type ExtensionWorktree = Arc<dyn LspAdapterDelegate>;
|
||||
|
||||
pub fn linker() -> &'static Linker<WasmState> {
|
||||
static LINKER: OnceLock<Linker<WasmState>> = OnceLock::new();
|
||||
LINKER.get_or_init(|| super::new_linker(Extension::add_to_linker))
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl HostWorktree for WasmState {
|
||||
async fn read_text_file(
|
||||
&mut self,
|
||||
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
|
||||
path: String,
|
||||
) -> wasmtime::Result<Result<String, String>> {
|
||||
let delegate = self.table.get(&delegate)?;
|
||||
Ok(delegate
|
||||
.read_text_file(path.into())
|
||||
.await
|
||||
.map_err(|error| error.to_string()))
|
||||
}
|
||||
|
||||
async fn shell_env(
|
||||
&mut self,
|
||||
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
|
||||
) -> wasmtime::Result<EnvVars> {
|
||||
let delegate = self.table.get(&delegate)?;
|
||||
Ok(delegate.shell_env().await.into_iter().collect())
|
||||
}
|
||||
|
||||
async fn which(
|
||||
&mut self,
|
||||
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
|
||||
binary_name: String,
|
||||
) -> wasmtime::Result<Option<String>> {
|
||||
let delegate = self.table.get(&delegate)?;
|
||||
Ok(delegate
|
||||
.which(binary_name.as_ref())
|
||||
.await
|
||||
.map(|path| path.to_string_lossy().to_string()))
|
||||
}
|
||||
|
||||
fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {
|
||||
// we only ever hand out borrows of worktrees
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ExtensionImports for WasmState {
|
||||
async fn node_binary_path(&mut self) -> wasmtime::Result<Result<String, String>> {
|
||||
convert_result(
|
||||
self.host
|
||||
.node_runtime
|
||||
.binary_path()
|
||||
.await
|
||||
.map(|path| path.to_string_lossy().to_string()),
|
||||
)
|
||||
}
|
||||
|
||||
async fn npm_package_latest_version(
|
||||
&mut self,
|
||||
package_name: String,
|
||||
) -> wasmtime::Result<Result<String, String>> {
|
||||
convert_result(
|
||||
self.host
|
||||
.node_runtime
|
||||
.npm_package_latest_version(&package_name)
|
||||
.await,
|
||||
)
|
||||
}
|
||||
|
||||
async fn npm_package_installed_version(
|
||||
&mut self,
|
||||
package_name: String,
|
||||
) -> wasmtime::Result<Result<Option<String>, String>> {
|
||||
convert_result(
|
||||
self.host
|
||||
.node_runtime
|
||||
.npm_package_installed_version(&self.work_dir(), &package_name)
|
||||
.await,
|
||||
)
|
||||
}
|
||||
|
||||
async fn npm_install_package(
|
||||
&mut self,
|
||||
package_name: String,
|
||||
version: String,
|
||||
) -> wasmtime::Result<Result<(), String>> {
|
||||
convert_result(
|
||||
self.host
|
||||
.node_runtime
|
||||
.npm_install_packages(&self.work_dir(), &[(&package_name, &version)])
|
||||
.await,
|
||||
)
|
||||
}
|
||||
|
||||
async fn latest_github_release(
|
||||
&mut self,
|
||||
repo: String,
|
||||
options: GithubReleaseOptions,
|
||||
) -> wasmtime::Result<Result<GithubRelease, String>> {
|
||||
convert_result(
|
||||
maybe!(async {
|
||||
let release = util::github::latest_github_release(
|
||||
&repo,
|
||||
options.require_assets,
|
||||
options.pre_release,
|
||||
self.host.http_client.clone(),
|
||||
)
|
||||
.await?;
|
||||
Ok(GithubRelease {
|
||||
version: release.tag_name,
|
||||
assets: release
|
||||
.assets
|
||||
.into_iter()
|
||||
.map(|asset| GithubReleaseAsset {
|
||||
name: asset.name,
|
||||
download_url: asset.browser_download_url,
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
})
|
||||
.await,
|
||||
)
|
||||
}
|
||||
|
||||
async fn current_platform(&mut self) -> Result<(Os, Architecture)> {
|
||||
Ok((
|
||||
match env::consts::OS {
|
||||
"macos" => Os::Mac,
|
||||
"linux" => Os::Linux,
|
||||
"windows" => Os::Windows,
|
||||
_ => panic!("unsupported os"),
|
||||
},
|
||||
match env::consts::ARCH {
|
||||
"aarch64" => Architecture::Aarch64,
|
||||
"x86" => Architecture::X86,
|
||||
"x86_64" => Architecture::X8664,
|
||||
_ => panic!("unsupported architecture"),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
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<Result<(), String>> {
|
||||
let 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(&zip_path)
|
||||
.output()?
|
||||
.status;
|
||||
if !unzip_status.success() {
|
||||
Err(anyhow!("failed to unzip {} archive", path.display()))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await;
|
||||
convert_result(result)
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_result<T>(result: Result<T>) -> wasmtime::Result<Result<T, String>> {
|
||||
Ok(result.map_err(|error| error.to_string()))
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue