Add `label_for_completion` to extension API (#10175)

Marshall Bowers , Max , and Max Brunsfeld created

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>

Change summary

Cargo.lock                                          |  12 
crates/extension/src/extension_lsp_adapter.rs       | 251 +++++++++++
crates/extension/src/extension_store.rs             |   6 
crates/extension/src/extension_store_test.rs        |  47 ++
crates/extension/src/wasm_host/wit.rs               |  77 ++
crates/extension/src/wasm_host/wit/since_v0_0_1.rs  |   5 
crates/extension/src/wasm_host/wit/since_v0_0_4.rs  | 311 +++++---------
crates/extension/src/wasm_host/wit/since_v0_0_6.rs  | 299 ++++++++++++++
crates/extension_api/Cargo.toml                     |   2 
crates/extension_api/src/extension_api.rs           | 121 +++++
crates/extension_api/wit/since_v0.0.6/extension.wit | 120 +++++
crates/extension_api/wit/since_v0.0.6/lsp.wit       |  44 ++
crates/language/src/language.rs                     |   9 
crates/language/src/language_registry.rs            |   5 
crates/project/src/project.rs                       |   2 
extensions/gleam/Cargo.toml                         |   3 
extensions/gleam/languages/gleam/highlights.scm     |   8 
extensions/gleam/src/gleam.rs                       |  48 ++
18 files changed, 1,121 insertions(+), 249 deletions(-)

Detailed changes

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

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

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

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::<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();

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<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,29 +60,36 @@ impl Extension {
         version: SemanticVersion,
         component: &Component,
     ) -> Result<(Self, Instance)> {
-        if version < latest::MIN_VERSION {
-            let (extension, instance) = since_v0_0_1::Extension::instantiate_async(
+        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,
-                since_v0_0_1::linker(),
+                since_v0_0_4::linker(),
             )
             .await
             .context("failed to instantiate wasm extension")?;
-            Ok((Self::V001(extension), instance))
+            Ok((Self::V004(extension), instance))
         } else {
-            let (extension, instance) = since_v0_0_4::Extension::instantiate_async(
+            let (extension, instance) = since_v0_0_1::Extension::instantiate_async(
                 store,
                 &component,
-                since_v0_0_4::linker(),
+                since_v0_0_1::linker(),
             )
             .await
             .context("failed to instantiate wasm extension")?;
-            Ok((Self::V004(extension), instance))
+            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> {

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

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

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

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"

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<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,
+        }
     }
 }

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<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>;
+}

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<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),
+    }
+}

crates/language/src/language.rs 🔗

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

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,

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

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

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"

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<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);