From 608addf6419ab533e412c80baef31f01a0ff3553 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 6 Nov 2024 10:06:25 -0700 Subject: [PATCH] Extension refactor (#20305) This contains the main changes to the extensions crate from #20049. The primary goal here is removing dependencies that we can't include on the remote. Release Notes: - N/A --------- Co-authored-by: Mikayla Co-authored-by: Marshall Bowers Co-authored-by: Marshall --- Cargo.lock | 26 +- .../remote_editing_collaboration_tests.rs | 22 +- crates/extension/src/extension_builder.rs | 7 +- crates/extension_host/Cargo.toml | 8 +- crates/extension_host/src/extension_host.rs | 318 ++++++++++-------- crates/extension_host/src/wasm_host.rs | 102 +++++- crates/extension_host/src/wasm_host/wit.rs | 6 +- .../src/wasm_host/wit/since_v0_0_1.rs | 2 +- .../src/wasm_host/wit/since_v0_1_0.rs | 6 +- .../src/wasm_host/wit/since_v0_2_0.rs | 6 +- crates/extensions_ui/Cargo.toml | 22 ++ .../src/extension_indexed_docs_provider.rs | 4 +- .../src/extension_registration_hooks.rs | 153 +++++++++ .../src/extension_slash_command.rs | 6 +- .../src/extension_store_test.rs | 60 ++-- crates/extensions_ui/src/extensions_ui.rs | 8 + crates/indexed_docs/Cargo.toml | 1 + crates/indexed_docs/src/providers/rustdoc.rs | 3 +- crates/indexed_docs/src/store.rs | 4 +- crates/language/src/language_registry.rs | 7 +- crates/languages/src/lib.rs | 16 +- crates/paths/src/paths.rs | 16 + crates/remote/src/ssh_session.rs | 72 +++- crates/remote_server/src/headless_project.rs | 4 +- .../remote_server/src/remote_editing_tests.rs | 3 + crates/remote_server/src/unix.rs | 6 + crates/workspace/src/pane_group.rs | 2 +- crates/zed/Cargo.toml | 2 + crates/zed/src/main.rs | 14 +- crates/zed/src/reliability.rs | 5 +- 30 files changed, 675 insertions(+), 236 deletions(-) rename crates/{extension_host => extensions_ui}/src/extension_indexed_docs_provider.rs (96%) create mode 100644 crates/extensions_ui/src/extension_registration_hooks.rs rename crates/{extension_host => extensions_ui}/src/extension_slash_command.rs (96%) rename crates/{extension_host => extensions_ui}/src/extension_store_test.rs (97%) diff --git a/Cargo.lock b/Cargo.lock index 3b23b1bf8d..a526be8c8f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4138,7 +4138,6 @@ name = "extension_host" version = "0.1.0" dependencies = [ "anyhow", - "assistant_slash_command", "async-compression", "async-tar", "async-trait", @@ -4151,7 +4150,6 @@ dependencies = [ "futures 0.3.30", "gpui", "http_client", - "indexed_docs", "language", "log", "lsp", @@ -4167,16 +4165,13 @@ dependencies = [ "serde_json", "serde_json_lenient", "settings", - "snippet_provider", "task", - "theme", "toml 0.8.19", - "ui", "url", "util", + "wasmparser 0.215.0", "wasmtime", "wasmtime-wasi", - "workspace", ] [[package]] @@ -4184,28 +4179,44 @@ name = "extensions_ui" version = "0.1.0" dependencies = [ "anyhow", + "assistant_slash_command", + "async-compression", + "async-tar", + "async-trait", "client", "collections", + "ctor", "db", "editor", + "env_logger 0.11.5", "extension_host", "fs", + "futures 0.3.30", "fuzzy", "gpui", + "http_client", + "indexed_docs", "language", + "lsp", + "node_runtime", "num-format", + "parking_lot", "picker", "project", "release_channel", + "reqwest_client", "semantic_version", "serde", + "serde_json", "settings", "smallvec", + "snippet_provider", "theme", "theme_selector", "ui", "util", "vim", + "wasmtime-wasi", "workspace", ] @@ -5822,6 +5833,7 @@ dependencies = [ "cargo_metadata", "collections", "derive_more", + "extension_host", "fs", "futures 0.3.30", "fuzzy", @@ -15070,6 +15082,7 @@ dependencies = [ "ashpd", "assets", "assistant", + "assistant_slash_command", "async-watch", "audio", "auto_update", @@ -15104,6 +15117,7 @@ dependencies = [ "gpui", "http_client", "image_viewer", + "indexed_docs", "inline_completion_button", "install_cli", "journal", diff --git a/crates/collab/src/tests/remote_editing_collaboration_tests.rs b/crates/collab/src/tests/remote_editing_collaboration_tests.rs index 0e29bd5ef3..00f52e9972 100644 --- a/crates/collab/src/tests/remote_editing_collaboration_tests.rs +++ b/crates/collab/src/tests/remote_editing_collaboration_tests.rs @@ -3,7 +3,7 @@ use call::ActiveCall; use collections::HashSet; use fs::{FakeFs, Fs as _}; use futures::StreamExt as _; -use gpui::{BackgroundExecutor, Context as _, TestAppContext, UpdateGlobal as _}; +use gpui::{BackgroundExecutor, Context as _, SemanticVersion, TestAppContext, UpdateGlobal as _}; use http_client::BlockedHttpClient; use language::{ language_settings::{ @@ -31,6 +31,12 @@ async fn test_sharing_an_ssh_remote_project( server_cx: &mut TestAppContext, ) { let executor = cx_a.executor(); + cx_a.update(|cx| { + release_channel::init(SemanticVersion::default(), cx); + }); + server_cx.update(|cx| { + release_channel::init(SemanticVersion::default(), cx); + }); let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; @@ -199,6 +205,13 @@ async fn test_ssh_collaboration_git_branches( cx_b.set_name("b"); server_cx.set_name("server"); + cx_a.update(|cx| { + release_channel::init(SemanticVersion::default(), cx); + }); + server_cx.update(|cx| { + release_channel::init(SemanticVersion::default(), cx); + }); + let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; @@ -329,6 +342,13 @@ async fn test_ssh_collaboration_formatting_with_prettier( cx_b.set_name("b"); server_cx.set_name("server"); + cx_a.update(|cx| { + release_channel::init(SemanticVersion::default(), cx); + }); + server_cx.update(|cx| { + release_channel::init(SemanticVersion::default(), cx); + }); + let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; diff --git a/crates/extension/src/extension_builder.rs b/crates/extension/src/extension_builder.rs index ca15e18478..ebb46993f4 100644 --- a/crates/extension/src/extension_builder.rs +++ b/crates/extension/src/extension_builder.rs @@ -365,12 +365,15 @@ impl ExtensionBuilder { let output = Command::new("rustup") .args(["target", "add", RUST_TARGET]) - .stderr(Stdio::inherit()) + .stderr(Stdio::piped()) .stdout(Stdio::inherit()) .output() .context("failed to run `rustup target add`")?; if !output.status.success() { - bail!("failed to install the `{RUST_TARGET}` target"); + bail!( + "failed to install the `{RUST_TARGET}` target: {}", + String::from_utf8_lossy(&rustc_output.stderr) + ); } Ok(()) diff --git a/crates/extension_host/Cargo.toml b/crates/extension_host/Cargo.toml index 4eb5d22f72..ebe02fca04 100644 --- a/crates/extension_host/Cargo.toml +++ b/crates/extension_host/Cargo.toml @@ -14,7 +14,6 @@ doctest = false [dependencies] anyhow.workspace = true -assistant_slash_command.workspace = true async-compression.workspace = true async-tar.workspace = true async-trait.workspace = true @@ -25,7 +24,6 @@ fs.workspace = true futures.workspace = true gpui.workspace = true http_client.workspace = true -indexed_docs.workspace = true language.workspace = true log.workspace = true lsp.workspace = true @@ -39,16 +37,13 @@ serde.workspace = true serde_json.workspace = true serde_json_lenient.workspace = true settings.workspace = true -snippet_provider.workspace = true task.workspace = true -theme.workspace = true toml.workspace = true -ui.workspace = true url.workspace = true util.workspace = true +wasmparser.workspace = true wasmtime-wasi.workspace = true wasmtime.workspace = true -workspace.workspace = true [dev-dependencies] ctor.workspace = true @@ -59,4 +54,3 @@ language = { workspace = true, features = ["test-support"] } parking_lot.workspace = true project = { workspace = true, features = ["test-support"] } reqwest_client.workspace = true -workspace = { workspace = true, features = ["test-support"] } diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index b27a486b4b..c69463c0f1 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -1,23 +1,15 @@ -mod extension_indexed_docs_provider; -mod extension_lsp_adapter; -mod extension_settings; -mod extension_slash_command; -mod wasm_host; +pub mod extension_lsp_adapter; +pub mod extension_settings; +pub mod wasm_host; -#[cfg(test)] -mod extension_store_test; - -use crate::extension_indexed_docs_provider::ExtensionIndexedDocsProvider; -use crate::extension_slash_command::ExtensionSlashCommand; use crate::{extension_lsp_adapter::ExtensionLspAdapter, wasm_host::wit}; use anyhow::{anyhow, bail, Context as _, Result}; -use assistant_slash_command::SlashCommandRegistry; use async_compression::futures::bufread::GzipDecoder; use async_tar::Archive; use client::{telemetry::Telemetry, Client, ExtensionMetadata, GetExtensionsResponse}; use collections::{btree_map, BTreeMap, HashSet}; use extension::extension_builder::{CompileExtensionOptions, ExtensionBuilder}; -use extension::SchemaVersion; +pub use extension::ExtensionManifest; use fs::{Fs, RemoveOptions}; use futures::{ channel::{ @@ -28,14 +20,13 @@ 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 indexed_docs::{IndexedDocsRegistry, ProviderId}; use language::{ - LanguageConfig, LanguageMatcher, LanguageName, LanguageQueries, LanguageRegistry, - LoadedLanguage, QUERY_FILENAME_PREFIXES, + LanguageConfig, LanguageMatcher, LanguageName, LanguageQueries, LoadedLanguage, + QUERY_FILENAME_PREFIXES, }; use node_runtime::NodeRuntime; use project::ContextProviderWithTasks; @@ -43,7 +34,6 @@ use release_channel::ReleaseChannel; use semantic_version::SemanticVersion; use serde::{Deserialize, Serialize}; use settings::Settings; -use snippet_provider::SnippetRegistry; use std::ops::RangeInclusive; use std::str::FromStr; use std::{ @@ -52,20 +42,19 @@ use std::{ sync::Arc, time::{Duration, Instant}, }; -use theme::{ThemeRegistry, ThemeSettings}; use url::Url; -use util::{maybe, ResultExt}; +use util::ResultExt; use wasm_host::{ wit::{is_supported_wasm_api_version, wasm_api_version_range}, WasmExtension, WasmHost, }; pub use extension::{ - ExtensionLibraryKind, ExtensionManifest, GrammarManifestEntry, OldExtensionManifest, + ExtensionLibraryKind, GrammarManifestEntry, OldExtensionManifest, SchemaVersion, }; pub use extension_settings::ExtensionSettings; -const RELOAD_DEBOUNCE_DURATION: Duration = Duration::from_millis(200); +pub 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. @@ -100,26 +89,98 @@ pub fn is_version_compatible( true } +pub trait DocsDatabase: Send + Sync + 'static { + fn insert(&self, key: String, docs: String) -> Task>; +} + +pub trait ExtensionRegistrationHooks: Send + Sync + 'static { + fn remove_user_themes(&self, _themes: Vec) {} + + fn load_user_theme(&self, _theme_path: PathBuf, _fs: Arc) -> Task> { + Task::ready(Ok(())) + } + + fn list_theme_names( + &self, + _theme_path: PathBuf, + _fs: Arc, + ) -> Task>> { + Task::ready(Ok(Vec::new())) + } + + fn reload_current_theme(&self, _cx: &mut AppContext) {} + + fn register_language( + &self, + _language: LanguageName, + _grammar: Option>, + _matcher: language::LanguageMatcher, + _load: Arc Result + 'static + Send + Sync>, + ) { + } + + fn register_lsp_adapter(&self, _language: LanguageName, _adapter: ExtensionLspAdapter) {} + + fn remove_lsp_adapter( + &self, + _language: &LanguageName, + _server_name: &language::LanguageServerName, + ) { + } + + fn register_wasm_grammars(&self, _grammars: Vec<(Arc, PathBuf)>) {} + + fn remove_languages( + &self, + _languages_to_remove: &[LanguageName], + _grammars_to_remove: &[Arc], + ) { + } + + fn register_slash_command( + &self, + _slash_command: wit::SlashCommand, + _extension: WasmExtension, + _host: Arc, + ) { + } + + fn register_docs_provider( + &self, + _extension: WasmExtension, + _host: Arc, + _provider_id: Arc, + ) { + } + + fn register_snippets(&self, _path: &PathBuf, _snippet_contents: &str) -> Result<()> { + Ok(()) + } + + fn update_lsp_status( + &self, + _server_name: language::LanguageServerName, + _status: language::LanguageServerBinaryStatus, + ) { + } +} + pub struct ExtensionStore { - 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>, + pub registration_hooks: Arc, + pub builder: 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 modified_extensions: HashSet>, + pub wasm_host: Arc, + pub wasm_extensions: Vec<(Arc, WasmExtension)>, + pub tasks: Vec>, } #[derive(Clone, Copy)] @@ -158,26 +219,25 @@ pub struct ExtensionIndexEntry { #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Deserialize, Serialize)] pub struct ExtensionIndexThemeEntry { - extension: Arc, - path: PathBuf, + pub extension: Arc, + pub path: PathBuf, } #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Deserialize, Serialize)] pub struct ExtensionIndexLanguageEntry { - extension: Arc, - path: PathBuf, - matcher: LanguageMatcher, - grammar: Option>, + pub extension: Arc, + pub path: PathBuf, + pub matcher: LanguageMatcher, + pub grammar: Option>, } actions!(zed, [ReloadExtensions]); pub fn init( + registration_hooks: Arc, fs: Arc, client: Arc, node_runtime: NodeRuntime, - language_registry: Arc, - theme_registry: Arc, cx: &mut AppContext, ) { ExtensionSettings::register(cx); @@ -186,16 +246,12 @@ pub fn init( ExtensionStore::new( paths::extensions_dir().clone(), None, + registration_hooks, fs, client.http_client().clone(), client.http_client().clone(), Some(client.telemetry().clone()), node_runtime, - language_registry, - theme_registry, - SlashCommandRegistry::global(cx), - IndexedDocsRegistry::global(cx), - SnippetRegistry::global(cx), cx, ) }); @@ -222,16 +278,12 @@ impl ExtensionStore { pub fn new( extensions_dir: PathBuf, build_dir: Option, + extension_api: Arc, fs: Arc, http_client: Arc, builder_client: Arc, telemetry: Option>, node_runtime: NodeRuntime, - language_registry: Arc, - theme_registry: Arc, - slash_command_registry: Arc, - indexed_docs_registry: Arc, - snippet_registry: Arc, cx: &mut ModelContext, ) -> Self { let work_dir = extensions_dir.join("work"); @@ -241,6 +293,7 @@ impl ExtensionStore { let (reload_tx, mut reload_rx) = unbounded(); let mut this = Self { + registration_hooks: extension_api.clone(), extension_index: Default::default(), installed_dir, index_path, @@ -252,7 +305,7 @@ impl ExtensionStore { fs.clone(), http_client.clone(), node_runtime, - language_registry.clone(), + extension_api, work_dir, cx, ), @@ -260,11 +313,6 @@ impl ExtensionStore { fs, http_client, telemetry, - language_registry, - theme_registry, - slash_command_registry, - indexed_docs_registry, - snippet_registry, reload_tx, tasks: Vec::new(), }; @@ -325,6 +373,7 @@ impl ExtensionStore { async move { load_initial_extensions.await; + let mut index_changed = false; let mut debounce_timer = cx .background_executor() .spawn(futures::future::pending()) @@ -332,17 +381,21 @@ impl ExtensionStore { 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; + if index_changed { + 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; + index_changed = false; + } } extension_id = reload_rx.next() => { let Some(extension_id) = extension_id else { break; }; this.update(&mut cx, |this, _| { this.modified_extensions.extend(extension_id); })?; + index_changed = true; debounce_timer = cx .background_executor() .timer(RELOAD_DEBOUNCE_DURATION) @@ -386,7 +439,7 @@ impl ExtensionStore { this } - fn reload( + pub fn reload( &mut self, modified_extension: Option>, cx: &mut ModelContext, @@ -1039,7 +1092,7 @@ impl ExtensionStore { 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 + self.registration_hooks .remove_lsp_adapter(&language, language_server_name); } } @@ -1047,8 +1100,8 @@ impl ExtensionStore { self.wasm_extensions .retain(|(extension, _)| !extensions_to_unload.contains(&extension.id)); - self.theme_registry.remove_user_themes(&themes_to_remove); - self.language_registry + self.registration_hooks.remove_user_themes(themes_to_remove); + self.registration_hooks .remove_languages(&languages_to_remove, &grammars_to_remove); let languages_to_add = new_index @@ -1083,7 +1136,7 @@ impl ExtensionStore { })); } - self.language_registry + self.registration_hooks .register_wasm_grammars(grammars_to_add); for (language_name, language) in languages_to_add { @@ -1092,11 +1145,11 @@ impl ExtensionStore { Path::new(language.extension.as_ref()), language.path.as_path(), ]); - self.language_registry.register_language( + self.registration_hooks.register_language( language_name.clone(), language.grammar.clone(), language.matcher.clone(), - move || { + Arc::new(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); @@ -1115,15 +1168,14 @@ impl ExtensionStore { 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 api = self.registration_hooks.clone(); let extension_entries = extensions_to_load .iter() .filter_map(|name| new_index.extensions.get(name).cloned()) @@ -1138,18 +1190,14 @@ impl ExtensionStore { .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 theme_path in themes_to_add.into_iter() { + api.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) + api.register_snippets(snippets_path, &snippets_contents) .log_err(); } } @@ -1163,30 +1211,13 @@ impl ExtensionStore { 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) - }) - }) + let extension_path = root_dir.join(extension.manifest.id.as_ref()); + let wasm_extension = WasmExtension::load( + extension_path, + &extension.manifest, + wasm_host.clone(), + &cx, + ) .await; if let Some(wasm_extension) = wasm_extension.log_err() { @@ -1205,9 +1236,9 @@ impl ExtensionStore { 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( + this.registration_hooks.register_lsp_adapter( language.clone(), - Arc::new(ExtensionLspAdapter { + ExtensionLspAdapter { extension: wasm_extension.clone(), host: this.wasm_host.clone(), language_server_id: language_server_id.clone(), @@ -1215,43 +1246,38 @@ impl ExtensionStore { 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(), + this.registration_hooks.register_slash_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, }, - false, + wasm_extension.clone(), + this.wasm_host.clone(), ); } 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.registration_hooks.register_docs_provider( + wasm_extension.clone(), + this.wasm_host.clone(), + provider_id.clone(), + ); } } this.wasm_extensions.extend(wasm_extensions); - ThemeSettings::reload_current_theme(cx) + this.registration_hooks.reload_current_theme(cx); }) .ok(); }) @@ -1262,6 +1288,7 @@ impl ExtensionStore { let work_dir = self.wasm_host.work_dir.clone(); let extensions_dir = self.installed_dir.clone(); let index_path = self.index_path.clone(); + let extension_api = self.registration_hooks.clone(); cx.background_executor().spawn(async move { let start_time = Instant::now(); let mut index = ExtensionIndex::default(); @@ -1283,9 +1310,14 @@ impl ExtensionStore { continue; } - Self::add_extension_to_index(fs.clone(), extension_dir, &mut index) - .await - .log_err(); + Self::add_extension_to_index( + fs.clone(), + extension_dir, + &mut index, + extension_api.clone(), + ) + .await + .log_err(); } } @@ -1305,6 +1337,7 @@ impl ExtensionStore { fs: Arc, extension_dir: PathBuf, index: &mut ExtensionIndex, + extension_api: Arc, ) -> Result<()> { let mut extension_manifest = ExtensionManifest::load(fs.clone(), &extension_dir).await?; let extension_id = extension_manifest.id.clone(); @@ -1356,7 +1389,8 @@ impl ExtensionStore { continue; }; - let Some(theme_family) = theme::read_user_theme(&theme_path, fs.clone()) + let Some(theme_families) = extension_api + .list_theme_names(theme_path.clone(), fs.clone()) .await .log_err() else { @@ -1368,9 +1402,9 @@ impl ExtensionStore { extension_manifest.themes.push(relative_path.clone()); } - for theme in theme_family.themes { + for theme_name in theme_families { index.themes.insert( - theme.name.into(), + theme_name.into(), ExtensionIndexThemeEntry { extension: extension_id.clone(), path: relative_path.clone(), diff --git a/crates/extension_host/src/wasm_host.rs b/crates/extension_host/src/wasm_host.rs index 4241f3f551..fe00cf9486 100644 --- a/crates/extension_host/src/wasm_host.rs +++ b/crates/extension_host/src/wasm_host.rs @@ -1,7 +1,7 @@ -pub(crate) mod wit; +pub mod wit; -use crate::ExtensionManifest; -use anyhow::{anyhow, Context as _, Result}; +use crate::{ExtensionManifest, ExtensionRegistrationHooks}; +use anyhow::{anyhow, bail, Context as _, Result}; use fs::{normalize_path, Fs}; use futures::future::LocalBoxFuture; use futures::{ @@ -14,7 +14,6 @@ use futures::{ }; 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; @@ -28,15 +27,16 @@ use wasmtime::{ }; use wasmtime_wasi as wasi; use wit::Extension; +pub use wit::SlashCommand; -pub(crate) struct WasmHost { +pub struct WasmHost { engine: Engine, release_channel: ReleaseChannel, http_client: Arc, node_runtime: NodeRuntime, - pub(crate) language_registry: Arc, + pub registration_hooks: Arc, fs: Arc, - pub(crate) work_dir: PathBuf, + pub work_dir: PathBuf, _main_thread_message_task: Task<()>, main_thread_message_tx: mpsc::UnboundedSender, } @@ -44,16 +44,16 @@ pub(crate) struct WasmHost { #[derive(Clone)] pub struct WasmExtension { tx: UnboundedSender, - pub(crate) manifest: Arc, + pub manifest: Arc, #[allow(unused)] pub zed_api_version: SemanticVersion, } -pub(crate) struct WasmState { +pub struct WasmState { manifest: Arc, - pub(crate) table: ResourceTable, + pub table: ResourceTable, ctx: wasi::WasiCtx, - pub(crate) host: Arc, + pub host: Arc, } type MainThreadCall = @@ -81,7 +81,7 @@ impl WasmHost { fs: Arc, http_client: Arc, node_runtime: NodeRuntime, - language_registry: Arc, + registration_hooks: Arc, work_dir: PathBuf, cx: &mut AppContext, ) -> Arc { @@ -97,7 +97,7 @@ impl WasmHost { work_dir, http_client, node_runtime, - language_registry, + registration_hooks, release_channel: ReleaseChannel::global(cx), _main_thread_message_task: task, main_thread_message_tx: tx, @@ -107,13 +107,13 @@ impl WasmHost { pub fn load_extension( self: &Arc, wasm_bytes: Vec, - manifest: Arc, + manifest: &Arc, executor: BackgroundExecutor, ) -> Task> { let this = self.clone(); + let manifest = manifest.clone(); executor.clone().spawn(async move { - let zed_api_version = - extension::parse_wasm_extension_version(&manifest.id, &wasm_bytes)?; + 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")?; @@ -151,7 +151,7 @@ impl WasmHost { .detach(); Ok(WasmExtension { - manifest, + manifest: manifest.clone(), tx, zed_api_version, }) @@ -198,7 +198,75 @@ impl WasmHost { } } +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 load( + extension_dir: PathBuf, + manifest: &Arc, + wasm_host: Arc, + cx: &AsyncAppContext, + ) -> Result { + let path = extension_dir.join("extension.wasm"); + + let mut wasm_file = wasm_host + .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, manifest, cx.background_executor().clone()) + .await + .with_context(|| format!("failed to load wasm extension {}", manifest.id)) + } + pub async fn call(&self, f: Fn) -> T where T: 'static + Send, diff --git a/crates/extension_host/src/wasm_host/wit.rs b/crates/extension_host/src/wasm_host/wit.rs index 3aff59ee2a..dfee3cec9d 100644 --- a/crates/extension_host/src/wasm_host/wit.rs +++ b/crates/extension_host/src/wasm_host/wit.rs @@ -3,10 +3,12 @@ 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 indexed_docs::IndexedDocsDatabase; use release_channel::ReleaseChannel; use since_v0_2_0 as latest; +use crate::DocsDatabase; + use super::{wasm_engine, WasmState}; use anyhow::{anyhow, Context, Result}; use language::{LanguageServerName, LspAdapterDelegate}; @@ -394,7 +396,7 @@ impl Extension { store: &mut Store, provider: &str, package_name: &str, - database: Resource>, + database: Resource>, ) -> Result> { match self { Extension::V020(ext) => { diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_0_1.rs b/crates/extension_host/src/wasm_host/wit/since_v0_0_1.rs index 1e6b74f56d..e3e951a4e1 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_0_1.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_0_1.rs @@ -148,7 +148,7 @@ impl ExtensionImports for WasmState { }; self.host - .language_registry + .registration_hooks .update_lsp_status(language::LanguageServerName(server_name.into()), status); Ok(()) } diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs index d8f4cc35b4..706a8ac22d 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs @@ -1,4 +1,5 @@ use crate::wasm_host::{wit::ToWasmtimeResult, WasmState}; +use crate::DocsDatabase; use ::http_client::{AsyncBody, HttpRequestExt}; use ::settings::{Settings, WorktreeId}; use anyhow::{anyhow, bail, Context, Result}; @@ -7,7 +8,6 @@ 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, }; @@ -48,7 +48,7 @@ mod settings { } pub type ExtensionWorktree = Arc; -pub type ExtensionKeyValueStore = Arc; +pub type ExtensionKeyValueStore = Arc; pub type ExtensionHttpResponseStream = Arc>>; pub fn linker() -> &'static Linker { @@ -512,7 +512,7 @@ impl ExtensionImports for WasmState { }; self.host - .language_registry + .registration_hooks .update_lsp_status(language::LanguageServerName(server_name.into()), status); Ok(()) } diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_2_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_2_0.rs index da5632f3ae..f920b9215f 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_2_0.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_2_0.rs @@ -1,4 +1,5 @@ use crate::wasm_host::{wit::ToWasmtimeResult, WasmState}; +use crate::DocsDatabase; use ::http_client::{AsyncBody, HttpRequestExt}; use ::settings::{Settings, WorktreeId}; use anyhow::{anyhow, bail, Context, Result}; @@ -7,7 +8,6 @@ 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, }; @@ -43,7 +43,7 @@ mod settings { } pub type ExtensionWorktree = Arc; -pub type ExtensionKeyValueStore = Arc; +pub type ExtensionKeyValueStore = Arc; pub type ExtensionHttpResponseStream = Arc>>; pub fn linker() -> &'static Linker { @@ -459,7 +459,7 @@ impl ExtensionImports for WasmState { }; self.host - .language_registry + .registration_hooks .update_lsp_status(language::LanguageServerName(server_name.into()), status); Ok(()) } diff --git a/crates/extensions_ui/Cargo.toml b/crates/extensions_ui/Cargo.toml index 0de1fd947a..fb00011692 100644 --- a/crates/extensions_ui/Cargo.toml +++ b/crates/extensions_ui/Cargo.toml @@ -16,14 +16,18 @@ test-support = [] [dependencies] anyhow.workspace = true +assistant_slash_command.workspace = true +async-trait.workspace = true client.workspace = true collections.workspace = true db.workspace = true editor.workspace = true extension_host.workspace = true fs.workspace = true +futures.workspace = true fuzzy.workspace = true gpui.workspace = true +indexed_docs.workspace = true language.workspace = true num-format.workspace = true picker.workspace = true @@ -33,12 +37,30 @@ semantic_version.workspace = true serde.workspace = true settings.workspace = true smallvec.workspace = true +snippet_provider.workspace = true theme.workspace = true theme_selector.workspace = true ui.workspace = true util.workspace = true vim.workspace = true +wasmtime-wasi.workspace = true workspace.workspace = true [dev-dependencies] +async-compression.workspace = true +async-tar.workspace = true +ctor.workspace = true editor = { workspace = true, features = ["test-support"] } +env_logger.workspace = true +fs = { workspace = true, features = ["test-support"] } +gpui = { workspace = true, features = ["test-support"] } +http_client.workspace = true +indexed_docs.workspace = true +language = { workspace = true, features = ["test-support"] } +lsp.workspace = true +node_runtime.workspace = true +parking_lot.workspace = true +project = { workspace = true, features = ["test-support"] } +reqwest_client.workspace = true +serde_json.workspace = true +workspace = { workspace = true, features = ["test-support"] } diff --git a/crates/extension_host/src/extension_indexed_docs_provider.rs b/crates/extensions_ui/src/extension_indexed_docs_provider.rs similarity index 96% rename from crates/extension_host/src/extension_indexed_docs_provider.rs rename to crates/extensions_ui/src/extension_indexed_docs_provider.rs index 957af63afd..cc2611040b 100644 --- a/crates/extension_host/src/extension_indexed_docs_provider.rs +++ b/crates/extensions_ui/src/extension_indexed_docs_provider.rs @@ -7,7 +7,7 @@ use futures::FutureExt; use indexed_docs::{IndexedDocsDatabase, IndexedDocsProvider, PackageName, ProviderId}; use wasmtime_wasi::WasiView; -use crate::wasm_host::{WasmExtension, WasmHost}; +use extension_host::wasm_host::{WasmExtension, WasmHost}; pub struct ExtensionIndexedDocsProvider { pub(crate) extension: WasmExtension, @@ -58,7 +58,7 @@ impl IndexedDocsProvider for ExtensionIndexedDocsProvider { let id = self.id.clone(); |extension, store| { async move { - let database_resource = store.data_mut().table().push(database)?; + let database_resource = store.data_mut().table().push(database as _)?; extension .call_index_docs( store, diff --git a/crates/extensions_ui/src/extension_registration_hooks.rs b/crates/extensions_ui/src/extension_registration_hooks.rs new file mode 100644 index 0000000000..0dcedf37aa --- /dev/null +++ b/crates/extensions_ui/src/extension_registration_hooks.rs @@ -0,0 +1,153 @@ +use std::{path::PathBuf, sync::Arc}; + +use anyhow::Result; +use assistant_slash_command::SlashCommandRegistry; +use extension_host::{extension_lsp_adapter::ExtensionLspAdapter, wasm_host}; +use fs::Fs; +use gpui::{AppContext, BackgroundExecutor, Task}; +use indexed_docs::{IndexedDocsRegistry, ProviderId}; +use language::{LanguageRegistry, LanguageServerBinaryStatus, LoadedLanguage}; +use snippet_provider::SnippetRegistry; +use theme::{ThemeRegistry, ThemeSettings}; +use ui::SharedString; + +use crate::{extension_indexed_docs_provider, extension_slash_command::ExtensionSlashCommand}; + +pub struct ConcreteExtensionRegistrationHooks { + slash_command_registry: Arc, + theme_registry: Arc, + indexed_docs_registry: Arc, + snippet_registry: Arc, + language_registry: Arc, + executor: BackgroundExecutor, +} + +impl ConcreteExtensionRegistrationHooks { + pub fn new( + theme_registry: Arc, + slash_command_registry: Arc, + indexed_docs_registry: Arc, + snippet_registry: Arc, + language_registry: Arc, + cx: &AppContext, + ) -> Arc { + Arc::new(Self { + theme_registry, + slash_command_registry, + indexed_docs_registry, + snippet_registry, + language_registry, + executor: cx.background_executor().clone(), + }) + } +} + +impl extension_host::ExtensionRegistrationHooks for ConcreteExtensionRegistrationHooks { + fn remove_user_themes(&self, themes: Vec) { + self.theme_registry.remove_user_themes(&themes); + } + + fn load_user_theme(&self, theme_path: PathBuf, fs: Arc) -> Task> { + let theme_registry = self.theme_registry.clone(); + self.executor + .spawn(async move { theme_registry.load_user_theme(&theme_path, fs).await }) + } + + fn register_slash_command( + &self, + command: wasm_host::SlashCommand, + extension: wasm_host::WasmExtension, + host: Arc, + ) { + self.slash_command_registry.register_command( + ExtensionSlashCommand { + command, + extension, + host, + }, + false, + ) + } + + fn register_docs_provider( + &self, + extension: wasm_host::WasmExtension, + host: Arc, + provider_id: Arc, + ) { + self.indexed_docs_registry.register_provider(Box::new( + extension_indexed_docs_provider::ExtensionIndexedDocsProvider { + extension, + host, + id: ProviderId(provider_id), + }, + )); + } + + fn register_snippets(&self, path: &PathBuf, snippet_contents: &str) -> Result<()> { + self.snippet_registry + .register_snippets(path, snippet_contents) + } + + fn update_lsp_status( + &self, + server_name: language::LanguageServerName, + status: LanguageServerBinaryStatus, + ) { + self.language_registry + .update_lsp_status(server_name, status); + } + + fn register_lsp_adapter( + &self, + language_name: language::LanguageName, + adapter: ExtensionLspAdapter, + ) { + self.language_registry + .register_lsp_adapter(language_name, Arc::new(adapter)); + } + + fn remove_lsp_adapter( + &self, + language_name: &language::LanguageName, + server_name: &language::LanguageServerName, + ) { + self.language_registry + .remove_lsp_adapter(language_name, server_name); + } + + fn remove_languages( + &self, + languages_to_remove: &[language::LanguageName], + grammars_to_remove: &[Arc], + ) { + self.language_registry + .remove_languages(&languages_to_remove, &grammars_to_remove); + } + + fn register_wasm_grammars(&self, grammars: Vec<(Arc, PathBuf)>) { + self.language_registry.register_wasm_grammars(grammars) + } + + fn register_language( + &self, + language: language::LanguageName, + grammar: Option>, + matcher: language::LanguageMatcher, + load: Arc Result + 'static + Send + Sync>, + ) { + self.language_registry + .register_language(language, grammar, matcher, load) + } + + fn reload_current_theme(&self, cx: &mut AppContext) { + ThemeSettings::reload_current_theme(cx) + } + + fn list_theme_names(&self, path: PathBuf, fs: Arc) -> Task>> { + self.executor.spawn(async move { + let themes = theme::read_user_theme(&path, fs).await?; + Ok(themes.themes.into_iter().map(|theme| theme.name).collect()) + }) + } +} diff --git a/crates/extension_host/src/extension_slash_command.rs b/crates/extensions_ui/src/extension_slash_command.rs similarity index 96% rename from crates/extension_host/src/extension_slash_command.rs rename to crates/extensions_ui/src/extension_slash_command.rs index 0a10e9e1a2..28e13163d4 100644 --- a/crates/extension_host/src/extension_slash_command.rs +++ b/crates/extensions_ui/src/extension_slash_command.rs @@ -5,20 +5,20 @@ use assistant_slash_command::{ ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection, SlashCommandResult, }; -use futures::FutureExt; +use futures::FutureExt as _; use gpui::{Task, WeakView, WindowContext}; use language::{BufferSnapshot, LspAdapterDelegate}; use ui::prelude::*; use wasmtime_wasi::WasiView; use workspace::Workspace; -use crate::wasm_host::{WasmExtension, WasmHost}; +use extension_host::wasm_host::{WasmExtension, WasmHost}; pub struct ExtensionSlashCommand { pub(crate) extension: WasmExtension, #[allow(unused)] pub(crate) host: Arc, - pub(crate) command: crate::wit::SlashCommand, + pub(crate) command: extension_host::wasm_host::SlashCommand, } impl SlashCommand for ExtensionSlashCommand { diff --git a/crates/extension_host/src/extension_store_test.rs b/crates/extensions_ui/src/extension_store_test.rs similarity index 97% rename from crates/extension_host/src/extension_store_test.rs rename to crates/extensions_ui/src/extension_store_test.rs index bd571703db..364f2397dd 100644 --- a/crates/extension_host/src/extension_store_test.rs +++ b/crates/extensions_ui/src/extension_store_test.rs @@ -1,13 +1,13 @@ -use crate::extension_settings::ExtensionSettings; -use crate::{ +use assistant_slash_command::SlashCommandRegistry; +use async_compression::futures::bufread::GzipEncoder; +use collections::BTreeMap; +use extension_host::ExtensionSettings; +use extension_host::SchemaVersion; +use extension_host::{ Event, ExtensionIndex, ExtensionIndexEntry, ExtensionIndexLanguageEntry, ExtensionIndexThemeEntry, ExtensionManifest, ExtensionStore, GrammarManifestEntry, RELOAD_DEBOUNCE_DURATION, }; -use assistant_slash_command::SlashCommandRegistry; -use async_compression::futures::bufread::GzipEncoder; -use collections::BTreeMap; -use extension::SchemaVersion; use fs::{FakeFs, Fs, RealFs}; use futures::{io::BufReader, AsyncReadExt, StreamExt}; use gpui::{Context, SemanticVersion, TestAppContext}; @@ -267,24 +267,29 @@ async fn test_extension_store(cx: &mut TestAppContext) { let node_runtime = NodeRuntime::unavailable(); let store = cx.new_model(|cx| { + let extension_registration_hooks = crate::ConcreteExtensionRegistrationHooks::new( + theme_registry.clone(), + slash_command_registry.clone(), + indexed_docs_registry.clone(), + snippet_registry.clone(), + language_registry.clone(), + cx, + ); + ExtensionStore::new( PathBuf::from("/the-extension-dir"), None, + extension_registration_hooks, fs.clone(), http_client.clone(), http_client.clone(), None, node_runtime.clone(), - language_registry.clone(), - theme_registry.clone(), - slash_command_registry.clone(), - indexed_docs_registry.clone(), - snippet_registry.clone(), cx, ) }); - cx.executor().advance_clock(super::RELOAD_DEBOUNCE_DURATION); + cx.executor().advance_clock(RELOAD_DEBOUNCE_DURATION); store.read_with(cx, |store, _| { let index = &store.extension_index; assert_eq!(index.extensions, expected_index.extensions); @@ -395,19 +400,24 @@ async fn test_extension_store(cx: &mut TestAppContext) { // Create new extension store, as if Zed were restarting. drop(store); let store = cx.new_model(|cx| { + let extension_api = crate::ConcreteExtensionRegistrationHooks::new( + theme_registry.clone(), + slash_command_registry, + indexed_docs_registry, + snippet_registry, + language_registry.clone(), + cx, + ); + ExtensionStore::new( PathBuf::from("/the-extension-dir"), None, + extension_api, fs.clone(), http_client.clone(), http_client.clone(), None, node_runtime.clone(), - language_registry.clone(), - theme_registry.clone(), - slash_command_registry, - indexed_docs_registry, - snippet_registry, cx, ) }); @@ -580,19 +590,23 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) { Arc::new(ReqwestClient::user_agent(&user_agent).expect("Could not create HTTP client")); let extension_store = cx.new_model(|cx| { + let extension_api = crate::ConcreteExtensionRegistrationHooks::new( + theme_registry.clone(), + slash_command_registry, + indexed_docs_registry, + snippet_registry, + language_registry.clone(), + cx, + ); ExtensionStore::new( extensions_dir.clone(), Some(cache_dir), + extension_api, fs.clone(), extension_client.clone(), builder_client, None, node_runtime, - language_registry.clone(), - theme_registry.clone(), - slash_command_registry, - indexed_docs_registry, - snippet_registry, cx, ) }); @@ -602,7 +616,7 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) { let executor = cx.executor(); let _task = cx.executor().spawn(async move { while let Some(event) = events.next().await { - if let crate::Event::StartedReloading = event { + if let extension_host::Event::StartedReloading = event { executor.advance_clock(RELOAD_DEBOUNCE_DURATION); } } diff --git a/crates/extensions_ui/src/extensions_ui.rs b/crates/extensions_ui/src/extensions_ui.rs index e6386ffe4a..8952effaad 100644 --- a/crates/extensions_ui/src/extensions_ui.rs +++ b/crates/extensions_ui/src/extensions_ui.rs @@ -1,7 +1,15 @@ mod components; +mod extension_indexed_docs_provider; +mod extension_registration_hooks; +mod extension_slash_command; mod extension_suggest; mod extension_version_selector; +#[cfg(test)] +mod extension_store_test; + +pub use extension_registration_hooks::ConcreteExtensionRegistrationHooks; + use std::ops::DerefMut; use std::sync::OnceLock; use std::time::Duration; diff --git a/crates/indexed_docs/Cargo.toml b/crates/indexed_docs/Cargo.toml index 145abd445e..2ef9826c03 100644 --- a/crates/indexed_docs/Cargo.toml +++ b/crates/indexed_docs/Cargo.toml @@ -30,6 +30,7 @@ paths.workspace = true serde.workspace = true strum.workspace = true util.workspace = true +extension_host.workspace = true [dev-dependencies] indoc.workspace = true diff --git a/crates/indexed_docs/src/providers/rustdoc.rs b/crates/indexed_docs/src/providers/rustdoc.rs index d4a69f0ebf..faff9bc1a7 100644 --- a/crates/indexed_docs/src/providers/rustdoc.rs +++ b/crates/indexed_docs/src/providers/rustdoc.rs @@ -2,6 +2,7 @@ mod item; mod to_markdown; use cargo_metadata::MetadataCommand; +use extension_host::DocsDatabase; use futures::future::BoxFuture; pub use item::*; use parking_lot::RwLock; @@ -208,7 +209,7 @@ impl IndexedDocsProvider for DocsDotRsProvider { async fn index_rustdoc( package: PackageName, - database: Arc, + database: Arc, fetch_page: impl Fn(&PackageName, Option<&RustdocItem>) -> BoxFuture<'static, Result>> + Send + Sync, diff --git a/crates/indexed_docs/src/store.rs b/crates/indexed_docs/src/store.rs index e3383f7b6b..d693c33f80 100644 --- a/crates/indexed_docs/src/store.rs +++ b/crates/indexed_docs/src/store.rs @@ -324,8 +324,10 @@ impl IndexedDocsDatabase { Ok(any) }) } +} - pub fn insert(&self, key: String, docs: String) -> Task> { +impl extension_host::DocsDatabase for IndexedDocsDatabase { + fn insert(&self, key: String, docs: String) -> Task> { let env = self.env.clone(); let entries = self.entries; diff --git a/crates/language/src/language_registry.rs b/crates/language/src/language_registry.rs index 3df614e1f3..d8c2b0d510 100644 --- a/crates/language/src/language_registry.rs +++ b/crates/language/src/language_registry.rs @@ -288,14 +288,14 @@ impl LanguageRegistry { config.name.clone(), config.grammar.clone(), config.matcher.clone(), - move || { + Arc::new(move || { Ok(LoadedLanguage { config: config.clone(), queries: Default::default(), toolchain_provider: None, context_provider: None, }) - }, + }), ) } @@ -436,9 +436,8 @@ impl LanguageRegistry { name: LanguageName, grammar_name: Option>, matcher: LanguageMatcher, - load: impl Fn() -> Result + 'static + Send + Sync, + load: Arc Result + 'static + Send + Sync>, ) { - let load = Arc::new(load); let state = &mut *self.state.write(); for existing_language in &mut state.available_languages { diff --git a/crates/languages/src/lib.rs b/crates/languages/src/lib.rs index 455b05b64c..7bc9d5bd8c 100644 --- a/crates/languages/src/lib.rs +++ b/crates/languages/src/lib.rs @@ -61,14 +61,14 @@ pub fn init(languages: Arc, node_runtime: NodeRuntime, cx: &mu config.name.clone(), config.grammar.clone(), config.matcher.clone(), - move || { + Arc::new(move || { Ok(LoadedLanguage { config: config.clone(), queries: load_queries($name), context_provider: None, toolchain_provider: None, }) - }, + }), ); }; ($name:literal, $adapters:expr) => { @@ -82,14 +82,14 @@ pub fn init(languages: Arc, node_runtime: NodeRuntime, cx: &mu config.name.clone(), config.grammar.clone(), config.matcher.clone(), - move || { + Arc::new(move || { Ok(LoadedLanguage { config: config.clone(), queries: load_queries($name), context_provider: None, toolchain_provider: None, }) - }, + }), ); }; ($name:literal, $adapters:expr, $context_provider:expr) => { @@ -103,14 +103,14 @@ pub fn init(languages: Arc, node_runtime: NodeRuntime, cx: &mu config.name.clone(), config.grammar.clone(), config.matcher.clone(), - move || { + Arc::new(move || { Ok(LoadedLanguage { config: config.clone(), queries: load_queries($name), context_provider: Some(Arc::new($context_provider)), toolchain_provider: None, }) - }, + }), ); }; ($name:literal, $adapters:expr, $context_provider:expr, $toolchain_provider:expr) => { @@ -124,14 +124,14 @@ pub fn init(languages: Arc, node_runtime: NodeRuntime, cx: &mu config.name.clone(), config.grammar.clone(), config.matcher.clone(), - move || { + Arc::new(move || { Ok(LoadedLanguage { config: config.clone(), queries: load_queries($name), context_provider: Some(Arc::new($context_provider)), toolchain_provider: Some($toolchain_provider), }) - }, + }), ); }; } diff --git a/crates/paths/src/paths.rs b/crates/paths/src/paths.rs index e29a94b6a0..fd118a3c49 100644 --- a/crates/paths/src/paths.rs +++ b/crates/paths/src/paths.rs @@ -165,6 +165,22 @@ pub fn extensions_dir() -> &'static PathBuf { EXTENSIONS_DIR.get_or_init(|| support_dir().join("extensions")) } +/// Returns the path to the extensions directory. +/// +/// This is where installed extensions are stored on a remote. +pub fn remote_extensions_dir() -> &'static PathBuf { + static EXTENSIONS_DIR: OnceLock = OnceLock::new(); + EXTENSIONS_DIR.get_or_init(|| support_dir().join("remote_extensions")) +} + +/// Returns the path to the extensions directory. +/// +/// This is where installed extensions are stored on a remote. +pub fn remote_extensions_uploads_dir() -> &'static PathBuf { + static UPLOAD_DIR: OnceLock = OnceLock::new(); + UPLOAD_DIR.get_or_init(|| remote_extensions_dir().join("uploads")) +} + /// Returns the path to the themes directory. /// /// This is where themes that are not provided by extensions are stored. diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index f6466e9973..c50a676612 100644 --- a/crates/remote/src/ssh_session.rs +++ b/crates/remote/src/ssh_session.rs @@ -990,6 +990,19 @@ impl SshRemoteClient { .map(|ssh_connection| ssh_connection.ssh_args()) } + pub fn upload_directory( + &self, + src_path: PathBuf, + dest_path: PathBuf, + cx: &AppContext, + ) -> Task> { + let state = self.state.lock(); + let Some(connection) = state.as_ref().and_then(|state| state.ssh_connection()) else { + return Task::ready(Err(anyhow!("no ssh connection"))); + }; + connection.upload_directory(src_path, dest_path, cx) + } + pub fn proto_client(&self) -> AnyProtoClient { self.client.clone().into() } @@ -1194,6 +1207,12 @@ trait RemoteConnection: Send + Sync { delegate: Arc, cx: &mut AsyncAppContext, ) -> Task>; + fn upload_directory( + &self, + src_path: PathBuf, + dest_path: PathBuf, + cx: &AppContext, + ) -> Task>; async fn kill(&self) -> Result<()>; fn has_been_killed(&self) -> bool; fn ssh_args(&self) -> Vec; @@ -1232,6 +1251,49 @@ impl RemoteConnection for SshRemoteConnection { fn connection_options(&self) -> SshConnectionOptions { self.socket.connection_options.clone() } + + fn upload_directory( + &self, + src_path: PathBuf, + dest_path: PathBuf, + cx: &AppContext, + ) -> Task> { + let mut command = process::Command::new("scp"); + let output = self + .socket + .ssh_options(&mut command) + .args( + self.socket + .connection_options + .port + .map(|port| vec!["-P".to_string(), port.to_string()]) + .unwrap_or_default(), + ) + .arg("-r") + .arg(&src_path) + .arg(format!( + "{}:{}", + self.socket.connection_options.scp_url(), + dest_path.display() + )) + .output(); + + cx.background_executor().spawn(async move { + let output = output.await?; + + if !output.status.success() { + return Err(anyhow!( + "failed to upload directory {} -> {}: {}", + src_path.display(), + dest_path.display(), + String::from_utf8_lossy(&output.stderr) + )); + } + + Ok(()) + }) + } + fn start_proxy( &self, unique_identifier: String, @@ -2286,7 +2348,7 @@ mod fake { }, select_biased, FutureExt, SinkExt, StreamExt, }; - use gpui::{AsyncAppContext, SemanticVersion, Task, TestAppContext}; + use gpui::{AppContext, AsyncAppContext, SemanticVersion, Task, TestAppContext}; use release_channel::ReleaseChannel; use rpc::proto::Envelope; @@ -2330,6 +2392,14 @@ mod fake { fn ssh_args(&self) -> Vec { Vec::new() } + fn upload_directory( + &self, + _src_path: PathBuf, + _dest_path: PathBuf, + _cx: &AppContext, + ) -> Task> { + unreachable!() + } fn connection_options(&self) -> SshConnectionOptions { self.connection_options.clone() diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index 773f6b381f..f2e691f2ef 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -78,7 +78,7 @@ impl HeadlessProject { }); let prettier_store = cx.new_model(|cx| { PrettierStore::new( - node_runtime, + node_runtime.clone(), fs.clone(), languages.clone(), worktree_store.clone(), @@ -124,7 +124,7 @@ impl HeadlessProject { toolchain_store.clone(), environment, languages.clone(), - http_client, + http_client.clone(), fs.clone(), cx, ); diff --git a/crates/remote_server/src/remote_editing_tests.rs b/crates/remote_server/src/remote_editing_tests.rs index a09d87f7c0..530e1c43e3 100644 --- a/crates/remote_server/src/remote_editing_tests.rs +++ b/crates/remote_server/src/remote_editing_tests.rs @@ -1187,6 +1187,9 @@ pub async fn init_test( cx.update(|cx| { release_channel::init(SemanticVersion::default(), cx); }); + server_cx.update(|cx| { + release_channel::init(SemanticVersion::default(), cx); + }); init_logger(); let (opts, ssh_server_client) = SshRemoteClient::fake_server(cx, server_cx); diff --git a/crates/remote_server/src/unix.rs b/crates/remote_server/src/unix.rs index 24c79dff9d..467fd452f8 100644 --- a/crates/remote_server/src/unix.rs +++ b/crates/remote_server/src/unix.rs @@ -14,6 +14,7 @@ use node_runtime::{NodeBinaryOptions, NodeRuntime}; use paths::logs_dir; use project::project_settings::ProjectSettings; +use release_channel::AppVersion; use remote::proxy::ProxyLaunchError; use remote::ssh_session::ChannelClient; use remote::{ @@ -377,6 +378,8 @@ fn init_paths() -> anyhow::Result<()> { paths::languages_dir(), paths::logs_dir(), paths::temp_dir(), + paths::remote_extensions_dir(), + paths::remote_extensions_uploads_dir(), ] .iter() { @@ -418,6 +421,9 @@ pub fn execute_run( let git_hosting_provider_registry = Arc::new(GitHostingProviderRegistry::new()); gpui::App::headless().run(move |cx| { settings::init(cx); + let app_version = AppVersion::init(env!("ZED_PKG_VERSION")); + release_channel::init(app_version, cx); + HeadlessProject::init(cx); log::info!("gpui app started, initializing server"); diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index 53d49fe607..390fa6d174 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -357,7 +357,7 @@ impl PaneAxis { pub fn load(axis: Axis, members: Vec, flexes: Option>) -> Self { let flexes = flexes.unwrap_or_else(|| vec![1.; members.len()]); - debug_assert!(members.len() == flexes.len()); + // debug_assert!(members.len() == flexes.len()); let flexes = Arc::new(Mutex::new(flexes)); let bounding_boxes = Arc::new(Mutex::new(vec![None; members.len()])); diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 009376e843..bebd045ba2 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -15,6 +15,7 @@ name = "zed" path = "src/main.rs" [dependencies] +assistant_slash_command.workspace = true activity_indicator.workspace = true anyhow.workspace = true assets.workspace = true @@ -53,6 +54,7 @@ go_to_line.workspace = true gpui = { workspace = true, features = ["wayland", "x11", "font-kit"] } http_client.workspace = true image_viewer.workspace = true +indexed_docs.workspace = true inline_completion_button.workspace = true install_cli.workspace = true journal.workspace = true diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 464b7ac706..e5f7817aec 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -7,6 +7,7 @@ mod reliability; mod zed; use anyhow::{anyhow, Context as _, Result}; +use assistant_slash_command::SlashCommandRegistry; use chrono::Offset; use clap::{command, Parser}; use cli::FORCE_CLI_MODE_ENV_VAR_NAME; @@ -23,6 +24,7 @@ use gpui::{ VisualContext, }; use http_client::{read_proxy_from_env, Uri}; +use indexed_docs::IndexedDocsRegistry; use language::LanguageRegistry; use log::LevelFilter; use reqwest_client::ReqwestClient; @@ -39,6 +41,7 @@ use settings::{ }; use simplelog::ConfigBuilder; use smol::process::Command; +use snippet_provider::SnippetRegistry; use std::{ env, fs::OpenOptions, @@ -402,12 +405,19 @@ fn main() { app_state.client.telemetry().clone(), cx, ); + let api = extensions_ui::ConcreteExtensionRegistrationHooks::new( + ThemeRegistry::global(cx), + SlashCommandRegistry::global(cx), + IndexedDocsRegistry::global(cx), + SnippetRegistry::global(cx), + app_state.languages.clone(), + cx, + ); extension_host::init( + api, app_state.fs.clone(), app_state.client.clone(), app_state.node_runtime.clone(), - app_state.languages.clone(), - ThemeRegistry::global(cx), cx, ); recent_projects::init(cx); diff --git a/crates/zed/src/reliability.rs b/crates/zed/src/reliability.rs index b02afb8c0d..681cc9834f 100644 --- a/crates/zed/src/reliability.rs +++ b/crates/zed/src/reliability.rs @@ -495,10 +495,7 @@ async fn upload_panic( ) -> Result { *most_recent_panic = Some((panic.panicked_on, panic.payload.clone())); - let json_bytes = serde_json::to_vec(&PanicRequest { - panic: panic.clone(), - }) - .unwrap(); + let json_bytes = serde_json::to_vec(&PanicRequest { panic }).unwrap(); let Some(checksum) = client::telemetry::calculate_json_checksum(&json_bytes) else { return Ok(false);