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:
parent
0f1c2e6f2b
commit
d306b531c7
18 changed files with 1124 additions and 252 deletions
12
Cargo.lock
generated
12
Cargo.lock
generated
|
@ -12515,15 +12515,15 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zed_extension_api"
|
name = "zed_extension_api"
|
||||||
version = "0.0.5"
|
version = "0.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a5f4ae4e302a80591635ef9a236b35fde6fcc26cfd060e66fde4ba9f9fd394a1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"wit-bindgen",
|
"wit-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zed_extension_api"
|
name = "zed_extension_api"
|
||||||
version = "0.0.5"
|
version = "0.0.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a5f4ae4e302a80591635ef9a236b35fde6fcc26cfd060e66fde4ba9f9fd394a1"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"wit-bindgen",
|
"wit-bindgen",
|
||||||
]
|
]
|
||||||
|
@ -12532,7 +12532,7 @@ dependencies = [
|
||||||
name = "zed_gleam"
|
name = "zed_gleam"
|
||||||
version = "0.0.2"
|
version = "0.0.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"zed_extension_api 0.0.4",
|
"zed_extension_api 0.0.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -12581,7 +12581,7 @@ dependencies = [
|
||||||
name = "zed_toml"
|
name = "zed_toml"
|
||||||
version = "0.0.2"
|
version = "0.0.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"zed_extension_api 0.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
"zed_extension_api 0.0.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -12595,7 +12595,7 @@ dependencies = [
|
||||||
name = "zed_zig"
|
name = "zed_zig"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"zed_extension_api 0.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
"zed_extension_api 0.0.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
@ -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 anyhow::{anyhow, Context, Result};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use futures::{Future, FutureExt};
|
use futures::{Future, FutureExt};
|
||||||
use gpui::AsyncAppContext;
|
use gpui::AsyncAppContext;
|
||||||
use language::{Language, LanguageServerName, LspAdapter, LspAdapterDelegate};
|
use language::{
|
||||||
|
CodeLabel, HighlightId, Language, LanguageServerName, LspAdapter, LspAdapterDelegate,
|
||||||
|
};
|
||||||
use lsp::LanguageServerBinary;
|
use lsp::LanguageServerBinary;
|
||||||
|
use std::ops::Range;
|
||||||
use std::{
|
use std::{
|
||||||
any::Any,
|
any::Any,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
pin::Pin,
|
pin::Pin,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
use util::{maybe, ResultExt};
|
||||||
use wasmtime_wasi::WasiView as _;
|
use wasmtime_wasi::WasiView as _;
|
||||||
|
|
||||||
pub struct ExtensionLspAdapter {
|
pub struct ExtensionLspAdapter {
|
||||||
pub(crate) extension: WasmExtension,
|
pub(crate) extension: WasmExtension,
|
||||||
|
pub(crate) language_server_id: LanguageServerName,
|
||||||
pub(crate) config: LanguageServerConfig,
|
pub(crate) config: LanguageServerConfig,
|
||||||
pub(crate) host: Arc<WasmHost>,
|
pub(crate) host: Arc<WasmHost>,
|
||||||
}
|
}
|
||||||
|
@ -43,7 +51,12 @@ impl LspAdapter for ExtensionLspAdapter {
|
||||||
async move {
|
async move {
|
||||||
let resource = store.data_mut().table().push(delegate)?;
|
let resource = store.data_mut().table().push(delegate)?;
|
||||||
let command = extension
|
let command = extension
|
||||||
.call_language_server_command(store, &this.config, resource)
|
.call_language_server_command(
|
||||||
|
store,
|
||||||
|
&this.language_server_id,
|
||||||
|
&this.config,
|
||||||
|
resource,
|
||||||
|
)
|
||||||
.await?
|
.await?
|
||||||
.map_err(|e| anyhow!("{}", e))?;
|
.map_err(|e| anyhow!("{}", e))?;
|
||||||
anyhow::Ok(command)
|
anyhow::Ok(command)
|
||||||
|
@ -146,6 +159,7 @@ impl LspAdapter for ExtensionLspAdapter {
|
||||||
let options = extension
|
let options = extension
|
||||||
.call_language_server_initialization_options(
|
.call_language_server_initialization_options(
|
||||||
store,
|
store,
|
||||||
|
&this.language_server_id,
|
||||||
&this.config,
|
&this.config,
|
||||||
resource,
|
resource,
|
||||||
)
|
)
|
||||||
|
@ -165,4 +179,235 @@ impl LspAdapter for ExtensionLspAdapter {
|
||||||
None
|
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()
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1101,15 +1101,15 @@ impl ExtensionStore {
|
||||||
this.reload_complete_senders.clear();
|
this.reload_complete_senders.clear();
|
||||||
|
|
||||||
for (manifest, wasm_extension) in &wasm_extensions {
|
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(
|
this.language_registry.register_lsp_adapter(
|
||||||
language_server_config.language.clone(),
|
language_server_config.language.clone(),
|
||||||
Arc::new(ExtensionLspAdapter {
|
Arc::new(ExtensionLspAdapter {
|
||||||
extension: wasm_extension.clone(),
|
extension: wasm_extension.clone(),
|
||||||
host: this.wasm_host.clone(),
|
host: this.wasm_host.clone(),
|
||||||
|
language_server_id: language_server_id.clone(),
|
||||||
config: wit::LanguageServerConfig {
|
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(),
|
language_name: language_server_config.language.to_string(),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -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
|
// Simulate a new version of the language server being released
|
||||||
language_server_version.lock().version = "v2.0.0".into();
|
language_server_version.lock().version = "v2.0.0".into();
|
||||||
language_server_version.lock().binary_contents = "the-new-binary-contents".into();
|
language_server_version.lock().binary_contents = "the-new-binary-contents".into();
|
||||||
|
|
|
@ -1,20 +1,28 @@
|
||||||
mod since_v0_0_1;
|
mod since_v0_0_1;
|
||||||
mod since_v0_0_4;
|
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::ops::RangeInclusive;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use anyhow::bail;
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use language::{LanguageServerName, LspAdapterDelegate};
|
||||||
|
use semantic_version::SemanticVersion;
|
||||||
use wasmtime::{
|
use wasmtime::{
|
||||||
component::{Component, Instance, Linker, Resource},
|
component::{Component, Instance, Linker, Resource},
|
||||||
Store,
|
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(
|
pub fn new_linker(
|
||||||
f: impl Fn(&mut Linker<WasmState>, fn(&mut WasmState) -> &mut WasmState) -> Result<()>,
|
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 {
|
pub enum Extension {
|
||||||
|
V006(since_v0_0_6::Extension),
|
||||||
V004(since_v0_0_4::Extension),
|
V004(since_v0_0_4::Extension),
|
||||||
V001(since_v0_0_1::Extension),
|
V001(since_v0_0_1::Extension),
|
||||||
}
|
}
|
||||||
|
@ -51,16 +60,13 @@ impl Extension {
|
||||||
version: SemanticVersion,
|
version: SemanticVersion,
|
||||||
component: &Component,
|
component: &Component,
|
||||||
) -> Result<(Self, Instance)> {
|
) -> Result<(Self, Instance)> {
|
||||||
if version < latest::MIN_VERSION {
|
if version >= latest::MIN_VERSION {
|
||||||
let (extension, instance) = since_v0_0_1::Extension::instantiate_async(
|
let (extension, instance) =
|
||||||
store,
|
latest::Extension::instantiate_async(store, &component, latest::linker())
|
||||||
&component,
|
.await
|
||||||
since_v0_0_1::linker(),
|
.context("failed to instantiate wasm extension")?;
|
||||||
)
|
Ok((Self::V006(extension), instance))
|
||||||
.await
|
} else if version >= since_v0_0_4::MIN_VERSION {
|
||||||
.context("failed to instantiate wasm extension")?;
|
|
||||||
Ok((Self::V001(extension), instance))
|
|
||||||
} else {
|
|
||||||
let (extension, instance) = since_v0_0_4::Extension::instantiate_async(
|
let (extension, instance) = since_v0_0_4::Extension::instantiate_async(
|
||||||
store,
|
store,
|
||||||
&component,
|
&component,
|
||||||
|
@ -69,11 +75,21 @@ impl Extension {
|
||||||
.await
|
.await
|
||||||
.context("failed to instantiate wasm extension")?;
|
.context("failed to instantiate wasm extension")?;
|
||||||
Ok((Self::V004(extension), instance))
|
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<()> {
|
pub async fn call_init_extension(&self, store: &mut Store<WasmState>) -> Result<()> {
|
||||||
match self {
|
match self {
|
||||||
|
Extension::V006(ext) => ext.call_init_extension(store).await,
|
||||||
Extension::V004(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,
|
Extension::V001(ext) => ext.call_init_extension(store).await,
|
||||||
}
|
}
|
||||||
|
@ -82,14 +98,19 @@ impl Extension {
|
||||||
pub async fn call_language_server_command(
|
pub async fn call_language_server_command(
|
||||||
&self,
|
&self,
|
||||||
store: &mut Store<WasmState>,
|
store: &mut Store<WasmState>,
|
||||||
|
language_server_id: &LanguageServerName,
|
||||||
config: &LanguageServerConfig,
|
config: &LanguageServerConfig,
|
||||||
resource: Resource<Arc<dyn LspAdapterDelegate>>,
|
resource: Resource<Arc<dyn LspAdapterDelegate>>,
|
||||||
) -> Result<Result<Command, String>> {
|
) -> Result<Result<Command, String>> {
|
||||||
match self {
|
match self {
|
||||||
Extension::V004(ext) => {
|
Extension::V006(ext) => {
|
||||||
ext.call_language_server_command(store, config, resource)
|
ext.call_language_server_command(store, &language_server_id.0, resource)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
Extension::V004(ext) => Ok(ext
|
||||||
|
.call_language_server_command(store, config, resource)
|
||||||
|
.await?
|
||||||
|
.map(|command| command.into())),
|
||||||
Extension::V001(ext) => Ok(ext
|
Extension::V001(ext) => Ok(ext
|
||||||
.call_language_server_command(store, &config.clone().into(), resource)
|
.call_language_server_command(store, &config.clone().into(), resource)
|
||||||
.await?
|
.await?
|
||||||
|
@ -100,10 +121,19 @@ impl Extension {
|
||||||
pub async fn call_language_server_initialization_options(
|
pub async fn call_language_server_initialization_options(
|
||||||
&self,
|
&self,
|
||||||
store: &mut Store<WasmState>,
|
store: &mut Store<WasmState>,
|
||||||
|
language_server_id: &LanguageServerName,
|
||||||
config: &LanguageServerConfig,
|
config: &LanguageServerConfig,
|
||||||
resource: Resource<Arc<dyn LspAdapterDelegate>>,
|
resource: Resource<Arc<dyn LspAdapterDelegate>>,
|
||||||
) -> Result<Result<Option<String>, String>> {
|
) -> Result<Result<Option<String>, String>> {
|
||||||
match self {
|
match self {
|
||||||
|
Extension::V006(ext) => {
|
||||||
|
ext.call_language_server_initialization_options(
|
||||||
|
store,
|
||||||
|
&language_server_id.0,
|
||||||
|
resource,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
Extension::V004(ext) => {
|
Extension::V004(ext) => {
|
||||||
ext.call_language_server_initialization_options(store, config, resource)
|
ext.call_language_server_initialization_options(store, config, resource)
|
||||||
.await
|
.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> {
|
trait ToWasmtimeResult<T> {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use super::latest;
|
use super::latest;
|
||||||
|
use crate::wasm_host::wit::since_v0_0_4;
|
||||||
use crate::wasm_host::WasmState;
|
use crate::wasm_host::WasmState;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
@ -82,8 +83,8 @@ impl From<DownloadedFileType> for latest::DownloadedFileType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<latest::LanguageServerConfig> for LanguageServerConfig {
|
impl From<since_v0_0_4::LanguageServerConfig> for LanguageServerConfig {
|
||||||
fn from(value: latest::LanguageServerConfig) -> Self {
|
fn from(value: since_v0_0_4::LanguageServerConfig) -> Self {
|
||||||
Self {
|
Self {
|
||||||
name: value.name,
|
name: value.name,
|
||||||
language_name: value.language_name,
|
language_name: value.language_name,
|
||||||
|
|
|
@ -1,23 +1,13 @@
|
||||||
use crate::wasm_host::wit::ToWasmtimeResult;
|
use super::latest;
|
||||||
use crate::wasm_host::WasmState;
|
use crate::wasm_host::WasmState;
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::Result;
|
||||||
use async_compression::futures::bufread::GzipDecoder;
|
|
||||||
use async_tar::Archive;
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use futures::io::BufReader;
|
use language::LspAdapterDelegate;
|
||||||
use language::{LanguageServerBinaryStatus, LspAdapterDelegate};
|
|
||||||
use semantic_version::SemanticVersion;
|
use semantic_version::SemanticVersion;
|
||||||
use std::path::Path;
|
use std::sync::{Arc, OnceLock};
|
||||||
use std::{
|
|
||||||
env,
|
|
||||||
path::PathBuf,
|
|
||||||
sync::{Arc, OnceLock},
|
|
||||||
};
|
|
||||||
use util::maybe;
|
|
||||||
use wasmtime::component::{Linker, Resource};
|
use wasmtime::component::{Linker, Resource};
|
||||||
|
|
||||||
pub const MIN_VERSION: SemanticVersion = SemanticVersion::new(0, 0, 4);
|
pub const MIN_VERSION: SemanticVersion = SemanticVersion::new(0, 0, 4);
|
||||||
pub const MAX_VERSION: SemanticVersion = SemanticVersion::new(0, 0, 5);
|
|
||||||
|
|
||||||
wasmtime::component::bindgen!({
|
wasmtime::component::bindgen!({
|
||||||
async: true,
|
async: true,
|
||||||
|
@ -34,6 +24,93 @@ pub fn linker() -> &'static Linker<WasmState> {
|
||||||
LINKER.get_or_init(|| super::new_linker(Extension::add_to_linker))
|
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]
|
#[async_trait]
|
||||||
impl HostWorktree for WasmState {
|
impl HostWorktree for WasmState {
|
||||||
async fn read_text_file(
|
async fn read_text_file(
|
||||||
|
@ -41,19 +118,14 @@ impl HostWorktree for WasmState {
|
||||||
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
|
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
|
||||||
path: String,
|
path: String,
|
||||||
) -> wasmtime::Result<Result<String, String>> {
|
) -> wasmtime::Result<Result<String, String>> {
|
||||||
let delegate = self.table.get(&delegate)?;
|
latest::HostWorktree::read_text_file(self, delegate, path).await
|
||||||
Ok(delegate
|
|
||||||
.read_text_file(path.into())
|
|
||||||
.await
|
|
||||||
.map_err(|error| error.to_string()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn shell_env(
|
async fn shell_env(
|
||||||
&mut self,
|
&mut self,
|
||||||
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
|
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
|
||||||
) -> wasmtime::Result<EnvVars> {
|
) -> wasmtime::Result<EnvVars> {
|
||||||
let delegate = self.table.get(&delegate)?;
|
latest::HostWorktree::shell_env(self, delegate).await
|
||||||
Ok(delegate.shell_env().await.into_iter().collect())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn which(
|
async fn which(
|
||||||
|
@ -61,15 +133,11 @@ impl HostWorktree for WasmState {
|
||||||
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
|
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
|
||||||
binary_name: String,
|
binary_name: String,
|
||||||
) -> wasmtime::Result<Option<String>> {
|
) -> wasmtime::Result<Option<String>> {
|
||||||
let delegate = self.table.get(&delegate)?;
|
latest::HostWorktree::which(self, delegate, binary_name).await
|
||||||
Ok(delegate
|
|
||||||
.which(binary_name.as_ref())
|
|
||||||
.await
|
|
||||||
.map(|path| path.to_string_lossy().to_string()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,34 +145,21 @@ impl HostWorktree for WasmState {
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl ExtensionImports for WasmState {
|
impl ExtensionImports for WasmState {
|
||||||
async fn node_binary_path(&mut self) -> wasmtime::Result<Result<String, String>> {
|
async fn node_binary_path(&mut self) -> wasmtime::Result<Result<String, String>> {
|
||||||
self.host
|
latest::ExtensionImports::node_binary_path(self).await
|
||||||
.node_runtime
|
|
||||||
.binary_path()
|
|
||||||
.await
|
|
||||||
.map(|path| path.to_string_lossy().to_string())
|
|
||||||
.to_wasmtime_result()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn npm_package_latest_version(
|
async fn npm_package_latest_version(
|
||||||
&mut self,
|
&mut self,
|
||||||
package_name: String,
|
package_name: String,
|
||||||
) -> wasmtime::Result<Result<String, String>> {
|
) -> wasmtime::Result<Result<String, String>> {
|
||||||
self.host
|
latest::ExtensionImports::npm_package_latest_version(self, package_name).await
|
||||||
.node_runtime
|
|
||||||
.npm_package_latest_version(&package_name)
|
|
||||||
.await
|
|
||||||
.to_wasmtime_result()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn npm_package_installed_version(
|
async fn npm_package_installed_version(
|
||||||
&mut self,
|
&mut self,
|
||||||
package_name: String,
|
package_name: String,
|
||||||
) -> wasmtime::Result<Result<Option<String>, String>> {
|
) -> wasmtime::Result<Result<Option<String>, String>> {
|
||||||
self.host
|
latest::ExtensionImports::npm_package_installed_version(self, package_name).await
|
||||||
.node_runtime
|
|
||||||
.npm_package_installed_version(&self.work_dir(), &package_name)
|
|
||||||
.await
|
|
||||||
.to_wasmtime_result()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn npm_install_package(
|
async fn npm_install_package(
|
||||||
|
@ -112,11 +167,7 @@ impl ExtensionImports for WasmState {
|
||||||
package_name: String,
|
package_name: String,
|
||||||
version: String,
|
version: String,
|
||||||
) -> wasmtime::Result<Result<(), String>> {
|
) -> wasmtime::Result<Result<(), String>> {
|
||||||
self.host
|
latest::ExtensionImports::npm_install_package(self, package_name, version).await
|
||||||
.node_runtime
|
|
||||||
.npm_install_packages(&self.work_dir(), &[(&package_name, &version)])
|
|
||||||
.await
|
|
||||||
.to_wasmtime_result()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn latest_github_release(
|
async fn latest_github_release(
|
||||||
|
@ -124,45 +175,17 @@ impl ExtensionImports for WasmState {
|
||||||
repo: String,
|
repo: String,
|
||||||
options: GithubReleaseOptions,
|
options: GithubReleaseOptions,
|
||||||
) -> wasmtime::Result<Result<GithubRelease, String>> {
|
) -> wasmtime::Result<Result<GithubRelease, String>> {
|
||||||
maybe!(async {
|
Ok(
|
||||||
let release = util::github::latest_github_release(
|
latest::ExtensionImports::latest_github_release(self, repo, options.into())
|
||||||
&repo,
|
.await?
|
||||||
options.require_assets,
|
.map(|github| github.into()),
|
||||||
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)> {
|
async fn current_platform(&mut self) -> Result<(Os, Architecture)> {
|
||||||
Ok((
|
latest::ExtensionImports::current_platform(self)
|
||||||
match env::consts::OS {
|
.await
|
||||||
"macos" => Os::Mac,
|
.map(|(os, arch)| (os.into(), arch.into()))
|
||||||
"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(
|
async fn set_language_server_installation_status(
|
||||||
|
@ -170,23 +193,12 @@ impl ExtensionImports for WasmState {
|
||||||
server_name: String,
|
server_name: String,
|
||||||
status: LanguageServerInstallationStatus,
|
status: LanguageServerInstallationStatus,
|
||||||
) -> wasmtime::Result<()> {
|
) -> wasmtime::Result<()> {
|
||||||
let status = match status {
|
latest::ExtensionImports::set_language_server_installation_status(
|
||||||
LanguageServerInstallationStatus::CheckingForUpdate => {
|
self,
|
||||||
LanguageServerBinaryStatus::CheckingForUpdate
|
server_name,
|
||||||
}
|
status.into(),
|
||||||
LanguageServerInstallationStatus::Downloading => {
|
)
|
||||||
LanguageServerBinaryStatus::Downloading
|
.await
|
||||||
}
|
|
||||||
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(
|
async fn download_file(
|
||||||
|
@ -195,103 +207,10 @@ impl ExtensionImports for WasmState {
|
||||||
path: String,
|
path: String,
|
||||||
file_type: DownloadedFileType,
|
file_type: DownloadedFileType,
|
||||||
) -> wasmtime::Result<Result<(), String>> {
|
) -> wasmtime::Result<Result<(), String>> {
|
||||||
maybe!(async {
|
latest::ExtensionImports::download_file(self, url, path, file_type.into()).await
|
||||||
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>> {
|
async fn make_file_executable(&mut self, path: String) -> wasmtime::Result<Result<(), String>> {
|
||||||
#[allow(unused)]
|
latest::ExtensionImports::make_file_executable(self, path).await
|
||||||
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(()))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
299
crates/extension/src/wasm_host/wit/since_v0_0_6.rs
Normal file
299
crates/extension/src/wasm_host/wit/since_v0_0_6.rs
Normal 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(()))
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "zed_extension_api"
|
name = "zed_extension_api"
|
||||||
version = "0.0.5"
|
version = "0.0.6"
|
||||||
description = "APIs for creating Zed extensions in Rust"
|
description = "APIs for creating Zed extensions in Rust"
|
||||||
repository = "https://github.com/zed-industries/zed"
|
repository = "https://github.com/zed-industries/zed"
|
||||||
documentation = "https://docs.rs/zed_extension_api"
|
documentation = "https://docs.rs/zed_extension_api"
|
||||||
|
|
|
@ -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>;
|
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 {
|
pub trait Extension: Send + Sync {
|
||||||
|
/// Returns a new instance of the extension.
|
||||||
fn new() -> Self
|
fn new() -> Self
|
||||||
where
|
where
|
||||||
Self: Sized;
|
Self: Sized;
|
||||||
|
|
||||||
|
/// Returns the command used to start the language server for the specified
|
||||||
|
/// language.
|
||||||
fn language_server_command(
|
fn language_server_command(
|
||||||
&mut self,
|
&mut self,
|
||||||
config: LanguageServerConfig,
|
language_server_id: &LanguageServerId,
|
||||||
worktree: &Worktree,
|
worktree: &Worktree,
|
||||||
) -> Result<Command>;
|
) -> Result<Command>;
|
||||||
|
|
||||||
|
/// Returns the initialization options to pass to the specified language server.
|
||||||
fn language_server_initialization_options(
|
fn language_server_initialization_options(
|
||||||
&mut self,
|
&mut self,
|
||||||
_config: LanguageServerConfig,
|
_language_server_id: &LanguageServerId,
|
||||||
_worktree: &Worktree,
|
_worktree: &Worktree,
|
||||||
) -> Result<Option<String>> {
|
) -> Result<Option<String>> {
|
||||||
Ok(None)
|
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]
|
#[macro_export]
|
||||||
|
@ -53,7 +98,7 @@ pub static ZED_API_VERSION: [u8; 6] = *include_bytes!(concat!(env!("OUT_DIR"), "
|
||||||
mod wit {
|
mod wit {
|
||||||
wit_bindgen::generate!({
|
wit_bindgen::generate!({
|
||||||
skip: ["init-extension"],
|
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 {
|
impl wit::Guest for Component {
|
||||||
fn language_server_command(
|
fn language_server_command(
|
||||||
config: wit::LanguageServerConfig,
|
language_server_id: String,
|
||||||
worktree: &wit::Worktree,
|
worktree: &wit::Worktree,
|
||||||
) -> Result<wit::Command> {
|
) -> 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(
|
fn language_server_initialization_options(
|
||||||
config: LanguageServerConfig,
|
language_server_id: String,
|
||||||
worktree: &Worktree,
|
worktree: &Worktree,
|
||||||
) -> Result<Option<String>, String> {
|
) -> 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,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
120
crates/extension_api/wit/since_v0.0.6/extension.wit
Normal file
120
crates/extension_api/wit/since_v0.0.6/extension.wit
Normal 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>;
|
||||||
|
}
|
44
crates/extension_api/wit/since_v0.0.6/lsp.wit
Normal file
44
crates/extension_api/wit/since_v0.0.6/lsp.wit
Normal 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),
|
||||||
|
}
|
||||||
|
}
|
|
@ -213,8 +213,9 @@ impl CachedLspAdapter {
|
||||||
&self,
|
&self,
|
||||||
completion_items: &[lsp::CompletionItem],
|
completion_items: &[lsp::CompletionItem],
|
||||||
language: &Arc<Language>,
|
language: &Arc<Language>,
|
||||||
) -> Vec<Option<CodeLabel>> {
|
) -> Result<Vec<Option<CodeLabel>>> {
|
||||||
self.adapter
|
self.adapter
|
||||||
|
.clone()
|
||||||
.labels_for_completions(completion_items, language)
|
.labels_for_completions(completion_items, language)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
@ -385,10 +386,10 @@ pub trait LspAdapter: 'static + Send + Sync {
|
||||||
async fn process_completions(&self, _: &mut [lsp::CompletionItem]) {}
|
async fn process_completions(&self, _: &mut [lsp::CompletionItem]) {}
|
||||||
|
|
||||||
async fn labels_for_completions(
|
async fn labels_for_completions(
|
||||||
&self,
|
self: Arc<Self>,
|
||||||
completions: &[lsp::CompletionItem],
|
completions: &[lsp::CompletionItem],
|
||||||
language: &Arc<Language>,
|
language: &Arc<Language>,
|
||||||
) -> Vec<Option<CodeLabel>> {
|
) -> Result<Vec<Option<CodeLabel>>> {
|
||||||
let mut labels = Vec::new();
|
let mut labels = Vec::new();
|
||||||
for (ix, completion) in completions.into_iter().enumerate() {
|
for (ix, completion) in completions.into_iter().enumerate() {
|
||||||
let label = self.label_for_completion(completion, language).await;
|
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.last_mut().unwrap() = Some(label);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
labels
|
Ok(labels)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn label_for_completion(
|
async fn label_for_completion(
|
||||||
|
|
|
@ -746,7 +746,10 @@ impl LanguageRegistry {
|
||||||
let capabilities = adapter
|
let capabilities = adapter
|
||||||
.as_fake()
|
.as_fake()
|
||||||
.map(|fake_adapter| fake_adapter.capabilities.clone())
|
.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(
|
let (server, mut fake_server) = lsp::FakeLanguageServer::new(
|
||||||
server_id,
|
server_id,
|
||||||
|
|
|
@ -9915,6 +9915,8 @@ async fn populate_labels_for_completions(
|
||||||
lsp_adapter
|
lsp_adapter
|
||||||
.labels_for_completions(&lsp_completions, language)
|
.labels_for_completions(&lsp_completions, language)
|
||||||
.await
|
.await
|
||||||
|
.log_err()
|
||||||
|
.unwrap_or_default()
|
||||||
} else {
|
} else {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
};
|
};
|
||||||
|
|
|
@ -13,4 +13,5 @@ path = "src/gleam.rs"
|
||||||
crate-type = ["cdylib"]
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
zed_extension_api = "0.0.4"
|
# zed_extension_api = "0.0.4"
|
||||||
|
zed_extension_api = { path = "../../crates/extension_api" }
|
||||||
|
|
|
@ -7,6 +7,10 @@
|
||||||
(constant
|
(constant
|
||||||
name: (identifier) @constant)
|
name: (identifier) @constant)
|
||||||
|
|
||||||
|
; Variables
|
||||||
|
(identifier) @variable
|
||||||
|
(discard) @comment.unused
|
||||||
|
|
||||||
; Modules
|
; Modules
|
||||||
(module) @module
|
(module) @module
|
||||||
(import alias: (identifier) @module)
|
(import alias: (identifier) @module)
|
||||||
|
@ -75,10 +79,6 @@
|
||||||
((identifier) @warning
|
((identifier) @warning
|
||||||
(#match? @warning "^(auto|delegate|derive|else|implement|macro|test|echo)$"))
|
(#match? @warning "^(auto|delegate|derive|else|implement|macro|test|echo)$"))
|
||||||
|
|
||||||
; Variables
|
|
||||||
(identifier) @variable
|
|
||||||
(discard) @comment.unused
|
|
||||||
|
|
||||||
; Keywords
|
; Keywords
|
||||||
[
|
[
|
||||||
(visibility_modifier) ; "pub"
|
(visibility_modifier) ; "pub"
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
use zed::lsp::CompletionKind;
|
||||||
|
use zed::{CodeLabel, CodeLabelSpan, LanguageServerId};
|
||||||
use zed_extension_api::{self as zed, Result};
|
use zed_extension_api::{self as zed, Result};
|
||||||
|
|
||||||
struct GleamExtension {
|
struct GleamExtension {
|
||||||
|
@ -8,7 +10,7 @@ struct GleamExtension {
|
||||||
impl GleamExtension {
|
impl GleamExtension {
|
||||||
fn language_server_binary_path(
|
fn language_server_binary_path(
|
||||||
&mut self,
|
&mut self,
|
||||||
config: zed::LanguageServerConfig,
|
language_server_id: &LanguageServerId,
|
||||||
worktree: &zed::Worktree,
|
worktree: &zed::Worktree,
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
if let Some(path) = &self.cached_binary_path {
|
if let Some(path) = &self.cached_binary_path {
|
||||||
|
@ -23,7 +25,7 @@ impl GleamExtension {
|
||||||
}
|
}
|
||||||
|
|
||||||
zed::set_language_server_installation_status(
|
zed::set_language_server_installation_status(
|
||||||
&config.name,
|
&language_server_id,
|
||||||
&zed::LanguageServerInstallationStatus::CheckingForUpdate,
|
&zed::LanguageServerInstallationStatus::CheckingForUpdate,
|
||||||
);
|
);
|
||||||
let release = zed::latest_github_release(
|
let release = zed::latest_github_release(
|
||||||
|
@ -61,7 +63,7 @@ impl GleamExtension {
|
||||||
|
|
||||||
if !fs::metadata(&binary_path).map_or(false, |stat| stat.is_file()) {
|
if !fs::metadata(&binary_path).map_or(false, |stat| stat.is_file()) {
|
||||||
zed::set_language_server_installation_status(
|
zed::set_language_server_installation_status(
|
||||||
&config.name,
|
&language_server_id,
|
||||||
&zed::LanguageServerInstallationStatus::Downloading,
|
&zed::LanguageServerInstallationStatus::Downloading,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -96,15 +98,51 @@ impl zed::Extension for GleamExtension {
|
||||||
|
|
||||||
fn language_server_command(
|
fn language_server_command(
|
||||||
&mut self,
|
&mut self,
|
||||||
config: zed::LanguageServerConfig,
|
language_server_id: &LanguageServerId,
|
||||||
worktree: &zed::Worktree,
|
worktree: &zed::Worktree,
|
||||||
) -> Result<zed::Command> {
|
) -> Result<zed::Command> {
|
||||||
Ok(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()],
|
args: vec!["lsp".to_string()],
|
||||||
env: Default::default(),
|
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);
|
zed::register_extension!(GleamExtension);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue