zed_extension_api: Fork new version of extension API (#29579)

This PR forks a new version of the `zed_extension_api` in preparation
for new changes.

Release Notes:

- N/A
This commit is contained in:
Marshall Bowers 2025-04-28 21:24:13 -04:00 committed by GitHub
parent bc665b2a76
commit 2cc5a0de26
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 1464 additions and 650 deletions

View file

@ -5,11 +5,12 @@ mod since_v0_1_0;
mod since_v0_2_0;
mod since_v0_3_0;
mod since_v0_4_0;
mod since_v0_5_0;
use extension::{KeyValueStoreDelegate, WorktreeDelegate};
use language::LanguageName;
use lsp::LanguageServerName;
use release_channel::ReleaseChannel;
use since_v0_4_0 as latest;
use since_v0_5_0 as latest;
use super::{WasmState, wasm_engine};
use anyhow::{Context as _, Result, anyhow};
@ -60,7 +61,7 @@ pub fn wasm_api_version_range(release_channel: ReleaseChannel) -> RangeInclusive
let max_version = match release_channel {
ReleaseChannel::Dev | ReleaseChannel::Nightly => latest::MAX_VERSION,
ReleaseChannel::Stable | ReleaseChannel::Preview => latest::MAX_VERSION,
ReleaseChannel::Stable | ReleaseChannel::Preview => since_v0_4_0::MAX_VERSION,
};
since_v0_0_1::MIN_VERSION..=max_version
@ -90,6 +91,7 @@ pub fn authorize_access_to_unreleased_wasm_api_version(
}
pub enum Extension {
V0_5_0(since_v0_5_0::Extension),
V0_4_0(since_v0_4_0::Extension),
V0_3_0(since_v0_3_0::Extension),
V0_2_0(since_v0_2_0::Extension),
@ -110,10 +112,21 @@ impl Extension {
let _ = release_channel;
if version >= latest::MIN_VERSION {
authorize_access_to_unreleased_wasm_api_version(release_channel)?;
let extension =
latest::Extension::instantiate_async(store, component, latest::linker())
.await
.context("failed to instantiate wasm extension")?;
Ok(Self::V0_5_0(extension))
} else if version >= since_v0_4_0::MIN_VERSION {
let extension = since_v0_4_0::Extension::instantiate_async(
store,
component,
since_v0_4_0::linker(),
)
.await
.context("failed to instantiate wasm extension")?;
Ok(Self::V0_4_0(extension))
} else if version >= since_v0_3_0::MIN_VERSION {
let extension = since_v0_3_0::Extension::instantiate_async(
@ -174,6 +187,7 @@ impl Extension {
pub async fn call_init_extension(&self, store: &mut Store<WasmState>) -> Result<()> {
match self {
Extension::V0_5_0(ext) => ext.call_init_extension(store).await,
Extension::V0_4_0(ext) => ext.call_init_extension(store).await,
Extension::V0_3_0(ext) => ext.call_init_extension(store).await,
Extension::V0_2_0(ext) => ext.call_init_extension(store).await,
@ -192,6 +206,10 @@ impl Extension {
resource: Resource<Arc<dyn WorktreeDelegate>>,
) -> Result<Result<Command, String>> {
match self {
Extension::V0_5_0(ext) => {
ext.call_language_server_command(store, &language_server_id.0, resource)
.await
}
Extension::V0_4_0(ext) => {
ext.call_language_server_command(store, &language_server_id.0, resource)
.await
@ -246,6 +264,14 @@ impl Extension {
resource: Resource<Arc<dyn WorktreeDelegate>>,
) -> Result<Result<Option<String>, String>> {
match self {
Extension::V0_5_0(ext) => {
ext.call_language_server_initialization_options(
store,
&language_server_id.0,
resource,
)
.await
}
Extension::V0_4_0(ext) => {
ext.call_language_server_initialization_options(
store,
@ -319,6 +345,14 @@ impl Extension {
resource: Resource<Arc<dyn WorktreeDelegate>>,
) -> Result<Result<Option<String>, String>> {
match self {
Extension::V0_5_0(ext) => {
ext.call_language_server_workspace_configuration(
store,
&language_server_id.0,
resource,
)
.await
}
Extension::V0_4_0(ext) => {
ext.call_language_server_workspace_configuration(
store,
@ -371,6 +405,15 @@ impl Extension {
resource: Resource<Arc<dyn WorktreeDelegate>>,
) -> Result<Result<Option<String>, String>> {
match self {
Extension::V0_5_0(ext) => {
ext.call_language_server_additional_initialization_options(
store,
&language_server_id.0,
&target_language_server_id.0,
resource,
)
.await
}
Extension::V0_4_0(ext) => {
ext.call_language_server_additional_initialization_options(
store,
@ -397,6 +440,15 @@ impl Extension {
resource: Resource<Arc<dyn WorktreeDelegate>>,
) -> Result<Result<Option<String>, String>> {
match self {
Extension::V0_5_0(ext) => {
ext.call_language_server_additional_workspace_configuration(
store,
&language_server_id.0,
&target_language_server_id.0,
resource,
)
.await
}
Extension::V0_4_0(ext) => {
ext.call_language_server_additional_workspace_configuration(
store,
@ -422,10 +474,23 @@ impl Extension {
completions: Vec<latest::Completion>,
) -> Result<Result<Vec<Option<CodeLabel>>, String>> {
match self {
Extension::V0_4_0(ext) => {
Extension::V0_5_0(ext) => {
ext.call_labels_for_completions(store, &language_server_id.0, &completions)
.await
}
Extension::V0_4_0(ext) => Ok(ext
.call_labels_for_completions(
store,
&language_server_id.0,
&completions.into_iter().collect::<Vec<_>>(),
)
.await?
.map(|labels| {
labels
.into_iter()
.map(|label| label.map(Into::into))
.collect()
})),
Extension::V0_3_0(ext) => Ok(ext
.call_labels_for_completions(
store,
@ -489,10 +554,23 @@ impl Extension {
symbols: Vec<latest::Symbol>,
) -> Result<Result<Vec<Option<CodeLabel>>, String>> {
match self {
Extension::V0_4_0(ext) => {
Extension::V0_5_0(ext) => {
ext.call_labels_for_symbols(store, &language_server_id.0, &symbols)
.await
}
Extension::V0_4_0(ext) => Ok(ext
.call_labels_for_symbols(
store,
&language_server_id.0,
&symbols.into_iter().collect::<Vec<_>>(),
)
.await?
.map(|labels| {
labels
.into_iter()
.map(|label| label.map(Into::into))
.collect()
})),
Extension::V0_3_0(ext) => Ok(ext
.call_labels_for_symbols(
store,
@ -556,6 +634,10 @@ impl Extension {
arguments: &[String],
) -> Result<Result<Vec<SlashCommandArgumentCompletion>, String>> {
match self {
Extension::V0_5_0(ext) => {
ext.call_complete_slash_command_argument(store, command, arguments)
.await
}
Extension::V0_4_0(ext) => {
ext.call_complete_slash_command_argument(store, command, arguments)
.await
@ -586,6 +668,10 @@ impl Extension {
resource: Option<Resource<Arc<dyn WorktreeDelegate>>>,
) -> Result<Result<SlashCommandOutput, String>> {
match self {
Extension::V0_5_0(ext) => {
ext.call_run_slash_command(store, command, arguments, resource)
.await
}
Extension::V0_4_0(ext) => {
ext.call_run_slash_command(store, command, arguments, resource)
.await
@ -615,6 +701,10 @@ impl Extension {
project: Resource<ExtensionProject>,
) -> Result<Result<Command, String>> {
match self {
Extension::V0_5_0(ext) => {
ext.call_context_server_command(store, &context_server_id, project)
.await
}
Extension::V0_4_0(ext) => {
ext.call_context_server_command(store, &context_server_id, project)
.await
@ -642,6 +732,7 @@ impl Extension {
provider: &str,
) -> Result<Result<Vec<String>, String>> {
match self {
Extension::V0_5_0(ext) => ext.call_suggest_docs_packages(store, provider).await,
Extension::V0_4_0(ext) => ext.call_suggest_docs_packages(store, provider).await,
Extension::V0_3_0(ext) => ext.call_suggest_docs_packages(store, provider).await,
Extension::V0_2_0(ext) => ext.call_suggest_docs_packages(store, provider).await,
@ -660,6 +751,10 @@ impl Extension {
kv_store: Resource<Arc<dyn KeyValueStoreDelegate>>,
) -> Result<Result<(), String>> {
match self {
Extension::V0_5_0(ext) => {
ext.call_index_docs(store, provider, package_name, kv_store)
.await
}
Extension::V0_4_0(ext) => {
ext.call_index_docs(store, provider, package_name, kv_store)
.await

View file

@ -1,29 +1,12 @@
use crate::wasm_host::wit::since_v0_4_0::slash_command::SlashCommandOutputSection;
use crate::wasm_host::wit::{CompletionKind, CompletionLabelDetails, InsertTextFormat, SymbolKind};
use crate::wasm_host::{WasmState, wit::ToWasmtimeResult};
use ::http_client::{AsyncBody, HttpRequestExt};
use ::settings::{Settings, WorktreeId};
use anyhow::{Context, Result, anyhow, bail};
use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use async_trait::async_trait;
use context_server_settings::ContextServerSettings;
use extension::{
ExtensionLanguageServerProxy, KeyValueStoreDelegate, ProjectDelegate, WorktreeDelegate,
};
use futures::{AsyncReadExt, lock::Mutex};
use futures::{FutureExt as _, io::BufReader};
use language::{BinaryStatus, LanguageName, language_settings::AllLanguageSettings};
use project::project_settings::ProjectSettings;
use crate::wasm_host::WasmState;
use anyhow::Result;
use extension::{KeyValueStoreDelegate, ProjectDelegate, WorktreeDelegate};
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};
use super::latest;
pub const MIN_VERSION: SemanticVersion = SemanticVersion::new(0, 4, 0);
pub const MAX_VERSION: SemanticVersion = SemanticVersion::new(0, 4, 0);
@ -32,15 +15,20 @@ wasmtime::component::bindgen!({
trappable_imports: true,
path: "../extension_api/wit/since_v0.4.0",
with: {
"worktree": ExtensionWorktree,
"project": ExtensionProject,
"key-value-store": ExtensionKeyValueStore,
"zed:extension/http-client/http-response-stream": ExtensionHttpResponseStream
"worktree": ExtensionWorktree,
"project": ExtensionProject,
"key-value-store": ExtensionKeyValueStore,
"zed:extension/common": latest::zed::extension::common,
"zed:extension/github": latest::zed::extension::github,
"zed:extension/http-client": latest::zed::extension::http_client,
"zed:extension/lsp": latest::zed::extension::lsp,
"zed:extension/nodejs": latest::zed::extension::nodejs,
"zed:extension/platform": latest::zed::extension::platform,
"zed:extension/process": latest::zed::extension::process,
"zed:extension/slash-command": latest::zed::extension::slash_command,
},
});
pub use self::zed::extension::*;
mod settings {
include!(concat!(env!("OUT_DIR"), "/since_v0.4.0/settings.rs"));
}
@ -48,51 +36,32 @@ mod settings {
pub type ExtensionWorktree = Arc<dyn WorktreeDelegate>;
pub type ExtensionProject = Arc<dyn ProjectDelegate>;
pub type ExtensionKeyValueStore = Arc<dyn KeyValueStoreDelegate>;
pub type ExtensionHttpResponseStream = Arc<Mutex<::http_client::Response<AsyncBody>>>;
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<Range> for std::ops::Range<usize> {
fn from(range: Range) -> Self {
let start = range.start as usize;
let end = range.end as usize;
start..end
}
}
impl From<Command> for extension::Command {
fn from(value: Command) -> Self {
Self {
command: value.command,
args: value.args,
env: value.env,
}
}
}
impl From<CodeLabel> for extension::CodeLabel {
impl From<CodeLabel> 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(),
filter_range: value.filter_range,
}
}
}
impl From<CodeLabelSpan> for extension::CodeLabelSpan {
impl From<CodeLabelSpan> for latest::CodeLabelSpan {
fn from(value: CodeLabelSpan) -> Self {
match value {
CodeLabelSpan::CodeRange(range) => Self::CodeRange(range.into()),
CodeLabelSpan::CodeRange(range) => Self::CodeRange(range),
CodeLabelSpan::Literal(literal) => Self::Literal(literal.into()),
}
}
}
impl From<CodeLabelSpanLiteral> for extension::CodeLabelSpanLiteral {
impl From<CodeLabelSpanLiteral> for latest::CodeLabelSpanLiteral {
fn from(value: CodeLabelSpanLiteral) -> Self {
Self {
text: value.text,
@ -101,148 +70,33 @@ impl From<CodeLabelSpanLiteral> for extension::CodeLabelSpanLiteral {
}
}
impl From<extension::Completion> for Completion {
fn from(value: extension::Completion) -> Self {
impl From<SettingsLocation> for latest::SettingsLocation {
fn from(value: SettingsLocation) -> Self {
Self {
label: value.label,
label_details: value.label_details.map(Into::into),
detail: value.detail,
kind: value.kind.map(Into::into),
insert_text_format: value.insert_text_format.map(Into::into),
worktree_id: value.worktree_id,
path: value.path,
}
}
}
impl From<extension::CompletionLabelDetails> for CompletionLabelDetails {
fn from(value: extension::CompletionLabelDetails) -> Self {
Self {
detail: value.detail,
description: value.description,
}
}
}
impl From<extension::CompletionKind> for CompletionKind {
fn from(value: extension::CompletionKind) -> Self {
impl From<LanguageServerInstallationStatus> for latest::LanguageServerInstallationStatus {
fn from(value: LanguageServerInstallationStatus) -> Self {
match value {
extension::CompletionKind::Text => Self::Text,
extension::CompletionKind::Method => Self::Method,
extension::CompletionKind::Function => Self::Function,
extension::CompletionKind::Constructor => Self::Constructor,
extension::CompletionKind::Field => Self::Field,
extension::CompletionKind::Variable => Self::Variable,
extension::CompletionKind::Class => Self::Class,
extension::CompletionKind::Interface => Self::Interface,
extension::CompletionKind::Module => Self::Module,
extension::CompletionKind::Property => Self::Property,
extension::CompletionKind::Unit => Self::Unit,
extension::CompletionKind::Value => Self::Value,
extension::CompletionKind::Enum => Self::Enum,
extension::CompletionKind::Keyword => Self::Keyword,
extension::CompletionKind::Snippet => Self::Snippet,
extension::CompletionKind::Color => Self::Color,
extension::CompletionKind::File => Self::File,
extension::CompletionKind::Reference => Self::Reference,
extension::CompletionKind::Folder => Self::Folder,
extension::CompletionKind::EnumMember => Self::EnumMember,
extension::CompletionKind::Constant => Self::Constant,
extension::CompletionKind::Struct => Self::Struct,
extension::CompletionKind::Event => Self::Event,
extension::CompletionKind::Operator => Self::Operator,
extension::CompletionKind::TypeParameter => Self::TypeParameter,
extension::CompletionKind::Other(value) => Self::Other(value),
LanguageServerInstallationStatus::None => Self::None,
LanguageServerInstallationStatus::Downloading => Self::Downloading,
LanguageServerInstallationStatus::CheckingForUpdate => Self::CheckingForUpdate,
LanguageServerInstallationStatus::Failed(message) => Self::Failed(message),
}
}
}
impl From<extension::InsertTextFormat> for InsertTextFormat {
fn from(value: extension::InsertTextFormat) -> Self {
impl From<DownloadedFileType> for latest::DownloadedFileType {
fn from(value: DownloadedFileType) -> Self {
match value {
extension::InsertTextFormat::PlainText => Self::PlainText,
extension::InsertTextFormat::Snippet => Self::Snippet,
extension::InsertTextFormat::Other(value) => Self::Other(value),
}
}
}
impl From<extension::Symbol> for Symbol {
fn from(value: extension::Symbol) -> Self {
Self {
kind: value.kind.into(),
name: value.name,
}
}
}
impl From<extension::SymbolKind> for SymbolKind {
fn from(value: extension::SymbolKind) -> Self {
match value {
extension::SymbolKind::File => Self::File,
extension::SymbolKind::Module => Self::Module,
extension::SymbolKind::Namespace => Self::Namespace,
extension::SymbolKind::Package => Self::Package,
extension::SymbolKind::Class => Self::Class,
extension::SymbolKind::Method => Self::Method,
extension::SymbolKind::Property => Self::Property,
extension::SymbolKind::Field => Self::Field,
extension::SymbolKind::Constructor => Self::Constructor,
extension::SymbolKind::Enum => Self::Enum,
extension::SymbolKind::Interface => Self::Interface,
extension::SymbolKind::Function => Self::Function,
extension::SymbolKind::Variable => Self::Variable,
extension::SymbolKind::Constant => Self::Constant,
extension::SymbolKind::String => Self::String,
extension::SymbolKind::Number => Self::Number,
extension::SymbolKind::Boolean => Self::Boolean,
extension::SymbolKind::Array => Self::Array,
extension::SymbolKind::Object => Self::Object,
extension::SymbolKind::Key => Self::Key,
extension::SymbolKind::Null => Self::Null,
extension::SymbolKind::EnumMember => Self::EnumMember,
extension::SymbolKind::Struct => Self::Struct,
extension::SymbolKind::Event => Self::Event,
extension::SymbolKind::Operator => Self::Operator,
extension::SymbolKind::TypeParameter => Self::TypeParameter,
extension::SymbolKind::Other(value) => Self::Other(value),
}
}
}
impl From<extension::SlashCommand> for SlashCommand {
fn from(value: extension::SlashCommand) -> Self {
Self {
name: value.name,
description: value.description,
tooltip_text: value.tooltip_text,
requires_argument: value.requires_argument,
}
}
}
impl From<SlashCommandOutput> for extension::SlashCommandOutput {
fn from(value: SlashCommandOutput) -> Self {
Self {
text: value.text,
sections: value.sections.into_iter().map(Into::into).collect(),
}
}
}
impl From<SlashCommandOutputSection> for extension::SlashCommandOutputSection {
fn from(value: SlashCommandOutputSection) -> Self {
Self {
range: value.range.start as usize..value.range.end as usize,
label: value.label,
}
}
}
impl From<SlashCommandArgumentCompletion> for extension::SlashCommandArgumentCompletion {
fn from(value: SlashCommandArgumentCompletion) -> Self {
Self {
label: value.label,
new_text: value.new_text,
run_command: value.run_command,
DownloadedFileType::Gzip => Self::Gzip,
DownloadedFileType::GzipTar => Self::GzipTar,
DownloadedFileType::Zip => Self::Zip,
DownloadedFileType::Uncompressed => Self::Uncompressed,
}
}
}
@ -254,8 +108,7 @@ impl HostKeyValueStore for WasmState {
key: String,
value: String,
) -> wasmtime::Result<Result<(), String>> {
let kv_store = self.table.get(&kv_store)?;
kv_store.insert(key, value).await.to_wasmtime_result()
latest::HostKeyValueStore::insert(self, kv_store, key, value).await
}
async fn drop(&mut self, _worktree: Resource<ExtensionKeyValueStore>) -> Result<()> {
@ -269,8 +122,7 @@ impl HostProject for WasmState {
&mut self,
project: Resource<ExtensionProject>,
) -> wasmtime::Result<Vec<u64>> {
let project = self.table.get(&project)?;
Ok(project.worktree_ids())
latest::HostProject::worktree_ids(self, project).await
}
async fn drop(&mut self, _project: Resource<Project>) -> Result<()> {
@ -281,16 +133,14 @@ impl HostProject for WasmState {
impl HostWorktree for WasmState {
async fn id(&mut self, delegate: Resource<Arc<dyn WorktreeDelegate>>) -> wasmtime::Result<u64> {
let delegate = self.table.get(&delegate)?;
Ok(delegate.id())
latest::HostWorktree::id(self, delegate).await
}
async fn root_path(
&mut self,
delegate: Resource<Arc<dyn WorktreeDelegate>>,
) -> wasmtime::Result<String> {
let delegate = self.table.get(&delegate)?;
Ok(delegate.root_path())
latest::HostWorktree::root_path(self, delegate).await
}
async fn read_text_file(
@ -298,19 +148,14 @@ impl HostWorktree for WasmState {
delegate: Resource<Arc<dyn WorktreeDelegate>>,
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()))
latest::HostWorktree::read_text_file(self, delegate, path).await
}
async fn shell_env(
&mut self,
delegate: Resource<Arc<dyn WorktreeDelegate>>,
) -> wasmtime::Result<EnvVars> {
let delegate = self.table.get(&delegate)?;
Ok(delegate.shell_env().await.into_iter().collect())
latest::HostWorktree::shell_env(self, delegate).await
}
async fn which(
@ -318,8 +163,7 @@ impl HostWorktree for WasmState {
delegate: Resource<Arc<dyn WorktreeDelegate>>,
binary_name: String,
) -> wasmtime::Result<Option<String>> {
let delegate = self.table.get(&delegate)?;
Ok(delegate.which(binary_name).await)
latest::HostWorktree::which(self, delegate, binary_name).await
}
async fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {
@ -328,288 +172,6 @@ impl HostWorktree for WasmState {
}
}
impl common::Host for WasmState {}
impl http_client::Host for WasmState {
async fn fetch(
&mut self,
request: http_client::HttpRequest,
) -> wasmtime::Result<Result<http_client::HttpResponse, String>> {
maybe!(async {
let url = &request.url;
let request = convert_request(&request)?;
let mut response = self.host.http_client.send(request).await?;
if response.status().is_client_error() || response.status().is_server_error() {
bail!("failed to fetch '{url}': status code {}", response.status())
}
convert_response(&mut response).await
})
.await
.to_wasmtime_result()
}
async fn fetch_stream(
&mut self,
request: http_client::HttpRequest,
) -> wasmtime::Result<Result<Resource<ExtensionHttpResponseStream>, String>> {
let request = convert_request(&request)?;
let response = self.host.http_client.send(request);
maybe!(async {
let response = response.await?;
let stream = Arc::new(Mutex::new(response));
let resource = self.table.push(stream)?;
Ok(resource)
})
.await
.to_wasmtime_result()
}
}
impl http_client::HostHttpResponseStream for WasmState {
async fn next_chunk(
&mut self,
resource: Resource<ExtensionHttpResponseStream>,
) -> wasmtime::Result<Result<Option<Vec<u8>>, String>> {
let stream = self.table.get(&resource)?.clone();
maybe!(async move {
let mut response = stream.lock().await;
let mut buffer = vec![0; 8192]; // 8KB buffer
let bytes_read = response.body_mut().read(&mut buffer).await?;
if bytes_read == 0 {
Ok(None)
} else {
buffer.truncate(bytes_read);
Ok(Some(buffer))
}
})
.await
.to_wasmtime_result()
}
async fn drop(&mut self, _resource: Resource<ExtensionHttpResponseStream>) -> Result<()> {
Ok(())
}
}
impl From<http_client::HttpMethod> for ::http_client::Method {
fn from(value: http_client::HttpMethod) -> Self {
match value {
http_client::HttpMethod::Get => Self::GET,
http_client::HttpMethod::Post => Self::POST,
http_client::HttpMethod::Put => Self::PUT,
http_client::HttpMethod::Delete => Self::DELETE,
http_client::HttpMethod::Head => Self::HEAD,
http_client::HttpMethod::Options => Self::OPTIONS,
http_client::HttpMethod::Patch => Self::PATCH,
}
}
}
fn convert_request(
extension_request: &http_client::HttpRequest,
) -> Result<::http_client::Request<AsyncBody>, anyhow::Error> {
let mut request = ::http_client::Request::builder()
.method(::http_client::Method::from(extension_request.method))
.uri(&extension_request.url)
.follow_redirects(match extension_request.redirect_policy {
http_client::RedirectPolicy::NoFollow => ::http_client::RedirectPolicy::NoFollow,
http_client::RedirectPolicy::FollowLimit(limit) => {
::http_client::RedirectPolicy::FollowLimit(limit)
}
http_client::RedirectPolicy::FollowAll => ::http_client::RedirectPolicy::FollowAll,
});
for (key, value) in &extension_request.headers {
request = request.header(key, value);
}
let body = extension_request
.body
.clone()
.map(AsyncBody::from)
.unwrap_or_default();
request.body(body).map_err(anyhow::Error::from)
}
async fn convert_response(
response: &mut ::http_client::Response<AsyncBody>,
) -> Result<http_client::HttpResponse, anyhow::Error> {
let mut extension_response = http_client::HttpResponse {
body: Vec::new(),
headers: Vec::new(),
};
for (key, value) in response.headers() {
extension_response
.headers
.push((key.to_string(), value.to_str().unwrap_or("").to_string()));
}
response
.body_mut()
.read_to_end(&mut extension_response.body)
.await?;
Ok(extension_response)
}
impl nodejs::Host for WasmState {
async fn node_binary_path(&mut self) -> wasmtime::Result<Result<String, String>> {
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<Result<String, String>> {
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<Result<Option<String>, 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<Result<(), String>> {
self.host
.node_runtime
.npm_install_packages(&self.work_dir(), &[(&package_name, &version)])
.await
.to_wasmtime_result()
}
}
#[async_trait]
impl lsp::Host for WasmState {}
impl From<::http_client::github::GithubRelease> for github::GithubRelease {
fn from(value: ::http_client::github::GithubRelease) -> Self {
Self {
version: value.tag_name,
assets: value.assets.into_iter().map(Into::into).collect(),
}
}
}
impl From<::http_client::github::GithubReleaseAsset> for github::GithubReleaseAsset {
fn from(value: ::http_client::github::GithubReleaseAsset) -> Self {
Self {
name: value.name,
download_url: value.browser_download_url,
}
}
}
impl github::Host for WasmState {
async fn latest_github_release(
&mut self,
repo: String,
options: github::GithubReleaseOptions,
) -> wasmtime::Result<Result<github::GithubRelease, String>> {
maybe!(async {
let release = ::http_client::github::latest_github_release(
&repo,
options.require_assets,
options.pre_release,
self.host.http_client.clone(),
)
.await?;
Ok(release.into())
})
.await
.to_wasmtime_result()
}
async fn github_release_by_tag_name(
&mut self,
repo: String,
tag: String,
) -> wasmtime::Result<Result<github::GithubRelease, String>> {
maybe!(async {
let release = ::http_client::github::get_release_by_tag_name(
&repo,
&tag,
self.host.http_client.clone(),
)
.await?;
Ok(release.into())
})
.await
.to_wasmtime_result()
}
}
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"),
},
))
}
}
impl From<std::process::Output> for process::Output {
fn from(output: std::process::Output) -> Self {
Self {
status: output.status.code(),
stdout: output.stdout,
stderr: output.stderr,
}
}
}
impl process::Host for WasmState {
async fn run_command(
&mut self,
command: process::Command,
) -> wasmtime::Result<Result<process::Output, String>> {
maybe!(async {
self.manifest.allow_exec(&command.command, &command.args)?;
let output = util::command::new_smol_command(command.command.as_str())
.args(&command.args)
.envs(command.env)
.output()
.await?;
Ok(output.into())
})
.await
.to_wasmtime_result()
}
}
#[async_trait]
impl slash_command::Host for WasmState {}
impl ExtensionImports for WasmState {
async fn get_settings(
&mut self,
@ -617,73 +179,13 @@ impl ExtensionImports for WasmState {
category: String,
key: Option<String>,
) -> wasmtime::Result<Result<String, String>> {
self.on_main_thread(|cx| {
async move {
let location = location
.as_ref()
.map(|location| ::settings::SettingsLocation {
worktree_id: WorktreeId::from_proto(location.worktree_id),
path: Path::new(&location.path),
});
cx.update(|cx| match category.as_str() {
"language" => {
let key = key.map(|k| LanguageName::new(&k));
let settings = AllLanguageSettings::get(location, cx).language(
location,
key.as_ref(),
cx,
);
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(&::lsp::LanguageServerName::from_proto(key))
})
.cloned()
.unwrap_or_default();
Ok(serde_json::to_string(&settings::LspSettings {
binary: settings.binary.map(|binary| settings::CommandSettings {
path: binary.path,
arguments: binary.arguments,
env: binary.env,
}),
settings: settings.settings,
initialization_options: settings.initialization_options,
})?)
}
"context_servers" => {
let settings = key
.and_then(|key| {
ContextServerSettings::get(location, cx)
.context_servers
.get(key.as_str())
})
.cloned()
.unwrap_or_default();
Ok(serde_json::to_string(&settings::ContextServerSettings {
command: settings.command.map(|command| settings::CommandSettings {
path: Some(command.path),
arguments: Some(command.args),
env: command.env.map(|env| env.into_iter().collect()),
}),
settings: settings.settings,
})?)
}
_ => {
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(
@ -691,18 +193,12 @@ impl ExtensionImports for WasmState {
server_name: String,
status: LanguageServerInstallationStatus,
) -> wasmtime::Result<()> {
let status = match status {
LanguageServerInstallationStatus::CheckingForUpdate => BinaryStatus::CheckingForUpdate,
LanguageServerInstallationStatus::Downloading => BinaryStatus::Downloading,
LanguageServerInstallationStatus::None => BinaryStatus::None,
LanguageServerInstallationStatus::Failed(error) => BinaryStatus::Failed { error },
};
self.host
.proxy
.update_language_server_status(::lsp::LanguageServerName(server_name.into()), status);
Ok(())
latest::ExtensionImports::set_language_server_installation_status(
self,
server_name,
status.into(),
)
.await
}
async fn download_file(
@ -711,86 +207,10 @@ impl ExtensionImports for WasmState {
path: String,
file_type: DownloadedFileType,
) -> wasmtime::Result<Result<(), String>> {
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 => {
futures::pin_mut!(body);
node_runtime::extract_zip(&destination_path, body)
.await
.with_context(|| format!("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<Result<(), String>> {
#[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
}
}

View file

@ -0,0 +1,796 @@
use crate::wasm_host::wit::since_v0_5_0::slash_command::SlashCommandOutputSection;
use crate::wasm_host::wit::{CompletionKind, CompletionLabelDetails, InsertTextFormat, SymbolKind};
use crate::wasm_host::{WasmState, wit::ToWasmtimeResult};
use ::http_client::{AsyncBody, HttpRequestExt};
use ::settings::{Settings, WorktreeId};
use anyhow::{Context, Result, anyhow, bail};
use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use async_trait::async_trait;
use context_server_settings::ContextServerSettings;
use extension::{
ExtensionLanguageServerProxy, KeyValueStoreDelegate, ProjectDelegate, WorktreeDelegate,
};
use futures::{AsyncReadExt, lock::Mutex};
use futures::{FutureExt as _, io::BufReader};
use language::{BinaryStatus, LanguageName, language_settings::AllLanguageSettings};
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, 5, 0);
pub const MAX_VERSION: SemanticVersion = SemanticVersion::new(0, 5, 0);
wasmtime::component::bindgen!({
async: true,
trappable_imports: true,
path: "../extension_api/wit/since_v0.5.0",
with: {
"worktree": ExtensionWorktree,
"project": ExtensionProject,
"key-value-store": ExtensionKeyValueStore,
"zed:extension/http-client/http-response-stream": ExtensionHttpResponseStream
},
});
pub use self::zed::extension::*;
mod settings {
include!(concat!(env!("OUT_DIR"), "/since_v0.5.0/settings.rs"));
}
pub type ExtensionWorktree = Arc<dyn WorktreeDelegate>;
pub type ExtensionProject = Arc<dyn ProjectDelegate>;
pub type ExtensionKeyValueStore = Arc<dyn KeyValueStoreDelegate>;
pub type ExtensionHttpResponseStream = Arc<Mutex<::http_client::Response<AsyncBody>>>;
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<Range> for std::ops::Range<usize> {
fn from(range: Range) -> Self {
let start = range.start as usize;
let end = range.end as usize;
start..end
}
}
impl From<Command> for extension::Command {
fn from(value: Command) -> Self {
Self {
command: value.command,
args: value.args,
env: value.env,
}
}
}
impl From<CodeLabel> for extension::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(),
}
}
}
impl From<CodeLabelSpan> for extension::CodeLabelSpan {
fn from(value: CodeLabelSpan) -> Self {
match value {
CodeLabelSpan::CodeRange(range) => Self::CodeRange(range.into()),
CodeLabelSpan::Literal(literal) => Self::Literal(literal.into()),
}
}
}
impl From<CodeLabelSpanLiteral> for extension::CodeLabelSpanLiteral {
fn from(value: CodeLabelSpanLiteral) -> Self {
Self {
text: value.text,
highlight_name: value.highlight_name,
}
}
}
impl From<extension::Completion> for Completion {
fn from(value: extension::Completion) -> Self {
Self {
label: value.label,
label_details: value.label_details.map(Into::into),
detail: value.detail,
kind: value.kind.map(Into::into),
insert_text_format: value.insert_text_format.map(Into::into),
}
}
}
impl From<extension::CompletionLabelDetails> for CompletionLabelDetails {
fn from(value: extension::CompletionLabelDetails) -> Self {
Self {
detail: value.detail,
description: value.description,
}
}
}
impl From<extension::CompletionKind> for CompletionKind {
fn from(value: extension::CompletionKind) -> Self {
match value {
extension::CompletionKind::Text => Self::Text,
extension::CompletionKind::Method => Self::Method,
extension::CompletionKind::Function => Self::Function,
extension::CompletionKind::Constructor => Self::Constructor,
extension::CompletionKind::Field => Self::Field,
extension::CompletionKind::Variable => Self::Variable,
extension::CompletionKind::Class => Self::Class,
extension::CompletionKind::Interface => Self::Interface,
extension::CompletionKind::Module => Self::Module,
extension::CompletionKind::Property => Self::Property,
extension::CompletionKind::Unit => Self::Unit,
extension::CompletionKind::Value => Self::Value,
extension::CompletionKind::Enum => Self::Enum,
extension::CompletionKind::Keyword => Self::Keyword,
extension::CompletionKind::Snippet => Self::Snippet,
extension::CompletionKind::Color => Self::Color,
extension::CompletionKind::File => Self::File,
extension::CompletionKind::Reference => Self::Reference,
extension::CompletionKind::Folder => Self::Folder,
extension::CompletionKind::EnumMember => Self::EnumMember,
extension::CompletionKind::Constant => Self::Constant,
extension::CompletionKind::Struct => Self::Struct,
extension::CompletionKind::Event => Self::Event,
extension::CompletionKind::Operator => Self::Operator,
extension::CompletionKind::TypeParameter => Self::TypeParameter,
extension::CompletionKind::Other(value) => Self::Other(value),
}
}
}
impl From<extension::InsertTextFormat> for InsertTextFormat {
fn from(value: extension::InsertTextFormat) -> Self {
match value {
extension::InsertTextFormat::PlainText => Self::PlainText,
extension::InsertTextFormat::Snippet => Self::Snippet,
extension::InsertTextFormat::Other(value) => Self::Other(value),
}
}
}
impl From<extension::Symbol> for Symbol {
fn from(value: extension::Symbol) -> Self {
Self {
kind: value.kind.into(),
name: value.name,
}
}
}
impl From<extension::SymbolKind> for SymbolKind {
fn from(value: extension::SymbolKind) -> Self {
match value {
extension::SymbolKind::File => Self::File,
extension::SymbolKind::Module => Self::Module,
extension::SymbolKind::Namespace => Self::Namespace,
extension::SymbolKind::Package => Self::Package,
extension::SymbolKind::Class => Self::Class,
extension::SymbolKind::Method => Self::Method,
extension::SymbolKind::Property => Self::Property,
extension::SymbolKind::Field => Self::Field,
extension::SymbolKind::Constructor => Self::Constructor,
extension::SymbolKind::Enum => Self::Enum,
extension::SymbolKind::Interface => Self::Interface,
extension::SymbolKind::Function => Self::Function,
extension::SymbolKind::Variable => Self::Variable,
extension::SymbolKind::Constant => Self::Constant,
extension::SymbolKind::String => Self::String,
extension::SymbolKind::Number => Self::Number,
extension::SymbolKind::Boolean => Self::Boolean,
extension::SymbolKind::Array => Self::Array,
extension::SymbolKind::Object => Self::Object,
extension::SymbolKind::Key => Self::Key,
extension::SymbolKind::Null => Self::Null,
extension::SymbolKind::EnumMember => Self::EnumMember,
extension::SymbolKind::Struct => Self::Struct,
extension::SymbolKind::Event => Self::Event,
extension::SymbolKind::Operator => Self::Operator,
extension::SymbolKind::TypeParameter => Self::TypeParameter,
extension::SymbolKind::Other(value) => Self::Other(value),
}
}
}
impl From<extension::SlashCommand> for SlashCommand {
fn from(value: extension::SlashCommand) -> Self {
Self {
name: value.name,
description: value.description,
tooltip_text: value.tooltip_text,
requires_argument: value.requires_argument,
}
}
}
impl From<SlashCommandOutput> for extension::SlashCommandOutput {
fn from(value: SlashCommandOutput) -> Self {
Self {
text: value.text,
sections: value.sections.into_iter().map(Into::into).collect(),
}
}
}
impl From<SlashCommandOutputSection> for extension::SlashCommandOutputSection {
fn from(value: SlashCommandOutputSection) -> Self {
Self {
range: value.range.start as usize..value.range.end as usize,
label: value.label,
}
}
}
impl From<SlashCommandArgumentCompletion> for extension::SlashCommandArgumentCompletion {
fn from(value: SlashCommandArgumentCompletion) -> Self {
Self {
label: value.label,
new_text: value.new_text,
run_command: value.run_command,
}
}
}
impl HostKeyValueStore for WasmState {
async fn insert(
&mut self,
kv_store: Resource<ExtensionKeyValueStore>,
key: String,
value: String,
) -> wasmtime::Result<Result<(), String>> {
let kv_store = self.table.get(&kv_store)?;
kv_store.insert(key, value).await.to_wasmtime_result()
}
async fn drop(&mut self, _worktree: Resource<ExtensionKeyValueStore>) -> Result<()> {
// We only ever hand out borrows of key-value stores.
Ok(())
}
}
impl HostProject for WasmState {
async fn worktree_ids(
&mut self,
project: Resource<ExtensionProject>,
) -> wasmtime::Result<Vec<u64>> {
let project = self.table.get(&project)?;
Ok(project.worktree_ids())
}
async fn drop(&mut self, _project: Resource<Project>) -> Result<()> {
// We only ever hand out borrows of projects.
Ok(())
}
}
impl HostWorktree for WasmState {
async fn id(&mut self, delegate: Resource<Arc<dyn WorktreeDelegate>>) -> wasmtime::Result<u64> {
let delegate = self.table.get(&delegate)?;
Ok(delegate.id())
}
async fn root_path(
&mut self,
delegate: Resource<Arc<dyn WorktreeDelegate>>,
) -> wasmtime::Result<String> {
let delegate = self.table.get(&delegate)?;
Ok(delegate.root_path())
}
async fn read_text_file(
&mut self,
delegate: Resource<Arc<dyn WorktreeDelegate>>,
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 WorktreeDelegate>>,
) -> 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 WorktreeDelegate>>,
binary_name: String,
) -> wasmtime::Result<Option<String>> {
let delegate = self.table.get(&delegate)?;
Ok(delegate.which(binary_name).await)
}
async fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {
// We only ever hand out borrows of worktrees.
Ok(())
}
}
impl common::Host for WasmState {}
impl http_client::Host for WasmState {
async fn fetch(
&mut self,
request: http_client::HttpRequest,
) -> wasmtime::Result<Result<http_client::HttpResponse, String>> {
maybe!(async {
let url = &request.url;
let request = convert_request(&request)?;
let mut response = self.host.http_client.send(request).await?;
if response.status().is_client_error() || response.status().is_server_error() {
bail!("failed to fetch '{url}': status code {}", response.status())
}
convert_response(&mut response).await
})
.await
.to_wasmtime_result()
}
async fn fetch_stream(
&mut self,
request: http_client::HttpRequest,
) -> wasmtime::Result<Result<Resource<ExtensionHttpResponseStream>, String>> {
let request = convert_request(&request)?;
let response = self.host.http_client.send(request);
maybe!(async {
let response = response.await?;
let stream = Arc::new(Mutex::new(response));
let resource = self.table.push(stream)?;
Ok(resource)
})
.await
.to_wasmtime_result()
}
}
impl http_client::HostHttpResponseStream for WasmState {
async fn next_chunk(
&mut self,
resource: Resource<ExtensionHttpResponseStream>,
) -> wasmtime::Result<Result<Option<Vec<u8>>, String>> {
let stream = self.table.get(&resource)?.clone();
maybe!(async move {
let mut response = stream.lock().await;
let mut buffer = vec![0; 8192]; // 8KB buffer
let bytes_read = response.body_mut().read(&mut buffer).await?;
if bytes_read == 0 {
Ok(None)
} else {
buffer.truncate(bytes_read);
Ok(Some(buffer))
}
})
.await
.to_wasmtime_result()
}
async fn drop(&mut self, _resource: Resource<ExtensionHttpResponseStream>) -> Result<()> {
Ok(())
}
}
impl From<http_client::HttpMethod> for ::http_client::Method {
fn from(value: http_client::HttpMethod) -> Self {
match value {
http_client::HttpMethod::Get => Self::GET,
http_client::HttpMethod::Post => Self::POST,
http_client::HttpMethod::Put => Self::PUT,
http_client::HttpMethod::Delete => Self::DELETE,
http_client::HttpMethod::Head => Self::HEAD,
http_client::HttpMethod::Options => Self::OPTIONS,
http_client::HttpMethod::Patch => Self::PATCH,
}
}
}
fn convert_request(
extension_request: &http_client::HttpRequest,
) -> Result<::http_client::Request<AsyncBody>, anyhow::Error> {
let mut request = ::http_client::Request::builder()
.method(::http_client::Method::from(extension_request.method))
.uri(&extension_request.url)
.follow_redirects(match extension_request.redirect_policy {
http_client::RedirectPolicy::NoFollow => ::http_client::RedirectPolicy::NoFollow,
http_client::RedirectPolicy::FollowLimit(limit) => {
::http_client::RedirectPolicy::FollowLimit(limit)
}
http_client::RedirectPolicy::FollowAll => ::http_client::RedirectPolicy::FollowAll,
});
for (key, value) in &extension_request.headers {
request = request.header(key, value);
}
let body = extension_request
.body
.clone()
.map(AsyncBody::from)
.unwrap_or_default();
request.body(body).map_err(anyhow::Error::from)
}
async fn convert_response(
response: &mut ::http_client::Response<AsyncBody>,
) -> Result<http_client::HttpResponse, anyhow::Error> {
let mut extension_response = http_client::HttpResponse {
body: Vec::new(),
headers: Vec::new(),
};
for (key, value) in response.headers() {
extension_response
.headers
.push((key.to_string(), value.to_str().unwrap_or("").to_string()));
}
response
.body_mut()
.read_to_end(&mut extension_response.body)
.await?;
Ok(extension_response)
}
impl nodejs::Host for WasmState {
async fn node_binary_path(&mut self) -> wasmtime::Result<Result<String, String>> {
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<Result<String, String>> {
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<Result<Option<String>, 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<Result<(), String>> {
self.host
.node_runtime
.npm_install_packages(&self.work_dir(), &[(&package_name, &version)])
.await
.to_wasmtime_result()
}
}
#[async_trait]
impl lsp::Host for WasmState {}
impl From<::http_client::github::GithubRelease> for github::GithubRelease {
fn from(value: ::http_client::github::GithubRelease) -> Self {
Self {
version: value.tag_name,
assets: value.assets.into_iter().map(Into::into).collect(),
}
}
}
impl From<::http_client::github::GithubReleaseAsset> for github::GithubReleaseAsset {
fn from(value: ::http_client::github::GithubReleaseAsset) -> Self {
Self {
name: value.name,
download_url: value.browser_download_url,
}
}
}
impl github::Host for WasmState {
async fn latest_github_release(
&mut self,
repo: String,
options: github::GithubReleaseOptions,
) -> wasmtime::Result<Result<github::GithubRelease, String>> {
maybe!(async {
let release = ::http_client::github::latest_github_release(
&repo,
options.require_assets,
options.pre_release,
self.host.http_client.clone(),
)
.await?;
Ok(release.into())
})
.await
.to_wasmtime_result()
}
async fn github_release_by_tag_name(
&mut self,
repo: String,
tag: String,
) -> wasmtime::Result<Result<github::GithubRelease, String>> {
maybe!(async {
let release = ::http_client::github::get_release_by_tag_name(
&repo,
&tag,
self.host.http_client.clone(),
)
.await?;
Ok(release.into())
})
.await
.to_wasmtime_result()
}
}
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"),
},
))
}
}
impl From<std::process::Output> for process::Output {
fn from(output: std::process::Output) -> Self {
Self {
status: output.status.code(),
stdout: output.stdout,
stderr: output.stderr,
}
}
}
impl process::Host for WasmState {
async fn run_command(
&mut self,
command: process::Command,
) -> wasmtime::Result<Result<process::Output, String>> {
maybe!(async {
self.manifest.allow_exec(&command.command, &command.args)?;
let output = util::command::new_smol_command(command.command.as_str())
.args(&command.args)
.envs(command.env)
.output()
.await?;
Ok(output.into())
})
.await
.to_wasmtime_result()
}
}
#[async_trait]
impl slash_command::Host for WasmState {}
impl ExtensionImports for WasmState {
async fn get_settings(
&mut self,
location: Option<self::SettingsLocation>,
category: String,
key: Option<String>,
) -> wasmtime::Result<Result<String, String>> {
self.on_main_thread(|cx| {
async move {
let location = location
.as_ref()
.map(|location| ::settings::SettingsLocation {
worktree_id: WorktreeId::from_proto(location.worktree_id),
path: Path::new(&location.path),
});
cx.update(|cx| match category.as_str() {
"language" => {
let key = key.map(|k| LanguageName::new(&k));
let settings = AllLanguageSettings::get(location, cx).language(
location,
key.as_ref(),
cx,
);
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(&::lsp::LanguageServerName::from_proto(key))
})
.cloned()
.unwrap_or_default();
Ok(serde_json::to_string(&settings::LspSettings {
binary: settings.binary.map(|binary| settings::CommandSettings {
path: binary.path,
arguments: binary.arguments,
env: binary.env,
}),
settings: settings.settings,
initialization_options: settings.initialization_options,
})?)
}
"context_servers" => {
let settings = key
.and_then(|key| {
ContextServerSettings::get(location, cx)
.context_servers
.get(key.as_str())
})
.cloned()
.unwrap_or_default();
Ok(serde_json::to_string(&settings::ContextServerSettings {
command: settings.command.map(|command| settings::CommandSettings {
path: Some(command.path),
arguments: Some(command.args),
env: command.env.map(|env| env.into_iter().collect()),
}),
settings: settings.settings,
})?)
}
_ => {
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 => BinaryStatus::CheckingForUpdate,
LanguageServerInstallationStatus::Downloading => BinaryStatus::Downloading,
LanguageServerInstallationStatus::None => BinaryStatus::None,
LanguageServerInstallationStatus::Failed(error) => BinaryStatus::Failed { error },
};
self.host
.proxy
.update_language_server_status(::lsp::LanguageServerName(server_name.into()), status);
Ok(())
}
async fn download_file(
&mut self,
url: String,
path: String,
file_type: DownloadedFileType,
) -> wasmtime::Result<Result<(), String>> {
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 => {
futures::pin_mut!(body);
node_runtime::extract_zip(&destination_path, body)
.await
.with_context(|| format!("failed to unzip {} archive", path.display()))?;
}
}
Ok(())
})
.await
.to_wasmtime_result()
}
async fn make_file_executable(&mut self, path: String) -> wasmtime::Result<Result<(), String>> {
#[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(()))
}
}