From 1cfff44add305945826810ce9037c3215d032b7e Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 29 Oct 2024 23:52:50 -0700 Subject: [PATCH] Continue splitting the extension into headless variant partially integrate headless into the extension crate --- Cargo.lock | 1 + crates/extension/Cargo.toml | 2 +- crates/extension/src/extension_lsp_adapter.rs | 585 -------- crates/extension/src/extension_manifest.rs | 212 --- crates/extension/src/extension_settings.rs | 51 - crates/extension/src/extension_store.rs | 1184 +---------------- crates/extension/src/wasm_host.rs | 300 ----- crates/extension/src/wasm_host/wit.rs | 405 ------ .../src/wasm_host/wit/since_v0_0_1.rs | 164 --- .../src/wasm_host/wit/since_v0_0_4.rs | 166 --- .../src/wasm_host/wit/since_v0_0_6.rs | 201 --- .../src/wasm_host/wit/since_v0_1_0.rs | 509 ------- .../src/wasm_host/wit/since_v0_2_0.rs | 555 -------- .../extension_headless/src/extension_store.rs | 183 +-- crates/extension_headless/src/wasm_host.rs | 2 +- crates/remote_server/src/unix.rs | 22 + 16 files changed, 167 insertions(+), 4375 deletions(-) delete mode 100644 crates/extension/src/extension_lsp_adapter.rs delete mode 100644 crates/extension/src/extension_manifest.rs delete mode 100644 crates/extension/src/extension_settings.rs delete mode 100644 crates/extension/src/wasm_host.rs delete mode 100644 crates/extension/src/wasm_host/wit.rs delete mode 100644 crates/extension/src/wasm_host/wit/since_v0_0_1.rs delete mode 100644 crates/extension/src/wasm_host/wit/since_v0_0_4.rs delete mode 100644 crates/extension/src/wasm_host/wit/since_v0_0_6.rs delete mode 100644 crates/extension/src/wasm_host/wit/since_v0_1_0.rs delete mode 100644 crates/extension/src/wasm_host/wit/since_v0_2_0.rs diff --git a/Cargo.lock b/Cargo.lock index 4a2216e9b2..1662245cbd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4101,6 +4101,7 @@ dependencies = [ "collections", "ctor", "env_logger 0.11.5", + "extension_headless", "fs", "futures 0.3.30", "gpui", diff --git a/crates/extension/Cargo.toml b/crates/extension/Cargo.toml index 69d5f74169..4975c28fbe 100644 --- a/crates/extension/Cargo.toml +++ b/crates/extension/Cargo.toml @@ -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 diff --git a/crates/extension/src/extension_lsp_adapter.rs b/crates/extension/src/extension_lsp_adapter.rs deleted file mode 100644 index 1557ef2153..0000000000 --- a/crates/extension/src/extension_lsp_adapter.rs +++ /dev/null @@ -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, -} - -#[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, - delegate: Arc, - _: LanguageServerBinaryOptions, - _: futures::lock::MutexGuard<'a, Option>, - _: &'a mut AsyncAppContext, - ) -> Pin>>> { - 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> { - unreachable!("get_language_server_command is overridden") - } - - async fn fetch_server_binary( - &self, - _: Box, - _: PathBuf, - _: &dyn LspAdapterDelegate, - ) -> Result { - unreachable!("get_language_server_command is overridden") - } - - async fn cached_server_binary( - &self, - _: PathBuf, - _: &dyn LspAdapterDelegate, - ) -> Option { - unreachable!("get_language_server_command is overridden") - } - - fn code_action_kinds(&self) -> Option> { - 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 { - // 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, - delegate: &Arc, - ) -> Result> { - 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, - delegate: &Arc, - _: Arc, - _cx: &mut AsyncAppContext, - ) -> Result { - let delegate = delegate.clone(); - let json_options: Option = 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, - completions: &[lsp::CompletionItem], - language: &Arc, - ) -> Result>> { - let completions = completions - .iter() - .map(|completion| wit::Completion::from(completion.clone())) - .collect::>(); - - let labels = self - .extension - .call({ - let this = self.clone(); - |extension, store| { - async move { - extension - .call_labels_for_completions( - store, - &this.language_server_id, - completions, - ) - .await? - .map_err(|e| anyhow!("{}", e)) - } - .boxed() - } - }) - .await?; - - Ok(labels_from_wit(labels, language)) - } - - async fn labels_for_symbols( - self: Arc, - symbols: &[(String, lsp::SymbolKind)], - language: &Arc, - ) -> Result>> { - let symbols = symbols - .iter() - .cloned() - .map(|(name, kind)| wit::Symbol { - name, - kind: kind.into(), - }) - .collect::>(); - - 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>, - language: &Arc, -) -> Vec> { - 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, HighlightId)], - language: &Arc, -) -> Option { - 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 for Range { - fn from(range: wit::Range) -> Self { - let start = range.start as usize; - let end = range.end as usize; - start..end - } -} - -impl From for wit::Completion { - fn from(value: lsp::CompletionItem) -> Self { - Self { - label: value.label, - detail: value.detail, - kind: value.kind.map(Into::into), - insert_text_format: value.insert_text_format.map(Into::into), - } - } -} - -impl From for wit::CompletionKind { - fn from(value: lsp::CompletionItemKind) -> Self { - match value { - lsp::CompletionItemKind::TEXT => Self::Text, - lsp::CompletionItemKind::METHOD => Self::Method, - lsp::CompletionItemKind::FUNCTION => Self::Function, - lsp::CompletionItemKind::CONSTRUCTOR => Self::Constructor, - lsp::CompletionItemKind::FIELD => Self::Field, - lsp::CompletionItemKind::VARIABLE => Self::Variable, - lsp::CompletionItemKind::CLASS => Self::Class, - lsp::CompletionItemKind::INTERFACE => Self::Interface, - lsp::CompletionItemKind::MODULE => Self::Module, - lsp::CompletionItemKind::PROPERTY => Self::Property, - lsp::CompletionItemKind::UNIT => Self::Unit, - lsp::CompletionItemKind::VALUE => Self::Value, - lsp::CompletionItemKind::ENUM => Self::Enum, - lsp::CompletionItemKind::KEYWORD => Self::Keyword, - lsp::CompletionItemKind::SNIPPET => Self::Snippet, - lsp::CompletionItemKind::COLOR => Self::Color, - lsp::CompletionItemKind::FILE => Self::File, - lsp::CompletionItemKind::REFERENCE => Self::Reference, - lsp::CompletionItemKind::FOLDER => Self::Folder, - lsp::CompletionItemKind::ENUM_MEMBER => Self::EnumMember, - lsp::CompletionItemKind::CONSTANT => Self::Constant, - lsp::CompletionItemKind::STRUCT => Self::Struct, - lsp::CompletionItemKind::EVENT => Self::Event, - lsp::CompletionItemKind::OPERATOR => Self::Operator, - lsp::CompletionItemKind::TYPE_PARAMETER => Self::TypeParameter, - _ => Self::Other(extract_int(value)), - } - } -} - -impl From 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 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(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::>(); - - 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::>(); - - 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::>(); - - // 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()); -} diff --git a/crates/extension/src/extension_manifest.rs b/crates/extension/src/extension_manifest.rs deleted file mode 100644 index 3dfd7e0d41..0000000000 --- a/crates/extension/src/extension_manifest.rs +++ /dev/null @@ -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, - - #[serde(default)] - pub description: Option, - #[serde(default)] - pub repository: Option, - #[serde(default)] - pub authors: Vec, - - #[serde(default)] - pub themes: BTreeMap, PathBuf>, - #[serde(default)] - pub languages: BTreeMap, PathBuf>, - #[serde(default)] - pub grammars: BTreeMap, 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, - pub name: String, - pub version: Arc, - pub schema_version: SchemaVersion, - - #[serde(default)] - pub description: Option, - #[serde(default)] - pub repository: Option, - #[serde(default)] - pub authors: Vec, - #[serde(default)] - pub lib: LibManifestEntry, - - #[serde(default)] - pub themes: Vec, - #[serde(default)] - pub languages: Vec, - #[serde(default)] - pub grammars: BTreeMap, GrammarManifestEntry>, - #[serde(default)] - pub language_servers: BTreeMap, - #[serde(default)] - pub slash_commands: BTreeMap, SlashCommandManifestEntry>, - #[serde(default)] - pub indexed_docs_providers: BTreeMap, IndexedDocsProviderEntry>, - #[serde(default)] - pub snippets: Option, -} - -#[derive(Clone, Default, PartialEq, Eq, Debug, Deserialize, Serialize)] -pub struct LibManifestEntry { - pub kind: Option, - pub version: Option, -} - -#[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, -} - -#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] -pub struct LanguageServerManifestEntry { - /// Deprecated in favor of `languages`. - #[serde(default)] - language: Option, - /// The list of languages this language server should work with. - #[serde(default)] - languages: Vec, - #[serde(default)] - pub language_ids: HashMap, - #[serde(default)] - pub code_action_kinds: Option>, -} - -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 + '_ { - 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, extension_dir: &Path) -> Result { - 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::(&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::>(); - themes.sort(); - themes.dedup(); - themes - }, - languages: { - let mut languages = manifest_json.languages.into_values().collect::>(); - 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, - } -} diff --git a/crates/extension/src/extension_settings.rs b/crates/extension/src/extension_settings.rs deleted file mode 100644 index cfae4482c9..0000000000 --- a/crates/extension/src/extension_settings.rs +++ /dev/null @@ -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, bool>, - #[serde(default)] - pub auto_update_extensions: HashMap, 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, _cx: &mut AppContext) -> Result { - SettingsSources::::json_merge_with( - [sources.default] - .into_iter() - .chain(sources.user) - .chain(sources.server), - ) - } -} diff --git a/crates/extension/src/extension_store.rs b/crates/extension/src/extension_store.rs index 0a9299a8be..722ab5ff02 100644 --- a/crates/extension/src/extension_store.rs +++ b/crates/extension/src/extension_store.rs @@ -1,8 +1,5 @@ pub mod extension_builder; mod extension_indexed_docs_provider; -mod extension_lsp_adapter; -mod extension_manifest; -mod extension_settings; mod extension_slash_command; mod wasm_host; @@ -20,6 +17,7 @@ use async_tar::Archive; use client::{telemetry::Telemetry, Client, ExtensionMetadata, GetExtensionsResponse}; use collections::{btree_map, BTreeMap, HashSet}; use extension_builder::{CompileExtensionOptions, ExtensionBuilder}; +use extension_headless::HeadlessExtensionStore; use fs::{Fs, RemoveOptions}; use futures::{ channel::{ @@ -67,61 +65,13 @@ pub use extension_manifest::{ }; pub use extension_settings::ExtensionSettings; -const RELOAD_DEBOUNCE_DURATION: Duration = Duration::from_millis(200); -const FS_WATCH_LATENCY: Duration = Duration::from_millis(100); - -/// The current extension [`SchemaVersion`] supported by Zed. -const CURRENT_SCHEMA_VERSION: SchemaVersion = SchemaVersion(1); - -/// Returns the [`SchemaVersion`] range that is compatible with this version of Zed. -pub fn schema_version_range() -> RangeInclusive { - SchemaVersion::ZERO..=CURRENT_SCHEMA_VERSION -} - -/// Returns whether the given extension version is compatible with this version of Zed. -pub fn is_version_compatible( - release_channel: ReleaseChannel, - extension_version: &ExtensionMetadata, -) -> bool { - let schema_version = extension_version.manifest.schema_version.unwrap_or(0); - if CURRENT_SCHEMA_VERSION.0 < schema_version { - return false; - } - - if let Some(wasm_api_version) = extension_version - .manifest - .wasm_api_version - .as_ref() - .and_then(|wasm_api_version| SemanticVersion::from_str(wasm_api_version).ok()) - { - if !is_supported_wasm_api_version(release_channel, wasm_api_version) { - return false; - } - } - - true -} - pub struct ExtensionStore { + headless_store: Model, builder: Arc, - extension_index: ExtensionIndex, - fs: Arc, - http_client: Arc, - telemetry: Option>, - reload_tx: UnboundedSender>>, - reload_complete_senders: Vec>, - installed_dir: PathBuf, - outstanding_operations: BTreeMap, ExtensionOperation>, - index_path: PathBuf, - language_registry: Arc, theme_registry: Arc, slash_command_registry: Arc, indexed_docs_registry: Arc, snippet_registry: Arc, - modified_extensions: HashSet>, - wasm_host: Arc, - wasm_extensions: Vec<(Arc, WasmExtension)>, - tasks: Vec>, } #[derive(Clone, Copy)] @@ -145,35 +95,38 @@ struct GlobalExtensionStore(Model); impl Global for GlobalExtensionStore {} -#[derive(Debug, Deserialize, Serialize, Default, PartialEq, Eq)] -pub struct ExtensionIndex { - pub extensions: BTreeMap, ExtensionIndexEntry>, - pub themes: BTreeMap, ExtensionIndexThemeEntry>, - pub languages: BTreeMap, -} - -#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] -pub struct ExtensionIndexEntry { - pub manifest: Arc, - pub dev: bool, -} - -#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Deserialize, Serialize)] -pub struct ExtensionIndexThemeEntry { - extension: Arc, - path: PathBuf, -} - -#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Deserialize, Serialize)] -pub struct ExtensionIndexLanguageEntry { - extension: Arc, - path: PathBuf, - matcher: LanguageMatcher, - grammar: Option>, -} - actions!(zed, [ReloadExtensions]); +pub struct ExtensionFeatures { + theme_registry: Arc, + slash_command_registry: Arc, + indexed_docs_registry: Arc, + snippet_registry: Arc, + language_registry: Arc, +} + +impl extension_headless::ExtensionFeatures for ExtensionFeatures { + fn remove_user_themes(&self, themes: &[ui::SharedString], _cx: &mut AppContext) { + self.theme_registry.remove_user_themes(themes_to_remove) + } + + fn register_wasm_grammars(&self, grammars: Vec<(Arc, PathBuf)>, cx: &mut AppContext) { + self.language_registry.register_wasm_grammars(grammars) + } + + fn load_user_theme( + &self, + themes_path: &Path, + fs: Arc, + ) -> std::pin::Pin> + Send>> { + Box::pin(self.theme_registry.load_user_theme(themes_path, fs)) + } + + fn register_snippets(&self, file_path: &Path, contents: &str) -> Result<()> { + self.snippet_registry.register_snippets(file_path, contents) + } +} + pub fn init( fs: Arc, client: Arc, @@ -243,575 +196,24 @@ impl ExtensionStore { let (reload_tx, mut reload_rx) = unbounded(); let mut this = Self { - extension_index: Default::default(), - installed_dir, - index_path, + headless_store: cx.new_model(|cx| { + HeadlessExtensionStore::new( + extensions_dir, + fs, + http_client, + telemetry, + node_runtime, + language_registry, + feature_provider, + cx, + ) + }), builder: Arc::new(ExtensionBuilder::new(builder_client, build_dir)), - outstanding_operations: Default::default(), - modified_extensions: Default::default(), - reload_complete_senders: Vec::new(), - wasm_host: WasmHost::new( - fs.clone(), - http_client.clone(), - node_runtime, - language_registry.clone(), - work_dir, - cx, - ), - wasm_extensions: Vec::new(), - fs, - http_client, - telemetry, - language_registry, theme_registry, slash_command_registry, indexed_docs_registry, snippet_registry, - reload_tx, - tasks: Vec::new(), }; - - // The extensions store maintains an index file, which contains a complete - // list of the installed extensions and the resources that they provide. - // This index is loaded synchronously on startup. - let (index_content, index_metadata, extensions_metadata) = - cx.background_executor().block(async { - futures::join!( - this.fs.load(&this.index_path), - this.fs.metadata(&this.index_path), - this.fs.metadata(&this.installed_dir), - ) - }); - - // Normally, there is no need to rebuild the index. But if the index file - // is invalid or is out-of-date according to the filesystem mtimes, then - // it must be asynchronously rebuilt. - let mut extension_index = ExtensionIndex::default(); - let mut extension_index_needs_rebuild = true; - if let Ok(index_content) = index_content { - if let Some(index) = serde_json::from_str(&index_content).log_err() { - extension_index = index; - if let (Ok(Some(index_metadata)), Ok(Some(extensions_metadata))) = - (index_metadata, extensions_metadata) - { - if index_metadata.mtime > extensions_metadata.mtime { - extension_index_needs_rebuild = false; - } - } - } - } - - // Immediately load all of the extensions in the initial manifest. If the - // index needs to be rebuild, then enqueue - let load_initial_extensions = this.extensions_updated(extension_index, cx); - let mut reload_future = None; - if extension_index_needs_rebuild { - reload_future = Some(this.reload(None, cx)); - } - - cx.spawn(|this, mut cx| async move { - if let Some(future) = reload_future { - future.await; - } - this.update(&mut cx, |this, cx| this.auto_install_extensions(cx)) - .ok(); - this.update(&mut cx, |this, cx| this.check_for_updates(cx)) - .ok(); - }) - .detach(); - - // Perform all extension loading in a single task to ensure that we - // never attempt to simultaneously load/unload extensions from multiple - // parallel tasks. - this.tasks.push(cx.spawn(|this, mut cx| { - async move { - load_initial_extensions.await; - - let mut debounce_timer = cx - .background_executor() - .spawn(futures::future::pending()) - .fuse(); - loop { - select_biased! { - _ = debounce_timer => { - let index = this - .update(&mut cx, |this, cx| this.rebuild_extension_index(cx))? - .await; - this.update(&mut cx, |this, cx| this.extensions_updated(index, cx))? - .await; - } - extension_id = reload_rx.next() => { - let Some(extension_id) = extension_id else { break; }; - this.update(&mut cx, |this, _| { - this.modified_extensions.extend(extension_id); - })?; - debounce_timer = cx - .background_executor() - .timer(RELOAD_DEBOUNCE_DURATION) - .fuse(); - } - } - } - - anyhow::Ok(()) - } - .map(drop) - })); - - // Watch the installed extensions directory for changes. Whenever changes are - // detected, rebuild the extension index, and load/unload any extensions that - // have been added, removed, or modified. - this.tasks.push(cx.background_executor().spawn({ - let fs = this.fs.clone(); - let reload_tx = this.reload_tx.clone(); - let installed_dir = this.installed_dir.clone(); - async move { - let (mut paths, _) = fs.watch(&installed_dir, FS_WATCH_LATENCY).await; - while let Some(events) = paths.next().await { - for event in events { - let Ok(event_path) = event.path.strip_prefix(&installed_dir) else { - continue; - }; - - if let Some(path::Component::Normal(extension_dir_name)) = - event_path.components().next() - { - if let Some(extension_id) = extension_dir_name.to_str() { - reload_tx.unbounded_send(Some(extension_id.into())).ok(); - } - } - } - } - } - })); - - this - } - - fn reload( - &mut self, - modified_extension: Option>, - cx: &mut ModelContext, - ) -> impl Future { - let (tx, rx) = oneshot::channel(); - self.reload_complete_senders.push(tx); - self.reload_tx - .unbounded_send(modified_extension) - .expect("reload task exited"); - cx.emit(Event::StartedReloading); - - async move { - rx.await.ok(); - } - } - - fn extensions_dir(&self) -> PathBuf { - self.installed_dir.clone() - } - - pub fn outstanding_operations(&self) -> &BTreeMap, ExtensionOperation> { - &self.outstanding_operations - } - - pub fn installed_extensions(&self) -> &BTreeMap, ExtensionIndexEntry> { - &self.extension_index.extensions - } - - pub fn dev_extensions(&self) -> impl Iterator> { - self.extension_index - .extensions - .values() - .filter_map(|extension| extension.dev.then_some(&extension.manifest)) - } - - /// Returns the names of themes provided by extensions. - pub fn extension_themes<'a>( - &'a self, - extension_id: &'a str, - ) -> impl Iterator> { - self.extension_index - .themes - .iter() - .filter_map(|(name, theme)| theme.extension.as_ref().eq(extension_id).then_some(name)) - } - - pub fn fetch_extensions( - &self, - search: Option<&str>, - cx: &mut ModelContext, - ) -> Task>> { - let version = CURRENT_SCHEMA_VERSION.to_string(); - let mut query = vec![("max_schema_version", version.as_str())]; - if let Some(search) = search { - query.push(("filter", search)); - } - - self.fetch_extensions_from_api("/extensions", &query, cx) - } - - pub fn fetch_extensions_with_update_available( - &mut self, - cx: &mut ModelContext, - ) -> Task>> { - let schema_versions = schema_version_range(); - let wasm_api_versions = wasm_api_version_range(ReleaseChannel::global(cx)); - let extension_settings = ExtensionSettings::get_global(cx); - let extension_ids = self - .extension_index - .extensions - .iter() - .filter(|(id, entry)| !entry.dev && extension_settings.should_auto_update(id)) - .map(|(id, _)| id.as_ref()) - .collect::>() - .join(","); - let task = self.fetch_extensions_from_api( - "/extensions/updates", - &[ - ("min_schema_version", &schema_versions.start().to_string()), - ("max_schema_version", &schema_versions.end().to_string()), - ( - "min_wasm_api_version", - &wasm_api_versions.start().to_string(), - ), - ("max_wasm_api_version", &wasm_api_versions.end().to_string()), - ("ids", &extension_ids), - ], - cx, - ); - cx.spawn(move |this, mut cx| async move { - let extensions = task.await?; - this.update(&mut cx, |this, _cx| { - extensions - .into_iter() - .filter(|extension| { - this.extension_index.extensions.get(&extension.id).map_or( - true, - |installed_extension| { - installed_extension.manifest.version != extension.manifest.version - }, - ) - }) - .collect() - }) - }) - } - - pub fn fetch_extension_versions( - &self, - extension_id: &str, - cx: &mut ModelContext, - ) -> Task>> { - self.fetch_extensions_from_api(&format!("/extensions/{extension_id}"), &[], cx) - } - - /// Installs any extensions that should be included with Zed by default. - /// - /// This can be used to make certain functionality provided by extensions - /// available out-of-the-box. - pub fn auto_install_extensions(&mut self, cx: &mut ModelContext) { - let extension_settings = ExtensionSettings::get_global(cx); - - let extensions_to_install = extension_settings - .auto_install_extensions - .keys() - .filter(|extension_id| extension_settings.should_auto_install(extension_id)) - .filter(|extension_id| { - let is_already_installed = self - .extension_index - .extensions - .contains_key(extension_id.as_ref()); - !is_already_installed - }) - .cloned() - .collect::>(); - - cx.spawn(move |this, mut cx| async move { - for extension_id in extensions_to_install { - this.update(&mut cx, |this, cx| { - this.install_latest_extension(extension_id.clone(), cx); - }) - .ok(); - } - }) - .detach(); - } - - pub fn check_for_updates(&mut self, cx: &mut ModelContext) { - let task = self.fetch_extensions_with_update_available(cx); - cx.spawn(move |this, mut cx| async move { - Self::upgrade_extensions(this, task.await?, &mut cx).await - }) - .detach(); - } - - async fn upgrade_extensions( - this: WeakModel, - extensions: Vec, - cx: &mut AsyncAppContext, - ) -> Result<()> { - for extension in extensions { - let task = this.update(cx, |this, cx| { - if let Some(installed_extension) = - this.extension_index.extensions.get(&extension.id) - { - let installed_version = - SemanticVersion::from_str(&installed_extension.manifest.version).ok()?; - let latest_version = - SemanticVersion::from_str(&extension.manifest.version).ok()?; - - if installed_version >= latest_version { - return None; - } - } - - Some(this.upgrade_extension(extension.id, extension.manifest.version, cx)) - })?; - - if let Some(task) = task { - task.await.log_err(); - } - } - anyhow::Ok(()) - } - - fn fetch_extensions_from_api( - &self, - path: &str, - query: &[(&str, &str)], - cx: &mut ModelContext<'_, ExtensionStore>, - ) -> Task>> { - let url = self.http_client.build_zed_api_url(path, query); - let http_client = self.http_client.clone(); - cx.spawn(move |_, _| async move { - let mut response = http_client - .get(url?.as_ref(), AsyncBody::empty(), true) - .await?; - - let mut body = Vec::new(); - response - .body_mut() - .read_to_end(&mut body) - .await - .context("error reading extensions")?; - - if response.status().is_client_error() { - let text = String::from_utf8_lossy(body.as_slice()); - bail!( - "status error {}, response: {text:?}", - response.status().as_u16() - ); - } - - let response: GetExtensionsResponse = serde_json::from_slice(&body)?; - Ok(response.data) - }) - } - - pub fn install_extension( - &mut self, - extension_id: Arc, - version: Arc, - cx: &mut ModelContext, - ) { - self.install_or_upgrade_extension(extension_id, version, ExtensionOperation::Install, cx) - .detach_and_log_err(cx); - } - - fn install_or_upgrade_extension_at_endpoint( - &mut self, - extension_id: Arc, - url: Url, - operation: ExtensionOperation, - cx: &mut ModelContext, - ) -> Task> { - let extension_dir = self.installed_dir.join(extension_id.as_ref()); - let http_client = self.http_client.clone(); - let fs = self.fs.clone(); - - match self.outstanding_operations.entry(extension_id.clone()) { - btree_map::Entry::Occupied(_) => return Task::ready(Ok(())), - btree_map::Entry::Vacant(e) => e.insert(operation), - }; - cx.notify(); - - cx.spawn(move |this, mut cx| async move { - let _finish = util::defer({ - let this = this.clone(); - let mut cx = cx.clone(); - let extension_id = extension_id.clone(); - move || { - this.update(&mut cx, |this, cx| { - this.outstanding_operations.remove(extension_id.as_ref()); - cx.notify(); - }) - .ok(); - } - }); - - let mut response = http_client - .get(url.as_ref(), Default::default(), true) - .await - .map_err(|err| anyhow!("error downloading extension: {}", err))?; - - fs.remove_dir( - &extension_dir, - RemoveOptions { - recursive: true, - ignore_if_not_exists: true, - }, - ) - .await?; - - let content_length = response - .headers() - .get(http_client::http::header::CONTENT_LENGTH) - .and_then(|value| value.to_str().ok()?.parse::().ok()); - - let mut body = BufReader::new(response.body_mut()); - let mut tar_gz_bytes = Vec::new(); - body.read_to_end(&mut tar_gz_bytes).await?; - - if let Some(content_length) = content_length { - let actual_len = tar_gz_bytes.len(); - if content_length != actual_len { - bail!("downloaded extension size {actual_len} does not match content length {content_length}"); - } - } - let decompressed_bytes = GzipDecoder::new(BufReader::new(tar_gz_bytes.as_slice())); - let archive = Archive::new(decompressed_bytes); - archive.unpack(extension_dir).await?; - this.update(&mut cx, |this, cx| { - this.reload(Some(extension_id.clone()), cx) - })? - .await; - - if let ExtensionOperation::Install = operation { - this.update(&mut cx, |_, cx| { - cx.emit(Event::ExtensionInstalled(extension_id)); - }) - .ok(); - } - - anyhow::Ok(()) - }) - } - - pub fn install_latest_extension( - &mut self, - extension_id: Arc, - cx: &mut ModelContext, - ) { - log::info!("installing extension {extension_id} latest version"); - - let schema_versions = schema_version_range(); - let wasm_api_versions = wasm_api_version_range(ReleaseChannel::global(cx)); - - let Some(url) = self - .http_client - .build_zed_api_url( - &format!("/extensions/{extension_id}/download"), - &[ - ("min_schema_version", &schema_versions.start().to_string()), - ("max_schema_version", &schema_versions.end().to_string()), - ( - "min_wasm_api_version", - &wasm_api_versions.start().to_string(), - ), - ("max_wasm_api_version", &wasm_api_versions.end().to_string()), - ], - ) - .log_err() - else { - return; - }; - - self.install_or_upgrade_extension_at_endpoint( - extension_id, - url, - ExtensionOperation::Install, - cx, - ) - .detach_and_log_err(cx); - } - - pub fn upgrade_extension( - &mut self, - extension_id: Arc, - version: Arc, - cx: &mut ModelContext, - ) -> Task> { - self.install_or_upgrade_extension(extension_id, version, ExtensionOperation::Upgrade, cx) - } - - fn install_or_upgrade_extension( - &mut self, - extension_id: Arc, - version: Arc, - operation: ExtensionOperation, - cx: &mut ModelContext, - ) -> Task> { - log::info!("installing extension {extension_id} {version}"); - let Some(url) = self - .http_client - .build_zed_api_url( - &format!("/extensions/{extension_id}/{version}/download"), - &[], - ) - .log_err() - else { - return Task::ready(Ok(())); - }; - - self.install_or_upgrade_extension_at_endpoint(extension_id, url, operation, cx) - } - - pub fn uninstall_extension(&mut self, extension_id: Arc, cx: &mut ModelContext) { - let extension_dir = self.installed_dir.join(extension_id.as_ref()); - let work_dir = self.wasm_host.work_dir.join(extension_id.as_ref()); - let fs = self.fs.clone(); - - match self.outstanding_operations.entry(extension_id.clone()) { - btree_map::Entry::Occupied(_) => return, - btree_map::Entry::Vacant(e) => e.insert(ExtensionOperation::Remove), - }; - - cx.spawn(move |this, mut cx| async move { - let _finish = util::defer({ - let this = this.clone(); - let mut cx = cx.clone(); - let extension_id = extension_id.clone(); - move || { - this.update(&mut cx, |this, cx| { - this.outstanding_operations.remove(extension_id.as_ref()); - cx.notify(); - }) - .ok(); - } - }); - - fs.remove_dir( - &work_dir, - RemoveOptions { - recursive: true, - ignore_if_not_exists: true, - }, - ) - .await?; - - fs.remove_dir( - &extension_dir, - RemoveOptions { - recursive: true, - ignore_if_not_exists: true, - }, - ) - .await?; - - this.update(&mut cx, |this, cx| this.reload(None, cx))? - .await; - anyhow::Ok(()) - }) - .detach_and_log_err(cx) } pub fn install_dev_extension( @@ -931,502 +333,4 @@ impl ExtensionStore { }) .detach_and_log_err(cx) } - - /// Updates the set of installed extensions. - /// - /// First, this unloads any themes, languages, or grammars that are - /// no longer in the manifest, or whose files have changed on disk. - /// Then it loads any themes, languages, or grammars that are newly - /// added to the manifest, or whose files have changed on disk. - fn extensions_updated( - &mut self, - new_index: ExtensionIndex, - cx: &mut ModelContext, - ) -> Task<()> { - let old_index = &self.extension_index; - - // Determine which extensions need to be loaded and unloaded, based - // on the changes to the manifest and the extensions that we know have been - // modified. - let mut extensions_to_unload = Vec::default(); - let mut extensions_to_load = Vec::default(); - { - let mut old_keys = old_index.extensions.iter().peekable(); - let mut new_keys = new_index.extensions.iter().peekable(); - loop { - match (old_keys.peek(), new_keys.peek()) { - (None, None) => break, - (None, Some(_)) => { - extensions_to_load.push(new_keys.next().unwrap().0.clone()); - } - (Some(_), None) => { - extensions_to_unload.push(old_keys.next().unwrap().0.clone()); - } - (Some((old_key, _)), Some((new_key, _))) => match old_key.cmp(new_key) { - Ordering::Equal => { - let (old_key, old_value) = old_keys.next().unwrap(); - let (new_key, new_value) = new_keys.next().unwrap(); - if old_value != new_value || self.modified_extensions.contains(old_key) - { - extensions_to_unload.push(old_key.clone()); - extensions_to_load.push(new_key.clone()); - } - } - Ordering::Less => { - extensions_to_unload.push(old_keys.next().unwrap().0.clone()); - } - Ordering::Greater => { - extensions_to_load.push(new_keys.next().unwrap().0.clone()); - } - }, - } - } - self.modified_extensions.clear(); - } - - if extensions_to_load.is_empty() && extensions_to_unload.is_empty() { - return Task::ready(()); - } - - let reload_count = extensions_to_unload - .iter() - .filter(|id| extensions_to_load.contains(id)) - .count(); - - log::info!( - "extensions updated. loading {}, reloading {}, unloading {}", - extensions_to_load.len() - reload_count, - reload_count, - extensions_to_unload.len() - reload_count - ); - - if let Some(telemetry) = &self.telemetry { - for extension_id in &extensions_to_load { - if let Some(extension) = new_index.extensions.get(extension_id) { - telemetry.report_extension_event( - extension_id.clone(), - extension.manifest.version.clone(), - ); - } - } - } - - 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::>(); - let languages_to_remove = old_index - .languages - .iter() - .filter_map(|(name, entry)| { - if extensions_to_unload.contains(&entry.extension) { - Some(name.clone()) - } else { - None - } - }) - .collect::>(); - let mut grammars_to_remove = Vec::new(); - for extension_id in &extensions_to_unload { - let Some(extension) = old_index.extensions.get(extension_id) else { - continue; - }; - grammars_to_remove.extend(extension.manifest.grammars.keys().cloned()); - for (language_server_name, config) in extension.manifest.language_servers.iter() { - for language in config.languages() { - self.language_registry - .remove_lsp_adapter(&language, language_server_name); - } - } - } - - self.wasm_extensions - .retain(|(extension, _)| !extensions_to_unload.contains(&extension.id)); - self.theme_registry.remove_user_themes(&themes_to_remove); - self.language_registry - .remove_languages(&languages_to_remove, &grammars_to_remove); - - let languages_to_add = new_index - .languages - .iter() - .filter(|(_, entry)| extensions_to_load.contains(&entry.extension)) - .collect::>(); - 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 { - let Some(extension) = new_index.extensions.get(extension_id) else { - continue; - }; - - 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); - - for (language_name, language) in languages_to_add { - let mut language_path = self.installed_dir.clone(); - language_path.extend([ - Path::new(language.extension.as_ref()), - language.path.as_path(), - ]); - self.language_registry.register_language( - language_name.clone(), - language.grammar.clone(), - language.matcher.clone(), - move || { - let config = std::fs::read_to_string(language_path.join("config.toml"))?; - let config: LanguageConfig = ::toml::from_str(&config)?; - let queries = load_plugin_queries(&language_path); - let context_provider = - std::fs::read_to_string(language_path.join("tasks.json")) - .ok() - .and_then(|contents| { - let definitions = - serde_json_lenient::from_str(&contents).log_err()?; - Some(Arc::new(ContextProviderWithTasks::new(definitions)) as Arc<_>) - }); - - Ok(LoadedLanguage { - config, - queries, - context_provider, - toolchain_provider: None, - }) - }, - ); - } - - 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 extension_entries = extensions_to_load - .iter() - .filter_map(|name| new_index.extensions.get(name).cloned()) - .collect::>(); - - self.extension_index = new_index; - cx.notify(); - cx.emit(Event::ExtensionsUpdated); - - cx.spawn(|this, mut cx| async move { - cx.background_executor() - .spawn({ - let fs = fs.clone(); - async move { - for theme_path in &themes_to_add { - theme_registry - .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(); - } - } - } - }) - .await; - - let mut wasm_extensions = Vec::new(); - for extension in extension_entries { - if extension.manifest.lib.kind.is_none() { - continue; - }; - - let wasm_extension = maybe!(async { - let mut path = root_dir.clone(); - path.extend([extension.manifest.clone().id.as_ref(), "extension.wasm"]); - let mut wasm_file = fs - .open_sync(&path) - .await - .context("failed to open wasm file")?; - - let mut wasm_bytes = Vec::new(); - wasm_file - .read_to_end(&mut wasm_bytes) - .context("failed to read wasm")?; - - wasm_host - .load_extension( - wasm_bytes, - extension.manifest.clone().clone(), - cx.background_executor().clone(), - ) - .await - .with_context(|| { - format!("failed to load wasm extension {}", extension.manifest.id) - }) - }) - .await; - - if let Some(wasm_extension) = wasm_extension.log_err() { - wasm_extensions.push((extension.manifest.clone(), wasm_extension)); - } else { - this.update(&mut cx, |_, cx| { - cx.emit(Event::ExtensionFailedToLoad(extension.manifest.id.clone())) - }) - .ok(); - } - } - - this.update(&mut cx, |this, cx| { - this.reload_complete_senders.clear(); - - for (manifest, wasm_extension) in &wasm_extensions { - for (language_server_id, language_server_config) in &manifest.language_servers { - for language in language_server_config.languages() { - this.language_registry.register_lsp_adapter( - language.clone(), - Arc::new(ExtensionLspAdapter { - extension: wasm_extension.clone(), - host: this.wasm_host.clone(), - language_server_id: language_server_id.clone(), - config: wit::LanguageServerConfig { - name: language_server_id.0.to_string(), - language_name: language.to_string(), - }, - }), - ); - } - } - - for (slash_command_name, slash_command) in &manifest.slash_commands { - this.slash_command_registry.register_command( - ExtensionSlashCommand { - command: crate::wit::SlashCommand { - name: slash_command_name.to_string(), - description: slash_command.description.to_string(), - // We don't currently expose this as a configurable option, as it currently drives - // the `menu_text` on the `SlashCommand` trait, which is not used for slash commands - // defined in extensions, as they are not able to be added to the menu. - tooltip_text: String::new(), - requires_argument: slash_command.requires_argument, - }, - extension: wasm_extension.clone(), - host: this.wasm_host.clone(), - }, - false, - ); - } - - for (provider_id, _provider) in &manifest.indexed_docs_providers { - this.indexed_docs_registry.register_provider(Box::new( - ExtensionIndexedDocsProvider { - extension: wasm_extension.clone(), - host: this.wasm_host.clone(), - id: ProviderId(provider_id.clone()), - }, - )); - } - } - - this.wasm_extensions.extend(wasm_extensions); - ThemeSettings::reload_current_theme(cx) - }) - .ok(); - }) - } - - fn rebuild_extension_index(&self, cx: &mut ModelContext) -> Task { - let fs = self.fs.clone(); - let work_dir = self.wasm_host.work_dir.clone(); - let extensions_dir = self.installed_dir.clone(); - let index_path = self.index_path.clone(); - cx.background_executor().spawn(async move { - let start_time = Instant::now(); - let mut index = ExtensionIndex::default(); - - fs.create_dir(&work_dir).await.log_err(); - fs.create_dir(&extensions_dir).await.log_err(); - - let extension_paths = fs.read_dir(&extensions_dir).await; - if let Ok(mut extension_paths) = extension_paths { - while let Some(extension_dir) = extension_paths.next().await { - let Ok(extension_dir) = extension_dir else { - continue; - }; - - if extension_dir - .file_name() - .map_or(false, |file_name| file_name == ".DS_Store") - { - continue; - } - - Self::add_extension_to_index(fs.clone(), extension_dir, &mut index) - .await - .log_err(); - } - } - - if let Ok(index_json) = serde_json::to_string_pretty(&index) { - fs.save(&index_path, &index_json.as_str().into(), Default::default()) - .await - .context("failed to save extension index") - .log_err(); - } - - log::info!("rebuilt extension index in {:?}", start_time.elapsed()); - index - }) - } - - async fn add_extension_to_index( - fs: Arc, - extension_dir: PathBuf, - index: &mut ExtensionIndex, - ) -> Result<()> { - let mut extension_manifest = ExtensionManifest::load(fs.clone(), &extension_dir).await?; - let extension_id = extension_manifest.id.clone(); - - // TODO: distinguish dev extensions more explicitly, by the absence - // of a checksum file that we'll create when downloading normal extensions. - let is_dev = fs - .metadata(&extension_dir) - .await? - .ok_or_else(|| anyhow!("directory does not exist"))? - .is_symlink; - - if let Ok(mut language_paths) = fs.read_dir(&extension_dir.join("languages")).await { - while let Some(language_path) = language_paths.next().await { - let language_path = language_path?; - let Ok(relative_path) = language_path.strip_prefix(&extension_dir) else { - continue; - }; - let Ok(Some(fs_metadata)) = fs.metadata(&language_path).await else { - continue; - }; - if !fs_metadata.is_dir { - continue; - } - let config = fs.load(&language_path.join("config.toml")).await?; - let config = ::toml::from_str::(&config)?; - - let relative_path = relative_path.to_path_buf(); - if !extension_manifest.languages.contains(&relative_path) { - extension_manifest.languages.push(relative_path.clone()); - } - - index.languages.insert( - config.name.clone(), - ExtensionIndexLanguageEntry { - extension: extension_id.clone(), - path: relative_path, - matcher: config.matcher, - grammar: config.grammar, - }, - ); - } - } - - if let Ok(mut theme_paths) = fs.read_dir(&extension_dir.join("themes")).await { - while let Some(theme_path) = theme_paths.next().await { - let theme_path = theme_path?; - let Ok(relative_path) = theme_path.strip_prefix(&extension_dir) else { - continue; - }; - - let Some(theme_family) = ThemeRegistry::read_user_theme(&theme_path, fs.clone()) - .await - .log_err() - else { - continue; - }; - - let relative_path = relative_path.to_path_buf(); - if !extension_manifest.themes.contains(&relative_path) { - extension_manifest.themes.push(relative_path.clone()); - } - - for theme in theme_family.themes { - index.themes.insert( - theme.name.into(), - ExtensionIndexThemeEntry { - extension: extension_id.clone(), - path: relative_path.clone(), - }, - ); - } - } - } - - let extension_wasm_path = extension_dir.join("extension.wasm"); - if fs.is_file(&extension_wasm_path).await { - extension_manifest - .lib - .kind - .get_or_insert(ExtensionLibraryKind::Rust); - } - - index.extensions.insert( - extension_id.clone(), - ExtensionIndexEntry { - dev: is_dev, - manifest: Arc::new(extension_manifest), - }, - ); - - Ok(()) - } -} - -fn load_plugin_queries(root_path: &Path) -> LanguageQueries { - let mut result = LanguageQueries::default(); - if let Some(entries) = std::fs::read_dir(root_path).log_err() { - for entry in entries { - let Some(entry) = entry.log_err() else { - continue; - }; - let path = entry.path(); - if let Some(remainder) = path.strip_prefix(root_path).ok().and_then(|p| p.to_str()) { - if !remainder.ends_with(".scm") { - continue; - } - for (name, query) in QUERY_FILENAME_PREFIXES { - if remainder.starts_with(name) { - if let Some(contents) = std::fs::read_to_string(&path).log_err() { - match query(&mut result) { - None => *query(&mut result) = Some(contents.into()), - Some(r) => r.to_mut().push_str(contents.as_ref()), - } - } - break; - } - } - } - } - } - result } diff --git a/crates/extension/src/wasm_host.rs b/crates/extension/src/wasm_host.rs deleted file mode 100644 index b3fd13a5ba..0000000000 --- a/crates/extension/src/wasm_host.rs +++ /dev/null @@ -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, - node_runtime: NodeRuntime, - pub(crate) language_registry: Arc, - fs: Arc, - pub(crate) work_dir: PathBuf, - _main_thread_message_task: Task<()>, - main_thread_message_tx: mpsc::UnboundedSender, -} - -#[derive(Clone)] -pub struct WasmExtension { - tx: UnboundedSender, - pub(crate) manifest: Arc, - #[allow(unused)] - pub zed_api_version: SemanticVersion, -} - -pub(crate) struct WasmState { - manifest: Arc, - pub(crate) table: ResourceTable, - ctx: wasi::WasiCtx, - pub(crate) host: Arc, -} - -type MainThreadCall = - Box FnOnce(&'a mut AsyncAppContext) -> LocalBoxFuture<'a, ()>>; - -type ExtensionCall = Box< - dyn Send + for<'a> FnOnce(&'a mut Extension, &'a mut Store) -> BoxFuture<'a, ()>, ->; - -fn wasm_engine() -> wasmtime::Engine { - static WASM_ENGINE: OnceLock = 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, - http_client: Arc, - node_runtime: NodeRuntime, - language_registry: Arc, - work_dir: PathBuf, - cx: &mut AppContext, - ) -> Arc { - let (tx, mut rx) = mpsc::unbounded::(); - 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, - wasm_bytes: Vec, - manifest: Arc, - executor: BackgroundExecutor, - ) -> Task> { - 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::(); - 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) -> Result { - 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, 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, path: &Path) -> Result { - 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 { - 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 { - 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(&self, f: Fn) -> T - where - T: 'static + Send, - Fn: 'static - + Send - + for<'a> FnOnce(&'a mut Extension, &'a mut Store) -> 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(&self, f: Fn) -> impl 'static + Future - 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 - } -} diff --git a/crates/extension/src/wasm_host/wit.rs b/crates/extension/src/wasm_host/wit.rs deleted file mode 100644 index 6de8c89c77..0000000000 --- a/crates/extension/src/wasm_host/wit.rs +++ /dev/null @@ -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, fn(&mut WasmState) -> &mut WasmState) -> Result<()>, -) -> Linker { - 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 { - // 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, - release_channel: ReleaseChannel, - version: SemanticVersion, - component: &Component, - ) -> Result { - // 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) -> 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, - language_server_id: &LanguageServerName, - config: &LanguageServerConfig, - resource: Resource>, - ) -> Result> { - 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, - language_server_id: &LanguageServerName, - config: &LanguageServerConfig, - resource: Resource>, - ) -> Result, 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, - language_server_id: &LanguageServerName, - resource: Resource>, - ) -> Result, 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, - language_server_id: &LanguageServerName, - completions: Vec, - ) -> Result>, 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, - language_server_id: &LanguageServerName, - symbols: Vec, - ) -> Result>, 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, - command: &SlashCommand, - arguments: &[String], - ) -> Result, 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, - command: &SlashCommand, - arguments: &[String], - resource: Option>>, - ) -> Result> { - 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, - provider: &str, - ) -> Result, 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, - provider: &str, - package_name: &str, - database: Resource>, - ) -> Result> { - 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 { - fn to_wasmtime_result(self) -> wasmtime::Result>; -} - -impl ToWasmtimeResult for Result { - fn to_wasmtime_result(self) -> wasmtime::Result> { - Ok(self.map_err(|error| error.to_string())) - } -} diff --git a/crates/extension/src/wasm_host/wit/since_v0_0_1.rs b/crates/extension/src/wasm_host/wit/since_v0_0_1.rs deleted file mode 100644 index 1e6b74f56d..0000000000 --- a/crates/extension/src/wasm_host/wit/since_v0_0_1.rs +++ /dev/null @@ -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; - -pub fn linker() -> &'static Linker { - static LINKER: OnceLock> = OnceLock::new(); - LINKER.get_or_init(|| super::new_linker(Extension::add_to_linker)) -} - -impl From for latest::DownloadedFileType { - fn from(value: DownloadedFileType) -> Self { - match value { - DownloadedFileType::Gzip => latest::DownloadedFileType::Gzip, - DownloadedFileType::GzipTar => latest::DownloadedFileType::GzipTar, - DownloadedFileType::Zip => latest::DownloadedFileType::Zip, - DownloadedFileType::Uncompressed => latest::DownloadedFileType::Uncompressed, - } - } -} - -impl From for LanguageServerConfig { - fn from(value: since_v0_0_4::LanguageServerConfig) -> Self { - Self { - name: value.name, - language_name: value.language_name, - } - } -} - -impl From for latest::Command { - fn from(value: Command) -> Self { - Self { - command: value.command, - args: value.args, - env: value.env, - } - } -} - -#[async_trait] -impl HostWorktree for WasmState { - async fn read_text_file( - &mut self, - delegate: Resource>, - path: String, - ) -> wasmtime::Result> { - latest::HostWorktree::read_text_file(self, delegate, path).await - } - - async fn shell_env( - &mut self, - delegate: Resource>, - ) -> wasmtime::Result { - latest::HostWorktree::shell_env(self, delegate).await - } - - async fn which( - &mut self, - delegate: Resource>, - binary_name: String, - ) -> wasmtime::Result> { - latest::HostWorktree::which(self, delegate, binary_name).await - } - - fn drop(&mut self, _worktree: Resource) -> Result<()> { - Ok(()) - } -} - -#[async_trait] -impl ExtensionImports for WasmState { - async fn node_binary_path(&mut self) -> wasmtime::Result> { - latest::nodejs::Host::node_binary_path(self).await - } - - async fn npm_package_latest_version( - &mut self, - package_name: String, - ) -> wasmtime::Result> { - latest::nodejs::Host::npm_package_latest_version(self, package_name).await - } - - async fn npm_package_installed_version( - &mut self, - package_name: String, - ) -> wasmtime::Result, 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> { - latest::nodejs::Host::npm_install_package(self, package_name, version).await - } - - async fn latest_github_release( - &mut self, - repo: String, - options: GithubReleaseOptions, - ) -> wasmtime::Result> { - 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> { - latest::ExtensionImports::download_file(self, url, path, file_type.into()).await - } -} diff --git a/crates/extension/src/wasm_host/wit/since_v0_0_4.rs b/crates/extension/src/wasm_host/wit/since_v0_0_4.rs deleted file mode 100644 index eb52d64bf0..0000000000 --- a/crates/extension/src/wasm_host/wit/since_v0_0_4.rs +++ /dev/null @@ -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; - -pub fn linker() -> &'static Linker { - static LINKER: OnceLock> = OnceLock::new(); - LINKER.get_or_init(|| super::new_linker(Extension::add_to_linker)) -} - -impl From for latest::DownloadedFileType { - fn from(value: DownloadedFileType) -> Self { - match value { - DownloadedFileType::Gzip => latest::DownloadedFileType::Gzip, - DownloadedFileType::GzipTar => latest::DownloadedFileType::GzipTar, - DownloadedFileType::Zip => latest::DownloadedFileType::Zip, - DownloadedFileType::Uncompressed => latest::DownloadedFileType::Uncompressed, - } - } -} - -impl From for latest::LanguageServerInstallationStatus { - fn from(value: LanguageServerInstallationStatus) -> Self { - match value { - LanguageServerInstallationStatus::None => { - latest::LanguageServerInstallationStatus::None - } - LanguageServerInstallationStatus::Downloading => { - latest::LanguageServerInstallationStatus::Downloading - } - LanguageServerInstallationStatus::CheckingForUpdate => { - latest::LanguageServerInstallationStatus::CheckingForUpdate - } - LanguageServerInstallationStatus::Failed(error) => { - latest::LanguageServerInstallationStatus::Failed(error) - } - } - } -} - -impl From for latest::Command { - fn from(value: Command) -> Self { - Self { - command: value.command, - args: value.args, - env: value.env, - } - } -} - -#[async_trait] -impl HostWorktree for WasmState { - async fn read_text_file( - &mut self, - delegate: Resource>, - path: String, - ) -> wasmtime::Result> { - latest::HostWorktree::read_text_file(self, delegate, path).await - } - - async fn shell_env( - &mut self, - delegate: Resource>, - ) -> wasmtime::Result { - latest::HostWorktree::shell_env(self, delegate).await - } - - async fn which( - &mut self, - delegate: Resource>, - binary_name: String, - ) -> wasmtime::Result> { - latest::HostWorktree::which(self, delegate, binary_name).await - } - - fn drop(&mut self, _worktree: Resource) -> 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> { - latest::nodejs::Host::node_binary_path(self).await - } - - async fn npm_package_latest_version( - &mut self, - package_name: String, - ) -> wasmtime::Result> { - latest::nodejs::Host::npm_package_latest_version(self, package_name).await - } - - async fn npm_package_installed_version( - &mut self, - package_name: String, - ) -> wasmtime::Result, 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> { - latest::nodejs::Host::npm_install_package(self, package_name, version).await - } - - async fn latest_github_release( - &mut self, - repo: String, - options: GithubReleaseOptions, - ) -> wasmtime::Result> { - 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> { - latest::ExtensionImports::download_file(self, url, path, file_type.into()).await - } - - async fn make_file_executable(&mut self, path: String) -> wasmtime::Result> { - latest::ExtensionImports::make_file_executable(self, path).await - } -} diff --git a/crates/extension/src/wasm_host/wit/since_v0_0_6.rs b/crates/extension/src/wasm_host/wit/since_v0_0_6.rs deleted file mode 100644 index 29b00ac97d..0000000000 --- a/crates/extension/src/wasm_host/wit/since_v0_0_6.rs +++ /dev/null @@ -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; - -pub fn linker() -> &'static Linker { - static LINKER: OnceLock> = OnceLock::new(); - LINKER.get_or_init(|| super::new_linker(Extension::add_to_linker)) -} - -impl From for latest::Command { - fn from(value: Command) -> Self { - Self { - command: value.command, - args: value.args, - env: value.env, - } - } -} - -impl From for latest::SettingsLocation { - fn from(value: SettingsLocation) -> Self { - Self { - worktree_id: value.worktree_id, - path: value.path, - } - } -} - -impl From 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 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 for latest::Range { - fn from(value: Range) -> Self { - Self { - start: value.start, - end: value.end, - } - } -} - -impl From 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 for latest::CodeLabelSpanLiteral { - fn from(value: CodeLabelSpanLiteral) -> Self { - Self { - text: value.text, - highlight_name: value.highlight_name, - } - } -} - -impl From 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>, - ) -> wasmtime::Result { - latest::HostWorktree::id(self, delegate).await - } - - async fn root_path( - &mut self, - delegate: Resource>, - ) -> wasmtime::Result { - latest::HostWorktree::root_path(self, delegate).await - } - - async fn read_text_file( - &mut self, - delegate: Resource>, - path: String, - ) -> wasmtime::Result> { - latest::HostWorktree::read_text_file(self, delegate, path).await - } - - async fn shell_env( - &mut self, - delegate: Resource>, - ) -> wasmtime::Result { - latest::HostWorktree::shell_env(self, delegate).await - } - - async fn which( - &mut self, - delegate: Resource>, - binary_name: String, - ) -> wasmtime::Result> { - latest::HostWorktree::which(self, delegate, binary_name).await - } - - fn drop(&mut self, _worktree: Resource) -> Result<()> { - // We only ever hand out borrows of worktrees. - Ok(()) - } -} - -#[async_trait] -impl ExtensionImports for WasmState { - async fn get_settings( - &mut self, - location: Option, - category: String, - key: Option, - ) -> wasmtime::Result> { - 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> { - latest::ExtensionImports::download_file(self, url, path, file_type.into()).await - } - - async fn make_file_executable(&mut self, path: String) -> wasmtime::Result> { - latest::ExtensionImports::make_file_executable(self, path).await - } -} diff --git a/crates/extension/src/wasm_host/wit/since_v0_1_0.rs b/crates/extension/src/wasm_host/wit/since_v0_1_0.rs deleted file mode 100644 index 57b2edd301..0000000000 --- a/crates/extension/src/wasm_host/wit/since_v0_1_0.rs +++ /dev/null @@ -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; -pub type ExtensionKeyValueStore = Arc; -pub type ExtensionHttpResponseStream = Arc>>; - -pub fn linker() -> &'static Linker { - static LINKER: OnceLock> = OnceLock::new(); - LINKER.get_or_init(|| super::new_linker(Extension::add_to_linker)) -} - -impl From for latest::Command { - fn from(value: Command) -> Self { - Self { - command: value.command, - args: value.args, - env: value.env, - } - } -} - -impl From for latest::SettingsLocation { - fn from(value: SettingsLocation) -> Self { - Self { - worktree_id: value.worktree_id, - path: value.path, - } - } -} - -impl From 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 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 for latest::Range { - fn from(value: Range) -> Self { - Self { - start: value.start, - end: value.end, - } - } -} - -impl From 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 for latest::CodeLabelSpanLiteral { - fn from(value: CodeLabelSpanLiteral) -> Self { - Self { - text: value.text, - highlight_name: value.highlight_name, - } - } -} - -impl From 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, - key: String, - value: String, - ) -> wasmtime::Result> { - let kv_store = self.table.get(&kv_store)?; - kv_store.insert(key, value).await.to_wasmtime_result() - } - - fn drop(&mut self, _worktree: Resource) -> 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>, - ) -> wasmtime::Result { - let delegate = self.table.get(&delegate)?; - Ok(delegate.worktree_id().to_proto()) - } - - async fn root_path( - &mut self, - delegate: Resource>, - ) -> wasmtime::Result { - 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>, - path: String, - ) -> wasmtime::Result> { - let delegate = self.table.get(&delegate)?; - Ok(delegate - .read_text_file(path.into()) - .await - .map_err(|error| error.to_string())) - } - - async fn shell_env( - &mut self, - delegate: Resource>, - ) -> wasmtime::Result { - let delegate = self.table.get(&delegate)?; - Ok(delegate.shell_env().await.into_iter().collect()) - } - - async fn which( - &mut self, - delegate: Resource>, - binary_name: String, - ) -> wasmtime::Result> { - let delegate = self.table.get(&delegate)?; - Ok(delegate - .which(binary_name.as_ref()) - .await - .map(|path| path.to_string_lossy().to_string())) - } - - fn drop(&mut self, _worktree: Resource) -> Result<()> { - // We only ever hand out borrows of worktrees. - Ok(()) - } -} - -#[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> { - 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, 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, - ) -> wasmtime::Result>, 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) -> Result<()> { - Ok(()) - } -} - -impl From 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, 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, -) -> Result { - 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, - category: String, - key: Option, - ) -> wasmtime::Result> { - 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> { - 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> { - #[allow(unused)] - let path = self - .host - .writeable_path_from_extension(&self.manifest.id, Path::new(&path))?; - - #[cfg(unix)] - { - use std::fs::{self, Permissions}; - use std::os::unix::fs::PermissionsExt; - - return fs::set_permissions(&path, Permissions::from_mode(0o755)) - .map_err(|error| anyhow!("failed to set permissions for path {path:?}: {error}")) - .to_wasmtime_result(); - } - - #[cfg(not(unix))] - Ok(Ok(())) - } -} diff --git a/crates/extension/src/wasm_host/wit/since_v0_2_0.rs b/crates/extension/src/wasm_host/wit/since_v0_2_0.rs deleted file mode 100644 index da5632f3ae..0000000000 --- a/crates/extension/src/wasm_host/wit/since_v0_2_0.rs +++ /dev/null @@ -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; -pub type ExtensionKeyValueStore = Arc; -pub type ExtensionHttpResponseStream = Arc>>; - -pub fn linker() -> &'static Linker { - static LINKER: OnceLock> = OnceLock::new(); - LINKER.get_or_init(|| super::new_linker(Extension::add_to_linker)) -} - -#[async_trait] -impl HostKeyValueStore for WasmState { - async fn insert( - &mut self, - kv_store: Resource, - key: String, - value: String, - ) -> wasmtime::Result> { - let kv_store = self.table.get(&kv_store)?; - kv_store.insert(key, value).await.to_wasmtime_result() - } - - fn drop(&mut self, _worktree: Resource) -> 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>, - ) -> wasmtime::Result { - let delegate = self.table.get(&delegate)?; - Ok(delegate.worktree_id().to_proto()) - } - - async fn root_path( - &mut self, - delegate: Resource>, - ) -> wasmtime::Result { - 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>, - path: String, - ) -> wasmtime::Result> { - let delegate = self.table.get(&delegate)?; - Ok(delegate - .read_text_file(path.into()) - .await - .map_err(|error| error.to_string())) - } - - async fn shell_env( - &mut self, - delegate: Resource>, - ) -> wasmtime::Result { - let delegate = self.table.get(&delegate)?; - Ok(delegate.shell_env().await.into_iter().collect()) - } - - async fn which( - &mut self, - delegate: Resource>, - binary_name: String, - ) -> wasmtime::Result> { - let delegate = self.table.get(&delegate)?; - Ok(delegate - .which(binary_name.as_ref()) - .await - .map(|path| path.to_string_lossy().to_string())) - } - - fn drop(&mut self, _worktree: Resource) -> Result<()> { - // We only ever hand out borrows of worktrees. - Ok(()) - } -} - -#[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> { - 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, 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, - ) -> wasmtime::Result>, 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) -> Result<()> { - Ok(()) - } -} - -impl From 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, 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, -) -> Result { - 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> { - self.host - .node_runtime - .binary_path() - .await - .map(|path| path.to_string_lossy().to_string()) - .to_wasmtime_result() - } - - async fn npm_package_latest_version( - &mut self, - package_name: String, - ) -> wasmtime::Result> { - self.host - .node_runtime - .npm_package_latest_version(&package_name) - .await - .to_wasmtime_result() - } - - async fn npm_package_installed_version( - &mut self, - package_name: String, - ) -> wasmtime::Result, String>> { - self.host - .node_runtime - .npm_package_installed_version(&self.work_dir(), &package_name) - .await - .to_wasmtime_result() - } - - async fn npm_install_package( - &mut self, - package_name: String, - version: String, - ) -> wasmtime::Result> { - self.host - .node_runtime - .npm_install_packages(&self.work_dir(), &[(&package_name, &version)]) - .await - .to_wasmtime_result() - } -} - -#[async_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> { - 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> { - 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, - category: String, - key: Option, - ) -> wasmtime::Result> { - 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> { - 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> { - #[allow(unused)] - let path = self - .host - .writeable_path_from_extension(&self.manifest.id, Path::new(&path))?; - - #[cfg(unix)] - { - use std::fs::{self, Permissions}; - use std::os::unix::fs::PermissionsExt; - - return fs::set_permissions(&path, Permissions::from_mode(0o755)) - .map_err(|error| anyhow!("failed to set permissions for path {path:?}: {error}")) - .to_wasmtime_result(); - } - - #[cfg(not(unix))] - Ok(Ok(())) - } -} diff --git a/crates/extension_headless/src/extension_store.rs b/crates/extension_headless/src/extension_store.rs index 2a8de4d78f..8e8d064d70 100644 --- a/crates/extension_headless/src/extension_store.rs +++ b/crates/extension_headless/src/extension_store.rs @@ -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, PathBuf)>, cx: &mut AppContext); + fn load_user_theme( + &self, + themes_path: &Path, + fs: Arc, + ) -> Pin> + 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, - http_client: Arc, - telemetry: Option>, - reload_tx: UnboundedSender>>, - reload_complete_senders: Vec>, - installed_dir: PathBuf, - outstanding_operations: BTreeMap, ExtensionOperation>, - index_path: PathBuf, - language_registry: Arc, - modified_extensions: HashSet>, - wasm_host: Arc, - wasm_extensions: Vec<(Arc, WasmExtension)>, - tasks: Vec>, + pub feature_provider: Arc, + pub extension_index: ExtensionIndex, + pub fs: Arc, + pub http_client: Arc, + pub telemetry: Option>, + pub reload_tx: UnboundedSender>>, + pub reload_complete_senders: Vec>, + pub installed_dir: PathBuf, + pub outstanding_operations: BTreeMap, ExtensionOperation>, + pub index_path: PathBuf, + pub language_registry: Arc, + pub modified_extensions: HashSet>, + pub wasm_host: Arc, + pub wasm_extensions: Vec<(Arc, WasmExtension)>, + pub tasks: Vec>, } #[derive(Clone, Copy)] @@ -123,9 +136,9 @@ pub enum Event { impl EventEmitter for HeadlessExtensionStore {} -struct GlobalExtensionStore(Model); +struct GlobalHeadlessExtensionStore(Model); -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>, node_runtime: NodeRuntime, language_registry: Arc, + feature_provider: Arc, + 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> { - cx.try_global::() + cx.try_global::() .map(|store| store.0.clone()) } pub fn global(cx: &AppContext) -> Model { - cx.global::().0.clone() + cx.global::().0.clone() } #[allow(clippy::too_many_arguments)] @@ -201,6 +217,7 @@ impl HeadlessExtensionStore { telemetry: Option>, node_runtime: NodeRuntime, language_registry: Arc, + feature_provider: Arc, cx: &mut ModelContext, ) -> 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>, cx: &mut ModelContext, @@ -786,6 +804,7 @@ impl HeadlessExtensionStore { new_index: ExtensionIndex, cx: &mut ModelContext, ) -> 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::>(); + 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::>(); 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::>(); - // 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; diff --git a/crates/extension_headless/src/wasm_host.rs b/crates/extension_headless/src/wasm_host.rs index e931eb3e1e..042889c08c 100644 --- a/crates/extension_headless/src/wasm_host.rs +++ b/crates/extension_headless/src/wasm_host.rs @@ -49,7 +49,7 @@ pub struct WasmExtension { pub zed_api_version: SemanticVersion, } -pub(crate) struct WasmState { +pub struct WasmState { manifest: Arc, pub(crate) table: ResourceTable, ctx: wasi::WasiCtx, diff --git a/crates/remote_server/src/unix.rs b/crates/remote_server/src/unix.rs index 75dfe0c900..5f4f3a1034 100644 --- a/crates/remote_server/src/unix.rs +++ b/crates/remote_server/src/unix.rs @@ -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, PathBuf)>, _cx: &mut AppContext) {} + + fn load_user_theme( + &self, + _themes_path: &Path, + _fs: Arc, + ) -> std::pin::Pin> + Send>> { + Box::pin(async { Ok(()) }) + } + + fn register_snippets(&self, _file_path: &Path, _contents: &str) -> Result<()> { + Ok(()) + } +} + #[derive(Clone)] struct ServerPaths { log_file: PathBuf,