pub mod extension_settings;pub mod headless_host;pub mod wasm_host; #[cfg(test)]mod extension_store_test; use anyhow::{anyhow, bail, Context as _, Result};use async_compression::futures::bufread::GzipDecoder;use async_tar::Archive;use client::ExtensionProvides;use client::{proto, telemetry::Telemetry, Client, ExtensionMetadata, GetExtensionsResponse};use collections::{btree_map, BTreeMap, BTreeSet, HashMap, HashSet};use extension::extension_builder::{CompileExtensionOptions, ExtensionBuilder};pub use extension::ExtensionManifest;use extension::{ ExtensionContextServerProxy, ExtensionGrammarProxy, ExtensionHostProxy, ExtensionIndexedDocsProviderProxy, ExtensionLanguageProxy, ExtensionLanguageServerProxy, ExtensionSlashCommandProxy, ExtensionSnippetProxy, ExtensionThemeProxy,};use fs::{Fs, RemoveOptions};use futures::{ channel::{ mpsc::{unbounded, UnboundedSender}, oneshot, }, io::BufReader, select_biased, AsyncReadExt as _, Future, FutureExt as _, StreamExt as _,};use gpui::{ actions, App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Global, Task, WeakEntity,};use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};use language::{ LanguageConfig, LanguageMatcher, LanguageName, LanguageQueries, LoadedLanguage, Rope, QUERY_FILENAME_PREFIXES,};use node_runtime::NodeRuntime;use project::ContextProviderWithTasks;use release_channel::ReleaseChannel;use remote::SshRemoteClient;use semantic_version::SemanticVersion;use serde::{Deserialize, Serialize};use settings::Settings;use std::ops::RangeInclusive;use std::str::FromStr;use std::{ cmp::Ordering, path::{self, Path, PathBuf}, sync::Arc, time::{Duration, Instant},};use url::Url;use util::ResultExt;use wasm_host::{ wit::{is_supported_wasm_api_version, wasm_api_version_range}, WasmExtension, WasmHost,}; pub use extension::{ ExtensionLibraryKind, GrammarManifestEntry, OldExtensionManifest, SchemaVersion,};pub use extension_settings::ExtensionSettings; 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.const CURRENT_SCHEMA_VERSION: SchemaVersion = SchemaVersion(1); /// Returns the [`SchemaVersion`] range that is compatible with this version of Zed.pub fn schema_version_range() -> RangeInclusive<SchemaVersion> { SchemaVersion::ZERO..=CURRENT_SCHEMA_VERSION} /// Returns whether the given extension version is compatible with this version of Zed.pub fn is_version_compatible( release_channel: ReleaseChannel, extension_version: &ExtensionMetadata,) -> bool { let schema_version = extension_version.manifest.schema_version.unwrap_or(0); if CURRENT_SCHEMA_VERSION.0 < schema_version { return false; } if let Some(wasm_api_version) = extension_version .manifest .wasm_api_version .as_ref() .and_then(|wasm_api_version| SemanticVersion::from_str(wasm_api_version).ok()) { if !is_supported_wasm_api_version(release_channel, wasm_api_version) { return false; } } true} pub struct ExtensionStore { pub proxy: Arc<ExtensionHostProxy>, 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<()>>, pub ssh_clients: HashMap<String, WeakEntity<SshRemoteClient>>, pub ssh_registered_tx: UnboundedSender<()>,} #[derive(Clone, Copy)]pub enum ExtensionOperation { Upgrade, Install, Remove,} #[derive(Clone)]pub enum Event { ExtensionsUpdated, StartedReloading, ExtensionInstalled(Arc<str>), ExtensionFailedToLoad(Arc<str>),} impl EventEmitter<Event> for ExtensionStore {} struct GlobalExtensionStore(Entity<ExtensionStore>); impl Global for GlobalExtensionStore {} #[derive(Debug, Deserialize, Serialize, Default, PartialEq, Eq)]pub struct ExtensionIndex { pub extensions: BTreeMap<Arc<str>, ExtensionIndexEntry>, pub themes: BTreeMap<Arc<str>, ExtensionIndexThemeEntry>, #[serde(default)] pub icon_themes: BTreeMap<Arc<str>, ExtensionIndexIconThemeEntry>, pub languages: BTreeMap<LanguageName, ExtensionIndexLanguageEntry>,} #[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]pub struct ExtensionIndexEntry { pub manifest: Arc<ExtensionManifest>, pub dev: bool,} #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Deserialize, Serialize)]pub struct ExtensionIndexThemeEntry { pub extension: Arc<str>, pub path: PathBuf,} #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Deserialize, Serialize)]pub struct ExtensionIndexIconThemeEntry { pub extension: Arc<str>, pub path: PathBuf,} #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Deserialize, Serialize)]pub struct ExtensionIndexLanguageEntry { pub extension: Arc<str>, pub path: PathBuf, pub matcher: LanguageMatcher, pub hidden: bool, pub grammar: Option<Arc<str>>,} actions!(zed, [ReloadExtensions]); pub fn init( extension_host_proxy: Arc<ExtensionHostProxy>, fs: Arc<dyn Fs>, client: Arc<Client>, node_runtime: NodeRuntime, cx: &mut App,) { ExtensionSettings::register(cx); let store = cx.new(move |cx| { ExtensionStore::new( paths::extensions_dir().clone(), None, extension_host_proxy, fs, client.http_client().clone(), client.http_client().clone(), Some(client.telemetry().clone()), node_runtime, cx, ) }); cx.on_action(|_: &ReloadExtensions, cx| { let store = cx.global::<GlobalExtensionStore>().0.clone(); store.update(cx, |store, cx| drop(store.reload(None, cx))); }); cx.set_global(GlobalExtensionStore(store));} impl ExtensionStore { pub fn try_global(cx: &App) -> Option<Entity<Self>> { cx.try_global::<GlobalExtensionStore>() .map(|store| store.0.clone()) } pub fn global(cx: &App) -> Entity<Self> { cx.global::<GlobalExtensionStore>().0.clone() } #[allow(clippy::too_many_arguments)] pub fn new( extensions_dir: PathBuf, build_dir: Option<PathBuf>, extension_host_proxy: Arc<ExtensionHostProxy>, fs: Arc<dyn Fs>, http_client: Arc<HttpClientWithUrl>, builder_client: Arc<dyn HttpClient>, telemetry: Option<Arc<Telemetry>>, node_runtime: NodeRuntime, cx: &mut Context<Self>, ) -> Self { let work_dir = extensions_dir.join("work"); let build_dir = build_dir.unwrap_or_else(|| extensions_dir.join("build")); let installed_dir = extensions_dir.join("installed"); let index_path = extensions_dir.join("index.json"); let (reload_tx, mut reload_rx) = unbounded(); let (connection_registered_tx, mut connection_registered_rx) = unbounded(); let mut this = Self { proxy: extension_host_proxy.clone(), extension_index: Default::default(), installed_dir, index_path, builder: Arc::new(ExtensionBuilder::new(builder_client, build_dir)), outstanding_operations: Default::default(), modified_extensions: Default::default(), reload_complete_senders: Vec::new(), wasm_host: WasmHost::new( fs.clone(), http_client.clone(), node_runtime, extension_host_proxy, work_dir, cx, ), wasm_extensions: Vec::new(), fs, http_client, telemetry, reload_tx, tasks: Vec::new(), ssh_clients: HashMap::default(), ssh_registered_tx: connection_registered_tx, }; // The extensions store maintains an index file, which contains a complete // list of the installed extensions and the resources that they provide. // This index is loaded synchronously on startup. let (index_content, index_metadata, extensions_metadata) = cx.background_executor().block(async { futures::join!( this.fs.load(&this.index_path), this.fs.metadata(&this.index_path), this.fs.metadata(&this.installed_dir), ) }); // Normally, there is no need to rebuild the index. But if the index file // is invalid or is out-of-date according to the filesystem mtimes, then // it must be asynchronously rebuilt. let mut extension_index = ExtensionIndex::default(); let mut extension_index_needs_rebuild = true; if let Ok(index_content) = index_content { if let Some(index) = serde_json::from_str(&index_content).log_err() { extension_index = index; if let (Ok(Some(index_metadata)), Ok(Some(extensions_metadata))) = (index_metadata, extensions_metadata) { if index_metadata .mtime .bad_is_greater_than(extensions_metadata.mtime) { extension_index_needs_rebuild = false; } } } } // Immediately load all of the extensions in the initial manifest. If the // index needs to be rebuild, then enqueue let load_initial_extensions = this.extensions_updated(extension_index, cx); let mut reload_future = None; if extension_index_needs_rebuild { reload_future = Some(this.reload(None, cx)); } cx.spawn(|this, mut cx| async move { if let Some(future) = reload_future { future.await; } this.update(&mut cx, |this, cx| this.auto_install_extensions(cx)) .ok(); this.update(&mut cx, |this, cx| this.check_for_updates(cx)) .ok(); }) .detach(); // Perform all extension loading in a single task to ensure that we // never attempt to simultaneously load/unload extensions from multiple // parallel tasks. this.tasks.push(cx.spawn(|this, mut cx| { async move { load_initial_extensions.await; let mut index_changed = false; let mut debounce_timer = cx.background_spawn(futures::future::pending()).fuse(); loop { select_biased! { _ = debounce_timer => { 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; } Self::update_ssh_clients(&this, &mut cx).await?; } _ = connection_registered_rx.next() => { debounce_timer = cx .background_executor() .timer(RELOAD_DEBOUNCE_DURATION) .fuse(); } 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) .fuse(); } } } anyhow::Ok(()) } .map(drop) })); // Watch the installed extensions directory for changes. Whenever changes are // detected, rebuild the extension index, and load/unload any extensions that // have been added, removed, or modified. this.tasks.push(cx.background_spawn({ let fs = this.fs.clone(); let reload_tx = this.reload_tx.clone(); let installed_dir = this.installed_dir.clone(); async move { let (mut paths, _) = fs.watch(&installed_dir, FS_WATCH_LATENCY).await; while let Some(events) = paths.next().await { for event in events { let Ok(event_path) = event.path.strip_prefix(&installed_dir) else { continue; }; if let Some(path::Component::Normal(extension_dir_name)) = event_path.components().next() { if let Some(extension_id) = extension_dir_name.to_str() { reload_tx.unbounded_send(Some(extension_id.into())).ok(); } } } } } })); this } pub fn reload( &mut self, modified_extension: Option<Arc<str>>, cx: &mut Context<Self>, ) -> impl Future<Output = ()> { let (tx, rx) = oneshot::channel(); self.reload_complete_senders.push(tx); self.reload_tx .unbounded_send(modified_extension) .expect("reload task exited"); cx.emit(Event::StartedReloading); async move { rx.await.ok(); } } fn extensions_dir(&self) -> PathBuf { self.installed_dir.clone() } pub fn outstanding_operations(&self) -> &BTreeMap<Arc<str>, ExtensionOperation> { &self.outstanding_operations } pub fn installed_extensions(&self) -> &BTreeMap<Arc<str>, ExtensionIndexEntry> { &self.extension_index.extensions } pub fn dev_extensions(&self) -> impl Iterator<Item = &Arc<ExtensionManifest>> { self.extension_index .extensions .values() .filter_map(|extension| extension.dev.then_some(&extension.manifest)) } /// Returns the names of themes provided by extensions. pub fn extension_themes<'a>( &'a self, extension_id: &'a str, ) -> impl Iterator<Item = &'a Arc<str>> { self.extension_index .themes .iter() .filter_map(|(name, theme)| theme.extension.as_ref().eq(extension_id).then_some(name)) } /// Returns the path to the theme file within an extension, if there is an /// extension that provides the theme. pub fn path_to_extension_theme(&self, theme_name: &str) -> Option<PathBuf> { let entry = self.extension_index.themes.get(theme_name)?; Some( self.extensions_dir() .join(entry.extension.as_ref()) .join(&entry.path), ) } /// Returns the names of icon themes provided by extensions. pub fn extension_icon_themes<'a>( &'a self, extension_id: &'a str, ) -> impl Iterator<Item = &'a Arc<str>> { self.extension_index .icon_themes .iter() .filter_map(|(name, icon_theme)| { icon_theme .extension .as_ref() .eq(extension_id) .then_some(name) }) } /// Returns the path to the icon theme file within an extension, if there is /// an extension that provides the icon theme. pub fn path_to_extension_icon_theme( &self, icon_theme_name: &str, ) -> Option<(PathBuf, PathBuf)> { let entry = self.extension_index.icon_themes.get(icon_theme_name)?; let icon_theme_path = self .extensions_dir() .join(entry.extension.as_ref()) .join(&entry.path); let icons_root_path = self.extensions_dir().join(entry.extension.as_ref()); Some((icon_theme_path, icons_root_path)) } pub fn fetch_extensions( &self, search: Option<&str>, provides_filter: Option<&BTreeSet<ExtensionProvides>>, cx: &mut Context<Self>, ) -> Task<Result<Vec<ExtensionMetadata>>> { let version = CURRENT_SCHEMA_VERSION.to_string(); let mut query = vec![("max_schema_version", version.as_str())]; if let Some(search) = search { query.push(("filter", search));Lines 1–500 of 1693. Load more
pub mod extension_settings; pub mod headless_host; pub mod wasm_host; #[cfg(test)] mod extension_store_test; use anyhow::{anyhow, bail, Context as _, Result}; use async_compression::futures::bufread::GzipDecoder; use async_tar::Archive; use client::ExtensionProvides; use client::{proto, telemetry::Telemetry, Client, ExtensionMetadata, GetExtensionsResponse}; use collections::{btree_map, BTreeMap, BTreeSet, HashMap, HashSet}; use extension::extension_builder::{CompileExtensionOptions, ExtensionBuilder}; pub use extension::ExtensionManifest; use extension::{ ExtensionContextServerProxy, ExtensionGrammarProxy, ExtensionHostProxy, ExtensionIndexedDocsProviderProxy, ExtensionLanguageProxy, ExtensionLanguageServerProxy, ExtensionSlashCommandProxy, ExtensionSnippetProxy, ExtensionThemeProxy, }; use fs::{Fs, RemoveOptions}; use futures::{ channel::{ mpsc::{unbounded, UnboundedSender}, oneshot, }, io::BufReader, select_biased, AsyncReadExt as _, Future, FutureExt as _, StreamExt as _, }; use gpui::{ actions, App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Global, Task, WeakEntity, }; use http_client::{AsyncBody, HttpClient, HttpClientWithUrl}; use language::{ LanguageConfig, LanguageMatcher, LanguageName, LanguageQueries, LoadedLanguage, Rope, QUERY_FILENAME_PREFIXES, }; use node_runtime::NodeRuntime; use project::ContextProviderWithTasks; use release_channel::ReleaseChannel; use remote::SshRemoteClient; use semantic_version::SemanticVersion; use serde::{Deserialize, Serialize}; use settings::Settings; use std::ops::RangeInclusive; use std::str::FromStr; use std::{ cmp::Ordering, path::{self, Path, PathBuf}, sync::Arc, time::{Duration, Instant}, }; use url::Url; use util::ResultExt; use wasm_host::{ wit::{is_supported_wasm_api_version, wasm_api_version_range}, WasmExtension, WasmHost, }; pub use extension::{ ExtensionLibraryKind, GrammarManifestEntry, OldExtensionManifest, SchemaVersion, }; pub use extension_settings::ExtensionSettings; 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. const CURRENT_SCHEMA_VERSION: SchemaVersion = SchemaVersion(1); /// Returns the [`SchemaVersion`] range that is compatible with this version of Zed. pub fn schema_version_range() -> RangeInclusive<SchemaVersion> { SchemaVersion::ZERO..=CURRENT_SCHEMA_VERSION } /// Returns whether the given extension version is compatible with this version of Zed. pub fn is_version_compatible( release_channel: ReleaseChannel, extension_version: &ExtensionMetadata, ) -> bool { let schema_version = extension_version.manifest.schema_version.unwrap_or(0); if CURRENT_SCHEMA_VERSION.0 < schema_version { return false; } if let Some(wasm_api_version) = extension_version .manifest .wasm_api_version .as_ref() .and_then(|wasm_api_version| SemanticVersion::from_str(wasm_api_version).ok()) { if !is_supported_wasm_api_version(release_channel, wasm_api_version) { return false; } } true } pub struct ExtensionStore { pub proxy: Arc<ExtensionHostProxy>, 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<()>>, pub ssh_clients: HashMap<String, WeakEntity<SshRemoteClient>>, pub ssh_registered_tx: UnboundedSender<()>, } #[derive(Clone, Copy)] pub enum ExtensionOperation { Upgrade, Install, Remove, } #[derive(Clone)] pub enum Event { ExtensionsUpdated, StartedReloading, ExtensionInstalled(Arc<str>), ExtensionFailedToLoad(Arc<str>), } impl EventEmitter<Event> for ExtensionStore {} struct GlobalExtensionStore(Entity<ExtensionStore>); impl Global for GlobalExtensionStore {} #[derive(Debug, Deserialize, Serialize, Default, PartialEq, Eq)] pub struct ExtensionIndex { pub extensions: BTreeMap<Arc<str>, ExtensionIndexEntry>, pub themes: BTreeMap<Arc<str>, ExtensionIndexThemeEntry>, #[serde(default)] pub icon_themes: BTreeMap<Arc<str>, ExtensionIndexIconThemeEntry>, pub languages: BTreeMap<LanguageName, ExtensionIndexLanguageEntry>, } #[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] pub struct ExtensionIndexEntry { pub manifest: Arc<ExtensionManifest>, pub dev: bool, } #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Deserialize, Serialize)] pub struct ExtensionIndexThemeEntry { pub extension: Arc<str>, pub path: PathBuf, } #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Deserialize, Serialize)] pub struct ExtensionIndexIconThemeEntry { pub extension: Arc<str>, pub path: PathBuf, } #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Deserialize, Serialize)] pub struct ExtensionIndexLanguageEntry { pub extension: Arc<str>, pub path: PathBuf, pub matcher: LanguageMatcher, pub hidden: bool, pub grammar: Option<Arc<str>>, } actions!(zed, [ReloadExtensions]); pub fn init( extension_host_proxy: Arc<ExtensionHostProxy>, fs: Arc<dyn Fs>, client: Arc<Client>, node_runtime: NodeRuntime, cx: &mut App, ) { ExtensionSettings::register(cx); let store = cx.new(move |cx| { ExtensionStore::new( paths::extensions_dir().clone(), None, extension_host_proxy, fs, client.http_client().clone(), client.http_client().clone(), Some(client.telemetry().clone()), node_runtime, cx, ) }); cx.on_action(|_: &ReloadExtensions, cx| { let store = cx.global::<GlobalExtensionStore>().0.clone(); store.update(cx, |store, cx| drop(store.reload(None, cx))); }); cx.set_global(GlobalExtensionStore(store)); } impl ExtensionStore { pub fn try_global(cx: &App) -> Option<Entity<Self>> { cx.try_global::<GlobalExtensionStore>() .map(|store| store.0.clone()) } pub fn global(cx: &App) -> Entity<Self> { cx.global::<GlobalExtensionStore>().0.clone() } #[allow(clippy::too_many_arguments)] pub fn new( extensions_dir: PathBuf, build_dir: Option<PathBuf>, extension_host_proxy: Arc<ExtensionHostProxy>, fs: Arc<dyn Fs>, http_client: Arc<HttpClientWithUrl>, builder_client: Arc<dyn HttpClient>, telemetry: Option<Arc<Telemetry>>, node_runtime: NodeRuntime, cx: &mut Context<Self>, ) -> Self { let work_dir = extensions_dir.join("work"); let build_dir = build_dir.unwrap_or_else(|| extensions_dir.join("build")); let installed_dir = extensions_dir.join("installed"); let index_path = extensions_dir.join("index.json"); let (reload_tx, mut reload_rx) = unbounded(); let (connection_registered_tx, mut connection_registered_rx) = unbounded(); let mut this = Self { proxy: extension_host_proxy.clone(), extension_index: Default::default(), installed_dir, index_path, builder: Arc::new(ExtensionBuilder::new(builder_client, build_dir)), outstanding_operations: Default::default(), modified_extensions: Default::default(), reload_complete_senders: Vec::new(), wasm_host: WasmHost::new( fs.clone(), http_client.clone(), node_runtime, extension_host_proxy, work_dir, cx, ), wasm_extensions: Vec::new(), fs, http_client, telemetry, reload_tx, tasks: Vec::new(), ssh_clients: HashMap::default(), ssh_registered_tx: connection_registered_tx, }; // The extensions store maintains an index file, which contains a complete // list of the installed extensions and the resources that they provide. // This index is loaded synchronously on startup. let (index_content, index_metadata, extensions_metadata) = cx.background_executor().block(async { futures::join!( this.fs.load(&this.index_path), this.fs.metadata(&this.index_path), this.fs.metadata(&this.installed_dir), ) }); // Normally, there is no need to rebuild the index. But if the index file // is invalid or is out-of-date according to the filesystem mtimes, then // it must be asynchronously rebuilt. let mut extension_index = ExtensionIndex::default(); let mut extension_index_needs_rebuild = true; if let Ok(index_content) = index_content { if let Some(index) = serde_json::from_str(&index_content).log_err() { extension_index = index; if let (Ok(Some(index_metadata)), Ok(Some(extensions_metadata))) = (index_metadata, extensions_metadata) { if index_metadata .mtime .bad_is_greater_than(extensions_metadata.mtime) { extension_index_needs_rebuild = false; } } } } // Immediately load all of the extensions in the initial manifest. If the // index needs to be rebuild, then enqueue let load_initial_extensions = this.extensions_updated(extension_index, cx); let mut reload_future = None; if extension_index_needs_rebuild { reload_future = Some(this.reload(None, cx)); } cx.spawn(|this, mut cx| async move { if let Some(future) = reload_future { future.await; } this.update(&mut cx, |this, cx| this.auto_install_extensions(cx)) .ok(); this.update(&mut cx, |this, cx| this.check_for_updates(cx)) .ok(); }) .detach(); // Perform all extension loading in a single task to ensure that we // never attempt to simultaneously load/unload extensions from multiple // parallel tasks. this.tasks.push(cx.spawn(|this, mut cx| { async move { load_initial_extensions.await; let mut index_changed = false; let mut debounce_timer = cx.background_spawn(futures::future::pending()).fuse(); loop { select_biased! { _ = debounce_timer => { 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; } Self::update_ssh_clients(&this, &mut cx).await?; } _ = connection_registered_rx.next() => { debounce_timer = cx .background_executor() .timer(RELOAD_DEBOUNCE_DURATION) .fuse(); } 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) .fuse(); } } } anyhow::Ok(()) } .map(drop) })); // Watch the installed extensions directory for changes. Whenever changes are // detected, rebuild the extension index, and load/unload any extensions that // have been added, removed, or modified. this.tasks.push(cx.background_spawn({ let fs = this.fs.clone(); let reload_tx = this.reload_tx.clone(); let installed_dir = this.installed_dir.clone(); async move { let (mut paths, _) = fs.watch(&installed_dir, FS_WATCH_LATENCY).await; while let Some(events) = paths.next().await { for event in events { let Ok(event_path) = event.path.strip_prefix(&installed_dir) else { continue; }; if let Some(path::Component::Normal(extension_dir_name)) = event_path.components().next() { if let Some(extension_id) = extension_dir_name.to_str() { reload_tx.unbounded_send(Some(extension_id.into())).ok(); } } } } } })); this } pub fn reload( &mut self, modified_extension: Option<Arc<str>>, cx: &mut Context<Self>, ) -> impl Future<Output = ()> { let (tx, rx) = oneshot::channel(); self.reload_complete_senders.push(tx); self.reload_tx .unbounded_send(modified_extension) .expect("reload task exited"); cx.emit(Event::StartedReloading); async move { rx.await.ok(); } } fn extensions_dir(&self) -> PathBuf { self.installed_dir.clone() } pub fn outstanding_operations(&self) -> &BTreeMap<Arc<str>, ExtensionOperation> { &self.outstanding_operations } pub fn installed_extensions(&self) -> &BTreeMap<Arc<str>, ExtensionIndexEntry> { &self.extension_index.extensions } pub fn dev_extensions(&self) -> impl Iterator<Item = &Arc<ExtensionManifest>> { self.extension_index .extensions .values() .filter_map(|extension| extension.dev.then_some(&extension.manifest)) } /// Returns the names of themes provided by extensions. pub fn extension_themes<'a>( &'a self, extension_id: &'a str, ) -> impl Iterator<Item = &'a Arc<str>> { self.extension_index .themes .iter() .filter_map(|(name, theme)| theme.extension.as_ref().eq(extension_id).then_some(name)) } /// Returns the path to the theme file within an extension, if there is an /// extension that provides the theme. pub fn path_to_extension_theme(&self, theme_name: &str) -> Option<PathBuf> { let entry = self.extension_index.themes.get(theme_name)?; Some( self.extensions_dir() .join(entry.extension.as_ref()) .join(&entry.path), ) } /// Returns the names of icon themes provided by extensions. pub fn extension_icon_themes<'a>( &'a self, extension_id: &'a str, ) -> impl Iterator<Item = &'a Arc<str>> { self.extension_index .icon_themes .iter() .filter_map(|(name, icon_theme)| { icon_theme .extension .as_ref() .eq(extension_id) .then_some(name) }) } /// Returns the path to the icon theme file within an extension, if there is /// an extension that provides the icon theme. pub fn path_to_extension_icon_theme( &self, icon_theme_name: &str, ) -> Option<(PathBuf, PathBuf)> { let entry = self.extension_index.icon_themes.get(icon_theme_name)?; let icon_theme_path = self .extensions_dir() .join(entry.extension.as_ref()) .join(&entry.path); let icons_root_path = self.extensions_dir().join(entry.extension.as_ref()); Some((icon_theme_path, icons_root_path)) } pub fn fetch_extensions( &self, search: Option<&str>, provides_filter: Option<&BTreeSet<ExtensionProvides>>, cx: &mut Context<Self>, ) -> Task<Result<Vec<ExtensionMetadata>>> { let version = CURRENT_SCHEMA_VERSION.to_string(); let mut query = vec![("max_schema_version", version.as_str())]; if let Some(search) = search { query.push(("filter", search));