Continue splitting the extension into headless variant

partially integrate headless into the extension crate
This commit is contained in:
Mikayla 2024-10-29 23:52:50 -07:00
parent 1b120db2c0
commit 1cfff44add
No known key found for this signature in database
16 changed files with 167 additions and 4375 deletions

1
Cargo.lock generated
View file

@ -4101,6 +4101,7 @@ dependencies = [
"collections",
"ctor",
"env_logger 0.11.5",
"extension_headless",
"fs",
"futures 0.3.30",
"gpui",

View file

@ -23,7 +23,7 @@ async-tar.workspace = true
async-trait.workspace = true
client.workspace = true
collections.workspace = true
# extension_headless.workspace = true
extension_headless.workspace = true
fs.workspace = true
futures.workspace = true
gpui.workspace = true

View file

@ -1,585 +0,0 @@
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::{
CodeLabel, HighlightId, Language, LanguageServerName, LanguageToolchainStore, LspAdapter,
LspAdapterDelegate,
};
use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerBinaryOptions};
use serde::Serialize;
use serde_json::Value;
use std::ops::Range;
use std::{any::Any, 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>,
}
#[async_trait(?Send)]
impl LspAdapter for ExtensionLspAdapter {
fn name(&self) -> LanguageServerName {
LanguageServerName(self.config.name.clone().into())
}
fn get_language_server_command<'a>(
self: Arc<Self>,
delegate: Arc<dyn LspAdapterDelegate>,
_: LanguageServerBinaryOptions,
_: futures::lock::MutexGuard<'a, Option<LanguageServerBinary>>,
_: &'a mut AsyncAppContext,
) -> Pin<Box<dyn 'a + Future<Output = Result<LanguageServerBinary>>>> {
async move {
let command = self
.extension
.call({
let this = self.clone();
|extension, store| {
async move {
let resource = store.data_mut().table().push(delegate)?;
let command = extension
.call_language_server_command(
store,
&this.language_server_id,
&this.config,
resource,
)
.await?
.map_err(|e| anyhow!("{}", e))?;
anyhow::Ok(command)
}
.boxed()
}
})
.await?;
let path = self
.host
.path_from_extension(&self.extension.manifest.id, command.command.as_ref());
// TODO: This should now be done via the `zed::make_file_executable` function in
// Zed extension API, but we're leaving these existing usages in place temporarily
// to avoid any compatibility issues between Zed and the extension versions.
//
// We can remove once the following extension versions no longer see any use:
// - toml@0.0.2
// - zig@0.0.1
if ["toml", "zig"].contains(&self.extension.manifest.id.as_ref())
&& path.starts_with(&self.host.work_dir)
{
#[cfg(not(windows))]
{
use std::fs::{self, Permissions};
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(&path, Permissions::from_mode(0o755))
.context("failed to set file permissions")?;
}
}
Ok(LanguageServerBinary {
path,
arguments: command.args.into_iter().map(|arg| arg.into()).collect(),
env: Some(command.env.into_iter().collect()),
})
}
.boxed_local()
}
async fn fetch_latest_server_version(
&self,
_: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Send + Any>> {
unreachable!("get_language_server_command is overridden")
}
async fn fetch_server_binary(
&self,
_: Box<dyn 'static + Send + Any>,
_: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
unreachable!("get_language_server_command is overridden")
}
async fn cached_server_binary(
&self,
_: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
unreachable!("get_language_server_command is overridden")
}
fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
let code_action_kinds = self
.extension
.manifest
.language_servers
.get(&self.language_server_id)
.and_then(|server| server.code_action_kinds.clone());
code_action_kinds.or(Some(vec![
CodeActionKind::EMPTY,
CodeActionKind::QUICKFIX,
CodeActionKind::REFACTOR,
CodeActionKind::REFACTOR_EXTRACT,
CodeActionKind::SOURCE,
]))
}
fn language_ids(&self) -> HashMap<String, String> {
// TODO: The language IDs can be provided via the language server options
// in `extension.toml now but we're leaving these existing usages in place temporarily
// to avoid any compatibility issues between Zed and the extension versions.
//
// We can remove once the following extension versions no longer see any use:
// - php@0.0.1
if self.extension.manifest.id.as_ref() == "php" {
return HashMap::from_iter([("PHP".into(), "php".into())]);
}
self.extension
.manifest
.language_servers
.get(&LanguageServerName(self.config.name.clone().into()))
.map(|server| server.language_ids.clone())
.unwrap_or_default()
}
async fn initialization_options(
self: Arc<Self>,
delegate: &Arc<dyn LspAdapterDelegate>,
) -> Result<Option<serde_json::Value>> {
let delegate = delegate.clone();
let json_options = self
.extension
.call({
let this = self.clone();
|extension, store| {
async move {
let resource = store.data_mut().table().push(delegate)?;
let options = extension
.call_language_server_initialization_options(
store,
&this.language_server_id,
&this.config,
resource,
)
.await?
.map_err(|e| anyhow!("{}", e))?;
anyhow::Ok(options)
}
.boxed()
}
})
.await?;
Ok(if let Some(json_options) = json_options {
serde_json::from_str(&json_options).with_context(|| {
format!("failed to parse initialization_options from extension: {json_options}")
})?
} else {
None
})
}
async fn workspace_configuration(
self: Arc<Self>,
delegate: &Arc<dyn LspAdapterDelegate>,
_: Arc<dyn LanguageToolchainStore>,
_cx: &mut AsyncAppContext,
) -> Result<Value> {
let delegate = delegate.clone();
let json_options: Option<String> = self
.extension
.call({
let this = self.clone();
|extension, store| {
async move {
let resource = store.data_mut().table().push(delegate)?;
let options = extension
.call_language_server_workspace_configuration(
store,
&this.language_server_id,
resource,
)
.await?
.map_err(|e| anyhow!("{}", e))?;
anyhow::Ok(options)
}
.boxed()
}
})
.await?;
Ok(if let Some(json_options) = json_options {
serde_json::from_str(&json_options).with_context(|| {
format!("failed to parse initialization_options from extension: {json_options}")
})?
} else {
serde_json::json!({})
})
}
async fn labels_for_completions(
self: Arc<Self>,
completions: &[lsp::CompletionItem],
language: &Arc<Language>,
) -> Result<Vec<Option<CodeLabel>>> {
let completions = completions
.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_from_wit(labels, language))
}
async fn labels_for_symbols(
self: Arc<Self>,
symbols: &[(String, lsp::SymbolKind)],
language: &Arc<Language>,
) -> Result<Vec<Option<CodeLabel>>> {
let symbols = symbols
.iter()
.cloned()
.map(|(name, kind)| wit::Symbol {
name,
kind: kind.into(),
})
.collect::<Vec<_>>();
let labels = self
.extension
.call({
let this = self.clone();
|extension, store| {
async move {
extension
.call_labels_for_symbols(store, &this.language_server_id, symbols)
.await?
.map_err(|e| anyhow!("{}", e))
}
.boxed()
}
})
.await?;
Ok(labels_from_wit(labels, language))
}
}
fn labels_from_wit(
labels: Vec<Option<wit::CodeLabel>>,
language: &Arc<Language>,
) -> Vec<Option<CodeLabel>> {
labels
.into_iter()
.map(|label| {
let label = label?;
let runs = if label.code.is_empty() {
Vec::new()
} else {
language.highlight_text(&label.code.as_str().into(), 0..label.code.len())
};
build_code_label(&label, &runs, language)
})
.collect()
}
fn build_code_label(
label: &wit::CodeLabel,
parsed_runs: &[(Range<usize>, HighlightId)],
language: &Arc<Language>,
) -> Option<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 code_span = &label.code.get(range.clone())?;
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 {
let len = run_range.start - input_ix;
output_ix += len;
input_ix += len;
}
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(code_span);
}
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);
}
}
}
let filter_range = Range::from(label.filter_range);
text.get(filter_range.clone())?;
Some(CodeLabel {
text,
runs,
filter_range,
})
}
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,
_ => Self::Other(extract_int(value)),
}
}
}
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,
_ => Self::Other(extract_int(value)),
}
}
}
impl From<lsp::SymbolKind> for wit::SymbolKind {
fn from(value: lsp::SymbolKind) -> Self {
match value {
lsp::SymbolKind::FILE => Self::File,
lsp::SymbolKind::MODULE => Self::Module,
lsp::SymbolKind::NAMESPACE => Self::Namespace,
lsp::SymbolKind::PACKAGE => Self::Package,
lsp::SymbolKind::CLASS => Self::Class,
lsp::SymbolKind::METHOD => Self::Method,
lsp::SymbolKind::PROPERTY => Self::Property,
lsp::SymbolKind::FIELD => Self::Field,
lsp::SymbolKind::CONSTRUCTOR => Self::Constructor,
lsp::SymbolKind::ENUM => Self::Enum,
lsp::SymbolKind::INTERFACE => Self::Interface,
lsp::SymbolKind::FUNCTION => Self::Function,
lsp::SymbolKind::VARIABLE => Self::Variable,
lsp::SymbolKind::CONSTANT => Self::Constant,
lsp::SymbolKind::STRING => Self::String,
lsp::SymbolKind::NUMBER => Self::Number,
lsp::SymbolKind::BOOLEAN => Self::Boolean,
lsp::SymbolKind::ARRAY => Self::Array,
lsp::SymbolKind::OBJECT => Self::Object,
lsp::SymbolKind::KEY => Self::Key,
lsp::SymbolKind::NULL => Self::Null,
lsp::SymbolKind::ENUM_MEMBER => Self::EnumMember,
lsp::SymbolKind::STRUCT => Self::Struct,
lsp::SymbolKind::EVENT => Self::Event,
lsp::SymbolKind::OPERATOR => Self::Operator,
lsp::SymbolKind::TYPE_PARAMETER => Self::TypeParameter,
_ => Self::Other(extract_int(value)),
}
}
}
fn extract_int<T: Serialize>(value: T) -> i32 {
maybe!({
let kind = serde_json::to_value(&value)?;
serde_json::from_value(kind)
})
.log_err()
.unwrap_or(-1)
}
#[test]
fn test_build_code_label() {
use util::test::marked_text_ranges;
let (code, code_ranges) = marked_text_ranges(
"«const» «a»: «fn»(«Bcd»(«Efgh»)) -> «Ijklm» = pqrs.tuv",
false,
);
let code_runs = code_ranges
.into_iter()
.map(|range| (range, 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,
},
&code_runs,
&language::PLAIN_TEXT,
)
.unwrap();
let (label_text, label_ranges) =
marked_text_ranges("pqrs.tuv: «fn»(«Bcd»(«Efgh»)) -> «Ijklm»", false);
let label_runs = label_ranges
.into_iter()
.map(|range| (range, HighlightId(0)))
.collect::<Vec<_>>();
assert_eq!(
label,
CodeLabel {
text: label_text,
runs: label_runs,
filter_range: label.filter_range.clone()
}
)
}
#[test]
fn test_build_code_label_with_invalid_ranges() {
use util::test::marked_text_ranges;
let (code, code_ranges) = marked_text_ranges("const «a»: «B» = '🏀'", false);
let code_runs = code_ranges
.into_iter()
.map(|range| (range, HighlightId(0)))
.collect::<Vec<_>>();
// A span uses a code range that is invalid because it starts inside of
// a multi-byte character.
let label = build_code_label(
&wit::CodeLabel {
spans: vec![
wit::CodeLabelSpan::CodeRange(wit::Range {
start: code.find('B').unwrap() as u32,
end: code.find(" = ").unwrap() as u32,
}),
wit::CodeLabelSpan::CodeRange(wit::Range {
start: code.find('🏀').unwrap() as u32 + 1,
end: code.len() as u32,
}),
],
filter_range: wit::Range {
start: 0,
end: "B".len() as u32,
},
code,
},
&code_runs,
&language::PLAIN_TEXT,
);
assert!(label.is_none());
// Filter range extends beyond actual text
let label = build_code_label(
&wit::CodeLabel {
spans: vec![wit::CodeLabelSpan::Literal(wit::CodeLabelSpanLiteral {
text: "abc".into(),
highlight_name: Some("type".into()),
})],
filter_range: wit::Range { start: 0, end: 5 },
code: String::new(),
},
&code_runs,
&language::PLAIN_TEXT,
);
assert!(label.is_none());
}

