Add label_for_completion to extension API (#10175)

This PR adds the ability for extensions to implement
`label_for_completion` to customize completions coming back from the
language server.

We've used the Gleam extension as a motivating example, adding
`label_for_completion` support to it.

Release Notes:

- N/A

---------

Co-authored-by: Max <max@zed.dev>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
This commit is contained in:
Marshall Bowers 2024-04-04 13:56:04 -04:00 committed by GitHub
parent 0f1c2e6f2b
commit d306b531c7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 1124 additions and 252 deletions

12
Cargo.lock generated
View File

@ -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]]

View File

@ -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<WasmHost>,
}
@ -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<Self>,
completions: &[lsp::CompletionItem],
language: &Arc<Language>,
) -> Result<Vec<Option<CodeLabel>>> {
let completions = completions
.into_iter()
.map(|completion| wit::Completion::from(completion.clone()))
.collect::<Vec<_>>();
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<usize>, HighlightId)],
language: &Arc<Language>,
) -> 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<wit::Range> for Range<usize> {
fn from(range: wit::Range) -> Self {
let start = range.start as usize;
let end = range.end as usize;
start..end
}
}
impl From<lsp::CompletionItem> 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<lsp::CompletionItemKind> 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<lsp::InsertTextFormat> 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::<Vec<_>>();
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::<Vec<_>>();
assert_eq!(
label,
CodeLabel {
text,
runs,
filter_range: label.filter_range.clone()
}
)
}

View File

@ -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(),
},
}),

View File

