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:
Max Brunsfeld 2024-03-22 17:29:06 -07:00 committed by GitHub
parent 4459eacc98
commit 6ebe599c98
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
70 changed files with 1278 additions and 1223 deletions

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

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

View 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()))
}