Detailed changes
@@ -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",
@@ -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;
@@ -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(())
@@ -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"] }
@@ -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<Result<()>>;
+}
+
+pub trait ExtensionRegistrationHooks: Send + Sync + 'static {
+ fn remove_user_themes(&self, _themes: Vec<SharedString>) {}
+
+ fn load_user_theme(&self, _theme_path: PathBuf, _fs: Arc<dyn Fs>) -> Task<Result<()>> {
+ Task::ready(Ok(()))
+ }
+
+ fn list_theme_names(
+ &self,
+ _theme_path: PathBuf,
+ _fs: Arc<dyn Fs>,
+ ) -> Task<Result<Vec<String>>> {
+ Task::ready(Ok(Vec::new()))
+ }
+
+ fn reload_current_theme(&self, _cx: &mut AppContext) {}
+
+ fn register_language(
+ &self,
+ _language: LanguageName,
+ _grammar: Option<Arc<str>>,
+ _matcher: language::LanguageMatcher,
+ _load: Arc<dyn Fn() -> Result<LoadedLanguage> + '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<str>, PathBuf)>) {}
+
+ fn remove_languages(
+ &self,
+ _languages_to_remove: &[LanguageName],
+ _grammars_to_remove: &[Arc<str>],
+ ) {
+ }
+
+ fn register_slash_command(
+ &self,
+ _slash_command: wit::SlashCommand,
+ _extension: WasmExtension,
+ _host: Arc<WasmHost>,
+ ) {
+ }
+
+ fn register_docs_provider(
+ &self,
+ _extension: WasmExtension,
+ _host: Arc<WasmHost>,
+ _provider_id: Arc<str>,
+ ) {
+ }
+
+ 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<ExtensionBuilder>,
- extension_index: ExtensionIndex,
- fs: Arc<dyn Fs>,
- http_client: Arc<HttpClientWithUrl>,
- telemetry: Option<Arc<Telemetry>>,
- reload_tx: UnboundedSender<Option<Arc<str>>>,
- reload_complete_senders: Vec<oneshot::Sender<()>>,
- installed_dir: PathBuf,
- outstanding_operations: BTreeMap<Arc<str>, ExtensionOperation>,
- index_path: PathBuf,
- language_registry: Arc<LanguageRegistry>,
- theme_registry: Arc<ThemeRegistry>,
- slash_command_registry: Arc<SlashCommandRegistry>,
- indexed_docs_registry: Arc<IndexedDocsRegistry>,
- snippet_registry: Arc<SnippetRegistry>,
- modified_extensions: HashSet<Arc<str>>,
- wasm_host: Arc<WasmHost>,
- wasm_extensions: Vec<(Arc<ExtensionManifest>, WasmExtension)>,
- tasks: Vec<Task<()>>,
+ pub registration_hooks: Arc<dyn ExtensionRegistrationHooks>,
+ pub builder: Arc<ExtensionBuilder>,
+ pub extension_index: ExtensionIndex,
+ pub fs: Arc<dyn Fs>,
+ pub http_client: Arc<HttpClientWithUrl>,
+ pub telemetry: Option<Arc<Telemetry>>,
+ pub reload_tx: UnboundedSender<Option<Arc<str>>>,
+ pub reload_complete_senders: Vec<oneshot::Sender<()>>,
+ pub installed_dir: PathBuf,
+ pub outstanding_operations: BTreeMap<Arc<str>, ExtensionOperation>,
+ pub index_path: PathBuf,
+ pub modified_extensions: HashSet<Arc<str>>,
+ pub wasm_host: Arc<WasmHost>,
+ pub wasm_extensions: Vec<(Arc<ExtensionManifest>, WasmExtension)>,
+ pub tasks: Vec<Task<()>>,
}
#[derive(Clone, Copy)]
@@ -158,26 +219,25 @@ pub struct ExtensionIndexEntry {
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Deserialize, Serialize)]
pub struct ExtensionIndexThemeEntry {
- extension: Arc<str>,
- path: PathBuf,
+ pub extension: Arc<str>,
+ pub path: PathBuf,
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Deserialize, Serialize)]
pub struct ExtensionIndexLanguageEntry {
- extension: Arc<str>,
- path: PathBuf,
- matcher: LanguageMatcher,
- grammar: Option<Arc<str>>,
+ pub extension: Arc<str>,
+ pub path: PathBuf,
+ pub matcher: LanguageMatcher,
+ pub grammar: Option<Arc<str>>,
}
actions!(zed, [ReloadExtensions]);
pub fn init(
+ registration_hooks: Arc<dyn ExtensionRegistrationHooks>,
fs: Arc<dyn Fs>,
client: Arc<Client>,
node_runtime: NodeRuntime,
- language_registry: Arc<LanguageRegistry>,
- theme_registry: Arc<ThemeRegistry>,
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<PathBuf>,
+ extension_api: Arc<dyn ExtensionRegistrationHooks>,
fs: Arc<dyn Fs>,
http_client: Arc<HttpClientWithUrl>,
builder_client: Arc<dyn HttpClient>,
telemetry: Option<Arc<Telemetry>>,
node_runtime: NodeRuntime,
- language_registry: Arc<LanguageRegistry>,
- theme_registry: Arc<ThemeRegistry>,
- slash_command_registry: Arc<SlashCommandRegistry>,
- indexed_docs_registry: Arc<IndexedDocsRegistry>,
- snippet_registry: Arc<SnippetRegistry>,
cx: &mut ModelContext<Self>,
) -> 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<Arc<str>>,
cx: &mut ModelContext<Self>,
@@ -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<dyn Fs>,
extension_dir: PathBuf,
index: &mut ExtensionIndex,
+ extension_api: Arc<dyn ExtensionRegistrationHooks>,
) -> 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(),
@@ -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<dyn HttpClient>,
node_runtime: NodeRuntime,
- pub(crate) language_registry: Arc<LanguageRegistry>,
+ pub registration_hooks: Arc<dyn ExtensionRegistrationHooks>,
fs: Arc<dyn Fs>,
- pub(crate) work_dir: PathBuf,
+ pub work_dir: PathBuf,
_main_thread_message_task: Task<()>,
main_thread_message_tx: mpsc::UnboundedSender<MainThreadCall>,
}
@@ -44,16 +44,16 @@ pub(crate) struct WasmHost {
#[derive(Clone)]
pub struct WasmExtension {
tx: UnboundedSender<ExtensionCall>,
- pub(crate) manifest: Arc<ExtensionManifest>,
+ pub manifest: Arc<ExtensionManifest>,
#[allow(unused)]
pub zed_api_version: SemanticVersion,
}
-pub(crate) struct WasmState {
+pub struct WasmState {
manifest: Arc<ExtensionManifest>,
- pub(crate) table: ResourceTable,
+ pub table: ResourceTable,
ctx: wasi::WasiCtx,
- pub(crate) host: Arc<WasmHost>,
+ pub host: Arc<WasmHost>,
}
type MainThreadCall =
@@ -81,7 +81,7 @@ impl WasmHost {
fs: Arc<dyn Fs>,
http_client: Arc<dyn HttpClient>,
node_runtime: NodeRuntime,
- language_registry: Arc<LanguageRegistry>,
+ registration_hooks: Arc<dyn ExtensionRegistrationHooks>,
work_dir: PathBuf,
cx: &mut AppContext,
) -> Arc<Self> {
@@ -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<Self>,
wasm_bytes: Vec<u8>,
- manifest: Arc<ExtensionManifest>,
+ manifest: &Arc<ExtensionManifest>,
executor: BackgroundExecutor,
) -> Task<Result<WasmExtension>> {
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<SemanticVersion> {
+ let mut version = None;
+
+ for part in wasmparser::Parser::new(0).parse_all(wasm_bytes) {
+ if let wasmparser::Payload::CustomSection(s) =
+ part.context("error parsing wasm extension")?
+ {
+ if s.name() == "zed:api-version" {
+ version = parse_wasm_extension_version_custom_section(s.data());
+ if version.is_none() {
+ bail!(
+ "extension {} has invalid zed:api-version section: {:?}",
+ extension_id,
+ s.data()
+ );
+ }
+ }
+ }
+ }
+
+ // The reason we wait until we're done parsing all of the Wasm bytes to return the version
+ // is to work around a panic that can happen inside of Wasmtime when the bytes are invalid.
+ //
+ // By parsing the entirety of the Wasm bytes before we return, we're able to detect this problem
+ // earlier as an `Err` rather than as a panic.
+ version.ok_or_else(|| anyhow!("extension {} has no zed:api-version section", extension_id))
+}
+
+fn parse_wasm_extension_version_custom_section(data: &[u8]) -> Option<SemanticVersion> {
+ if data.len() == 6 {
+ Some(SemanticVersion::new(
+ u16::from_be_bytes([data[0], data[1]]) as _,
+ u16::from_be_bytes([data[2], data[3]]) as _,
+ u16::from_be_bytes([data[4], data[5]]) as _,
+ ))
+ } else {
+ None
+ }
+}
+
impl WasmExtension {
+ pub async fn load(
+ extension_dir: PathBuf,
+ manifest: &Arc<ExtensionManifest>,
+ wasm_host: Arc<WasmHost>,
+ cx: &AsyncAppContext,
+ ) -> Result<Self> {
+ 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<T, Fn>(&self, f: Fn) -> T
where
T: 'static + Send,
@@ -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<WasmState>,
provider: &str,
package_name: &str,
- database: Resource<Arc<IndexedDocsDatabase>>,
+ database: Resource<Arc<dyn DocsDatabase>>,
) -> Result<Result<(), String>> {
match self {
Extension::V020(ext) => {
@@ -148,7 +148,7 @@ impl ExtensionImports for WasmState {
};
self.host
- .language_registry
+ .registration_hooks
.update_lsp_status(language::LanguageServerName(server_name.into()), status);
Ok(())
}
@@ -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<dyn LspAdapterDelegate>;
-pub type ExtensionKeyValueStore = Arc<IndexedDocsDatabase>;
+pub type ExtensionKeyValueStore = Arc<dyn DocsDatabase>;
pub type ExtensionHttpResponseStream = Arc<Mutex<::http_client::Response<AsyncBody>>>;
pub fn linker() -> &'static Linker<WasmState> {
@@ -512,7 +512,7 @@ impl ExtensionImports for WasmState {
};
self.host
- .language_registry
+ .registration_hooks
.update_lsp_status(language::LanguageServerName(server_name.into()), status);
Ok(())
}
@@ -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<dyn LspAdapterDelegate>;
-pub type ExtensionKeyValueStore = Arc<IndexedDocsDatabase>;
+pub type ExtensionKeyValueStore = Arc<dyn DocsDatabase>;
pub type ExtensionHttpResponseStream = Arc<Mutex<::http_client::Response<AsyncBody>>>;
pub fn linker() -> &'static Linker<WasmState> {
@@ -459,7 +459,7 @@ impl ExtensionImports for WasmState {
};
self.host
- .language_registry
+ .registration_hooks
.update_lsp_status(language::LanguageServerName(server_name.into()), status);
Ok(())
}
@@ -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"] }
@@ -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,
@@ -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<SlashCommandRegistry>,
+ theme_registry: Arc<ThemeRegistry>,
+ indexed_docs_registry: Arc<IndexedDocsRegistry>,
+ snippet_registry: Arc<SnippetRegistry>,
+ language_registry: Arc<LanguageRegistry>,
+ executor: BackgroundExecutor,
+}
+
+impl ConcreteExtensionRegistrationHooks {
+ pub fn new(
+ theme_registry: Arc<ThemeRegistry>,
+ slash_command_registry: Arc<SlashCommandRegistry>,
+ indexed_docs_registry: Arc<IndexedDocsRegistry>,
+ snippet_registry: Arc<SnippetRegistry>,
+ language_registry: Arc<LanguageRegistry>,
+ cx: &AppContext,
+ ) -> Arc<dyn extension_host::ExtensionRegistrationHooks> {
+ 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<SharedString>) {
+ self.theme_registry.remove_user_themes(&themes);
+ }
+
+ fn load_user_theme(&self, theme_path: PathBuf, fs: Arc<dyn fs::Fs>) -> Task<Result<()>> {
+ 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<wasm_host::WasmHost>,
+ ) {
+ self.slash_command_registry.register_command(
+ ExtensionSlashCommand {
+ command,
+ extension,
+ host,
+ },
+ false,
+ )
+ }
+
+ fn register_docs_provider(
+ &self,
+ extension: wasm_host::WasmExtension,
+ host: Arc<wasm_host::WasmHost>,
+ provider_id: Arc<str>,
+ ) {
+ 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<str>],
+ ) {
+ self.language_registry
+ .remove_languages(&languages_to_remove, &grammars_to_remove);
+ }
+
+ fn register_wasm_grammars(&self, grammars: Vec<(Arc<str>, PathBuf)>) {
+ self.language_registry.register_wasm_grammars(grammars)
+ }
+
+ fn register_language(
+ &self,
+ language: language::LanguageName,
+ grammar: Option<Arc<str>>,
+ matcher: language::LanguageMatcher,
+ load: Arc<dyn Fn() -> Result<LoadedLanguage> + '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<dyn Fs>) -> Task<Result<Vec<String>>> {
+ self.executor.spawn(async move {
+ let themes = theme::read_user_theme(&path, fs).await?;
+ Ok(themes.themes.into_iter().map(|theme| theme.name).collect())
+ })
+ }
+}
@@ -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<WasmHost>,
- pub(crate) command: crate::wit::SlashCommand,
+ pub(crate) command: extension_host::wasm_host::SlashCommand,
}
impl SlashCommand for ExtensionSlashCommand {
@@ -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);
}
}
@@ -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;
@@ -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
@@ -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<IndexedDocsDatabase>,
+ database: Arc<dyn DocsDatabase>,
fetch_page: impl Fn(&PackageName, Option<&RustdocItem>) -> BoxFuture<'static, Result<Option<String>>>
+ Send
+ Sync,
@@ -324,8 +324,10 @@ impl IndexedDocsDatabase {
Ok(any)
})
}
+}
- pub fn insert(&self, key: String, docs: String) -> Task<Result<()>> {
+impl extension_host::DocsDatabase for IndexedDocsDatabase {
+ fn insert(&self, key: String, docs: String) -> Task<Result<()>> {
let env = self.env.clone();
let entries = self.entries;
@@ -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<Arc<str>>,
matcher: LanguageMatcher,
- load: impl Fn() -> Result<LoadedLanguage> + 'static + Send + Sync,
+ load: Arc<dyn Fn() -> Result<LoadedLanguage> + 'static + Send + Sync>,
) {
- let load = Arc::new(load);
let state = &mut *self.state.write();
for existing_language in &mut state.available_languages {
@@ -61,14 +61,14 @@ pub fn init(languages: Arc<LanguageRegistry>, 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<LanguageRegistry>, 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<LanguageRegistry>, 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<LanguageRegistry>, 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),
})
- },
+ }),
);
};
}
@@ -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<PathBuf> = 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<PathBuf> = 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.
@@ -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<Result<()>> {
+ 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<dyn SshClientDelegate>,
cx: &mut AsyncAppContext,
) -> Task<Result<i32>>;
+ fn upload_directory(
+ &self,
+ src_path: PathBuf,
+ dest_path: PathBuf,
+ cx: &AppContext,
+ ) -> Task<Result<()>>;
async fn kill(&self) -> Result<()>;
fn has_been_killed(&self) -> bool;
fn ssh_args(&self) -> Vec<String>;
@@ -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<Result<()>> {
+ 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<String> {
Vec::new()
}
+ fn upload_directory(
+ &self,
+ _src_path: PathBuf,
+ _dest_path: PathBuf,
+ _cx: &AppContext,
+ ) -> Task<Result<()>> {
+ unreachable!()
+ }
fn connection_options(&self) -> SshConnectionOptions {
self.connection_options.clone()
@@ -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,
);
@@ -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);
@@ -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");
@@ -357,7 +357,7 @@ impl PaneAxis {
pub fn load(axis: Axis, members: Vec<Member>, flexes: Option<Vec<f32>>) -> 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()]));
@@ -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
@@ -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);
@@ -495,10 +495,7 @@ async fn upload_panic(
) -> Result<bool> {
*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);