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 <mikayla@zed.dev>
Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
Co-authored-by: Marshall <marshall@zed.dev>
This commit is contained in:
Conrad Irwin 2024-11-06 10:06:25 -07:00 committed by GitHub
parent f22e56ff42
commit 608addf641
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 675 additions and 236 deletions

26
Cargo.lock generated
View file

@ -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",

View file

@ -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;

View file

@ -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(())

View file

@ -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"] }

View file

@ -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(),

View file

@ -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,

View file

@ -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) => {

View file

@ -148,7 +148,7 @@ impl ExtensionImports for WasmState {
};
self.host
.language_registry
.registration_hooks
.update_lsp_status(language::LanguageServerName(server_name.into()), status);
Ok(())
}

View file

@ -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(())
}

View file

@ -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(())
}

View file

@ -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"] }

View file

@ -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,

View file

@ -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())
})
}
}

View file

@ -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 {

View file

@ -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);
}
}

View file

@ -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;

View file

@ -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

View file

@ -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,

View file

@ -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;

View file

@ -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 {

View file

@ -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),
})
},
}),
);
};
}

View file

@ -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.

View file

@ -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()

View file

@ -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,
);

View file

@ -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);

View file

@ -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");

View file

@ -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()]));

View file

@ -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

View file

@ -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);

View file

@ -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);