View file

@ -1,212 +0,0 @@
use anyhow::{anyhow, Context, Result};
use collections::{BTreeMap, HashMap};
use fs::Fs;
use language::{LanguageName, LanguageServerName};
use semantic_version::SemanticVersion;
use serde::{Deserialize, Serialize};
use std::{
ffi::OsStr,
fmt,
path::{Path, PathBuf},
sync::Arc,
};
/// This is the old version of the extension manifest, from when it was `extension.json`.
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
pub struct OldExtensionManifest {
pub name: String,
pub version: Arc<str>,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub repository: Option<String>,
#[serde(default)]
pub authors: Vec<String>,
#[serde(default)]
pub themes: BTreeMap<Arc<str>, PathBuf>,
#[serde(default)]
pub languages: BTreeMap<Arc<str>, PathBuf>,
#[serde(default)]
pub grammars: BTreeMap<Arc<str>, PathBuf>,
}
/// The schema version of the [`ExtensionManifest`].
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
pub struct SchemaVersion(pub i32);
impl fmt::Display for SchemaVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl SchemaVersion {
pub const ZERO: Self = Self(0);
pub fn is_v0(&self) -> bool {
self == &Self::ZERO
}
}
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
pub struct ExtensionManifest {
pub id: Arc<str>,
pub name: String,
pub version: Arc<str>,
pub schema_version: SchemaVersion,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub repository: Option<String>,
#[serde(default)]
pub authors: Vec<String>,
#[serde(default)]
pub lib: LibManifestEntry,
#[serde(default)]
pub themes: Vec<PathBuf>,
#[serde(default)]
pub languages: Vec<PathBuf>,
#[serde(default)]
pub grammars: BTreeMap<Arc<str>, GrammarManifestEntry>,
#[serde(default)]
pub language_servers: BTreeMap<LanguageServerName, LanguageServerManifestEntry>,
#[serde(default)]
pub slash_commands: BTreeMap<Arc<str>, SlashCommandManifestEntry>,
#[serde(default)]
pub indexed_docs_providers: BTreeMap<Arc<str>, IndexedDocsProviderEntry>,
#[serde(default)]
pub snippets: Option<PathBuf>,
}
#[derive(Clone, Default, PartialEq, Eq, Debug, Deserialize, Serialize)]
pub struct LibManifestEntry {
pub kind: Option<ExtensionLibraryKind>,
pub version: Option<SemanticVersion>,
}
#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
pub enum ExtensionLibraryKind {
Rust,
}
#[derive(Clone, Default, PartialEq, Eq, Debug, Deserialize, Serialize)]
pub struct GrammarManifestEntry {
pub repository: String,
#[serde(alias = "commit")]
pub rev: String,
#[serde(default)]
pub path: Option<String>,
}
#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
pub struct LanguageServerManifestEntry {
/// Deprecated in favor of `languages`.
#[serde(default)]
language: Option<LanguageName>,
/// The list of languages this language server should work with.
#[serde(default)]
languages: Vec<LanguageName>,
#[serde(default)]
pub language_ids: HashMap<String, String>,
#[serde(default)]
pub code_action_kinds: Option<Vec<lsp::CodeActionKind>>,
}
impl LanguageServerManifestEntry {
/// Returns the list of languages for the language server.
///
/// Prefer this over accessing the `language` or `languages` fields directly,
/// as we currently support both.
///
/// We can replace this with just field access for the `languages` field once
/// we have removed `language`.
pub fn languages(&self) -> impl IntoIterator<Item = LanguageName> + '_ {
let language = if self.languages.is_empty() {
self.language.clone()
} else {
None
};
self.languages.iter().cloned().chain(language)
}
}
#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
pub struct SlashCommandManifestEntry {
pub description: String,
pub requires_argument: bool,
}
#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
pub struct IndexedDocsProviderEntry {}
impl ExtensionManifest {
pub async fn load(fs: Arc<dyn Fs>, extension_dir: &Path) -> Result<Self> {
let extension_name = extension_dir
.file_name()
.and_then(OsStr::to_str)
.ok_or_else(|| anyhow!("invalid extension name"))?;
let mut extension_manifest_path = extension_dir.join("extension.json");
if fs.is_file(&extension_manifest_path).await {
let manifest_content = fs
.load(&extension_manifest_path)
.await
.with_context(|| format!("failed to load {extension_name} extension.json"))?;
let manifest_json = serde_json::from_str::<OldExtensionManifest>(&manifest_content)
.with_context(|| {
format!("invalid extension.json for extension {extension_name}")
})?;
Ok(manifest_from_old_manifest(manifest_json, extension_name))
} else {
extension_manifest_path.set_extension("toml");
let manifest_content = fs
.load(&extension_manifest_path)
.await
.with_context(|| format!("failed to load {extension_name} extension.toml"))?;
toml::from_str(&manifest_content)
.with_context(|| format!("invalid extension.json for extension {extension_name}"))
}
}
}
fn manifest_from_old_manifest(
manifest_json: OldExtensionManifest,
extension_id: &str,
) -> ExtensionManifest {
ExtensionManifest {
id: extension_id.into(),
name: manifest_json.name,
version: manifest_json.version,
description: manifest_json.description,
repository: manifest_json.repository,
authors: manifest_json.authors,
schema_version: SchemaVersion::ZERO,
lib: Default::default(),
themes: {
let mut themes = manifest_json.themes.into_values().collect::<Vec<_>>();
themes.sort();
themes.dedup();
themes
},
languages: {
let mut languages = manifest_json.languages.into_values().collect::<Vec<_>>();
languages.sort();
languages.dedup();
languages
},
grammars: manifest_json
.grammars
.into_keys()
.map(|grammar_name| (grammar_name, Default::default()))
.collect(),
language_servers: Default::default(),
slash_commands: BTreeMap::default(),
indexed_docs_providers: BTreeMap::default(),
snippets: None,
}
}

View file

@ -1,51 +0,0 @@
use anyhow::Result;
use collections::HashMap;
use gpui::AppContext;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
use std::sync::Arc;
#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema)]
pub struct ExtensionSettings {
/// The extensions that should be automatically installed by Zed.
///
/// This is used to make functionality provided by extensions (e.g., language support)
/// available out-of-the-box.
#[serde(default)]
pub auto_install_extensions: HashMap<Arc<str>, bool>,
#[serde(default)]
pub auto_update_extensions: HashMap<Arc<str>, bool>,
}
impl ExtensionSettings {
/// Returns whether the given extension should be auto-installed.
pub fn should_auto_install(&self, extension_id: &str) -> bool {
self.auto_install_extensions
.get(extension_id)
.copied()
.unwrap_or(true)
}
pub fn should_auto_update(&self, extension_id: &str) -> bool {
self.auto_update_extensions
.get(extension_id)
.copied()
.unwrap_or(true)
}
}
impl Settings for ExtensionSettings {
const KEY: Option<&'static str> = None;
type FileContent = Self;
fn load(sources: SettingsSources<Self::FileContent>, _cx: &mut AppContext) -> Result<Self> {
SettingsSources::<Self::FileContent>::json_merge_with(
[sources.default]
.into_iter()
.chain(sources.user)
.chain(sources.server),
)
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,300 +0,0 @@
pub(crate) mod wit;
use crate::ExtensionManifest;
use anyhow::{anyhow, bail, Context as _, Result};
use fs::{normalize_path, Fs};
use futures::future::LocalBoxFuture;
use futures::{
channel::{
mpsc::{self, UnboundedSender},
oneshot,
},
future::BoxFuture,
Future, FutureExt, StreamExt as _,
};
use gpui::{AppContext, AsyncAppContext, BackgroundExecutor, Task};
use http_client::HttpClient;
use language::LanguageRegistry;
use node_runtime::NodeRuntime;
use release_channel::ReleaseChannel;
use semantic_version::SemanticVersion;
use std::{
path::{Path, PathBuf},
sync::{Arc, OnceLock},
};
use wasmtime::{
component::{Component, ResourceTable},
Engine, Store,
};
use wasmtime_wasi as wasi;
use wit::Extension;
pub(crate) struct WasmHost {
engine: Engine,
release_channel: ReleaseChannel,
http_client: Arc<dyn HttpClient>,
node_runtime: NodeRuntime,
pub(crate) language_registry: Arc<LanguageRegistry>,
fs: Arc<dyn Fs>,
pub(crate) work_dir: PathBuf,
_main_thread_message_task: Task<()>,
main_thread_message_tx: mpsc::UnboundedSender<MainThreadCall>,
}
#[derive(Clone)]
pub struct WasmExtension {
tx: UnboundedSender<ExtensionCall>,
pub(crate) manifest: Arc<ExtensionManifest>,
#[allow(unused)]
pub zed_api_version: SemanticVersion,
}
pub(crate) struct WasmState {
manifest: Arc<ExtensionManifest>,
pub(crate) table: ResourceTable,
ctx: wasi::WasiCtx,
pub(crate) host: Arc<WasmHost>,
}
type MainThreadCall =
Box<dyn Send + for<'a> FnOnce(&'a mut AsyncAppContext) -> LocalBoxFuture<'a, ()>>;
type ExtensionCall = Box<
dyn Send + for<'a> FnOnce(&'a mut Extension, &'a mut Store<WasmState>) -> BoxFuture<'a, ()>,
>;
fn wasm_engine() -> wasmtime::Engine {
static WASM_ENGINE: OnceLock<wasmtime::Engine> = OnceLock::new();
WASM_ENGINE
.get_or_init(|| {
let mut config = wasmtime::Config::new();
config.wasm_component_model(true);
config.async_support(true);
wasmtime::Engine::new(&config).unwrap()
})
.clone()
}
impl WasmHost {
pub fn new(
fs: Arc<dyn Fs>,
http_client: Arc<dyn HttpClient>,
node_runtime: NodeRuntime,
language_registry: Arc<LanguageRegistry>,
work_dir: PathBuf,
cx: &mut AppContext,
) -> Arc<Self> {
let (tx, mut rx) = mpsc::unbounded::<MainThreadCall>();
let task = cx.spawn(|mut cx| async move {
while let Some(message) = rx.next().await {
message(&mut cx).await;
}
});
Arc::new(Self {
engine: wasm_engine(),
fs,
work_dir,
http_client,
node_runtime,
language_registry,
release_channel: ReleaseChannel::global(cx),
_main_thread_message_task: task,
main_thread_message_tx: tx,
})
}
pub fn load_extension(
self: &Arc<Self>,
wasm_bytes: Vec<u8>,
manifest: Arc<ExtensionManifest>,
executor: BackgroundExecutor,
) -> Task<Result<WasmExtension>> {
let this = self.clone();
executor.clone().spawn(async move {
let zed_api_version = parse_wasm_extension_version(&manifest.id, &wasm_bytes)?;
let component = Component::from_binary(&this.engine, &wasm_bytes)
.context("failed to compile wasm component")?;
let mut store = wasmtime::Store::new(
&this.engine,
WasmState {
ctx: this.build_wasi_ctx(&manifest).await?,
manifest: manifest.clone(),
table: ResourceTable::new(),
host: this.clone(),
},
);
let mut extension = Extension::instantiate_async(
&mut store,
this.release_channel,
zed_api_version,
&component,
)
.await?;
extension
.call_init_extension(&mut store)
.await
.context("failed to initialize wasm extension")?;
let (tx, mut rx) = mpsc::unbounded::<ExtensionCall>();
executor
.spawn(async move {
while let Some(call) = rx.next().await {
(call)(&mut extension, &mut store).await;
}
})
.detach();
Ok(WasmExtension {
manifest,
tx,
zed_api_version,
})
})
}
async fn build_wasi_ctx(&self, manifest: &Arc<ExtensionManifest>) -> Result<wasi::WasiCtx> {
let extension_work_dir = self.work_dir.join(manifest.id.as_ref());
self.fs
.create_dir(&extension_work_dir)
.await
.context("failed to create extension work dir")?;
let file_perms = wasi::FilePerms::all();
let dir_perms = wasi::DirPerms::all();
Ok(wasi::WasiCtxBuilder::new()
.inherit_stdio()
.preopened_dir(&extension_work_dir, ".", dir_perms, file_perms)?
.preopened_dir(
&extension_work_dir,
extension_work_dir.to_string_lossy(),
dir_perms,
file_perms,
)?
.env("PWD", extension_work_dir.to_string_lossy())
.env("RUST_BACKTRACE", "full")
.build())
}
pub fn path_from_extension(&self, id: &Arc<str>, path: &Path) -> PathBuf {
let extension_work_dir = self.work_dir.join(id.as_ref());
normalize_path(&extension_work_dir.join(path))
}
pub fn writeable_path_from_extension(&self, id: &Arc<str>, path: &Path) -> Result<PathBuf> {
let extension_work_dir = self.work_dir.join(id.as_ref());
let path = normalize_path(&extension_work_dir.join(path));
if path.starts_with(&extension_work_dir) {
Ok(path)
} else {
Err(anyhow!("cannot write to path {}", path.display()))
}
}
}
pub fn parse_wasm_extension_version(
extension_id: &str,
wasm_bytes: &[u8],
) -> Result<SemanticVersion> {
let mut version = None;
for part in wasmparser::Parser::new(0).parse_all(wasm_bytes) {
if let wasmparser::Payload::CustomSection(s) =
part.context("error parsing wasm extension")?
{
if s.name() == "zed:api-version" {
version = parse_wasm_extension_version_custom_section(s.data());
if version.is_none() {
bail!(
"extension {} has invalid zed:api-version section: {:?}",
extension_id,
s.data()
);
}
}
}
}
// The reason we wait until we're done parsing all of the Wasm bytes to return the version
// is to work around a panic that can happen inside of Wasmtime when the bytes are invalid.
//
// By parsing the entirety of the Wasm bytes before we return, we're able to detect this problem
// earlier as an `Err` rather than as a panic.
version.ok_or_else(|| anyhow!("extension {} has no zed:api-version section", extension_id))
}
fn parse_wasm_extension_version_custom_section(data: &[u8]) -> Option<SemanticVersion> {
if data.len() == 6 {
Some(SemanticVersion::new(
u16::from_be_bytes([data[0], data[1]]) as _,
u16::from_be_bytes([data[2], data[3]]) as _,
u16::from_be_bytes([data[4], data[5]]) as _,
))
} else {
None
}
}
impl WasmExtension {
pub async fn call<T, Fn>(&self, f: Fn) -> T
where
T: 'static + Send,
Fn: 'static
+ Send
+ for<'a> FnOnce(&'a mut Extension, &'a mut Store<WasmState>) -> BoxFuture<'a, T>,
{
let (return_tx, return_rx) = oneshot::channel();
self.tx
.clone()
.unbounded_send(Box::new(move |extension, store| {
async {
let result = f(extension, store).await;
return_tx.send(result).ok();
}
.boxed()
}))
.expect("wasm extension channel should not be closed yet");
return_rx.await.expect("wasm extension channel")
}
}
impl WasmState {
fn on_main_thread<T, Fn>(&self, f: Fn) -> impl 'static + Future<Output = T>
where
T: 'static + Send,
Fn: 'static + Send + for<'a> FnOnce(&'a mut AsyncAppContext) -> LocalBoxFuture<'a, T>,
{
let (return_tx, return_rx) = oneshot::channel();
self.host
.main_thread_message_tx
.clone()
.unbounded_send(Box::new(move |cx| {
async {
let result = f(cx).await;
return_tx.send(result).ok();
}
.boxed_local()
}))
.expect("main thread message channel should not be closed yet");
async move { return_rx.await.expect("main thread message channel") }
}
fn work_dir(&self) -> PathBuf {
self.host.work_dir.join(self.manifest.id.as_ref())
}
}
impl wasi::WasiView for WasmState {
fn table(&mut self) -> &mut ResourceTable {
&mut self.table
}
fn ctx(&mut self) -> &mut wasi::WasiCtx {
&mut self.ctx
}
}

View file

@ -1,405 +0,0 @@
mod since_v0_0_1;
mod since_v0_0_4;
mod since_v0_0_6;
mod since_v0_1_0;
mod since_v0_2_0;
use indexed_docs::IndexedDocsDatabase;
use release_channel::ReleaseChannel;
use since_v0_2_0 as latest;
use super::{wasm_engine, WasmState};
use anyhow::{anyhow, Context, Result};
use language::{LanguageServerName, LspAdapterDelegate};
use semantic_version::SemanticVersion;
use std::{ops::RangeInclusive, sync::Arc};
use wasmtime::{
component::{Component, Linker, Resource},
Store,
};
#[cfg(test)]
pub use latest::CodeLabelSpanLiteral;
pub use latest::{
zed::extension::lsp::{Completion, CompletionKind, InsertTextFormat, Symbol, SymbolKind},
zed::extension::slash_command::{SlashCommandArgumentCompletion, SlashCommandOutput},
CodeLabel, CodeLabelSpan, Command, Range, SlashCommand,
};
pub use since_v0_0_4::LanguageServerConfig;
pub fn new_linker(
f: impl Fn(&mut Linker<WasmState>, fn(&mut WasmState) -> &mut WasmState) -> Result<()>,
) -> Linker<WasmState> {
let mut linker = Linker::new(&wasm_engine());
wasmtime_wasi::add_to_linker_async(&mut linker).unwrap();
f(&mut linker, wasi_view).unwrap();
linker
}
fn wasi_view(state: &mut WasmState) -> &mut WasmState {
state
}
/// Returns whether the given Wasm API version is supported by the Wasm host.
pub fn is_supported_wasm_api_version(
release_channel: ReleaseChannel,
version: SemanticVersion,
) -> bool {
wasm_api_version_range(release_channel).contains(&version)
}
/// Returns the Wasm API version range that is supported by the Wasm host.
#[inline(always)]
pub fn wasm_api_version_range(release_channel: ReleaseChannel) -> RangeInclusive<SemanticVersion> {
// Note: The release channel can be used to stage a new version of the extension API.
let _ = release_channel;
let max_version = match release_channel {
ReleaseChannel::Dev | ReleaseChannel::Nightly => latest::MAX_VERSION,
ReleaseChannel::Stable | ReleaseChannel::Preview => since_v0_1_0::MAX_VERSION,
};
since_v0_0_1::MIN_VERSION..=max_version
}
pub enum Extension {
V020(since_v0_2_0::Extension),
V010(since_v0_1_0::Extension),
V006(since_v0_0_6::Extension),
V004(since_v0_0_4::Extension),
V001(since_v0_0_1::Extension),
}
impl Extension {
pub async fn instantiate_async(
store: &mut Store<WasmState>,
release_channel: ReleaseChannel,
version: SemanticVersion,
component: &Component,
) -> Result<Self> {
// Note: The release channel can be used to stage a new version of the extension API.
let allow_latest_version = match release_channel {
ReleaseChannel::Dev | ReleaseChannel::Nightly => true,
ReleaseChannel::Stable | ReleaseChannel::Preview => false,
};
if allow_latest_version && version >= latest::MIN_VERSION {
let extension =
latest::Extension::instantiate_async(store, component, latest::linker())
.await
.context("failed to instantiate wasm extension")?;
Ok(Self::V020(extension))
} else if version >= since_v0_1_0::MIN_VERSION {
let extension = since_v0_1_0::Extension::instantiate_async(
store,
component,
since_v0_1_0::linker(),
)
.await
.context("failed to instantiate wasm extension")?;
Ok(Self::V010(extension))
} else if version >= since_v0_0_6::MIN_VERSION {
let extension = since_v0_0_6::Extension::instantiate_async(
store,
component,
since_v0_0_6::linker(),
)
.await
.context("failed to instantiate wasm extension")?;
Ok(Self::V006(extension))
} else if version >= since_v0_0_4::MIN_VERSION {
let extension = since_v0_0_4::Extension::instantiate_async(
store,
component,
since_v0_0_4::linker(),
)
.await
.context("failed to instantiate wasm extension")?;
Ok(Self::V004(extension))
} else {
let extension = 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))
}
}
pub async fn call_init_extension(&self, store: &mut Store<WasmState>) -> Result<()> {
match self {
Extension::V020(ext) => ext.call_init_extension(store).await,
Extension::V010(ext) => ext.call_init_extension(store).await,
Extension::V006(ext) => ext.call_init_extension(store).await,
Extension::V004(ext) => ext.call_init_extension(store).await,
Extension::V001(ext) => ext.call_init_extension(store).await,
}
}
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::V020(ext) => {
ext.call_language_server_command(store, &language_server_id.0, resource)
.await
}
Extension::V010(ext) => Ok(ext
.call_language_server_command(store, &language_server_id.0, resource)
.await?
.map(|command| command.into())),
Extension::V006(ext) => Ok(ext
.call_language_server_command(store, &language_server_id.0, resource)
.await?
.map(|command| command.into())),
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?
.map(|command| command.into())),
}
}
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::V020(ext) => {
ext.call_language_server_initialization_options(
store,
&language_server_id.0,
resource,
)
.await
}
Extension::V010(ext) => {
ext.call_language_server_initialization_options(
store,
&language_server_id.0,
resource,
)
.await
}
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
}
Extension::V001(ext) => {
ext.call_language_server_initialization_options(
store,
&config.clone().into(),
resource,
)
.await
}
}
}
pub async fn call_language_server_workspace_configuration(
&self,
store: &mut Store<WasmState>,
language_server_id: &LanguageServerName,
resource: Resource<Arc<dyn LspAdapterDelegate>>,
) -> Result<Result<Option<String>, String>> {
match self {
Extension::V020(ext) => {
ext.call_language_server_workspace_configuration(
store,
&language_server_id.0,
resource,
)
.await
}
Extension::V010(ext) => {
ext.call_language_server_workspace_configuration(
store,
&language_server_id.0,
resource,
)
.await
}
Extension::V006(ext) => {
ext.call_language_server_workspace_configuration(
store,
&language_server_id.0,
resource,
)
.await
}
Extension::V004(_) | Extension::V001(_) => Ok(Ok(None)),
}
}
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::V020(ext) => {
ext.call_labels_for_completions(store, &language_server_id.0, &completions)
.await
}
Extension::V010(ext) => Ok(ext
.call_labels_for_completions(store, &language_server_id.0, &completions)
.await?
.map(|labels| {
labels
.into_iter()
.map(|label| label.map(Into::into))
.collect()
})),
Extension::V006(ext) => Ok(ext
.call_labels_for_completions(store, &language_server_id.0, &completions)
.await?
.map(|labels| {
labels
.into_iter()
.map(|label| label.map(Into::into))
.collect()
})),
Extension::V001(_) | Extension::V004(_) => Ok(Ok(Vec::new())),
}
}
pub async fn call_labels_for_symbols(
&self,
store: &mut Store<WasmState>,
language_server_id: &LanguageServerName,
symbols: Vec<latest::Symbol>,
) -> Result<Result<Vec<Option<CodeLabel>>, String>> {
match self {
Extension::V020(ext) => {
ext.call_labels_for_symbols(store, &language_server_id.0, &symbols)
.await
}
Extension::V010(ext) => Ok(ext
.call_labels_for_symbols(store, &language_server_id.0, &symbols)
.await?
.map(|labels| {
labels
.into_iter()
.map(|label| label.map(Into::into))
.collect()
})),
Extension::V006(ext) => Ok(ext
.call_labels_for_symbols(store, &language_server_id.0, &symbols)
.await?
.map(|labels| {
labels
.into_iter()
.map(|label| label.map(Into::into))
.collect()
})),
Extension::V001(_) | Extension::V004(_) => Ok(Ok(Vec::new())),
}
}
pub async fn call_complete_slash_command_argument(
&self,
store: &mut Store<WasmState>,
command: &SlashCommand,
arguments: &[String],
) -> Result<Result<Vec<SlashCommandArgumentCompletion>, String>> {
match self {
Extension::V020(ext) => {
ext.call_complete_slash_command_argument(store, command, arguments)
.await
}
Extension::V010(ext) => {
ext.call_complete_slash_command_argument(store, command, arguments)
.await
}
Extension::V001(_) | Extension::V004(_) | Extension::V006(_) => Ok(Ok(Vec::new())),
}
}
pub async fn call_run_slash_command(
&self,
store: &mut Store<WasmState>,
command: &SlashCommand,
arguments: &[String],
resource: Option<Resource<Arc<dyn LspAdapterDelegate>>>,
) -> Result<Result<SlashCommandOutput, String>> {
match self {
Extension::V020(ext) => {
ext.call_run_slash_command(store, command, arguments, resource)
.await
}
Extension::V010(ext) => {
ext.call_run_slash_command(store, command, arguments, resource)
.await
}
Extension::V001(_) | Extension::V004(_) | Extension::V006(_) => {
Err(anyhow!("`run_slash_command` not available prior to v0.1.0"))
}
}
}
pub async fn call_suggest_docs_packages(
&self,
store: &mut Store<WasmState>,
provider: &str,
) -> Result<Result<Vec<String>, String>> {
match self {
Extension::V020(ext) => ext.call_suggest_docs_packages(store, provider).await,
Extension::V010(ext) => ext.call_suggest_docs_packages(store, provider).await,
Extension::V001(_) | Extension::V004(_) | Extension::V006(_) => Err(anyhow!(
"`suggest_docs_packages` not available prior to v0.1.0"
)),
}
}
pub async fn call_index_docs(
&self,
store: &mut Store<WasmState>,
provider: &str,
package_name: &str,
database: Resource<Arc<IndexedDocsDatabase>>,
) -> Result<Result<(), String>> {
match self {
Extension::V020(ext) => {
ext.call_index_docs(store, provider, package_name, database)
.await
}
Extension::V010(ext) => {
ext.call_index_docs(store, provider, package_name, database)
.await
}
Extension::V001(_) | Extension::V004(_) | Extension::V006(_) => {
Err(anyhow!("`index_docs` not available prior to v0.1.0"))
}
}
}
}
trait ToWasmtimeResult<T> {
fn to_wasmtime_result(self) -> wasmtime::Result<Result<T, String>>;
}
impl<T> ToWasmtimeResult<T> for Result<T> {
fn to_wasmtime_result(self) -> wasmtime::Result<Result<T, String>> {
Ok(self.map_err(|error| error.to_string()))
}
}

