diff --git a/Cargo.lock b/Cargo.lock index 36691e72ab..5eaf3ddde1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7873,7 +7873,7 @@ name = "perplexity" version = "0.1.0" dependencies = [ "serde", - "zed_extension_api 0.1.0", + "zed_extension_api 0.2.0", ] [[package]] @@ -10254,7 +10254,7 @@ dependencies = [ name = "slash_commands_example" version = "0.1.0" dependencies = [ - "zed_extension_api 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "zed_extension_api 0.1.0", ] [[package]] @@ -14317,72 +14317,63 @@ name = "zed_astro" version = "0.1.0" dependencies = [ "serde", - "zed_extension_api 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "zed_extension_api 0.1.0", ] [[package]] name = "zed_clojure" version = "0.0.3" dependencies = [ - "zed_extension_api 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "zed_extension_api 0.1.0", ] [[package]] name = "zed_csharp" version = "0.0.2" dependencies = [ - "zed_extension_api 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "zed_extension_api 0.1.0", ] [[package]] name = "zed_dart" version = "0.0.3" dependencies = [ - "zed_extension_api 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "zed_extension_api 0.1.0", ] [[package]] name = "zed_deno" version = "0.0.2" dependencies = [ - "zed_extension_api 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "zed_extension_api 0.1.0", ] [[package]] name = "zed_elixir" version = "0.0.9" dependencies = [ - "zed_extension_api 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "zed_extension_api 0.1.0", ] [[package]] name = "zed_elm" version = "0.0.1" dependencies = [ - "zed_extension_api 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "zed_extension_api 0.1.0", ] [[package]] name = "zed_emmet" version = "0.0.3" dependencies = [ - "zed_extension_api 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "zed_extension_api 0.1.0", ] [[package]] name = "zed_erlang" version = "0.1.0" dependencies = [ - "zed_extension_api 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "zed_extension_api" -version = "0.1.0" -dependencies = [ - "serde", - "serde_json", - "wit-bindgen", + "zed_extension_api 0.1.0", ] [[package]] @@ -14396,82 +14387,91 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "zed_extension_api" +version = "0.2.0" +dependencies = [ + "serde", + "serde_json", + "wit-bindgen", +] + [[package]] name = "zed_gleam" version = "0.2.0" dependencies = [ "html_to_markdown 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "zed_extension_api 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "zed_extension_api 0.1.0", ] [[package]] name = "zed_glsl" version = "0.1.0" dependencies = [ - "zed_extension_api 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "zed_extension_api 0.1.0", ] [[package]] name = "zed_haskell" version = "0.1.1" dependencies = [ - "zed_extension_api 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "zed_extension_api 0.1.0", ] [[package]] name = "zed_html" version = "0.1.2" dependencies = [ - "zed_extension_api 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "zed_extension_api 0.1.0", ] [[package]] name = "zed_lua" version = "0.0.3" dependencies = [ - "zed_extension_api 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "zed_extension_api 0.1.0", ] [[package]] name = "zed_ocaml" version = "0.0.2" dependencies = [ - "zed_extension_api 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "zed_extension_api 0.1.0", ] [[package]] name = "zed_php" version = "0.2.0" dependencies = [ - "zed_extension_api 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "zed_extension_api 0.1.0", ] [[package]] name = "zed_prisma" version = "0.0.3" dependencies = [ - "zed_extension_api 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "zed_extension_api 0.1.0", ] [[package]] name = "zed_purescript" version = "0.0.1" dependencies = [ - "zed_extension_api 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "zed_extension_api 0.1.0", ] [[package]] name = "zed_ruby" version = "0.2.0" dependencies = [ - "zed_extension_api 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "zed_extension_api 0.1.0", ] [[package]] name = "zed_ruff" version = "0.0.2" dependencies = [ - "zed_extension_api 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "zed_extension_api 0.1.0", ] [[package]] @@ -14479,42 +14479,42 @@ name = "zed_snippets" version = "0.0.5" dependencies = [ "serde_json", - "zed_extension_api 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "zed_extension_api 0.1.0", ] [[package]] name = "zed_svelte" version = "0.1.1" dependencies = [ - "zed_extension_api 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "zed_extension_api 0.1.0", ] [[package]] name = "zed_terraform" version = "0.1.0" dependencies = [ - "zed_extension_api 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "zed_extension_api 0.1.0", ] [[package]] name = "zed_test_extension" version = "0.1.0" dependencies = [ - "zed_extension_api 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "zed_extension_api 0.2.0", ] [[package]] name = "zed_toml" version = "0.1.1" dependencies = [ - "zed_extension_api 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "zed_extension_api 0.1.0", ] [[package]] name = "zed_uiua" version = "0.0.1" dependencies = [ - "zed_extension_api 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "zed_extension_api 0.1.0", ] [[package]] @@ -14522,14 +14522,14 @@ name = "zed_vue" version = "0.1.0" dependencies = [ "serde", - "zed_extension_api 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "zed_extension_api 0.1.0", ] [[package]] name = "zed_zig" version = "0.3.0" dependencies = [ - "zed_extension_api 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "zed_extension_api 0.1.0", ] [[package]] diff --git a/crates/extension/build.rs b/crates/extension/build.rs index c5f94abaa8..f2c2b19998 100644 --- a/crates/extension/build.rs +++ b/crates/extension/build.rs @@ -6,17 +6,21 @@ fn main() -> Result<(), Box> { copy_extension_api_rust_files() } -// rust-analyzer doesn't support include! for files from outside the crate. -// Copy them to the OUT_DIR, so we can include them from there, which is supported. +/// rust-analyzer doesn't support include! for files from outside the crate. +/// Copy them to the OUT_DIR, so we can include them from there, which is supported. fn copy_extension_api_rust_files() -> Result<(), Box> { let out_dir = env::var("OUT_DIR")?; let input_dir = PathBuf::from("../extension_api/wit"); let output_dir = PathBuf::from(out_dir); + println!("cargo:rerun-if-changed={}", input_dir.display()); + for entry in fs::read_dir(&input_dir)? { let entry = entry?; let path = entry.path(); if path.is_dir() { + println!("cargo:rerun-if-changed={}", path.display()); + for subentry in fs::read_dir(&path)? { let subentry = subentry?; let subpath = subentry.path(); @@ -26,7 +30,6 @@ fn copy_extension_api_rust_files() -> Result<(), Box> { fs::create_dir_all(destination.parent().unwrap())?; fs::copy(&subpath, &destination)?; - println!("cargo:rerun-if-changed={}", subpath.display()); } } } else if path.extension() == Some(std::ffi::OsStr::new("rs")) { diff --git a/crates/extension/src/wasm_host/wit.rs b/crates/extension/src/wasm_host/wit.rs index 7c7d71be3a..1c3cdd77f6 100644 --- a/crates/extension/src/wasm_host/wit.rs +++ b/crates/extension/src/wasm_host/wit.rs @@ -2,9 +2,10 @@ mod since_v0_0_1; mod since_v0_0_4; mod since_v0_0_6; mod since_v0_1_0; +mod since_v0_2_0; use indexed_docs::IndexedDocsDatabase; use release_channel::ReleaseChannel; -use since_v0_1_0 as latest; +use since_v0_2_0 as latest; use super::{wasm_engine, WasmState}; use anyhow::{anyhow, Context, Result}; @@ -52,10 +53,16 @@ pub fn wasm_api_version_range(release_channel: ReleaseChannel) -> RangeInclusive // Note: The release channel can be used to stage a new version of the extension API. let _ = release_channel; - since_v0_0_1::MIN_VERSION..=latest::MAX_VERSION + let max_version = match release_channel { + ReleaseChannel::Dev | ReleaseChannel::Nightly => latest::MAX_VERSION, + ReleaseChannel::Stable | ReleaseChannel::Preview => since_v0_1_0::MAX_VERSION, + }; + + since_v0_0_1::MIN_VERSION..=max_version } pub enum Extension { + V020(since_v0_2_0::Extension), V010(since_v0_1_0::Extension), V006(since_v0_0_6::Extension), V004(since_v0_0_4::Extension), @@ -72,11 +79,25 @@ impl Extension { // Note: The release channel can be used to stage a new version of the extension API. let _ = release_channel; - if version >= latest::MIN_VERSION { + let allow_latest_version = match release_channel { + ReleaseChannel::Dev | ReleaseChannel::Nightly => true, + ReleaseChannel::Stable | ReleaseChannel::Preview => false, + }; + + if allow_latest_version && version >= latest::MIN_VERSION { let (extension, instance) = latest::Extension::instantiate_async(store, component, latest::linker()) .await .context("failed to instantiate wasm extension")?; + Ok((Self::V020(extension), instance)) + } else if version >= since_v0_1_0::MIN_VERSION { + let (extension, instance) = since_v0_1_0::Extension::instantiate_async( + store, + component, + since_v0_1_0::linker(), + ) + .await + .context("failed to instantiate wasm extension")?; Ok((Self::V010(extension), instance)) } else if version >= since_v0_0_6::MIN_VERSION { let (extension, instance) = since_v0_0_6::Extension::instantiate_async( @@ -110,6 +131,7 @@ impl Extension { pub async fn call_init_extension(&self, store: &mut Store) -> Result<()> { match self { + Extension::V020(ext) => ext.call_init_extension(store).await, Extension::V010(ext) => ext.call_init_extension(store).await, Extension::V006(ext) => ext.call_init_extension(store).await, Extension::V004(ext) => ext.call_init_extension(store).await, @@ -125,10 +147,14 @@ impl Extension { resource: Resource>, ) -> Result> { match self { - Extension::V010(ext) => { + Extension::V020(ext) => { ext.call_language_server_command(store, &language_server_id.0, resource) .await } + Extension::V010(ext) => Ok(ext + .call_language_server_command(store, &language_server_id.0, resource) + .await? + .map(|command| command.into())), Extension::V006(ext) => Ok(ext .call_language_server_command(store, &language_server_id.0, resource) .await? @@ -152,6 +178,14 @@ impl Extension { resource: Resource>, ) -> Result, String>> { match self { + Extension::V020(ext) => { + ext.call_language_server_initialization_options( + store, + &language_server_id.0, + resource, + ) + .await + } Extension::V010(ext) => { ext.call_language_server_initialization_options( store, @@ -190,6 +224,14 @@ impl Extension { resource: Resource>, ) -> Result, String>> { match self { + Extension::V020(ext) => { + ext.call_language_server_workspace_configuration( + store, + &language_server_id.0, + resource, + ) + .await + } Extension::V010(ext) => { ext.call_language_server_workspace_configuration( store, @@ -217,10 +259,19 @@ impl Extension { completions: Vec, ) -> Result>, String>> { match self { - Extension::V010(ext) => { + Extension::V020(ext) => { ext.call_labels_for_completions(store, &language_server_id.0, &completions) .await } + Extension::V010(ext) => Ok(ext + .call_labels_for_completions(store, &language_server_id.0, &completions) + .await? + .map(|labels| { + labels + .into_iter() + .map(|label| label.map(Into::into)) + .collect() + })), Extension::V006(ext) => Ok(ext .call_labels_for_completions(store, &language_server_id.0, &completions) .await? @@ -241,10 +292,19 @@ impl Extension { symbols: Vec, ) -> Result>, String>> { match self { - Extension::V010(ext) => { + Extension::V020(ext) => { ext.call_labels_for_symbols(store, &language_server_id.0, &symbols) .await } + Extension::V010(ext) => Ok(ext + .call_labels_for_symbols(store, &language_server_id.0, &symbols) + .await? + .map(|labels| { + labels + .into_iter() + .map(|label| label.map(Into::into)) + .collect() + })), Extension::V006(ext) => Ok(ext .call_labels_for_symbols(store, &language_server_id.0, &symbols) .await? @@ -265,6 +325,10 @@ impl Extension { arguments: &[String], ) -> Result, String>> { match self { + Extension::V020(ext) => { + ext.call_complete_slash_command_argument(store, command, arguments) + .await + } Extension::V010(ext) => { ext.call_complete_slash_command_argument(store, command, arguments) .await @@ -281,6 +345,10 @@ impl Extension { resource: Option>>, ) -> Result> { match self { + Extension::V020(ext) => { + ext.call_run_slash_command(store, command, arguments, resource) + .await + } Extension::V010(ext) => { ext.call_run_slash_command(store, command, arguments, resource) .await @@ -297,6 +365,7 @@ impl Extension { provider: &str, ) -> Result, String>> { match self { + Extension::V020(ext) => ext.call_suggest_docs_packages(store, provider).await, Extension::V010(ext) => ext.call_suggest_docs_packages(store, provider).await, Extension::V001(_) | Extension::V004(_) | Extension::V006(_) => Err(anyhow!( "`suggest_docs_packages` not available prior to v0.1.0" @@ -312,6 +381,10 @@ impl Extension { database: Resource>, ) -> Result> { match self { + Extension::V020(ext) => { + ext.call_index_docs(store, provider, package_name, database) + .await + } Extension::V010(ext) => { ext.call_index_docs(store, provider, package_name, database) .await diff --git a/crates/extension/src/wasm_host/wit/since_v0_1_0.rs b/crates/extension/src/wasm_host/wit/since_v0_1_0.rs index 337bb8afb0..88d860391a 100644 --- a/crates/extension/src/wasm_host/wit/since_v0_1_0.rs +++ b/crates/extension/src/wasm_host/wit/since_v0_1_0.rs @@ -16,13 +16,14 @@ use language::{ 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}; +use super::latest; + pub const MIN_VERSION: SemanticVersion = SemanticVersion::new(0, 1, 0); pub const MAX_VERSION: SemanticVersion = SemanticVersion::new(0, 1, 0); @@ -33,7 +34,12 @@ wasmtime::component::bindgen!({ with: { "worktree": ExtensionWorktree, "key-value-store": ExtensionKeyValueStore, - "zed:extension/http-client/http-response-stream": ExtensionHttpResponseStream + "zed:extension/http-client/http-response-stream": ExtensionHttpResponseStream, + "zed:extension/github": latest::zed::extension::github, + "zed:extension/lsp": latest::zed::extension::lsp, + "zed:extension/nodejs": latest::zed::extension::nodejs, + "zed:extension/platform": latest::zed::extension::platform, + "zed:extension/slash-command": latest::zed::extension::slash_command, }, }); @@ -49,7 +55,94 @@ pub type ExtensionHttpResponseStream = Arc &'static Linker { static LINKER: OnceLock> = OnceLock::new(); - LINKER.get_or_init(|| super::new_linker(Extension::add_to_linker)) + LINKER.get_or_init(|| { + super::new_linker(|linker, f| { + Extension::add_to_linker(linker, f)?; + latest::zed::extension::github::add_to_linker(linker, f)?; + latest::zed::extension::nodejs::add_to_linker(linker, f)?; + latest::zed::extension::platform::add_to_linker(linker, f)?; + latest::zed::extension::slash_command::add_to_linker(linker, f)?; + Ok(()) + }) + }) +} + +impl From for latest::Command { + fn from(value: Command) -> Self { + Self { + command: value.command, + args: value.args, + env: value.env, + } + } +} + +impl From for latest::SettingsLocation { + fn from(value: SettingsLocation) -> Self { + Self { + worktree_id: value.worktree_id, + path: value.path, + } + } +} + +impl From for latest::LanguageServerInstallationStatus { + fn from(value: LanguageServerInstallationStatus) -> Self { + match value { + LanguageServerInstallationStatus::None => Self::None, + LanguageServerInstallationStatus::Downloading => Self::Downloading, + LanguageServerInstallationStatus::CheckingForUpdate => Self::CheckingForUpdate, + LanguageServerInstallationStatus::Failed(message) => Self::Failed(message), + } + } +} + +impl From for latest::DownloadedFileType { + fn from(value: DownloadedFileType) -> Self { + match value { + DownloadedFileType::Gzip => Self::Gzip, + DownloadedFileType::GzipTar => Self::GzipTar, + DownloadedFileType::Zip => Self::Zip, + DownloadedFileType::Uncompressed => Self::Uncompressed, + } + } +} + +impl From for latest::Range { + fn from(value: Range) -> Self { + Self { + start: value.start, + end: value.end, + } + } +} + +impl From for latest::CodeLabelSpan { + fn from(value: CodeLabelSpan) -> Self { + match value { + CodeLabelSpan::CodeRange(range) => Self::CodeRange(range.into()), + CodeLabelSpan::Literal(literal) => Self::Literal(literal.into()), + } + } +} + +impl From for latest::CodeLabelSpanLiteral { + fn from(value: CodeLabelSpanLiteral) -> Self { + Self { + text: value.text, + highlight_name: value.highlight_name, + } + } +} + +impl From for latest::CodeLabel { + fn from(value: CodeLabel) -> Self { + Self { + code: value.code, + spans: value.spans.into_iter().map(Into::into).collect(), + filter_range: value.filter_range.into(), + } + } } #[async_trait] @@ -251,136 +344,6 @@ async fn convert_response( Ok(extension_response) } -#[async_trait] -impl nodejs::Host for WasmState { - async fn node_binary_path(&mut self) -> wasmtime::Result> { - self.host - .node_runtime - .binary_path() - .await - .map(|path| path.to_string_lossy().to_string()) - .to_wasmtime_result() - } - - async fn npm_package_latest_version( - &mut self, - package_name: String, - ) -> wasmtime::Result> { - self.host - .node_runtime - .npm_package_latest_version(&package_name) - .await - .to_wasmtime_result() - } - - async fn npm_package_installed_version( - &mut self, - package_name: String, - ) -> wasmtime::Result, String>> { - self.host - .node_runtime - .npm_package_installed_version(&self.work_dir(), &package_name) - .await - .to_wasmtime_result() - } - - async fn npm_install_package( - &mut self, - package_name: String, - version: String, - ) -> wasmtime::Result> { - self.host - .node_runtime - .npm_install_packages(&self.work_dir(), &[(&package_name, &version)]) - .await - .to_wasmtime_result() - } -} - -#[async_trait] -impl lsp::Host for WasmState {} - -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, - } - } -} - -#[async_trait] -impl github::Host for WasmState { - async fn latest_github_release( - &mut self, - repo: String, - options: github::GithubReleaseOptions, - ) -> wasmtime::Result> { - maybe!(async { - let release = ::http_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> { - 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() - } -} - -#[async_trait] -impl platform::Host for WasmState { - async fn current_platform(&mut self) -> Result<(platform::Os, platform::Architecture)> { - Ok(( - match env::consts::OS { - "macos" => platform::Os::Mac, - "linux" => platform::Os::Linux, - "windows" => platform::Os::Windows, - _ => panic!("unsupported os"), - }, - match env::consts::ARCH { - "aarch64" => platform::Architecture::Aarch64, - "x86" => platform::Architecture::X86, - "x86_64" => platform::Architecture::X8664, - _ => panic!("unsupported architecture"), - }, - )) - } -} - -#[async_trait] -impl slash_command::Host for WasmState {} - #[async_trait] impl ExtensionImports for WasmState { async fn get_settings( diff --git a/crates/extension/src/wasm_host/wit/since_v0_2_0.rs b/crates/extension/src/wasm_host/wit/since_v0_2_0.rs new file mode 100644 index 0000000000..7fa79c2544 --- /dev/null +++ b/crates/extension/src/wasm_host/wit/since_v0_2_0.rs @@ -0,0 +1,551 @@ +use crate::wasm_host::{wit::ToWasmtimeResult, WasmState}; +use ::http_client::AsyncBody; +use ::settings::{Settings, WorktreeId}; +use anyhow::{anyhow, bail, Context, Result}; +use async_compression::futures::bufread::GzipDecoder; +use async_tar::Archive; +use async_trait::async_trait; +use futures::{io::BufReader, FutureExt as _}; +use futures::{lock::Mutex, AsyncReadExt}; +use indexed_docs::IndexedDocsDatabase; +use isahc::config::{Configurable, RedirectPolicy}; +use language::LanguageName; +use language::{ + language_settings::AllLanguageSettings, LanguageServerBinaryStatus, LspAdapterDelegate, +}; +use project::project_settings::ProjectSettings; +use semantic_version::SemanticVersion; +use std::{ + env, + path::{Path, PathBuf}, + sync::{Arc, OnceLock}, +}; +use util::maybe; +use wasmtime::component::{Linker, Resource}; + +pub const MIN_VERSION: SemanticVersion = SemanticVersion::new(0, 2, 0); +pub const MAX_VERSION: SemanticVersion = SemanticVersion::new(0, 2, 0); + +wasmtime::component::bindgen!({ + async: true, + trappable_imports: true, + path: "../extension_api/wit/since_v0.2.0", + with: { + "worktree": ExtensionWorktree, + "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.2.0/settings.rs")); +} + +pub type ExtensionWorktree = Arc; +pub type ExtensionKeyValueStore = Arc; +pub type ExtensionHttpResponseStream = Arc>>; + +pub fn linker() -> &'static Linker { + static LINKER: OnceLock> = OnceLock::new(); + LINKER.get_or_init(|| super::new_linker(Extension::add_to_linker)) +} + +#[async_trait] +impl HostKeyValueStore for WasmState { + async fn insert( + &mut self, + kv_store: Resource, + key: String, + value: String, + ) -> wasmtime::Result> { + let kv_store = self.table.get(&kv_store)?; + kv_store.insert(key, value).await.to_wasmtime_result() + } + + fn drop(&mut self, _worktree: Resource) -> Result<()> { + // We only ever hand out borrows of key-value stores. + Ok(()) + } +} + +#[async_trait] +impl HostWorktree for WasmState { + async fn id( + &mut self, + delegate: Resource>, + ) -> wasmtime::Result { + let delegate = self.table.get(&delegate)?; + Ok(delegate.worktree_id().to_proto()) + } + + async fn root_path( + &mut self, + delegate: Resource>, + ) -> wasmtime::Result { + let delegate = self.table.get(&delegate)?; + Ok(delegate.worktree_root_path().to_string_lossy().to_string()) + } + + async fn read_text_file( + &mut self, + delegate: Resource>, + path: String, + ) -> wasmtime::Result> { + let delegate = self.table.get(&delegate)?; + Ok(delegate + .read_text_file(path.into()) + .await + .map_err(|error| error.to_string())) + } + + async fn shell_env( + &mut self, + delegate: Resource>, + ) -> wasmtime::Result { + let delegate = self.table.get(&delegate)?; + Ok(delegate.shell_env().await.into_iter().collect()) + } + + async fn which( + &mut self, + delegate: Resource>, + binary_name: String, + ) -> wasmtime::Result> { + let delegate = self.table.get(&delegate)?; + Ok(delegate + .which(binary_name.as_ref()) + .await + .map(|path| path.to_string_lossy().to_string())) + } + + fn drop(&mut self, _worktree: Resource) -> Result<()> { + // We only ever hand out borrows of worktrees. + Ok(()) + } +} + +#[async_trait] +impl common::Host for WasmState {} + +#[async_trait] +impl http_client::Host for WasmState { + async fn fetch( + &mut self, + request: http_client::HttpRequest, + ) -> wasmtime::Result> { + 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, 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() + } +} + +#[async_trait] +impl http_client::HostHttpResponseStream for WasmState { + async fn next_chunk( + &mut self, + resource: Resource, + ) -> wasmtime::Result>, 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() + } + + fn drop(&mut self, _resource: Resource) -> Result<()> { + Ok(()) + } +} + +impl From 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, anyhow::Error> { + let mut request = ::http_client::Request::builder() + .method(::http_client::Method::from(extension_request.method)) + .uri(&extension_request.url) + .redirect_policy(match extension_request.redirect_policy { + http_client::RedirectPolicy::NoFollow => RedirectPolicy::None, + http_client::RedirectPolicy::FollowLimit(limit) => RedirectPolicy::Limit(limit), + http_client::RedirectPolicy::FollowAll => RedirectPolicy::Follow, + }); + 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, +) -> Result { + 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) +} + +#[async_trait] +impl nodejs::Host for WasmState { + async fn node_binary_path(&mut self) -> wasmtime::Result> { + self.host + .node_runtime + .binary_path() + .await + .map(|path| path.to_string_lossy().to_string()) + .to_wasmtime_result() + } + + async fn npm_package_latest_version( + &mut self, + package_name: String, + ) -> wasmtime::Result> { + self.host + .node_runtime + .npm_package_latest_version(&package_name) + .await + .to_wasmtime_result() + } + + async fn npm_package_installed_version( + &mut self, + package_name: String, + ) -> wasmtime::Result, String>> { + self.host + .node_runtime + .npm_package_installed_version(&self.work_dir(), &package_name) + .await + .to_wasmtime_result() + } + + async fn npm_install_package( + &mut self, + package_name: String, + version: String, + ) -> wasmtime::Result> { + self.host + .node_runtime + .npm_install_packages(&self.work_dir(), &[(&package_name, &version)]) + .await + .to_wasmtime_result() + } +} + +#[async_trait] +impl lsp::Host for WasmState {} + +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, + } + } +} + +#[async_trait] +impl github::Host for WasmState { + async fn latest_github_release( + &mut self, + repo: String, + options: github::GithubReleaseOptions, + ) -> wasmtime::Result> { + maybe!(async { + let release = ::http_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> { + 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() + } +} + +#[async_trait] +impl platform::Host for WasmState { + async fn current_platform(&mut self) -> Result<(platform::Os, platform::Architecture)> { + Ok(( + match env::consts::OS { + "macos" => platform::Os::Mac, + "linux" => platform::Os::Linux, + "windows" => platform::Os::Windows, + _ => panic!("unsupported os"), + }, + match env::consts::ARCH { + "aarch64" => platform::Architecture::Aarch64, + "x86" => platform::Architecture::X86, + "x86_64" => platform::Architecture::X8664, + _ => panic!("unsupported architecture"), + }, + )) + } +} + +#[async_trait] +impl slash_command::Host for WasmState {} + +#[async_trait] +impl ExtensionImports for WasmState { + async fn get_settings( + &mut self, + location: Option, + category: String, + key: Option, + ) -> wasmtime::Result> { + self.on_main_thread(|cx| { + async move { + let location = location + .as_ref() + .map(|location| ::settings::SettingsLocation { + worktree_id: 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(key.as_ref()); + Ok(serde_json::to_string(&settings::LanguageSettings { + tab_size: settings.tab_size, + })?) + } + "lsp" => { + let settings = key + .and_then(|key| { + ProjectSettings::get(location, cx) + .lsp + .get(&Arc::::from(key)) + }) + .cloned() + .unwrap_or_default(); + Ok(serde_json::to_string(&settings::LspSettings { + binary: settings.binary.map(|binary| settings::BinarySettings { + path: binary.path, + arguments: binary.arguments, + }), + settings: settings.settings, + initialization_options: settings.initialization_options, + })?) + } + _ => { + bail!("Unknown settings category: {}", category); + } + }) + } + .boxed_local() + }) + .await? + .to_wasmtime_result() + } + + async fn set_language_server_installation_status( + &mut self, + server_name: String, + status: LanguageServerInstallationStatus, + ) -> wasmtime::Result<()> { + let status = match status { + LanguageServerInstallationStatus::CheckingForUpdate => { + LanguageServerBinaryStatus::CheckingForUpdate + } + LanguageServerInstallationStatus::Downloading => { + LanguageServerBinaryStatus::Downloading + } + LanguageServerInstallationStatus::None => LanguageServerBinaryStatus::None, + LanguageServerInstallationStatus::Failed(error) => { + LanguageServerBinaryStatus::Failed { error } + } + }; + + self.host + .language_registry + .update_lsp_status(language::LanguageServerName(server_name.into()), status); + Ok(()) + } + + async fn download_file( + &mut self, + url: String, + path: String, + file_type: DownloadedFileType, + ) -> wasmtime::Result> { + maybe!(async { + let path = PathBuf::from(path); + let extension_work_dir = self.host.work_dir.join(self.manifest.id.as_ref()); + + self.host.fs.create_dir(&extension_work_dir).await?; + + let destination_path = self + .host + .writeable_path_from_extension(&self.manifest.id, &path)?; + + let mut response = self + .host + .http_client + .get(&url, Default::default(), true) + .await + .map_err(|err| anyhow!("error downloading release: {}", err))?; + + if !response.status().is_success() { + Err(anyhow!( + "download failed with status {}", + response.status().to_string() + ))?; + } + let body = BufReader::new(response.body_mut()); + + match file_type { + DownloadedFileType::Uncompressed => { + futures::pin_mut!(body); + self.host + .fs + .create_file_with(&destination_path, body) + .await?; + } + DownloadedFileType::Gzip => { + let body = GzipDecoder::new(body); + futures::pin_mut!(body); + self.host + .fs + .create_file_with(&destination_path, body) + .await?; + } + DownloadedFileType::GzipTar => { + let body = GzipDecoder::new(body); + futures::pin_mut!(body); + self.host + .fs + .extract_tar_file(&destination_path, Archive::new(body)) + .await?; + } + DownloadedFileType::Zip => { + 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> { + #[allow(unused)] + let path = self + .host + .writeable_path_from_extension(&self.manifest.id, Path::new(&path))?; + + #[cfg(unix)] + { + use std::fs::{self, Permissions}; + use std::os::unix::fs::PermissionsExt; + + return fs::set_permissions(&path, Permissions::from_mode(0o755)) + .map_err(|error| anyhow!("failed to set permissions for path {path:?}: {error}")) + .to_wasmtime_result(); + } + + #[cfg(not(unix))] + Ok(Ok(())) + } +} diff --git a/crates/extension_api/Cargo.toml b/crates/extension_api/Cargo.toml index 89d7ed947b..1a2b25b0f6 100644 --- a/crates/extension_api/Cargo.toml +++ b/crates/extension_api/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zed_extension_api" -version = "0.1.0" +version = "0.2.0" description = "APIs for creating Zed extensions in Rust" repository = "https://github.com/zed-industries/zed" documentation = "https://docs.rs/zed_extension_api" @@ -8,6 +8,9 @@ keywords = ["zed", "extension"] edition = "2021" license = "Apache-2.0" +# Remove when we're ready to publish v0.2.0. +publish = false + [lints] workspace = true diff --git a/crates/extension_api/wit/since_v0.2.0/common.wit b/crates/extension_api/wit/since_v0.2.0/common.wit new file mode 100644 index 0000000000..c4f321f4c7 --- /dev/null +++ b/crates/extension_api/wit/since_v0.2.0/common.wit @@ -0,0 +1,9 @@ +interface common { + /// A (half-open) range (`[start, end)`). + record range { + /// The start of the range (inclusive). + start: u32, + /// The end of the range (exclusive). + end: u32, + } +} diff --git a/crates/extension_api/wit/since_v0.2.0/extension.wit b/crates/extension_api/wit/since_v0.2.0/extension.wit new file mode 100644 index 0000000000..c7599f93ff --- /dev/null +++ b/crates/extension_api/wit/since_v0.2.0/extension.wit @@ -0,0 +1,147 @@ +package zed:extension; + +world extension { + import github; + import http-client; + import platform; + import nodejs; + + use common.{range}; + use lsp.{completion, symbol}; + use slash-command.{slash-command, slash-command-argument-completion, slash-command-output}; + + /// Initializes the extension. + export init-extension: func(); + + /// The type of a downloaded file. + enum downloaded-file-type { + /// A gzipped file (`.gz`). + gzip, + /// A gzipped tar archive (`.tar.gz`). + gzip-tar, + /// A ZIP file (`.zip`). + zip, + /// An uncompressed file. + uncompressed, + } + + /// The installation status for a language server. + variant language-server-installation-status { + /// The language server has no installation status. + none, + /// The language server is being downloaded. + downloading, + /// The language server is checking for updates. + checking-for-update, + /// The language server installation failed for specified reason. + failed(string), + } + + record settings-location { + worktree-id: u64, + path: string, + } + + import get-settings: func(path: option, category: string, key: option) -> result; + + /// Downloads a file from the given URL and saves it to the given path within the extension's + /// working directory. + /// + /// The file will be extracted according to the given file type. + import download-file: func(url: string, file-path: string, file-type: downloaded-file-type) -> result<_, string>; + + /// Makes the file at the given path executable. + import make-file-executable: func(filepath: string) -> result<_, string>; + + /// Updates the installation status for the given language server. + import set-language-server-installation-status: func(language-server-name: string, status: language-server-installation-status); + + /// A list of environment variables. + type env-vars = list>; + + /// A command. + record command { + /// The command to execute. + command: string, + /// The arguments to pass to the command. + args: list, + /// The environment variables to set for the command. + env: env-vars, + } + + /// A Zed worktree. + resource worktree { + /// Returns the ID of the worktree. + id: func() -> u64; + /// Returns the root path of the worktree. + root-path: func() -> string; + /// Returns the textual contents of the specified file in the worktree. + read-text-file: func(path: string) -> result; + /// Returns the path to the given binary name, if one is present on the `$PATH`. + which: func(binary-name: string) -> option; + /// Returns the current shell environment. + shell-env: func() -> env-vars; + } + + /// A key-value store. + resource key-value-store { + /// Inserts an entry under the specified key. + insert: func(key: string, value: string) -> result<_, string>; + } + + /// Returns the command used to start up the language server. + export language-server-command: func(language-server-id: string, worktree: borrow) -> result; + + /// Returns the initialization options to pass to the language server on startup. + /// + /// The initialization options are represented as a JSON string. + export language-server-initialization-options: func(language-server-id: string, worktree: borrow) -> result, string>; + + /// Returns the workspace configuration options to pass to the language server. + export language-server-workspace-configuration: func(language-server-id: string, worktree: borrow) -> result, string>; + + /// A label containing some code. + record code-label { + /// The source code to parse with Tree-sitter. + code: string, + /// The spans to display in the label. + spans: list, + /// The range of the displayed label to include when filtering. + filter-range: range, + } + + /// A span within a code label. + variant code-label-span { + /// A range into the parsed code. + code-range(range), + /// A span containing a code literal. + literal(code-label-span-literal), + } + + /// A span containing a code literal. + record code-label-span-literal { + /// The literal text. + text: string, + /// The name of the highlight to use for this literal. + highlight-name: option, + } + + export labels-for-completions: func(language-server-id: string, completions: list) -> result>, string>; + export labels-for-symbols: func(language-server-id: string, symbols: list) -> result>, string>; + + /// Returns the completions that should be shown when completing the provided slash command with the given query. + export complete-slash-command-argument: func(command: slash-command, args: list) -> result, string>; + + /// Returns the output from running the provided slash command. + export run-slash-command: func(command: slash-command, args: list, worktree: option>) -> result; + + /// Returns a list of packages as suggestions to be included in the `/docs` + /// search results. + /// + /// This can be used to provide completions for known packages (e.g., from the + /// local project or a registry) before a package has been indexed. + export suggest-docs-packages: func(provider-name: string) -> result, string>; + + /// Indexes the docs for the specified package. + export index-docs: func(provider-name: string, package-name: string, database: borrow) -> result<_, string>; +} diff --git a/crates/extension_api/wit/since_v0.2.0/github.wit b/crates/extension_api/wit/since_v0.2.0/github.wit new file mode 100644 index 0000000000..bb138f5d31 --- /dev/null +++ b/crates/extension_api/wit/since_v0.2.0/github.wit @@ -0,0 +1,33 @@ +interface github { + /// A GitHub release. + record github-release { + /// The version of the release. + version: string, + /// The list of assets attached to the release. + assets: list, + } + + /// An asset from a GitHub release. + record github-release-asset { + /// The name of the asset. + name: string, + /// The download URL for the asset. + download-url: string, + } + + /// The options used to filter down GitHub releases. + record github-release-options { + /// Whether releases without assets should be included. + require-assets: bool, + /// Whether pre-releases should be included. + pre-release: bool, + } + + /// Returns the latest release for the given GitHub repository. + latest-github-release: func(repo: string, options: github-release-options) -> result; + + /// Returns the GitHub release with the specified tag name for the given GitHub repository. + /// + /// Returns an error if a release with the given tag name does not exist. + github-release-by-tag-name: func(repo: string, tag: string) -> result; +} diff --git a/crates/extension_api/wit/since_v0.2.0/http-client.wit b/crates/extension_api/wit/since_v0.2.0/http-client.wit new file mode 100644 index 0000000000..bb0206c17a --- /dev/null +++ b/crates/extension_api/wit/since_v0.2.0/http-client.wit @@ -0,0 +1,67 @@ +interface http-client { + /// An HTTP request. + record http-request { + /// The HTTP method for the request. + method: http-method, + /// The URL to which the request should be made. + url: string, + /// The headers for the request. + headers: list>, + /// The request body. + body: option>, + /// The policy to use for redirects. + redirect-policy: redirect-policy, + } + + /// HTTP methods. + enum http-method { + /// `GET` + get, + /// `HEAD` + head, + /// `POST` + post, + /// `PUT` + put, + /// `DELETE` + delete, + /// `OPTIONS` + options, + /// `PATCH` + patch, + } + + /// The policy for dealing with redirects received from the server. + variant redirect-policy { + /// Redirects from the server will not be followed. + /// + /// This is the default behavior. + no-follow, + /// Redirects from the server will be followed up to the specified limit. + follow-limit(u32), + /// All redirects from the server will be followed. + follow-all, + } + + /// An HTTP response. + record http-response { + /// The response headers. + headers: list>, + /// The response body. + body: list, + } + + /// Performs an HTTP request and returns the response. + fetch: func(req: http-request) -> result; + + /// An HTTP response stream. + resource http-response-stream { + /// Retrieves the next chunk of data from the response stream. + /// + /// Returns `Ok(None)` if the stream has ended. + next-chunk: func() -> result>, string>; + } + + /// Performs an HTTP request and returns a response stream. + fetch-stream: func(req: http-request) -> result; +} diff --git a/crates/extension_api/wit/since_v0.2.0/lsp.wit b/crates/extension_api/wit/since_v0.2.0/lsp.wit new file mode 100644 index 0000000000..19e81b6b14 --- /dev/null +++ b/crates/extension_api/wit/since_v0.2.0/lsp.wit @@ -0,0 +1,83 @@ +interface lsp { + /// An LSP completion. + record completion { + label: string, + detail: option, + kind: option, + insert-text-format: option, + } + + /// The kind of an LSP completion. + variant completion-kind { + text, + method, + function, + %constructor, + field, + variable, + class, + %interface, + module, + property, + unit, + value, + %enum, + keyword, + snippet, + color, + file, + reference, + folder, + enum-member, + constant, + struct, + event, + operator, + type-parameter, + other(s32), + } + + /// Defines how to interpret the insert text in a completion item. + variant insert-text-format { + plain-text, + snippet, + other(s32), + } + + /// An LSP symbol. + record symbol { + kind: symbol-kind, + name: string, + } + + /// The kind of an LSP symbol. + variant symbol-kind { + file, + module, + namespace, + %package, + class, + method, + property, + field, + %constructor, + %enum, + %interface, + function, + variable, + constant, + %string, + number, + boolean, + array, + object, + key, + null, + enum-member, + struct, + event, + operator, + type-parameter, + other(s32), + } +} diff --git a/crates/extension_api/wit/since_v0.2.0/nodejs.wit b/crates/extension_api/wit/since_v0.2.0/nodejs.wit new file mode 100644 index 0000000000..c814548314 --- /dev/null +++ b/crates/extension_api/wit/since_v0.2.0/nodejs.wit @@ -0,0 +1,13 @@ +interface nodejs { + /// Returns the path to the Node binary used by Zed. + node-binary-path: func() -> result; + + /// Returns the latest version of the given NPM package. + npm-package-latest-version: func(package-name: string) -> result; + + /// Returns the installed version of the given NPM package, if it exists. + npm-package-installed-version: func(package-name: string) -> result, string>; + + /// Installs the specified NPM package. + npm-install-package: func(package-name: string, version: string) -> result<_, string>; +} diff --git a/crates/extension_api/wit/since_v0.2.0/platform.wit b/crates/extension_api/wit/since_v0.2.0/platform.wit new file mode 100644 index 0000000000..48472a99bc --- /dev/null +++ b/crates/extension_api/wit/since_v0.2.0/platform.wit @@ -0,0 +1,24 @@ +interface platform { + /// An operating system. + enum os { + /// macOS. + mac, + /// Linux. + linux, + /// Windows. + windows, + } + + /// A platform architecture. + enum architecture { + /// AArch64 (e.g., Apple Silicon). + aarch64, + /// x86. + x86, + /// x86-64. + x8664, + } + + /// Gets the current operating system and architecture. + current-platform: func() -> tuple; +} diff --git a/crates/extension_api/wit/since_v0.2.0/settings.rs b/crates/extension_api/wit/since_v0.2.0/settings.rs new file mode 100644 index 0000000000..5c6cae7064 --- /dev/null +++ b/crates/extension_api/wit/since_v0.2.0/settings.rs @@ -0,0 +1,29 @@ +use serde::{Deserialize, Serialize}; +use std::num::NonZeroU32; + +/// The settings for a particular language. +#[derive(Debug, Serialize, Deserialize)] +pub struct LanguageSettings { + /// How many columns a tab should occupy. + pub tab_size: NonZeroU32, +} + +/// The settings for a particular language server. +#[derive(Default, Debug, Serialize, Deserialize)] +pub struct LspSettings { + /// The settings for the language server binary. + pub binary: Option, + /// The initialization options to pass to the language server. + pub initialization_options: Option, + /// The settings to pass to language server. + pub settings: Option, +} + +/// The settings for a language server binary. +#[derive(Debug, Serialize, Deserialize)] +pub struct BinarySettings { + /// The path to the binary. + pub path: Option, + /// The arguments to pass to the binary. + pub arguments: Option>, +} diff --git a/crates/extension_api/wit/since_v0.2.0/slash-command.wit b/crates/extension_api/wit/since_v0.2.0/slash-command.wit new file mode 100644 index 0000000000..f52561c2ef --- /dev/null +++ b/crates/extension_api/wit/since_v0.2.0/slash-command.wit @@ -0,0 +1,41 @@ +interface slash-command { + use common.{range}; + + /// A slash command for use in the Assistant. + record slash-command { + /// The name of the slash command. + name: string, + /// The description of the slash command. + description: string, + /// The tooltip text to display for the run button. + tooltip-text: string, + /// Whether this slash command requires an argument. + requires-argument: bool, + } + + /// The output of a slash command. + record slash-command-output { + /// The text produced by the slash command. + text: string, + /// The list of sections to show in the slash command placeholder. + sections: list, + } + + /// A section in the slash command output. + record slash-command-output-section { + /// The range this section occupies. + range: range, + /// The label to display in the placeholder for this section. + label: string, + } + + /// A completion for a slash command argument. + record slash-command-argument-completion { + /// The label to display for this completion. + label: string, + /// The new text that should be inserted into the command when this completion is accepted. + new-text: string, + /// Whether the command should be run when accepting this completion. + run-command: bool, + } +} diff --git a/extensions/test-extension/Cargo.toml b/extensions/test-extension/Cargo.toml index 094302e89f..5e17a9a6a3 100644 --- a/extensions/test-extension/Cargo.toml +++ b/extensions/test-extension/Cargo.toml @@ -13,4 +13,4 @@ path = "src/test_extension.rs" crate-type = ["cdylib"] [dependencies] -zed_extension_api = "0.1.0" +zed_extension_api = { path = "../../crates/extension_api" }