diff --git a/Cargo.lock b/Cargo.lock index c67050dbea..c88281dfbe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12515,15 +12515,15 @@ dependencies = [ [[package]] name = "zed_extension_api" version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5f4ae4e302a80591635ef9a236b35fde6fcc26cfd060e66fde4ba9f9fd394a1" dependencies = [ "wit-bindgen", ] [[package]] name = "zed_extension_api" -version = "0.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5f4ae4e302a80591635ef9a236b35fde6fcc26cfd060e66fde4ba9f9fd394a1" +version = "0.0.6" dependencies = [ "wit-bindgen", ] @@ -12532,7 +12532,7 @@ dependencies = [ name = "zed_gleam" version = "0.0.2" dependencies = [ - "zed_extension_api 0.0.4", + "zed_extension_api 0.0.6", ] [[package]] @@ -12581,7 +12581,7 @@ dependencies = [ name = "zed_toml" version = "0.0.2" dependencies = [ - "zed_extension_api 0.0.5 (registry+https://github.com/rust-lang/crates.io-index)", + "zed_extension_api 0.0.5", ] [[package]] @@ -12595,7 +12595,7 @@ dependencies = [ name = "zed_zig" version = "0.0.1" dependencies = [ - "zed_extension_api 0.0.5 (registry+https://github.com/rust-lang/crates.io-index)", + "zed_extension_api 0.0.5", ] [[package]] diff --git a/crates/extension/src/extension_lsp_adapter.rs b/crates/extension/src/extension_lsp_adapter.rs index cb5d13135b..d622fec8b7 100644 --- a/crates/extension/src/extension_lsp_adapter.rs +++ b/crates/extension/src/extension_lsp_adapter.rs @@ -1,21 +1,29 @@ -use crate::wasm_host::{wit::LanguageServerConfig, WasmExtension, WasmHost}; +use crate::wasm_host::{ + wit::{self, LanguageServerConfig}, + WasmExtension, WasmHost, +}; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; use collections::HashMap; use futures::{Future, FutureExt}; use gpui::AsyncAppContext; -use language::{Language, LanguageServerName, LspAdapter, LspAdapterDelegate}; +use language::{ + CodeLabel, HighlightId, Language, LanguageServerName, LspAdapter, LspAdapterDelegate, +}; use lsp::LanguageServerBinary; +use std::ops::Range; use std::{ any::Any, path::{Path, PathBuf}, pin::Pin, sync::Arc, }; +use util::{maybe, ResultExt}; use wasmtime_wasi::WasiView as _; pub struct ExtensionLspAdapter { pub(crate) extension: WasmExtension, + pub(crate) language_server_id: LanguageServerName, pub(crate) config: LanguageServerConfig, pub(crate) host: Arc, } @@ -43,7 +51,12 @@ impl LspAdapter for ExtensionLspAdapter { async move { let resource = store.data_mut().table().push(delegate)?; let command = extension - .call_language_server_command(store, &this.config, resource) + .call_language_server_command( + store, + &this.language_server_id, + &this.config, + resource, + ) .await? .map_err(|e| anyhow!("{}", e))?; anyhow::Ok(command) @@ -146,6 +159,7 @@ impl LspAdapter for ExtensionLspAdapter { let options = extension .call_language_server_initialization_options( store, + &this.language_server_id, &this.config, resource, ) @@ -165,4 +179,235 @@ impl LspAdapter for ExtensionLspAdapter { None }) } + + async fn labels_for_completions( + self: Arc, + completions: &[lsp::CompletionItem], + language: &Arc, + ) -> Result>> { + let completions = completions + .into_iter() + .map(|completion| wit::Completion::from(completion.clone())) + .collect::>(); + + let labels = self + .extension + .call({ + let this = self.clone(); + |extension, store| { + async move { + extension + .call_labels_for_completions( + store, + &this.language_server_id, + completions, + ) + .await? + .map_err(|e| anyhow!("{}", e)) + } + .boxed() + } + }) + .await?; + + Ok(labels + .into_iter() + .map(|label| { + label.map(|label| { + build_code_label( + &label, + &language.highlight_text(&label.code.as_str().into(), 0..label.code.len()), + &language, + ) + }) + }) + .collect()) + } +} + +fn build_code_label( + label: &wit::CodeLabel, + parsed_runs: &[(Range, HighlightId)], + language: &Arc, +) -> CodeLabel { + let mut text = String::new(); + let mut runs = vec![]; + + for span in &label.spans { + match span { + wit::CodeLabelSpan::CodeRange(range) => { + let range = Range::from(*range); + + let mut input_ix = range.start; + let mut output_ix = text.len(); + for (run_range, id) in parsed_runs { + if run_range.start >= range.end { + break; + } + if run_range.end <= input_ix { + continue; + } + + if run_range.start > input_ix { + output_ix += run_range.start - input_ix; + input_ix = run_range.start; + } + + { + let len = range.end.min(run_range.end) - input_ix; + runs.push((output_ix..output_ix + len, *id)); + output_ix += len; + input_ix += len; + } + } + + text.push_str(&label.code[range]); + } + wit::CodeLabelSpan::Literal(span) => { + let highlight_id = language + .grammar() + .zip(span.highlight_name.as_ref()) + .and_then(|(grammar, highlight_name)| { + grammar.highlight_id_for_name(&highlight_name) + }) + .unwrap_or_default(); + let ix = text.len(); + runs.push((ix..ix + span.text.len(), highlight_id)); + text.push_str(&span.text); + } + } + } + + CodeLabel { + text, + runs, + filter_range: label.filter_range.into(), + } +} + +impl From for Range { + fn from(range: wit::Range) -> Self { + let start = range.start as usize; + let end = range.end as usize; + start..end + } +} + +impl From for wit::Completion { + fn from(value: lsp::CompletionItem) -> Self { + Self { + label: value.label, + detail: value.detail, + kind: value.kind.map(Into::into), + insert_text_format: value.insert_text_format.map(Into::into), + } + } +} + +impl From for wit::CompletionKind { + fn from(value: lsp::CompletionItemKind) -> Self { + match value { + lsp::CompletionItemKind::TEXT => Self::Text, + lsp::CompletionItemKind::METHOD => Self::Method, + lsp::CompletionItemKind::FUNCTION => Self::Function, + lsp::CompletionItemKind::CONSTRUCTOR => Self::Constructor, + lsp::CompletionItemKind::FIELD => Self::Field, + lsp::CompletionItemKind::VARIABLE => Self::Variable, + lsp::CompletionItemKind::CLASS => Self::Class, + lsp::CompletionItemKind::INTERFACE => Self::Interface, + lsp::CompletionItemKind::MODULE => Self::Module, + lsp::CompletionItemKind::PROPERTY => Self::Property, + lsp::CompletionItemKind::UNIT => Self::Unit, + lsp::CompletionItemKind::VALUE => Self::Value, + lsp::CompletionItemKind::ENUM => Self::Enum, + lsp::CompletionItemKind::KEYWORD => Self::Keyword, + lsp::CompletionItemKind::SNIPPET => Self::Snippet, + lsp::CompletionItemKind::COLOR => Self::Color, + lsp::CompletionItemKind::FILE => Self::File, + lsp::CompletionItemKind::REFERENCE => Self::Reference, + lsp::CompletionItemKind::FOLDER => Self::Folder, + lsp::CompletionItemKind::ENUM_MEMBER => Self::EnumMember, + lsp::CompletionItemKind::CONSTANT => Self::Constant, + lsp::CompletionItemKind::STRUCT => Self::Struct, + lsp::CompletionItemKind::EVENT => Self::Event, + lsp::CompletionItemKind::OPERATOR => Self::Operator, + lsp::CompletionItemKind::TYPE_PARAMETER => Self::TypeParameter, + _ => { + let value = maybe!({ + let kind = serde_json::to_value(&value)?; + serde_json::from_value(kind) + }); + + Self::Other(value.log_err().unwrap_or(-1)) + } + } + } +} + +impl From for wit::InsertTextFormat { + fn from(value: lsp::InsertTextFormat) -> Self { + match value { + lsp::InsertTextFormat::PLAIN_TEXT => Self::PlainText, + lsp::InsertTextFormat::SNIPPET => Self::Snippet, + _ => { + let value = maybe!({ + let kind = serde_json::to_value(&value)?; + serde_json::from_value(kind) + }); + + Self::Other(value.log_err().unwrap_or(-1)) + } + } + } +} + +#[test] +fn test_build_code_label() { + use util::test::marked_text_ranges; + + let (code, ranges) = marked_text_ranges( + "«const» «a»: «fn»(«Bcd»(«Efgh»)) -> «Ijklm» = pqrs.tuv", + false, + ); + let runs = ranges + .iter() + .map(|range| (range.clone(), HighlightId(0))) + .collect::>(); + + let label = build_code_label( + &wit::CodeLabel { + spans: vec![ + wit::CodeLabelSpan::CodeRange(wit::Range { + start: code.find("pqrs").unwrap() as u32, + end: code.len() as u32, + }), + wit::CodeLabelSpan::CodeRange(wit::Range { + start: code.find(": fn").unwrap() as u32, + end: code.find(" = ").unwrap() as u32, + }), + ], + filter_range: wit::Range { + start: 0, + end: "pqrs.tuv".len() as u32, + }, + code, + }, + &runs, + &language::PLAIN_TEXT, + ); + + let (text, ranges) = marked_text_ranges("pqrs.tuv: «fn»(«Bcd»(«Efgh»)) -> «Ijklm»", false); + let runs = ranges + .iter() + .map(|range| (range.clone(), HighlightId(0))) + .collect::>(); + + assert_eq!( + label, + CodeLabel { + text, + runs, + filter_range: label.filter_range.clone() + } + ) } diff --git a/crates/extension/src/extension_store.rs b/crates/extension/src/extension_store.rs index 72001a0f73..2634fc1e68 100644 --- a/crates/extension/src/extension_store.rs +++ b/crates/extension/src/extension_store.rs @@ -1101,15 +1101,15 @@ impl ExtensionStore { this.reload_complete_senders.clear(); for (manifest, wasm_extension) in &wasm_extensions { - for (language_server_name, language_server_config) in &manifest.language_servers - { + for (language_server_id, language_server_config) in &manifest.language_servers { this.language_registry.register_lsp_adapter( language_server_config.language.clone(), Arc::new(ExtensionLspAdapter { extension: wasm_extension.clone(), host: this.wasm_host.clone(), + language_server_id: language_server_id.clone(), config: wit::LanguageServerConfig { - name: language_server_name.0.to_string(), + name: language_server_id.0.to_string(), language_name: language_server_config.language.to_string(), }, }), diff --git a/crates/extension/src/extension_store_test.rs b/crates/extension/src/extension_store_test.rs index 6dfff7b0e5..ad0488d7aa 100644 --- a/crates/extension/src/extension_store_test.rs +++ b/crates/extension/src/extension_store_test.rs @@ -619,6 +619,53 @@ async fn test_extension_store_with_gleam_extension(cx: &mut TestAppContext) { ] ); + // The extension creates custom labels for completion items. + fake_server.handle_request::(|_, _| async move { + Ok(Some(lsp::CompletionResponse::Array(vec![ + lsp::CompletionItem { + label: "foo".into(), + kind: Some(lsp::CompletionItemKind::FUNCTION), + detail: Some("fn() -> Result(Nil, Error)".into()), + ..Default::default() + }, + lsp::CompletionItem { + label: "bar.baz".into(), + kind: Some(lsp::CompletionItemKind::FUNCTION), + detail: Some("fn(List(a)) -> a".into()), + ..Default::default() + }, + lsp::CompletionItem { + label: "Quux".into(), + kind: Some(lsp::CompletionItemKind::CONSTRUCTOR), + detail: Some("fn(String) -> T".into()), + ..Default::default() + }, + lsp::CompletionItem { + label: "my_string".into(), + kind: Some(lsp::CompletionItemKind::CONSTANT), + detail: Some("String".into()), + ..Default::default() + }, + ]))) + }); + + let completion_labels = project + .update(cx, |project, cx| project.completions(&buffer, 0, cx)) + .await + .unwrap() + .into_iter() + .map(|c| c.label.text) + .collect::>(); + assert_eq!( + completion_labels, + [ + "foo: fn() -> Result(Nil, Error)".to_string(), + "bar.baz: fn(List(a)) -> a".to_string(), + "Quux: fn(String) -> T".to_string(), + "my_string: String".to_string(), + ] + ); + // Simulate a new version of the language server being released language_server_version.lock().version = "v2.0.0".into(); language_server_version.lock().binary_contents = "the-new-binary-contents".into(); diff --git a/crates/extension/src/wasm_host/wit.rs b/crates/extension/src/wasm_host/wit.rs index a59b94a265..29a8a01321 100644 --- a/crates/extension/src/wasm_host/wit.rs +++ b/crates/extension/src/wasm_host/wit.rs @@ -1,20 +1,28 @@ mod since_v0_0_1; mod since_v0_0_4; +mod since_v0_0_6; -use super::{wasm_engine, WasmState}; -use anyhow::{Context, Result}; -use language::LspAdapterDelegate; -use semantic_version::SemanticVersion; use std::ops::RangeInclusive; use std::sync::Arc; + +use anyhow::bail; +use anyhow::{Context, Result}; +use language::{LanguageServerName, LspAdapterDelegate}; +use semantic_version::SemanticVersion; use wasmtime::{ component::{Component, Instance, Linker, Resource}, Store, }; -use since_v0_0_4 as latest; +use super::{wasm_engine, WasmState}; -pub use latest::{Command, LanguageServerConfig}; +use since_v0_0_6 as latest; + +pub use latest::{ + zed::extension::lsp::{Completion, CompletionKind, InsertTextFormat}, + CodeLabel, CodeLabelSpan, Command, Range, +}; +pub use since_v0_0_4::LanguageServerConfig; pub fn new_linker( f: impl Fn(&mut Linker, fn(&mut WasmState) -> &mut WasmState) -> Result<()>, @@ -41,6 +49,7 @@ pub fn wasm_api_version_range() -> RangeInclusive { } pub enum Extension { + V006(since_v0_0_6::Extension), V004(since_v0_0_4::Extension), V001(since_v0_0_1::Extension), } @@ -51,16 +60,13 @@ impl Extension { version: SemanticVersion, component: &Component, ) -> Result<(Self, Instance)> { - if version < latest::MIN_VERSION { - let (extension, instance) = since_v0_0_1::Extension::instantiate_async( - store, - &component, - since_v0_0_1::linker(), - ) - .await - .context("failed to instantiate wasm extension")?; - Ok((Self::V001(extension), instance)) - } else { + if version >= latest::MIN_VERSION { + let (extension, instance) = + latest::Extension::instantiate_async(store, &component, latest::linker()) + .await + .context("failed to instantiate wasm extension")?; + Ok((Self::V006(extension), instance)) + } else if version >= since_v0_0_4::MIN_VERSION { let (extension, instance) = since_v0_0_4::Extension::instantiate_async( store, &component, @@ -69,11 +75,21 @@ impl Extension { .await .context("failed to instantiate wasm extension")?; Ok((Self::V004(extension), instance)) + } else { + let (extension, instance) = since_v0_0_1::Extension::instantiate_async( + store, + &component, + since_v0_0_1::linker(), + ) + .await + .context("failed to instantiate wasm extension")?; + Ok((Self::V001(extension), instance)) } } pub async fn call_init_extension(&self, store: &mut Store) -> Result<()> { match self { + Extension::V006(ext) => ext.call_init_extension(store).await, Extension::V004(ext) => ext.call_init_extension(store).await, Extension::V001(ext) => ext.call_init_extension(store).await, } @@ -82,14 +98,19 @@ impl Extension { pub async fn call_language_server_command( &self, store: &mut Store, + language_server_id: &LanguageServerName, config: &LanguageServerConfig, resource: Resource>, ) -> Result> { match self { - Extension::V004(ext) => { - ext.call_language_server_command(store, config, resource) + Extension::V006(ext) => { + ext.call_language_server_command(store, &language_server_id.0, resource) .await } + Extension::V004(ext) => Ok(ext + .call_language_server_command(store, config, resource) + .await? + .map(|command| command.into())), Extension::V001(ext) => Ok(ext .call_language_server_command(store, &config.clone().into(), resource) .await? @@ -100,10 +121,19 @@ impl Extension { pub async fn call_language_server_initialization_options( &self, store: &mut Store, + language_server_id: &LanguageServerName, config: &LanguageServerConfig, resource: Resource>, ) -> Result, String>> { match self { + Extension::V006(ext) => { + ext.call_language_server_initialization_options( + store, + &language_server_id.0, + resource, + ) + .await + } Extension::V004(ext) => { ext.call_language_server_initialization_options(store, config, resource) .await @@ -118,6 +148,23 @@ impl Extension { } } } + + pub async fn call_labels_for_completions( + &self, + store: &mut Store, + language_server_id: &LanguageServerName, + completions: Vec, + ) -> Result>, String>> { + match self { + Extension::V001(_) | Extension::V004(_) => { + bail!("unsupported function: 'labels_for_completions'") + } + Extension::V006(ext) => { + ext.call_labels_for_completions(store, &language_server_id.0, &completions) + .await + } + } + } } trait ToWasmtimeResult { diff --git a/crates/extension/src/wasm_host/wit/since_v0_0_1.rs b/crates/extension/src/wasm_host/wit/since_v0_0_1.rs index 246b06028e..3fd76eaaf9 100644 --- a/crates/extension/src/wasm_host/wit/since_v0_0_1.rs +++ b/crates/extension/src/wasm_host/wit/since_v0_0_1.rs @@ -1,4 +1,5 @@ use super::latest; +use crate::wasm_host::wit::since_v0_0_4; use crate::wasm_host::WasmState; use anyhow::Result; use async_trait::async_trait; @@ -82,8 +83,8 @@ impl From for latest::DownloadedFileType { } } -impl From for LanguageServerConfig { - fn from(value: latest::LanguageServerConfig) -> Self { +impl From for LanguageServerConfig { + fn from(value: since_v0_0_4::LanguageServerConfig) -> Self { Self { name: value.name, language_name: value.language_name, diff --git a/crates/extension/src/wasm_host/wit/since_v0_0_4.rs b/crates/extension/src/wasm_host/wit/since_v0_0_4.rs index d168681b5f..6020f34263 100644 --- a/crates/extension/src/wasm_host/wit/since_v0_0_4.rs +++ b/crates/extension/src/wasm_host/wit/since_v0_0_4.rs @@ -1,23 +1,13 @@ -use crate::wasm_host::wit::ToWasmtimeResult; +use super::latest; use crate::wasm_host::WasmState; -use anyhow::{anyhow, Result}; -use async_compression::futures::bufread::GzipDecoder; -use async_tar::Archive; +use anyhow::Result; use async_trait::async_trait; -use futures::io::BufReader; -use language::{LanguageServerBinaryStatus, LspAdapterDelegate}; +use language::LspAdapterDelegate; use semantic_version::SemanticVersion; -use std::path::Path; -use std::{ - env, - path::PathBuf, - sync::{Arc, OnceLock}, -}; -use util::maybe; +use std::sync::{Arc, OnceLock}; use wasmtime::component::{Linker, Resource}; pub const MIN_VERSION: SemanticVersion = SemanticVersion::new(0, 0, 4); -pub const MAX_VERSION: SemanticVersion = SemanticVersion::new(0, 0, 5); wasmtime::component::bindgen!({ async: true, @@ -34,6 +24,93 @@ pub fn linker() -> &'static Linker { LINKER.get_or_init(|| super::new_linker(Extension::add_to_linker)) } +impl From for Os { + fn from(value: latest::Os) -> Self { + match value { + latest::Os::Mac => Os::Mac, + latest::Os::Linux => Os::Linux, + latest::Os::Windows => Os::Windows, + } + } +} + +impl From for Architecture { + fn from(value: latest::Architecture) -> Self { + match value { + latest::Architecture::Aarch64 => Self::Aarch64, + latest::Architecture::X86 => Self::X86, + latest::Architecture::X8664 => Self::X8664, + } + } +} + +impl From for GithubRelease { + fn from(value: latest::GithubRelease) -> Self { + Self { + version: value.version, + assets: value.assets.into_iter().map(|asset| asset.into()).collect(), + } + } +} + +impl From for GithubReleaseAsset { + fn from(value: latest::GithubReleaseAsset) -> Self { + Self { + name: value.name, + download_url: value.download_url, + } + } +} + +impl From for latest::GithubReleaseOptions { + fn from(value: GithubReleaseOptions) -> Self { + Self { + require_assets: value.require_assets, + pre_release: value.pre_release, + } + } +} + +impl From for latest::DownloadedFileType { + fn from(value: DownloadedFileType) -> Self { + match value { + DownloadedFileType::Gzip => latest::DownloadedFileType::Gzip, + DownloadedFileType::GzipTar => latest::DownloadedFileType::GzipTar, + DownloadedFileType::Zip => latest::DownloadedFileType::Zip, + DownloadedFileType::Uncompressed => latest::DownloadedFileType::Uncompressed, + } + } +} + +impl From for latest::LanguageServerInstallationStatus { + fn from(value: LanguageServerInstallationStatus) -> Self { + match value { + LanguageServerInstallationStatus::None => { + latest::LanguageServerInstallationStatus::None + } + LanguageServerInstallationStatus::Downloading => { + latest::LanguageServerInstallationStatus::Downloading + } + LanguageServerInstallationStatus::CheckingForUpdate => { + latest::LanguageServerInstallationStatus::CheckingForUpdate + } + LanguageServerInstallationStatus::Failed(error) => { + latest::LanguageServerInstallationStatus::Failed(error) + } + } + } +} + +impl From for latest::Command { + fn from(value: Command) -> Self { + Self { + command: value.command, + args: value.args, + env: value.env, + } + } +} + #[async_trait] impl HostWorktree for WasmState { async fn read_text_file( @@ -41,19 +118,14 @@ impl HostWorktree for WasmState { delegate: Resource>, path: String, ) -> wasmtime::Result> { - let delegate = self.table.get(&delegate)?; - Ok(delegate - .read_text_file(path.into()) - .await - .map_err(|error| error.to_string())) + latest::HostWorktree::read_text_file(self, delegate, path).await } async fn shell_env( &mut self, delegate: Resource>, ) -> wasmtime::Result { - let delegate = self.table.get(&delegate)?; - Ok(delegate.shell_env().await.into_iter().collect()) + latest::HostWorktree::shell_env(self, delegate).await } async fn which( @@ -61,15 +133,11 @@ impl HostWorktree for WasmState { delegate: Resource>, binary_name: String, ) -> wasmtime::Result> { - let delegate = self.table.get(&delegate)?; - Ok(delegate - .which(binary_name.as_ref()) - .await - .map(|path| path.to_string_lossy().to_string())) + latest::HostWorktree::which(self, delegate, binary_name).await } fn drop(&mut self, _worktree: Resource) -> Result<()> { - // we only ever hand out borrows of worktrees + // We only ever hand out borrows of worktrees. Ok(()) } } @@ -77,34 +145,21 @@ impl HostWorktree for WasmState { #[async_trait] impl ExtensionImports 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() + latest::ExtensionImports::node_binary_path(self).await } 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() + latest::ExtensionImports::npm_package_latest_version(self, package_name).await } 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() + latest::ExtensionImports::npm_package_installed_version(self, package_name).await } async fn npm_install_package( @@ -112,11 +167,7 @@ impl ExtensionImports for WasmState { package_name: String, version: String, ) -> wasmtime::Result> { - self.host - .node_runtime - .npm_install_packages(&self.work_dir(), &[(&package_name, &version)]) - .await - .to_wasmtime_result() + latest::ExtensionImports::npm_install_package(self, package_name, version).await } async fn latest_github_release( @@ -124,45 +175,17 @@ impl ExtensionImports for WasmState { repo: String, options: GithubReleaseOptions, ) -> wasmtime::Result> { - maybe!(async { - let release = util::github::latest_github_release( - &repo, - options.require_assets, - options.pre_release, - self.host.http_client.clone(), - ) - .await?; - Ok(GithubRelease { - version: release.tag_name, - assets: release - .assets - .into_iter() - .map(|asset| GithubReleaseAsset { - name: asset.name, - download_url: asset.browser_download_url, - }) - .collect(), - }) - }) - .await - .to_wasmtime_result() + Ok( + latest::ExtensionImports::latest_github_release(self, repo, options.into()) + .await? + .map(|github| github.into()), + ) } async fn current_platform(&mut self) -> Result<(Os, Architecture)> { - Ok(( - match env::consts::OS { - "macos" => Os::Mac, - "linux" => Os::Linux, - "windows" => Os::Windows, - _ => panic!("unsupported os"), - }, - match env::consts::ARCH { - "aarch64" => Architecture::Aarch64, - "x86" => Architecture::X86, - "x86_64" => Architecture::X8664, - _ => panic!("unsupported architecture"), - }, - )) + latest::ExtensionImports::current_platform(self) + .await + .map(|(os, arch)| (os.into(), arch.into())) } async fn set_language_server_installation_status( @@ -170,23 +193,12 @@ impl ExtensionImports for WasmState { server_name: String, status: LanguageServerInstallationStatus, ) -> wasmtime::Result<()> { - let status = match status { - LanguageServerInstallationStatus::CheckingForUpdate => { - LanguageServerBinaryStatus::CheckingForUpdate - } - LanguageServerInstallationStatus::Downloading => { - LanguageServerBinaryStatus::Downloading - } - LanguageServerInstallationStatus::None => LanguageServerBinaryStatus::None, - LanguageServerInstallationStatus::Failed(error) => { - LanguageServerBinaryStatus::Failed { error } - } - }; - - self.host - .language_registry - .update_lsp_status(language::LanguageServerName(server_name.into()), status); - Ok(()) + latest::ExtensionImports::set_language_server_installation_status( + self, + server_name, + status.into(), + ) + .await } async fn download_file( @@ -195,103 +207,10 @@ impl ExtensionImports for WasmState { path: String, file_type: DownloadedFileType, ) -> wasmtime::Result> { - maybe!(async { - let path = PathBuf::from(path); - let extension_work_dir = self.host.work_dir.join(self.manifest.id.as_ref()); - - self.host.fs.create_dir(&extension_work_dir).await?; - - let destination_path = self - .host - .writeable_path_from_extension(&self.manifest.id, &path)?; - - let mut response = self - .host - .http_client - .get(&url, Default::default(), true) - .await - .map_err(|err| anyhow!("error downloading release: {}", err))?; - - if !response.status().is_success() { - Err(anyhow!( - "download failed with status {}", - response.status().to_string() - ))?; - } - let body = BufReader::new(response.body_mut()); - - match file_type { - DownloadedFileType::Uncompressed => { - futures::pin_mut!(body); - self.host - .fs - .create_file_with(&destination_path, body) - .await?; - } - DownloadedFileType::Gzip => { - let body = GzipDecoder::new(body); - futures::pin_mut!(body); - self.host - .fs - .create_file_with(&destination_path, body) - .await?; - } - DownloadedFileType::GzipTar => { - let body = GzipDecoder::new(body); - futures::pin_mut!(body); - self.host - .fs - .extract_tar_file(&destination_path, Archive::new(body)) - .await?; - } - DownloadedFileType::Zip => { - let file_name = destination_path - .file_name() - .ok_or_else(|| anyhow!("invalid download path"))? - .to_string_lossy(); - let zip_filename = format!("{file_name}.zip"); - let mut zip_path = destination_path.clone(); - zip_path.set_file_name(zip_filename); - - futures::pin_mut!(body); - self.host.fs.create_file_with(&zip_path, body).await?; - - let unzip_status = std::process::Command::new("unzip") - .current_dir(&extension_work_dir) - .arg("-d") - .arg(&destination_path) - .arg(&zip_path) - .output()? - .status; - if !unzip_status.success() { - Err(anyhow!("failed to unzip {} archive", path.display()))?; - } - } - } - - Ok(()) - }) - .await - .to_wasmtime_result() + latest::ExtensionImports::download_file(self, url, path, file_type.into()).await } async fn make_file_executable(&mut self, path: String) -> wasmtime::Result> { - #[allow(unused)] - let path = self - .host - .writeable_path_from_extension(&self.manifest.id, Path::new(&path))?; - - #[cfg(unix)] - { - use std::fs::{self, Permissions}; - use std::os::unix::fs::PermissionsExt; - - return fs::set_permissions(&path, Permissions::from_mode(0o755)) - .map_err(|error| anyhow!("failed to set permissions for path {path:?}: {error}")) - .to_wasmtime_result(); - } - - #[cfg(not(unix))] - Ok(Ok(())) + latest::ExtensionImports::make_file_executable(self, path).await } } diff --git a/crates/extension/src/wasm_host/wit/since_v0_0_6.rs b/crates/extension/src/wasm_host/wit/since_v0_0_6.rs new file mode 100644 index 0000000000..a46de96afa --- /dev/null +++ b/crates/extension/src/wasm_host/wit/since_v0_0_6.rs @@ -0,0 +1,299 @@ +use crate::wasm_host::wit::ToWasmtimeResult; +use crate::wasm_host::WasmState; +use anyhow::{anyhow, Result}; +use async_compression::futures::bufread::GzipDecoder; +use async_tar::Archive; +use async_trait::async_trait; +use futures::io::BufReader; +use language::{LanguageServerBinaryStatus, LspAdapterDelegate}; +use semantic_version::SemanticVersion; +use std::path::Path; +use std::{ + env, + path::PathBuf, + sync::{Arc, OnceLock}, +}; +use util::maybe; +use wasmtime::component::{Linker, Resource}; + +pub const MIN_VERSION: SemanticVersion = SemanticVersion::new(0, 0, 6); +pub const MAX_VERSION: SemanticVersion = SemanticVersion::new(0, 0, 6); + +wasmtime::component::bindgen!({ + async: true, + path: "../extension_api/wit/since_v0.0.6", + with: { + "worktree": ExtensionWorktree, + }, +}); + +pub type ExtensionWorktree = Arc; + +pub fn linker() -> &'static Linker { + static LINKER: OnceLock> = OnceLock::new(); + LINKER.get_or_init(|| super::new_linker(Extension::add_to_linker)) +} + +#[async_trait] +impl HostWorktree for WasmState { + async fn 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(()) + } +} + +impl self::zed::extension::lsp::Host for WasmState {} + +#[async_trait] +impl ExtensionImports 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 fn latest_github_release( + &mut self, + repo: String, + options: GithubReleaseOptions, + ) -> wasmtime::Result> { + maybe!(async { + let release = util::github::latest_github_release( + &repo, + options.require_assets, + options.pre_release, + self.host.http_client.clone(), + ) + .await?; + Ok(GithubRelease { + version: release.tag_name, + assets: release + .assets + .into_iter() + .map(|asset| GithubReleaseAsset { + name: asset.name, + download_url: asset.browser_download_url, + }) + .collect(), + }) + }) + .await + .to_wasmtime_result() + } + + async fn current_platform(&mut self) -> Result<(Os, Architecture)> { + Ok(( + match env::consts::OS { + "macos" => Os::Mac, + "linux" => Os::Linux, + "windows" => Os::Windows, + _ => panic!("unsupported os"), + }, + match env::consts::ARCH { + "aarch64" => Architecture::Aarch64, + "x86" => Architecture::X86, + "x86_64" => Architecture::X8664, + _ => panic!("unsupported architecture"), + }, + )) + } + + async fn set_language_server_installation_status( + &mut self, + server_name: String, + status: LanguageServerInstallationStatus, + ) -> wasmtime::Result<()> { + let status = match status { + LanguageServerInstallationStatus::CheckingForUpdate => { + LanguageServerBinaryStatus::CheckingForUpdate + } + LanguageServerInstallationStatus::Downloading => { + LanguageServerBinaryStatus::Downloading + } + LanguageServerInstallationStatus::None => LanguageServerBinaryStatus::None, + LanguageServerInstallationStatus::Failed(error) => { + LanguageServerBinaryStatus::Failed { error } + } + }; + + self.host + .language_registry + .update_lsp_status(language::LanguageServerName(server_name.into()), status); + Ok(()) + } + + async fn download_file( + &mut self, + url: String, + path: String, + file_type: DownloadedFileType, + ) -> wasmtime::Result> { + maybe!(async { + let path = PathBuf::from(path); + let extension_work_dir = self.host.work_dir.join(self.manifest.id.as_ref()); + + self.host.fs.create_dir(&extension_work_dir).await?; + + let destination_path = self + .host + .writeable_path_from_extension(&self.manifest.id, &path)?; + + let mut response = self + .host + .http_client + .get(&url, Default::default(), true) + .await + .map_err(|err| anyhow!("error downloading release: {}", err))?; + + if !response.status().is_success() { + Err(anyhow!( + "download failed with status {}", + response.status().to_string() + ))?; + } + let body = BufReader::new(response.body_mut()); + + match file_type { + DownloadedFileType::Uncompressed => { + futures::pin_mut!(body); + self.host + .fs + .create_file_with(&destination_path, body) + .await?; + } + DownloadedFileType::Gzip => { + let body = GzipDecoder::new(body); + futures::pin_mut!(body); + self.host + .fs + .create_file_with(&destination_path, body) + .await?; + } + DownloadedFileType::GzipTar => { + let body = GzipDecoder::new(body); + futures::pin_mut!(body); + self.host + .fs + .extract_tar_file(&destination_path, Archive::new(body)) + .await?; + } + DownloadedFileType::Zip => { + let file_name = destination_path + .file_name() + .ok_or_else(|| anyhow!("invalid download path"))? + .to_string_lossy(); + let zip_filename = format!("{file_name}.zip"); + let mut zip_path = destination_path.clone(); + zip_path.set_file_name(zip_filename); + + futures::pin_mut!(body); + self.host.fs.create_file_with(&zip_path, body).await?; + + let unzip_status = std::process::Command::new("unzip") + .current_dir(&extension_work_dir) + .arg("-d") + .arg(&destination_path) + .arg(&zip_path) + .output()? + .status; + if !unzip_status.success() { + Err(anyhow!("failed to unzip {} archive", path.display()))?; + } + } + } + + Ok(()) + }) + .await + .to_wasmtime_result() + } + + async fn make_file_executable(&mut self, path: String) -> wasmtime::Result> { + #[allow(unused)] + let path = self + .host + .writeable_path_from_extension(&self.manifest.id, Path::new(&path))?; + + #[cfg(unix)] + { + use std::fs::{self, Permissions}; + use std::os::unix::fs::PermissionsExt; + + return fs::set_permissions(&path, Permissions::from_mode(0o755)) + .map_err(|error| anyhow!("failed to set permissions for path {path:?}: {error}")) + .to_wasmtime_result(); + } + + #[cfg(not(unix))] + Ok(Ok(())) + } +} diff --git a/crates/extension_api/Cargo.toml b/crates/extension_api/Cargo.toml index c5d75259dc..2bd89ed93b 100644 --- a/crates/extension_api/Cargo.toml +++ b/crates/extension_api/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zed_extension_api" -version = "0.0.5" +version = "0.0.6" description = "APIs for creating Zed extensions in Rust" repository = "https://github.com/zed-industries/zed" documentation = "https://docs.rs/zed_extension_api" diff --git a/crates/extension_api/src/extension_api.rs b/crates/extension_api/src/extension_api.rs index 435e83a77c..db9297f085 100644 --- a/crates/extension_api/src/extension_api.rs +++ b/crates/extension_api/src/extension_api.rs @@ -1,24 +1,69 @@ -pub use wit::*; +use core::fmt; + +use wit::*; + +// WIT re-exports. +// +// We explicitly enumerate the symbols we want to re-export, as there are some +// that we may want to shadow to provide a cleaner Rust API. +pub use wit::{ + current_platform, download_file, latest_github_release, make_file_executable, node_binary_path, + npm_install_package, npm_package_installed_version, npm_package_latest_version, + zed::extension::lsp, Architecture, CodeLabel, CodeLabelSpan, CodeLabelSpanLiteral, Command, + DownloadedFileType, EnvVars, GithubRelease, GithubReleaseAsset, GithubReleaseOptions, + LanguageServerInstallationStatus, Os, Range, Worktree, +}; + +// Undocumented WIT re-exports. +// +// These are symbols that need to be public for the purposes of implementing +// the extension host, but aren't relevant to extension authors. +#[doc(hidden)] +pub use wit::Guest; + +/// A result returned from a Zed extension. pub type Result = core::result::Result; +/// Updates the installation status for the given language server. +pub fn set_language_server_installation_status( + language_server_id: &LanguageServerId, + status: &LanguageServerInstallationStatus, +) { + wit::set_language_server_installation_status(&language_server_id.0, status) +} + +/// A Zed extension. pub trait Extension: Send + Sync { + /// Returns a new instance of the extension. fn new() -> Self where Self: Sized; + /// Returns the command used to start the language server for the specified + /// language. fn language_server_command( &mut self, - config: LanguageServerConfig, + language_server_id: &LanguageServerId, worktree: &Worktree, ) -> Result; + /// Returns the initialization options to pass to the specified language server. fn language_server_initialization_options( &mut self, - _config: LanguageServerConfig, + _language_server_id: &LanguageServerId, _worktree: &Worktree, ) -> Result> { Ok(None) } + + /// Returns the label for the given completion. + fn label_for_completion( + &self, + _language_server_id: &LanguageServerId, + _completion: Completion, + ) -> Option { + None + } } #[macro_export] @@ -53,7 +98,7 @@ pub static ZED_API_VERSION: [u8; 6] = *include_bytes!(concat!(env!("OUT_DIR"), " mod wit { wit_bindgen::generate!({ skip: ["init-extension"], - path: "./wit/since_v0.0.4", + path: "./wit/since_v0.0.6", }); } @@ -63,16 +108,76 @@ struct Component; impl wit::Guest for Component { fn language_server_command( - config: wit::LanguageServerConfig, + language_server_id: String, worktree: &wit::Worktree, ) -> Result { - extension().language_server_command(config, worktree) + let language_server_id = LanguageServerId(language_server_id); + extension().language_server_command(&language_server_id, worktree) } fn language_server_initialization_options( - config: LanguageServerConfig, + language_server_id: String, worktree: &Worktree, ) -> Result, String> { - extension().language_server_initialization_options(config, worktree) + let language_server_id = LanguageServerId(language_server_id); + extension().language_server_initialization_options(&language_server_id, worktree) + } + + fn labels_for_completions( + language_server_id: String, + completions: Vec, + ) -> Result>, String> { + let language_server_id = LanguageServerId(language_server_id); + let mut labels = Vec::new(); + for (ix, completion) in completions.into_iter().enumerate() { + let label = extension().label_for_completion(&language_server_id, completion); + if let Some(label) = label { + labels.resize(ix + 1, None); + *labels.last_mut().unwrap() = Some(label); + } + } + Ok(labels) + } +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)] +pub struct LanguageServerId(String); + +impl fmt::Display for LanguageServerId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl CodeLabelSpan { + /// Returns a [`CodeLabelSpan::CodeRange`]. + pub fn code_range(range: impl Into) -> Self { + Self::CodeRange(range.into()) + } + + /// Returns a [`CodeLabelSpan::Literal`]. + pub fn literal(text: impl Into, highlight_name: Option) -> Self { + Self::Literal(CodeLabelSpanLiteral { + text: text.into(), + highlight_name, + }) + } +} + +impl From> for wit::Range { + fn from(value: std::ops::Range) -> Self { + Self { + start: value.start, + end: value.end, + } + } +} + +impl From> for wit::Range { + fn from(value: std::ops::Range) -> Self { + Self { + start: value.start as u32, + end: value.end as u32, + } } } diff --git a/crates/extension_api/wit/since_v0.0.6/extension.wit b/crates/extension_api/wit/since_v0.0.6/extension.wit new file mode 100644 index 0000000000..b10e69c024 --- /dev/null +++ b/crates/extension_api/wit/since_v0.0.6/extension.wit @@ -0,0 +1,120 @@ +package zed:extension; + +world extension { + import lsp; + + use lsp.{completion}; + + export init-extension: func(); + + record github-release { + version: string, + assets: list, + } + + record github-release-asset { + name: string, + download-url: string, + } + + record github-release-options { + require-assets: bool, + pre-release: bool, + } + + enum os { + mac, + linux, + windows, + } + + enum architecture { + aarch64, + x86, + x8664, + } + + enum downloaded-file-type { + gzip, + gzip-tar, + zip, + uncompressed, + } + + variant language-server-installation-status { + none, + downloading, + checking-for-update, + failed(string), + } + + /// Gets the current operating system and architecture + import current-platform: func() -> tuple; + + /// Get the path to the node binary used by Zed. + import node-binary-path: func() -> result; + + /// Gets the latest version of the given NPM package. + import npm-package-latest-version: func(package-name: string) -> result; + + /// Returns the installed version of the given NPM package, if it exists. + import npm-package-installed-version: func(package-name: string) -> result, string>; + + /// Installs the specified NPM package. + import npm-install-package: func(package-name: string, version: string) -> result<_, string>; + + /// Gets the latest release for the given GitHub repository. + import latest-github-release: func(repo: string, options: github-release-options) -> result; + + /// Downloads a file from the given url, and saves it to the given path within the extension's + /// working directory. Extracts the file 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); + + type env-vars = list>; + + record command { + command: string, + args: list, + env: env-vars, + } + + resource worktree { + read-text-file: func(path: string) -> result; + which: func(binary-name: string) -> option; + shell-env: func() -> env-vars; + } + + export language-server-command: func(language-server-id: string, worktree: borrow) -> result; + export language-server-initialization-options: func(language-server-id: string, worktree: borrow) -> result, string>; + + record code-label { + /// The source code to parse with Tree-sitter. + code: string, + spans: list, + filter-range: range, + } + + variant code-label-span { + /// A range into the parsed code. + code-range(range), + literal(code-label-span-literal), + } + + record code-label-span-literal { + text: string, + highlight-name: option, + } + + record range { + start: u32, + end: u32, + } + + export labels-for-completions: func(language-server-id: string, completions: list) -> result>, string>; +} diff --git a/crates/extension_api/wit/since_v0.0.6/lsp.wit b/crates/extension_api/wit/since_v0.0.6/lsp.wit new file mode 100644 index 0000000000..89501ca9c0 --- /dev/null +++ b/crates/extension_api/wit/since_v0.0.6/lsp.wit @@ -0,0 +1,44 @@ +interface lsp { + /// An LSP completion. + record completion { + label: string, + detail: option, + kind: option, + insert-text-format: option, + } + + 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), + } + + variant insert-text-format { + plain-text, + snippet, + other(s32), + } +} diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index ea5825e477..4ff18b0bbb 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -213,8 +213,9 @@ impl CachedLspAdapter { &self, completion_items: &[lsp::CompletionItem], language: &Arc, - ) -> Vec> { + ) -> Result>> { self.adapter + .clone() .labels_for_completions(completion_items, language) .await } @@ -385,10 +386,10 @@ pub trait LspAdapter: 'static + Send + Sync { async fn process_completions(&self, _: &mut [lsp::CompletionItem]) {} async fn labels_for_completions( - &self, + self: Arc, completions: &[lsp::CompletionItem], language: &Arc, - ) -> Vec> { + ) -> Result>> { let mut labels = Vec::new(); for (ix, completion) in completions.into_iter().enumerate() { let label = self.label_for_completion(completion, language).await; @@ -397,7 +398,7 @@ pub trait LspAdapter: 'static + Send + Sync { *labels.last_mut().unwrap() = Some(label); } } - labels + Ok(labels) } async fn label_for_completion( diff --git a/crates/language/src/language_registry.rs b/crates/language/src/language_registry.rs index 468a4e93d3..11e7bc796b 100644 --- a/crates/language/src/language_registry.rs +++ b/crates/language/src/language_registry.rs @@ -746,7 +746,10 @@ impl LanguageRegistry { let capabilities = adapter .as_fake() .map(|fake_adapter| fake_adapter.capabilities.clone()) - .unwrap_or_default(); + .unwrap_or_else(|| lsp::ServerCapabilities { + completion_provider: Some(Default::default()), + ..Default::default() + }); let (server, mut fake_server) = lsp::FakeLanguageServer::new( server_id, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 3b153319d7..cd7d297183 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -9915,6 +9915,8 @@ async fn populate_labels_for_completions( lsp_adapter .labels_for_completions(&lsp_completions, language) .await + .log_err() + .unwrap_or_default() } else { Vec::new() }; diff --git a/extensions/gleam/Cargo.toml b/extensions/gleam/Cargo.toml index 4e4a88835b..8aba5e54c9 100644 --- a/extensions/gleam/Cargo.toml +++ b/extensions/gleam/Cargo.toml @@ -13,4 +13,5 @@ path = "src/gleam.rs" crate-type = ["cdylib"] [dependencies] -zed_extension_api = "0.0.4" +# zed_extension_api = "0.0.4" +zed_extension_api = { path = "../../crates/extension_api" } diff --git a/extensions/gleam/languages/gleam/highlights.scm b/extensions/gleam/languages/gleam/highlights.scm index a95f6cb031..4b85b88d01 100644 --- a/extensions/gleam/languages/gleam/highlights.scm +++ b/extensions/gleam/languages/gleam/highlights.scm @@ -7,6 +7,10 @@ (constant name: (identifier) @constant) +; Variables +(identifier) @variable +(discard) @comment.unused + ; Modules (module) @module (import alias: (identifier) @module) @@ -75,10 +79,6 @@ ((identifier) @warning (#match? @warning "^(auto|delegate|derive|else|implement|macro|test|echo)$")) -; Variables -(identifier) @variable -(discard) @comment.unused - ; Keywords [ (visibility_modifier) ; "pub" diff --git a/extensions/gleam/src/gleam.rs b/extensions/gleam/src/gleam.rs index a5b594759d..6fe4024785 100644 --- a/extensions/gleam/src/gleam.rs +++ b/extensions/gleam/src/gleam.rs @@ -1,4 +1,6 @@ use std::fs; +use zed::lsp::CompletionKind; +use zed::{CodeLabel, CodeLabelSpan, LanguageServerId}; use zed_extension_api::{self as zed, Result}; struct GleamExtension { @@ -8,7 +10,7 @@ struct GleamExtension { impl GleamExtension { fn language_server_binary_path( &mut self, - config: zed::LanguageServerConfig, + language_server_id: &LanguageServerId, worktree: &zed::Worktree, ) -> Result { if let Some(path) = &self.cached_binary_path { @@ -23,7 +25,7 @@ impl GleamExtension { } zed::set_language_server_installation_status( - &config.name, + &language_server_id, &zed::LanguageServerInstallationStatus::CheckingForUpdate, ); let release = zed::latest_github_release( @@ -61,7 +63,7 @@ impl GleamExtension { if !fs::metadata(&binary_path).map_or(false, |stat| stat.is_file()) { zed::set_language_server_installation_status( - &config.name, + &language_server_id, &zed::LanguageServerInstallationStatus::Downloading, ); @@ -96,15 +98,51 @@ impl zed::Extension for GleamExtension { fn language_server_command( &mut self, - config: zed::LanguageServerConfig, + language_server_id: &LanguageServerId, worktree: &zed::Worktree, ) -> Result { Ok(zed::Command { - command: self.language_server_binary_path(config, worktree)?, + command: self.language_server_binary_path(language_server_id, worktree)?, args: vec!["lsp".to_string()], env: Default::default(), }) } + + fn label_for_completion( + &self, + _language_server_id: &LanguageServerId, + completion: zed::lsp::Completion, + ) -> Option { + let name = &completion.label; + let ty = completion.detail?; + let let_binding = "let a"; + let colon = ": "; + let assignment = " = "; + let call = match completion.kind? { + CompletionKind::Function | CompletionKind::Constructor => "()", + _ => "", + }; + let code = format!("{let_binding}{colon}{ty}{assignment}{name}{call}"); + + Some(CodeLabel { + spans: vec![ + CodeLabelSpan::code_range({ + let start = let_binding.len() + colon.len() + ty.len() + assignment.len(); + start..start + name.len() + }), + CodeLabelSpan::code_range({ + let start = let_binding.len(); + start..start + colon.len() + }), + CodeLabelSpan::code_range({ + let start = let_binding.len() + colon.len(); + start..start + ty.len() + }), + ], + filter_range: (0..name.len()).into(), + code, + }) + } } zed::register_extension!(GleamExtension);