View file

@ -1,164 +0,0 @@
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;
use language::{LanguageServerBinaryStatus, LspAdapterDelegate};
use semantic_version::SemanticVersion;
use std::sync::{Arc, OnceLock};
use wasmtime::component::{Linker, Resource};
pub const MIN_VERSION: SemanticVersion = SemanticVersion::new(0, 0, 1);
wasmtime::component::bindgen!({
async: true,
trappable_imports: true,
path: "../extension_api/wit/since_v0.0.1",
with: {
"worktree": ExtensionWorktree,
"zed:extension/github": latest::zed::extension::github,
"zed:extension/platform": latest::zed::extension::platform,
},
});
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))
}
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<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,
}
}
}
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(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
path: String,
) -> wasmtime::Result<Result<String, String>> {
latest::HostWorktree::read_text_file(self, delegate, path).await
}
async fn shell_env(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
) -> wasmtime::Result<EnvVars> {
latest::HostWorktree::shell_env(self, delegate).await
}
async fn which(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
binary_name: String,
) -> wasmtime::Result<Option<String>> {
latest::HostWorktree::which(self, delegate, binary_name).await
}
fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {
Ok(())
}
}
#[async_trait]
impl ExtensionImports for WasmState {
async fn node_binary_path(&mut self) -> wasmtime::Result<Result<String, String>> {
latest::nodejs::Host::node_binary_path(self).await
}
async fn npm_package_latest_version(
&mut self,
package_name: String,
) -> wasmtime::Result<Result<String, String>> {
latest::nodejs::Host::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>> {
latest::nodejs::Host::npm_package_installed_version(self, package_name).await
}
async fn npm_install_package(
&mut self,
package_name: String,
version: String,
) -> wasmtime::Result<Result<(), String>> {
latest::nodejs::Host::npm_install_package(self, package_name, version).await
}
async fn latest_github_release(
&mut self,
repo: String,
options: GithubReleaseOptions,
) -> wasmtime::Result<Result<GithubRelease, String>> {
latest::zed::extension::github::Host::latest_github_release(self, repo, options).await
}
async fn current_platform(&mut self) -> Result<(Os, Architecture)> {
latest::zed::extension::platform::Host::current_platform(self).await
}
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::Cached
| LanguageServerInstallationStatus::Downloaded => 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>> {
latest::ExtensionImports::download_file(self, url, path, file_type.into()).await
}
}

View file

@ -1,166 +0,0 @@
use super::latest;
use crate::wasm_host::WasmState;
use anyhow::Result;
use async_trait::async_trait;
use language::LspAdapterDelegate;
use semantic_version::SemanticVersion;
use std::sync::{Arc, OnceLock};
use wasmtime::component::{Linker, Resource};
pub const MIN_VERSION: SemanticVersion = SemanticVersion::new(0, 0, 4);
wasmtime::component::bindgen!({
async: true,
trappable_imports: true,
path: "../extension_api/wit/since_v0.0.4",
with: {
"worktree": ExtensionWorktree,
"zed:extension/github": latest::zed::extension::github,
"zed:extension/platform": latest::zed::extension::platform,
},
});
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))
}
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(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
path: String,
) -> wasmtime::Result<Result<String, String>> {
latest::HostWorktree::read_text_file(self, delegate, path).await
}
async fn shell_env(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
) -> wasmtime::Result<EnvVars> {
latest::HostWorktree::shell_env(self, delegate).await
}
async fn which(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
binary_name: String,
) -> wasmtime::Result<Option<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.
Ok(())
}
}
#[async_trait]
impl ExtensionImports for WasmState {
async fn node_binary_path(&mut self) -> wasmtime::Result<Result<String, String>> {
latest::nodejs::Host::node_binary_path(self).await
}
async fn npm_package_latest_version(
&mut self,
package_name: String,
) -> wasmtime::Result<Result<String, String>> {
latest::nodejs::Host::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>> {
latest::nodejs::Host::npm_package_installed_version(self, package_name).await
}
async fn npm_install_package(
&mut self,
package_name: String,
version: String,
) -> wasmtime::Result<Result<(), String>> {
latest::nodejs::Host::npm_install_package(self, package_name, version).await
}
async fn latest_github_release(
&mut self,
repo: String,
options: GithubReleaseOptions,
) -> wasmtime::Result<Result<GithubRelease, String>> {
latest::zed::extension::github::Host::latest_github_release(self, repo, options).await
}
async fn current_platform(&mut self) -> Result<(Os, Architecture)> {
latest::zed::extension::platform::Host::current_platform(self).await
}
async fn set_language_server_installation_status(
&mut self,
server_name: String,
status: LanguageServerInstallationStatus,
) -> wasmtime::Result<()> {
latest::ExtensionImports::set_language_server_installation_status(
self,
server_name,
status.into(),
)
.await
}
async fn download_file(
&mut self,
url: String,
path: String,
file_type: DownloadedFileType,
) -> wasmtime::Result<Result<(), String>> {
latest::ExtensionImports::download_file(self, url, path, file_type.into()).await
}
async fn make_file_executable(&mut self, path: String) -> wasmtime::Result<Result<(), String>> {
latest::ExtensionImports::make_file_executable(self, path).await
}
}

View file

@ -1,201 +0,0 @@
use super::latest;
use crate::wasm_host::WasmState;
use anyhow::Result;
use async_trait::async_trait;
use language::LspAdapterDelegate;
use semantic_version::SemanticVersion;
use std::sync::{Arc, OnceLock};
use wasmtime::component::{Linker, Resource};
pub const MIN_VERSION: SemanticVersion = SemanticVersion::new(0, 0, 6);
wasmtime::component::bindgen!({
async: true,
trappable_imports: true,
path: "../extension_api/wit/since_v0.0.6",
with: {
"worktree": ExtensionWorktree,
"zed:extension/github": latest::zed::extension::github,
"zed:extension/lsp": latest::zed::extension::lsp,
"zed:extension/nodejs": latest::zed::extension::nodejs,
"zed:extension/platform": latest::zed::extension::platform,
},
});
mod settings {
include!(concat!(env!("OUT_DIR"), "/since_v0.0.6/settings.rs"));
}
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))
}
impl From<Command> for latest::Command {
fn from(value: Command) -> Self {
Self {
command: value.command,
args: value.args,
env: value.env,
}
}
}
impl From<SettingsLocation> for latest::SettingsLocation {
fn from(value: SettingsLocation) -> Self {
Self {
worktree_id: value.worktree_id,
path: value.path,
}
}
}
impl From<LanguageServerInstallationStatus> for latest::LanguageServerInstallationStatus {
fn from(value: LanguageServerInstallationStatus) -> Self {
match value {
LanguageServerInstallationStatus::None => Self::None,
LanguageServerInstallationStatus::Downloading => Self::Downloading,
LanguageServerInstallationStatus::CheckingForUpdate => Self::CheckingForUpdate,
LanguageServerInstallationStatus::Failed(message) => Self::Failed(message),
}
}
}
impl From<DownloadedFileType> for latest::DownloadedFileType {
fn from(value: DownloadedFileType) -> Self {
match value {
DownloadedFileType::Gzip => Self::Gzip,
DownloadedFileType::GzipTar => Self::GzipTar,
DownloadedFileType::Zip => Self::Zip,
DownloadedFileType::Uncompressed => Self::Uncompressed,
}
}
}
impl From<Range> for latest::Range {
fn from(value: Range) -> Self {
Self {
start: value.start,
end: value.end,
}
}
}
impl From<CodeLabelSpan> for latest::CodeLabelSpan {
fn from(value: CodeLabelSpan) -> Self {
match value {
CodeLabelSpan::CodeRange(range) => Self::CodeRange(range.into()),
CodeLabelSpan::Literal(literal) => Self::Literal(literal.into()),
}
}
}
impl From<CodeLabelSpanLiteral> for latest::CodeLabelSpanLiteral {
fn from(value: CodeLabelSpanLiteral) -> Self {
Self {
text: value.text,
highlight_name: value.highlight_name,
}
}
}
impl From<CodeLabel> for latest::CodeLabel {
fn from(value: CodeLabel) -> Self {
Self {
code: value.code,
spans: value.spans.into_iter().map(Into::into).collect(),
filter_range: value.filter_range.into(),
}
}
}
#[async_trait]
impl HostWorktree for WasmState {
async fn id(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
) -> wasmtime::Result<u64> {
latest::HostWorktree::id(self, delegate).await
}
async fn root_path(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
) -> wasmtime::Result<String> {
latest::HostWorktree::root_path(self, delegate).await
}
async fn read_text_file(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
path: String,
) -> wasmtime::Result<Result<String, String>> {
latest::HostWorktree::read_text_file(self, delegate, path).await
}
async fn shell_env(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
) -> wasmtime::Result<EnvVars> {
latest::HostWorktree::shell_env(self, delegate).await
}
async fn which(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
binary_name: String,
) -> wasmtime::Result<Option<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.
Ok(())
}
}
#[async_trait]
impl ExtensionImports for WasmState {
async fn get_settings(
&mut self,
location: Option<self::SettingsLocation>,
category: String,
key: Option<String>,
) -> wasmtime::Result<Result<String, String>> {
latest::ExtensionImports::get_settings(
self,
location.map(|location| location.into()),
category,
key,
)
.await
}
async fn set_language_server_installation_status(
&mut self,
server_name: String,
status: LanguageServerInstallationStatus,
) -> wasmtime::Result<()> {
latest::ExtensionImports::set_language_server_installation_status(
self,
server_name,
status.into(),
)
.await
}
async fn download_file(
&mut self,
url: String,
path: String,
file_type: DownloadedFileType,
) -> wasmtime::Result<Result<(), String>> {
latest::ExtensionImports::download_file(self, url, path, file_type.into()).await
}
async fn make_file_executable(&mut self, path: String) -> wasmtime::Result<Result<(), String>> {
latest::ExtensionImports::make_file_executable(self, path).await
}
}