@ -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::<lsp::request::Completion, _, _>(|_, _| 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::<Vec<_>>();
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();

View File

@ -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<WasmState>, fn(&mut WasmState) -> &mut WasmState) -> Result<()>,
@ -41,6 +49,7 @@ pub fn wasm_api_version_range() -> RangeInclusive<SemanticVersion> {
}
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<WasmState>) -> 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<WasmState>,
language_server_id: &LanguageServerName,
config: &LanguageServerConfig,
resource: Resource<Arc<dyn LspAdapterDelegate>>,
) -> Result<Result<Command, String>> {
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<WasmState>,
language_server_id: &LanguageServerName,
config: &LanguageServerConfig,
resource: Resource<Arc<dyn LspAdapterDelegate>>,
) -> Result<Result<Option<String>, 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<WasmState>,
language_server_id: &LanguageServerName,
completions: Vec<latest::Completion>,
) -> Result<Result<Vec<Option<CodeLabel>>, 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<T> {

View File

@ -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<DownloadedFileType> for latest::DownloadedFileType {
}
}
impl From<latest::LanguageServerConfig> for LanguageServerConfig {
fn from(value: latest::LanguageServerConfig) -> Self {
impl From<since_v0_0_4::LanguageServerConfig> for LanguageServerConfig {
fn from(value: since_v0_0_4::LanguageServerConfig) -> Self {
Self {
name: value.name,
language_name: value.language_name,

View File

@ -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<WasmState> {
LINKER.get_or_init(|| super::new_linker(Extension::add_to_linker))
}
impl From<latest::Os> for Os {
fn from(value: latest::Os) -> Self {
match value {
latest::Os::Mac => Os::Mac,
latest::Os::Linux => Os::Linux,
latest::Os::Windows => Os::Windows,
}
}
}
impl From<latest::Architecture> for Architecture {
fn from(value: latest::Architecture) -> Self {
match value {
latest::Architecture::Aarch64 => Self::Aarch64,
latest::Architecture::X86 => Self::X86,
latest::Architecture::X8664 => Self::X8664,
}
}
}
impl From<latest::GithubRelease> for GithubRelease {
fn from(value: latest::GithubRelease) -> Self {
Self {
version: value.version,
assets: value.assets.into_iter().map(|asset| asset.into()).collect(),
}
}
}
impl From<latest::GithubReleaseAsset> for GithubReleaseAsset {
fn from(value: latest::GithubReleaseAsset) -> Self {
Self {
name: value.name,
download_url: value.download_url,
}
}
}
impl From<GithubReleaseOptions> for latest::GithubReleaseOptions {
fn from(value: GithubReleaseOptions) -> Self {
Self {
require_assets: value.require_assets,
pre_release: value.pre_release,
}
}
}
impl From<DownloadedFileType> for latest::DownloadedFileType {
fn from(value: DownloadedFileType) -> Self {
match value {
DownloadedFileType::Gzip => latest::DownloadedFileType::Gzip,
DownloadedFileType::GzipTar => latest::DownloadedFileType::GzipTar,
DownloadedFileType::Zip => latest::DownloadedFileType::Zip,
DownloadedFileType::Uncompressed => latest::DownloadedFileType::Uncompressed,
}
}
}
impl From<LanguageServerInstallationStatus> 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<Command> for latest::Command {
fn from(value: Command) -> Self {
Self {
command: value.command,
args: value.args,
env: value.env,
}
}
}
#[async_trait]
impl HostWorktree for WasmState {
async fn read_text_file(
@ -41,19 +118,14 @@ impl HostWorktree for WasmState {
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
path: String,
) -> wasmtime::Result<Result<String, String>> {
let delegate = self.table.get(&delegate)?;
Ok(delegate
.read_text_file(path.into())
.await
.map_err(|error| error.to_string()))
latest::HostWorktree::read_text_file(self, delegate, path).await
}
async fn shell_env(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
) -> wasmtime::Result<EnvVars> {
let delegate = self.table.get(&delegate)?;
Ok(delegate.shell_env().await.into_iter().collect())
latest::HostWorktree::shell_env(self, delegate).await
}
async fn which(
@ -61,15 +133,11 @@ impl HostWorktree for WasmState {
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
binary_name: String,
) -> wasmtime::Result<Option<String>> {
let delegate = self.table.get(&delegate)?;
Ok(delegate
.which(binary_name.as_ref())
.await
.map(|path| path.to_string_lossy().to_string()))
latest::HostWorktree::which(self, delegate, binary_name).await
}
fn drop(&mut self, _worktree: Resource<Worktree>) -> 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<Result<String, String>> {
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<Result<String, String>> {
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<Result<Option<String>, 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<Result<(), String>> {
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<Result<GithubRelease, String>> {
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<Result<(), String>> {
maybe!(async {
let path = PathBuf::from(path);
let extension_work_dir = self.host.work_dir.join(self.manifest.id.as_ref());
self.host.fs.create_dir(&extension_work_dir).await?;
let destination_path = self
.host
.writeable_path_from_extension(&self.manifest.id, &path)?;
let mut response = self
.host
.http_client
.get(&url, Default::default(), true)
.await
.map_err(|err| anyhow!("error downloading release: {}", err))?;
if !response.status().is_success() {
Err(anyhow!(
"download failed with status {}",
response.status().to_string()
))?;
}
let body = BufReader::new(response.body_mut());
match file_type {
DownloadedFileType::Uncompressed => {
futures::pin_mut!(body);
self.host
.fs
.create_file_with(&destination_path, body)
.await?;
}
DownloadedFileType::Gzip => {
let body = GzipDecoder::new(body);
futures::pin_mut!(body);
self.host
.fs
.create_file_with(&destination_path, body)
.await?;
}
DownloadedFileType::GzipTar => {
let body = GzipDecoder::new(body);
futures::pin_mut!(body);
self.host
.fs
.extract_tar_file(&destination_path, Archive::new(body))
.await?;
}
DownloadedFileType::Zip => {
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<Result<(), String>> {
#[allow(unused)]
let path = self
.host
.writeable_path_from_extension(&self.manifest.id, Path::new(&path))?;
#[cfg(unix)]
{
use std::fs::{self, Permissions};
use std::os::unix::fs::PermissionsExt;
return fs::set_permissions(&path, Permissions::from_mode(0o755))
.map_err(|error| anyhow!("failed to set permissions for path {path:?}: {error}"))
.to_wasmtime_result();
}
#[cfg(not(unix))]
Ok(Ok(()))
latest::ExtensionImports::make_file_executable(self, path).await
}
}

View File

@ -0,0 +1,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<dyn LspAdapterDelegate>;
pub fn linker() -> &'static Linker<WasmState> {
static LINKER: OnceLock<Linker<WasmState>> = OnceLock::new();
LINKER.get_or_init(|| super::new_linker(Extension::add_to_linker))
}
#[async_trait]
impl HostWorktree for WasmState {
async fn read_text_file(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
path: String,
) -> wasmtime::Result<Result<String, String>> {
let delegate = self.table.get(&delegate)?;
Ok(delegate
.read_text_file(path.into())
.await
.map_err(|error| error.to_string()))
}
async fn shell_env(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
) -> wasmtime::Result<EnvVars> {
let delegate = self.table.get(&delegate)?;
Ok(delegate.shell_env().await.into_iter().collect())
}
async fn which(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
binary_name: String,
) -> wasmtime::Result<Option<String>> {
let delegate = self.table.get(&delegate)?;
Ok(delegate
.which(binary_name.as_ref())
.await
.map(|path| path.to_string_lossy().to_string()))
}
fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {
// We only ever hand out borrows of worktrees.
Ok(())
}
}
impl self::zed::extension::lsp::Host for WasmState {}
#[async_trait]
impl ExtensionImports for WasmState {
async fn node_binary_path(&mut self) -> wasmtime::Result<Result<String, String>> {
self.host
.node_runtime
.binary_path()
.await
.map(|path| path.to_string_lossy().to_string())
.to_wasmtime_result()
}
async fn npm_package_latest_version(
&mut self,
package_name: String,
) -> wasmtime::Result<Result<String, String>> {
self.host
.node_runtime
.npm_package_latest_version(&package_name)
.await
.to_wasmtime_result()
}
async fn npm_package_installed_version(
&mut self,
package_name: String,
) -> wasmtime::Result<Result<Option<String>, String>> {
self.host
.node_runtime
.npm_package_installed_version(&self.work_dir(), &package_name)
.await
.to_wasmtime_result()
}
async fn npm_install_package(
&mut self,
package_name: String,
version: String,
) -> wasmtime::Result<Result<(), String>> {
self.host
.node_runtime
.npm_install_packages(&self.work_dir(), &[(&package_name, &version)])
.await
.to_wasmtime_result()
}
async fn latest_github_release(
&mut self,
repo: String,
options: GithubReleaseOptions,
) -> wasmtime::Result<Result<GithubRelease, String>> {
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<Result<(), String>> {
maybe!(async {
let path = PathBuf::from(path);
let extension_work_dir = self.host.work_dir.join(self.manifest.id.as_ref());
self.host.fs.create_dir(&extension_work_dir).await?;
let destination_path = self
.host
.writeable_path_from_extension(&self.manifest.id, &path)?;
let mut response = self
.host
.http_client
.get(&url, Default::default(), true)
.await
.map_err(|err| anyhow!("error downloading release: {}", err))?;
if !response.status().is_success() {
Err(anyhow!(
"download failed with status {}",
response.status().to_string()
))?;
}
let body = BufReader::new(response.body_mut());
match file_type {
DownloadedFileType::Uncompressed => {
futures::pin_mut!(body);
self.host
.fs
.create_file_with(&destination_path, body)
.await?;
}
DownloadedFileType::Gzip => {
let body = GzipDecoder::new(body);
futures::pin_mut!(body);
self.host
.fs
.create_file_with(&destination_path, body)
.await?;
}
DownloadedFileType::GzipTar => {
let body = GzipDecoder::new(body);
futures::pin_mut!(body);
self.host
.fs
.extract_tar_file(&destination_path, Archive::new(body))
.await?;
}
DownloadedFileType::Zip => {
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<Result<(), String>> {
#[allow(unused)]
let path = self
.host
.writeable_path_from_extension(&self.manifest.id, Path::new(&path))?;
#[cfg(unix)]
{
use std::fs::{self, Permissions};
use std::os::unix::fs::PermissionsExt;
return fs::set_permissions(&path, Permissions::from_mode(0o755))
.map_err(|error| anyhow!("failed to set permissions for path {path:?}: {error}"))
.to_wasmtime_result();
}
#[cfg(not(unix))]
Ok(Ok(()))
}
}

View File

@ -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"

View File

@ -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<T, E = String> = core::result::Result<T, E>;
/// 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<Command>;
/// 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<Option<String>> {
Ok(None)
}
/// Returns the label for the given completion.
fn label_for_completion(
&self,
_language_server_id: &LanguageServerId,
_completion: Completion,
) -> Option<CodeLabel> {
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<wit::Command> {
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<Option<String>, 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<Completion>,
) -> Result<Vec<Option<CodeLabel>>, 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<wit::Range>) -> Self {
Self::CodeRange(range.into())
}
/// Returns a [`CodeLabelSpan::Literal`].
pub fn literal(text: impl Into<String>, highlight_name: Option<String>) -> Self {
Self::Literal(CodeLabelSpanLiteral {
text: text.into(),
highlight_name,
})
}
}
impl From<std::ops::Range<u32>> for wit::Range {
fn from(value: std::ops::Range<u32>) -> Self {
Self {
start: value.start,
end: value.end,
}
}
}
impl From<std::ops::Range<usize>> for wit::Range {
fn from(value: std::ops::Range<usize>) -> Self {
Self {
start: value.start as u32,
end: value.end as u32,
}
}
}

View File

@ -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<github-release-asset>,
}
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<os, architecture>;
/// Get the path to the node binary used by Zed.
import node-binary-path: func() -> result<string, string>;
/// Gets the latest version of the given NPM package.
import npm-package-latest-version: func(package-name: string) -> result<string, string>;
/// Returns the installed version of the given NPM package, if it exists.
import npm-package-installed-version: func(package-name: string) -> result<option<string>, 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<github-release, string>;
/// 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<tuple<string, string>>;
record command {
command: string,
args: list<string>,
env: env-vars,
}
resource worktree {
read-text-file: func(path: string) -> result<string, string>;
which: func(binary-name: string) -> option<string>;
shell-env: func() -> env-vars;
}
export language-server-command: func(language-server-id: string, worktree: borrow<worktree>) -> result<command, string>;
export language-server-initialization-options: func(language-server-id: string, worktree: borrow<worktree>) -> result<option<string>, string>;
record code-label {
/// The source code to parse with Tree-sitter.
code: string,
spans: list<code-label-span>,
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<string>,
}
record range {
start: u32,
end: u32,
}
export labels-for-completions: func(language-server-id: string, completions: list<completion>) -> result<list<option<code-label>>, string>;
}

View File

@ -0,0 +1,44 @@
interface lsp {
/// An LSP completion.
record completion {
label: string,
detail: option<string>,
kind: option<completion-kind>,
insert-text-format: option<insert-text-format>,
}
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),
}
}

View File

@ -213,8 +213,9 @@ impl CachedLspAdapter {
&self,
completion_items: &[lsp::CompletionItem],
language: &Arc<Language>,
) -> Vec<Option<CodeLabel>> {
) -> Result<Vec<Option<CodeLabel>>> {
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<Self>,
completions: &[lsp::CompletionItem],
language: &Arc<Language>,
) -> Vec<Option<CodeLabel>> {
) -> Result<Vec<Option<CodeLabel>>> {
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(

View File

@ -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,

View File

@ -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()
};

View File

@ -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" }

View File

@ -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"

View File

@ -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<String> {
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<zed::Command> {
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<zed::CodeLabel> {
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);