View file

@ -1,509 +0,0 @@
use crate::wasm_host::{wit::ToWasmtimeResult, WasmState};
use ::http_client::{AsyncBody, HttpRequestExt};
use ::settings::{Settings, WorktreeId};
use anyhow::{anyhow, bail, Context, Result};
use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use async_trait::async_trait;
use futures::{io::BufReader, FutureExt as _};
use futures::{lock::Mutex, AsyncReadExt};
use indexed_docs::IndexedDocsDatabase;
use language::{
language_settings::AllLanguageSettings, LanguageServerBinaryStatus, LspAdapterDelegate,
};
use language::{LanguageName, LanguageServerName};
use project::project_settings::ProjectSettings;
use semantic_version::SemanticVersion;
use std::{
path::{Path, PathBuf},
sync::{Arc, OnceLock},
};
use util::maybe;
use wasmtime::component::{Linker, Resource};
use super::latest;
pub const MIN_VERSION: SemanticVersion = SemanticVersion::new(0, 1, 0);
pub const MAX_VERSION: SemanticVersion = SemanticVersion::new(0, 1, 0);
wasmtime::component::bindgen!({
async: true,
trappable_imports: true,
path: "../extension_api/wit/since_v0.1.0",
with: {
"worktree": ExtensionWorktree,
"key-value-store": ExtensionKeyValueStore,
"zed:extension/http-client/http-response-stream": ExtensionHttpResponseStream,
"zed:extension/github": latest::zed::extension::github,
"zed:extension/lsp": latest::zed::extension::lsp,
"zed:extension/nodejs": latest::zed::extension::nodejs,
"zed:extension/platform": latest::zed::extension::platform,
"zed:extension/slash-command": latest::zed::extension::slash_command,
},
});
pub use self::zed::extension::*;
mod settings {
include!(concat!(env!("OUT_DIR"), "/since_v0.1.0/settings.rs"));
}
pub type ExtensionWorktree = Arc<dyn LspAdapterDelegate>;
pub type ExtensionKeyValueStore = Arc<IndexedDocsDatabase>;
pub type ExtensionHttpResponseStream = Arc<Mutex<::http_client::Response<AsyncBody>>>;
pub fn linker() -> &'static Linker<WasmState> {
static LINKER: OnceLock<Linker<WasmState>> = OnceLock::new();
LINKER.get_or_init(|| super::new_linker(Extension::add_to_linker))
}
impl From<Command> for latest::Command {
fn from(value: Command) -> Self {
Self {
command: value.command,
args: value.args,
env: value.env,
}
}
}
impl From<SettingsLocation> for latest::SettingsLocation {
fn from(value: SettingsLocation) -> Self {
Self {
worktree_id: value.worktree_id,
path: value.path,
}
}
}
impl From<LanguageServerInstallationStatus> for latest::LanguageServerInstallationStatus {
fn from(value: LanguageServerInstallationStatus) -> Self {
match value {
LanguageServerInstallationStatus::None => Self::None,
LanguageServerInstallationStatus::Downloading => Self::Downloading,
LanguageServerInstallationStatus::CheckingForUpdate => Self::CheckingForUpdate,
LanguageServerInstallationStatus::Failed(message) => Self::Failed(message),
}
}
}
impl From<DownloadedFileType> for latest::DownloadedFileType {
fn from(value: DownloadedFileType) -> Self {
match value {
DownloadedFileType::Gzip => Self::Gzip,
DownloadedFileType::GzipTar => Self::GzipTar,
DownloadedFileType::Zip => Self::Zip,
DownloadedFileType::Uncompressed => Self::Uncompressed,
}
}
}
impl From<Range> for latest::Range {
fn from(value: Range) -> Self {
Self {
start: value.start,
end: value.end,
}
}
}
impl From<CodeLabelSpan> for latest::CodeLabelSpan {
fn from(value: CodeLabelSpan) -> Self {
match value {
CodeLabelSpan::CodeRange(range) => Self::CodeRange(range.into()),
CodeLabelSpan::Literal(literal) => Self::Literal(literal.into()),
}
}
}
impl From<CodeLabelSpanLiteral> for latest::CodeLabelSpanLiteral {
fn from(value: CodeLabelSpanLiteral) -> Self {
Self {
text: value.text,
highlight_name: value.highlight_name,
}
}
}
impl From<CodeLabel> for latest::CodeLabel {
fn from(value: CodeLabel) -> Self {
Self {
code: value.code,
spans: value.spans.into_iter().map(Into::into).collect(),
filter_range: value.filter_range.into(),
}
}
}
#[async_trait]
impl HostKeyValueStore for WasmState {
async fn insert(
&mut self,
kv_store: Resource<ExtensionKeyValueStore>,
key: String,
value: String,
) -> wasmtime::Result<Result<(), String>> {
let kv_store = self.table.get(&kv_store)?;
kv_store.insert(key, value).await.to_wasmtime_result()
}
fn drop(&mut self, _worktree: Resource<ExtensionKeyValueStore>) -> Result<()> {
// We only ever hand out borrows of key-value stores.
Ok(())
}
}
#[async_trait]
impl HostWorktree for WasmState {
async fn id(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
) -> wasmtime::Result<u64> {
let delegate = self.table.get(&delegate)?;
Ok(delegate.worktree_id().to_proto())
}
async fn root_path(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
) -> wasmtime::Result<String> {
let delegate = self.table.get(&delegate)?;
Ok(delegate.worktree_root_path().to_string_lossy().to_string())
}
async fn read_text_file(
&mut self,
delegate: Resource<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(())
}
}
#[async_trait]
impl common::Host for WasmState {}
#[async_trait]
impl http_client::Host for WasmState {
async fn fetch(
&mut self,
request: http_client::HttpRequest,
) -> wasmtime::Result<Result<http_client::HttpResponse, String>> {
maybe!(async {
let url = &request.url;
let request = convert_request(&request)?;
let mut response = self.host.http_client.send(request).await?;
if response.status().is_client_error() || response.status().is_server_error() {
bail!("failed to fetch '{url}': status code {}", response.status())
}
convert_response(&mut response).await
})
.await
.to_wasmtime_result()
}
async fn fetch_stream(
&mut self,
request: http_client::HttpRequest,
) -> wasmtime::Result<Result<Resource<ExtensionHttpResponseStream>, String>> {
let request = convert_request(&request)?;
let response = self.host.http_client.send(request);
maybe!(async {
let response = response.await?;
let stream = Arc::new(Mutex::new(response));
let resource = self.table.push(stream)?;
Ok(resource)
})
.await
.to_wasmtime_result()
}
}
#[async_trait]
impl http_client::HostHttpResponseStream for WasmState {
async fn next_chunk(
&mut self,
resource: Resource<ExtensionHttpResponseStream>,
) -> wasmtime::Result<Result<Option<Vec<u8>>, String>> {
let stream = self.table.get(&resource)?.clone();
maybe!(async move {
let mut response = stream.lock().await;
let mut buffer = vec![0; 8192]; // 8KB buffer
let bytes_read = response.body_mut().read(&mut buffer).await?;
if bytes_read == 0 {
Ok(None)
} else {
buffer.truncate(bytes_read);
Ok(Some(buffer))
}
})
.await
.to_wasmtime_result()
}
fn drop(&mut self, _resource: Resource<ExtensionHttpResponseStream>) -> Result<()> {
Ok(())
}
}
impl From<http_client::HttpMethod> for ::http_client::Method {
fn from(value: http_client::HttpMethod) -> Self {
match value {
http_client::HttpMethod::Get => Self::GET,
http_client::HttpMethod::Post => Self::POST,
http_client::HttpMethod::Put => Self::PUT,
http_client::HttpMethod::Delete => Self::DELETE,
http_client::HttpMethod::Head => Self::HEAD,
http_client::HttpMethod::Options => Self::OPTIONS,
http_client::HttpMethod::Patch => Self::PATCH,
}
}
}
fn convert_request(
extension_request: &http_client::HttpRequest,
) -> Result<::http_client::Request<AsyncBody>, anyhow::Error> {
let mut request = ::http_client::Request::builder()
.method(::http_client::Method::from(extension_request.method))
.uri(&extension_request.url)
.follow_redirects(match extension_request.redirect_policy {
http_client::RedirectPolicy::NoFollow => ::http_client::RedirectPolicy::NoFollow,
http_client::RedirectPolicy::FollowLimit(limit) => {
::http_client::RedirectPolicy::FollowLimit(limit)
}
http_client::RedirectPolicy::FollowAll => ::http_client::RedirectPolicy::FollowAll,
});
for (key, value) in &extension_request.headers {
request = request.header(key, value);
}
let body = extension_request
.body
.clone()
.map(AsyncBody::from)
.unwrap_or_default();
request.body(body).map_err(anyhow::Error::from)
}
async fn convert_response(
response: &mut ::http_client::Response<AsyncBody>,
) -> Result<http_client::HttpResponse, anyhow::Error> {
let mut extension_response = http_client::HttpResponse {
body: Vec::new(),
headers: Vec::new(),
};
for (key, value) in response.headers() {
extension_response
.headers
.push((key.to_string(), value.to_str().unwrap_or("").to_string()));
}
response
.body_mut()
.read_to_end(&mut extension_response.body)
.await?;
Ok(extension_response)
}
#[async_trait]
impl ExtensionImports for WasmState {
async fn get_settings(
&mut self,
location: Option<self::SettingsLocation>,
category: String,
key: Option<String>,
) -> wasmtime::Result<Result<String, String>> {
self.on_main_thread(|cx| {
async move {
let location = location
.as_ref()
.map(|location| ::settings::SettingsLocation {
worktree_id: WorktreeId::from_proto(location.worktree_id),
path: Path::new(&location.path),
});
cx.update(|cx| match category.as_str() {
"language" => {
let key = key.map(|k| LanguageName::new(&k));
let settings = AllLanguageSettings::get(location, cx).language(
location,
key.as_ref(),
cx,
);
Ok(serde_json::to_string(&settings::LanguageSettings {
tab_size: settings.tab_size,
})?)
}
"lsp" => {
let settings = key
.and_then(|key| {
ProjectSettings::get(location, cx)
.lsp
.get(&LanguageServerName(key.into()))
})
.cloned()
.unwrap_or_default();
Ok(serde_json::to_string(&settings::LspSettings {
binary: settings.binary.map(|binary| settings::BinarySettings {
path: binary.path,
arguments: binary.arguments,
}),
settings: settings.settings,
initialization_options: settings.initialization_options,
})?)
}
_ => {
bail!("Unknown settings category: {}", category);
}
})
}
.boxed_local()
})
.await?
.to_wasmtime_result()
}
async fn set_language_server_installation_status(
&mut self,
server_name: String,
status: LanguageServerInstallationStatus,
) -> wasmtime::Result<()> {
let status = match status {
LanguageServerInstallationStatus::CheckingForUpdate => {
LanguageServerBinaryStatus::CheckingForUpdate
}
LanguageServerInstallationStatus::Downloading => {
LanguageServerBinaryStatus::Downloading
}
LanguageServerInstallationStatus::None => LanguageServerBinaryStatus::None,
LanguageServerInstallationStatus::Failed(error) => {
LanguageServerBinaryStatus::Failed { error }
}
};
self.host
.language_registry
.update_lsp_status(language::LanguageServerName(server_name.into()), status);
Ok(())
}
async fn download_file(
&mut self,
url: String,
path: String,
file_type: DownloadedFileType,
) -> wasmtime::Result<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 => {
futures::pin_mut!(body);
node_runtime::extract_zip(&destination_path, body)
.await
.with_context(|| format!("failed to unzip {} archive", path.display()))?;
}
}
Ok(())
})
.await
.to_wasmtime_result()
}
async fn make_file_executable(&mut self, path: String) -> wasmtime::Result<Result<(), String>> {
#[allow(unused)]
let path = self
.host
.writeable_path_from_extension(&self.manifest.id, Path::new(&path))?;
#[cfg(unix)]
{
use std::fs::{self, Permissions};
use std::os::unix::fs::PermissionsExt;
return fs::set_permissions(&path, Permissions::from_mode(0o755))
.map_err(|error| anyhow!("failed to set permissions for path {path:?}: {error}"))
.to_wasmtime_result();
}
#[cfg(not(unix))]
Ok(Ok(()))
}
}

View file

@ -1,555 +0,0 @@
use crate::wasm_host::{wit::ToWasmtimeResult, WasmState};
use ::http_client::{AsyncBody, HttpRequestExt};
use ::settings::{Settings, WorktreeId};
use anyhow::{anyhow, bail, Context, Result};
use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use async_trait::async_trait;
use futures::{io::BufReader, FutureExt as _};
use futures::{lock::Mutex, AsyncReadExt};
use indexed_docs::IndexedDocsDatabase;
use language::{
language_settings::AllLanguageSettings, LanguageServerBinaryStatus, LspAdapterDelegate,
};
use language::{LanguageName, LanguageServerName};
use project::project_settings::ProjectSettings;
use semantic_version::SemanticVersion;
use std::{
env,
path::{Path, PathBuf},
sync::{Arc, OnceLock},
};
use util::maybe;
use wasmtime::component::{Linker, Resource};
pub const MIN_VERSION: SemanticVersion = SemanticVersion::new(0, 2, 0);
pub const MAX_VERSION: SemanticVersion = SemanticVersion::new(0, 2, 0);
wasmtime::component::bindgen!({
async: true,
trappable_imports: true,
path: "../extension_api/wit/since_v0.2.0",
with: {
"worktree": ExtensionWorktree,
"key-value-store": ExtensionKeyValueStore,
"zed:extension/http-client/http-response-stream": ExtensionHttpResponseStream
},
});
pub use self::zed::extension::*;
mod settings {
include!(concat!(env!("OUT_DIR"), "/since_v0.2.0/settings.rs"));
}
pub type ExtensionWorktree = Arc<dyn LspAdapterDelegate>;
pub type ExtensionKeyValueStore = Arc<IndexedDocsDatabase>;
pub type ExtensionHttpResponseStream = Arc<Mutex<::http_client::Response<AsyncBody>>>;
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 HostKeyValueStore for WasmState {
async fn insert(
&mut self,
kv_store: Resource<ExtensionKeyValueStore>,
key: String,
value: String,
) -> wasmtime::Result<Result<(), String>> {
let kv_store = self.table.get(&kv_store)?;
kv_store.insert(key, value).await.to_wasmtime_result()
}
fn drop(&mut self, _worktree: Resource<ExtensionKeyValueStore>) -> Result<()> {
// We only ever hand out borrows of key-value stores.
Ok(())
}
}
#[async_trait]
impl HostWorktree for WasmState {
async fn id(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
) -> wasmtime::Result<u64> {
let delegate = self.table.get(&delegate)?;
Ok(delegate.worktree_id().to_proto())
}
async fn root_path(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
) -> wasmtime::Result<String> {
let delegate = self.table.get(&delegate)?;
Ok(delegate.worktree_root_path().to_string_lossy().to_string())
}
async fn read_text_file(
&mut self,
delegate: Resource<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(())
}
}
#[async_trait]
impl common::Host for WasmState {}
#[async_trait]
impl http_client::Host for WasmState {
async fn fetch(
&mut self,
request: http_client::HttpRequest,
) -> wasmtime::Result<Result<http_client::HttpResponse, String>> {
maybe!(async {
let url = &request.url;
let request = convert_request(&request)?;
let mut response = self.host.http_client.send(request).await?;
if response.status().is_client_error() || response.status().is_server_error() {
bail!("failed to fetch '{url}': status code {}", response.status())
}
convert_response(&mut response).await
})
.await
.to_wasmtime_result()
}
async fn fetch_stream(
&mut self,
request: http_client::HttpRequest,
) -> wasmtime::Result<Result<Resource<ExtensionHttpResponseStream>, String>> {
let request = convert_request(&request)?;
let response = self.host.http_client.send(request);
maybe!(async {
let response = response.await?;
let stream = Arc::new(Mutex::new(response));
let resource = self.table.push(stream)?;
Ok(resource)
})
.await
.to_wasmtime_result()
}
}
#[async_trait]
impl http_client::HostHttpResponseStream for WasmState {
async fn next_chunk(
&mut self,
resource: Resource<ExtensionHttpResponseStream>,
) -> wasmtime::Result<Result<Option<Vec<u8>>, String>> {
let stream = self.table.get(&resource)?.clone();
maybe!(async move {
let mut response = stream.lock().await;
let mut buffer = vec![0; 8192]; // 8KB buffer
let bytes_read = response.body_mut().read(&mut buffer).await?;
if bytes_read == 0 {
Ok(None)
} else {
buffer.truncate(bytes_read);
Ok(Some(buffer))
}
})
.await
.to_wasmtime_result()
}
fn drop(&mut self, _resource: Resource<ExtensionHttpResponseStream>) -> Result<()> {
Ok(())
}
}
impl From<http_client::HttpMethod> for ::http_client::Method {
fn from(value: http_client::HttpMethod) -> Self {
match value {
http_client::HttpMethod::Get => Self::GET,
http_client::HttpMethod::Post => Self::POST,
http_client::HttpMethod::Put => Self::PUT,
http_client::HttpMethod::Delete => Self::DELETE,
http_client::HttpMethod::Head => Self::HEAD,
http_client::HttpMethod::Options => Self::OPTIONS,
http_client::HttpMethod::Patch => Self::PATCH,
}
}
}
fn convert_request(
extension_request: &http_client::HttpRequest,
) -> Result<::http_client::Request<AsyncBody>, anyhow::Error> {
let mut request = ::http_client::Request::builder()
.method(::http_client::Method::from(extension_request.method))
.uri(&extension_request.url)
.follow_redirects(match extension_request.redirect_policy {
http_client::RedirectPolicy::NoFollow => ::http_client::RedirectPolicy::NoFollow,
http_client::RedirectPolicy::FollowLimit(limit) => {
::http_client::RedirectPolicy::FollowLimit(limit)
}
http_client::RedirectPolicy::FollowAll => ::http_client::RedirectPolicy::FollowAll,
});
for (key, value) in &extension_request.headers {
request = request.header(key, value);
}
let body = extension_request
.body
.clone()
.map(AsyncBody::from)
.unwrap_or_default();
request.body(body).map_err(anyhow::Error::from)
}
async fn convert_response(
response: &mut ::http_client::Response<AsyncBody>,
) -> Result<http_client::HttpResponse, anyhow::Error> {
let mut extension_response = http_client::HttpResponse {
body: Vec::new(),
headers: Vec::new(),
};
for (key, value) in response.headers() {
extension_response
.headers
.push((key.to_string(), value.to_str().unwrap_or("").to_string()));
}
response
.body_mut()
.read_to_end(&mut extension_response.body)
.await?;
Ok(extension_response)
}
#[async_trait]
impl nodejs::Host for WasmState {
async fn node_binary_path(&mut self) -> wasmtime::Result<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_trait]
impl lsp::Host for WasmState {}
impl From<::http_client::github::GithubRelease> for github::GithubRelease {
fn from(value: ::http_client::github::GithubRelease) -> Self {
Self {
version: value.tag_name,
assets: value.assets.into_iter().map(Into::into).collect(),
}
}
}
impl From<::http_client::github::GithubReleaseAsset> for github::GithubReleaseAsset {
fn from(value: ::http_client::github::GithubReleaseAsset) -> Self {
Self {
name: value.name,
download_url: value.browser_download_url,
}
}
}
#[async_trait]
impl github::Host for WasmState {
async fn latest_github_release(
&mut self,
repo: String,
options: github::GithubReleaseOptions,
) -> wasmtime::Result<Result<github::GithubRelease, String>> {
maybe!(async {
let release = ::http_client::github::latest_github_release(
&repo,
options.require_assets,
options.pre_release,
self.host.http_client.clone(),
)
.await?;
Ok(release.into())
})
.await
.to_wasmtime_result()
}
async fn github_release_by_tag_name(
&mut self,
repo: String,
tag: String,
) -> wasmtime::Result<Result<github::GithubRelease, String>> {
maybe!(async {
let release = ::http_client::github::get_release_by_tag_name(
&repo,
&tag,
self.host.http_client.clone(),
)
.await?;
Ok(release.into())
})
.await
.to_wasmtime_result()
}
}
#[async_trait]
impl platform::Host for WasmState {
async fn current_platform(&mut self) -> Result<(platform::Os, platform::Architecture)> {
Ok((
match env::consts::OS {
"macos" => platform::Os::Mac,
"linux" => platform::Os::Linux,
"windows" => platform::Os::Windows,
_ => panic!("unsupported os"),
},
match env::consts::ARCH {
"aarch64" => platform::Architecture::Aarch64,
"x86" => platform::Architecture::X86,
"x86_64" => platform::Architecture::X8664,
_ => panic!("unsupported architecture"),
},
))
}
}
#[async_trait]
impl slash_command::Host for WasmState {}
#[async_trait]
impl ExtensionImports for WasmState {
async fn get_settings(
&mut self,
location: Option<self::SettingsLocation>,
category: String,
key: Option<String>,
) -> wasmtime::Result<Result<String, String>> {
self.on_main_thread(|cx| {
async move {
let location = location
.as_ref()
.map(|location| ::settings::SettingsLocation {
worktree_id: WorktreeId::from_proto(location.worktree_id),
path: Path::new(&location.path),
});
cx.update(|cx| match category.as_str() {
"language" => {
let key = key.map(|k| LanguageName::new(&k));
let settings = AllLanguageSettings::get(location, cx).language(
location,
key.as_ref(),
cx,
);
Ok(serde_json::to_string(&settings::LanguageSettings {
tab_size: settings.tab_size,
})?)
}
"lsp" => {
let settings = key
.and_then(|key| {
ProjectSettings::get(location, cx)
.lsp
.get(&LanguageServerName::from_proto(key))
})
.cloned()
.unwrap_or_default();
Ok(serde_json::to_string(&settings::LspSettings {
binary: settings.binary.map(|binary| settings::BinarySettings {
path: binary.path,
arguments: binary.arguments,
}),
settings: settings.settings,
initialization_options: settings.initialization_options,
})?)
}
_ => {
bail!("Unknown settings category: {}", category);
}
})
}
.boxed_local()
})
.await?
.to_wasmtime_result()
}
async fn set_language_server_installation_status(
&mut self,
server_name: String,
status: LanguageServerInstallationStatus,
) -> wasmtime::Result<()> {
let status = match status {
LanguageServerInstallationStatus::CheckingForUpdate => {
LanguageServerBinaryStatus::CheckingForUpdate
}
LanguageServerInstallationStatus::Downloading => {
LanguageServerBinaryStatus::Downloading
}
LanguageServerInstallationStatus::None => LanguageServerBinaryStatus::None,
LanguageServerInstallationStatus::Failed(error) => {
LanguageServerBinaryStatus::Failed { error }
}
};
self.host
.language_registry
.update_lsp_status(language::LanguageServerName(server_name.into()), status);
Ok(())
}
async fn download_file(
&mut self,
url: String,
path: String,
file_type: DownloadedFileType,
) -> wasmtime::Result<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 => {
futures::pin_mut!(body);
node_runtime::extract_zip(&destination_path, body)
.await
.with_context(|| format!("failed to unzip {} archive", path.display()))?;
}
}
Ok(())
})
.await
.to_wasmtime_result()
}
async fn make_file_executable(&mut self, path: String) -> wasmtime::Result<Result<(), String>> {
#[allow(unused)]
let path = self
.host
.writeable_path_from_extension(&self.manifest.id, Path::new(&path))?;
#[cfg(unix)]
{
use std::fs::{self, Permissions};
use std::os::unix::fs::PermissionsExt;
return fs::set_permissions(&path, Permissions::from_mode(0o755))
.map_err(|error| anyhow!("failed to set permissions for path {path:?}: {error}"))
.to_wasmtime_result();
}
#[cfg(not(unix))]
Ok(Ok(()))
}
}

View file

@ -1,7 +1,7 @@
mod extension_lsp_adapter;
mod extension_manifest;
mod extension_settings;
mod wasm_host;
pub mod extension_lsp_adapter;
pub mod extension_manifest;
pub mod extension_settings;
pub mod wasm_host;
use crate::extension_manifest::SchemaVersion;
use crate::{extension_lsp_adapter::ExtensionLspAdapter, wasm_host::wit};
@ -20,8 +20,8 @@ use futures::{
select_biased, AsyncReadExt as _, Future, FutureExt as _, StreamExt as _,
};
use gpui::{
actions, AppContext, AsyncAppContext, Context, EventEmitter, Global, Model, ModelContext, Task,
WeakModel,
actions, AppContext, AsyncAppContext, Context, EventEmitter, Global, Model, ModelContext,
SharedString, Task, WeakModel,
};
use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
use language::{
@ -35,6 +35,7 @@ use semantic_version::SemanticVersion;
use serde::{Deserialize, Serialize};
use settings::Settings;
use std::ops::RangeInclusive;
use std::pin::Pin;
use std::str::FromStr;
use std::{
cmp::Ordering,
@ -54,6 +55,17 @@ pub use extension_manifest::{
};
pub use extension_settings::ExtensionSettings;
pub trait ExtensionFeatures: Sync + Send + 'static {
fn remove_user_themes(&self, themes: &[SharedString], cx: &mut AppContext);
fn register_wasm_grammars(&self, grammars: Vec<(Arc<str>, PathBuf)>, cx: &mut AppContext);
fn load_user_theme(
&self,
themes_path: &Path,
fs: Arc<dyn Fs>,
) -> Pin<Box<dyn Future<Output = Result<()>> + Send>>;
fn register_snippets(&self, file_path: &Path, contents: &str) -> Result<()>;
}
const RELOAD_DEBOUNCE_DURATION: Duration = Duration::from_millis(200);
const FS_WATCH_LATENCY: Duration = Duration::from_millis(100);
@ -90,20 +102,21 @@ pub fn is_version_compatible(
}
pub struct HeadlessExtensionStore {
extension_index: ExtensionIndex,
fs: Arc<dyn Fs>,
http_client: Arc<HttpClientWithUrl>,
telemetry: Option<Arc<Telemetry>>,
reload_tx: UnboundedSender<Option<Arc<str>>>,
reload_complete_senders: Vec<oneshot::Sender<()>>,
installed_dir: PathBuf,
outstanding_operations: BTreeMap<Arc<str>, ExtensionOperation>,
index_path: PathBuf,
language_registry: Arc<LanguageRegistry>,
modified_extensions: HashSet<Arc<str>>,
wasm_host: Arc<WasmHost>,
wasm_extensions: Vec<(Arc<ExtensionManifest>, WasmExtension)>,
tasks: Vec<Task<()>>,
pub feature_provider: Arc<dyn ExtensionFeatures>,
pub extension_index: ExtensionIndex,
pub fs: Arc<dyn Fs>,
pub http_client: Arc<HttpClientWithUrl>,
pub telemetry: Option<Arc<Telemetry>>,
pub reload_tx: UnboundedSender<Option<Arc<str>>>,
pub reload_complete_senders: Vec<oneshot::Sender<()>>,
pub installed_dir: PathBuf,
pub outstanding_operations: BTreeMap<Arc<str>, ExtensionOperation>,
pub index_path: PathBuf,
pub language_registry: Arc<LanguageRegistry>,
pub modified_extensions: HashSet<Arc<str>>,
pub wasm_host: Arc<WasmHost>,
pub wasm_extensions: Vec<(Arc<ExtensionManifest>, WasmExtension)>,
pub tasks: Vec<Task<()>>,
}
#[derive(Clone, Copy)]
@ -123,9 +136,9 @@ pub enum Event {
impl EventEmitter<Event> for HeadlessExtensionStore {}
struct GlobalExtensionStore(Model<HeadlessExtensionStore>);
struct GlobalHeadlessExtensionStore(Model<HeadlessExtensionStore>);
impl Global for GlobalExtensionStore {}
impl Global for GlobalHeadlessExtensionStore {}
#[derive(Debug, Deserialize, Serialize, Default, PartialEq, Eq)]
pub struct ExtensionIndex {
@ -162,6 +175,8 @@ pub fn init(
telemetry: Option<Arc<Telemetry>>,
node_runtime: NodeRuntime,
language_registry: Arc<LanguageRegistry>,
feature_provider: Arc<dyn ExtensionFeatures>,
cx: &mut AppContext,
) {
ExtensionSettings::register(cx);
@ -174,23 +189,24 @@ pub fn init(
telemetry,
node_runtime,
language_registry,
feature_provider,
cx,
)
});
// TODO: make the reload extensions command proto aware
cx.set_global(GlobalExtensionStore(store));
cx.set_global(GlobalHeadlessExtensionStore(store));
}
impl HeadlessExtensionStore {
pub fn try_global(cx: &AppContext) -> Option<Model<Self>> {
cx.try_global::<GlobalExtensionStore>()
cx.try_global::<GlobalHeadlessExtensionStore>()
.map(|store| store.0.clone())
}
pub fn global(cx: &AppContext) -> Model<Self> {
cx.global::<GlobalExtensionStore>().0.clone()
cx.global::<GlobalHeadlessExtensionStore>().0.clone()
}
#[allow(clippy::too_many_arguments)]
@ -201,6 +217,7 @@ impl HeadlessExtensionStore {
telemetry: Option<Arc<Telemetry>>,
node_runtime: NodeRuntime,
language_registry: Arc<LanguageRegistry>,
feature_provider: Arc<dyn ExtensionFeatures>,
cx: &mut ModelContext<Self>,
) -> Self {
let work_dir = extensions_dir.join("work");
@ -209,7 +226,9 @@ impl HeadlessExtensionStore {
let (reload_tx, mut reload_rx) = unbounded();
let mut this = Self {
feature_provider,
extension_index: Default::default(),
language_registry: language_registry.clone(),
installed_dir,
index_path,
outstanding_operations: Default::default(),
@ -227,7 +246,6 @@ impl HeadlessExtensionStore {
fs,
http_client,
telemetry,
language_registry,
reload_tx,
tasks: Vec::new(),
};
@ -349,7 +367,7 @@ impl HeadlessExtensionStore {
this
}
fn reload(
pub fn reload(
&mut self,
modified_extension: Option<Arc<str>>,
cx: &mut ModelContext<Self>,
@ -786,6 +804,7 @@ impl HeadlessExtensionStore {
new_index: ExtensionIndex,
cx: &mut ModelContext<Self>,
) -> Task<()> {
let feature_provider = &self.feature_provider;
let old_index = &self.extension_index;
// Determine which extensions need to be loaded and unloaded, based
@ -854,18 +873,17 @@ impl HeadlessExtensionStore {
}
}
// TODO: use remove API on hypothetical extension providers for this
// let themes_to_remove = old_index
// .themes
// .iter()
// .filter_map(|(name, entry)| {
// if extensions_to_unload.contains(&entry.extension) {
// Some(name.clone().into())
// } else {
// None
// }
// })
// .collect::<Vec<_>>();
let themes_to_remove = old_index
.themes
.iter()
.filter_map(|(name, entry)| {
if extensions_to_unload.contains(&entry.extension) {
Some(name.clone().into())
} else {
None
}
})
.collect::<Vec<_>>();
let languages_to_remove = old_index
.languages
.iter()
@ -893,48 +911,45 @@ impl HeadlessExtensionStore {
self.wasm_extensions
.retain(|(extension, _)| !extensions_to_unload.contains(&extension.id));
// TODO: here
// self.theme_registry.remove_user_themes(&themes_to_remove);
self.language_registry
.remove_languages(&languages_to_remove, &grammars_to_remove);
feature_provider.remove_user_themes(&themes_to_remove, cx);
let languages_to_add = new_index
.languages
.iter()
.filter(|(_, entry)| extensions_to_load.contains(&entry.extension))
.collect::<Vec<_>>();
// TODO here
// let mut grammars_to_add = Vec::new();
// let mut themes_to_add = Vec::new();
// let mut snippets_to_add = Vec::new();
let mut grammars_to_add = Vec::new();
let mut themes_to_add = Vec::new();
let mut snippets_to_add = Vec::new();
for extension_id in &extensions_to_load {
// TODO
let Some(_extension) = new_index.extensions.get(extension_id) else {
let Some(extension) = new_index.extensions.get(extension_id) else {
continue;
};
// TODO here:
// grammars_to_add.extend(extension.manifest.grammars.keys().map(|grammar_name| {
// let mut grammar_path = self.installed_dir.clone();
// grammar_path.extend([extension_id.as_ref(), "grammars"]);
// grammar_path.push(grammar_name.as_ref());
// grammar_path.set_extension("wasm");
// (grammar_name.clone(), grammar_path)
// }));
// themes_to_add.extend(extension.manifest.themes.iter().map(|theme_path| {
// let mut path = self.installed_dir.clone();
// path.extend([Path::new(extension_id.as_ref()), theme_path.as_path()]);
// path
// }));
// snippets_to_add.extend(extension.manifest.snippets.iter().map(|snippets_path| {
// let mut path = self.installed_dir.clone();
// path.extend([Path::new(extension_id.as_ref()), snippets_path.as_path()]);
// path
// }));
grammars_to_add.extend(extension.manifest.grammars.keys().map(|grammar_name| {
let mut grammar_path = self.installed_dir.clone();
grammar_path.extend([extension_id.as_ref(), "grammars"]);
grammar_path.push(grammar_name.as_ref());
grammar_path.set_extension("wasm");
(grammar_name.clone(), grammar_path)
}));
themes_to_add.extend(extension.manifest.themes.iter().map(|theme_path| {
let mut path = self.installed_dir.clone();
path.extend([Path::new(extension_id.as_ref()), theme_path.as_path()]);
path
}));
snippets_to_add.extend(extension.manifest.snippets.iter().map(|snippets_path| {
let mut path = self.installed_dir.clone();
path.extend([Path::new(extension_id.as_ref()), snippets_path.as_path()]);
path
}));
}
// self.language_registry
// .register_wasm_grammars(grammars_to_add);
feature_provider.register_wasm_grammars(grammars_to_add, cx);
for (language_name, language) in languages_to_add {
let mut language_path = self.installed_dir.clone();
@ -972,8 +987,8 @@ impl HeadlessExtensionStore {
let fs = self.fs.clone();
let wasm_host = self.wasm_host.clone();
let root_dir = self.installed_dir.clone();
// let theme_registry = self.theme_registry.clone();
// let snippet_registry = self.snippet_registry.clone();
let feature_provider = feature_provider.clone();
let extension_entries = extensions_to_load
.iter()
.filter_map(|name| new_index.extensions.get(name).cloned())
@ -986,25 +1001,23 @@ impl HeadlessExtensionStore {
cx.spawn(|this, mut cx| async move {
cx.background_executor()
.spawn({
//TODO:
let _fs = fs.clone();
let fs = fs.clone();
async move {
// TODO: Extension provider something
// for theme_path in &themes_to_add {
// theme_registry
// .load_user_theme(theme_path, fs.clone())
// .await
// .log_err();
// }
for theme_path in &themes_to_add {
feature_provider
.load_user_theme(theme_path, fs.clone())
.await
.log_err();
}
// for snippets_path in &snippets_to_add {
// if let Some(snippets_contents) = fs.load(snippets_path).await.log_err()
// {
// snippet_registry
// .register_snippets(snippets_path, &snippets_contents)
// .log_err();
// }
// }
for snippets_path in &snippets_to_add {
if let Some(snippets_contents) = fs.load(snippets_path).await.log_err()
{
feature_provider
.register_snippets(snippets_path, &snippets_contents)
.log_err();
}
}
}
})
.await;

View file

@ -49,7 +49,7 @@ pub struct WasmExtension {
pub zed_api_version: SemanticVersion,
}
pub(crate) struct WasmState {
pub struct WasmState {
manifest: Arc<ExtensionManifest>,
pub(crate) table: ResourceTable,
ctx: wasi::WasiCtx,

View file

@ -2,6 +2,7 @@ use crate::headless_project::HeadlessAppState;
use crate::HeadlessProject;
use anyhow::{anyhow, Context, Result};
use client::{ClientSettings, ProxySettings};
use extension_headless::ExtensionFeatures;
use fs::{Fs, RealFs};
use futures::channel::mpsc;
use futures::{select, select_biased, AsyncRead, AsyncWrite, AsyncWriteExt, FutureExt, SinkExt};
@ -378,6 +379,7 @@ pub fn execute_run(
None,
node_runtime.clone(),
languages.clone(),
Arc::new(NoFeaturesProvider {}),
cx,
);
@ -399,6 +401,26 @@ pub fn execute_run(
Ok(())
}
struct NoFeaturesProvider {}
impl ExtensionFeatures for NoFeaturesProvider {
fn remove_user_themes(&self, _themes: &[gpui::SharedString], _cx: &mut AppContext) {}
fn register_wasm_grammars(&self, _grammars: Vec<(Arc<str>, PathBuf)>, _cx: &mut AppContext) {}
fn load_user_theme(
&self,
_themes_path: &Path,
_fs: Arc<dyn Fs>,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send>> {
Box::pin(async { Ok(()) })
}
fn register_snippets(&self, _file_path: &Path, _contents: &str) -> Result<()> {
Ok(())
}
}
#[derive(Clone)]
struct ServerPaths {
log_file: PathBuf,