extension: Add `ExtensionEvents` for listening to extension-related events (#26562)

Marshall Bowers created

This PR adds a new `ExtensionEvents` event bus that can be used to
listen for extension-related events throughout the app.

Today you need to have a handle to the `ExtensionStore` (which entails
depending on `extension_host`) in order to listen for extension events.

With this change subscribers only need to depend on `extension`, which
has a leaner dependency graph.

Release Notes:

- N/A

Change summary

Cargo.lock                                        |  1 
crates/extension/src/extension.rs                 |  3 +
crates/extension/src/extension_events.rs          | 35 +++++++++++++++++
crates/extension_host/src/extension_host.rs       |  7 +-
crates/extension_host/src/extension_store_test.rs |  1 
crates/extensions_ui/Cargo.toml                   |  1 
crates/extensions_ui/src/extensions_ui.rs         | 17 +++++--
7 files changed, 57 insertions(+), 8 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -4639,6 +4639,7 @@ dependencies = [
  "collections",
  "db",
  "editor",
+ "extension",
  "extension_host",
  "feature_flags",
  "fs",

crates/extension/src/extension.rs 🔗

@@ -1,4 +1,5 @@
 pub mod extension_builder;
+mod extension_events;
 mod extension_host_proxy;
 mod extension_manifest;
 mod types;
@@ -14,12 +15,14 @@ use gpui::{App, Task};
 use language::LanguageName;
 use semantic_version::SemanticVersion;
 
+pub use crate::extension_events::*;
 pub use crate::extension_host_proxy::*;
 pub use crate::extension_manifest::*;
 pub use crate::types::*;
 
 /// Initializes the `extension` crate.
 pub fn init(cx: &mut App) {
+    extension_events::init(cx);
     ExtensionHostProxy::default_global(cx);
 }
 

crates/extension/src/extension_events.rs 🔗

@@ -0,0 +1,35 @@
+use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Global, ReadGlobal as _};
+
+pub fn init(cx: &mut App) {
+    let extension_events = cx.new(ExtensionEvents::new);
+    cx.set_global(GlobalExtensionEvents(extension_events));
+}
+
+struct GlobalExtensionEvents(Entity<ExtensionEvents>);
+
+impl Global for GlobalExtensionEvents {}
+
+/// An event bus for broadcasting extension-related events throughout the app.
+pub struct ExtensionEvents;
+
+impl ExtensionEvents {
+    /// Returns the global [`ExtensionEvents`].
+    pub fn global(cx: &App) -> Entity<Self> {
+        GlobalExtensionEvents::global(cx).0.clone()
+    }
+
+    fn new(_cx: &mut Context<Self>) -> Self {
+        Self
+    }
+
+    pub fn emit(&mut self, event: Event, cx: &mut Context<Self>) {
+        cx.emit(event)
+    }
+}
+
+#[derive(Clone)]
+pub enum Event {
+    ExtensionsUpdated,
+}
+
+impl EventEmitter<Event> for ExtensionEvents {}

crates/extension_host/src/extension_host.rs 🔗

@@ -14,7 +14,7 @@ use collections::{btree_map, BTreeMap, BTreeSet, HashMap, HashSet};
 use extension::extension_builder::{CompileExtensionOptions, ExtensionBuilder};
 pub use extension::ExtensionManifest;
 use extension::{
-    ExtensionContextServerProxy, ExtensionGrammarProxy, ExtensionHostProxy,
+    ExtensionContextServerProxy, ExtensionEvents, ExtensionGrammarProxy, ExtensionHostProxy,
     ExtensionIndexedDocsProviderProxy, ExtensionLanguageProxy, ExtensionLanguageServerProxy,
     ExtensionSlashCommandProxy, ExtensionSnippetProxy, ExtensionThemeProxy,
 };
@@ -127,7 +127,6 @@ pub enum ExtensionOperation {
 
 #[derive(Clone)]
 pub enum Event {
-    ExtensionsUpdated,
     StartedReloading,
     ExtensionInstalled(Arc<str>),
     ExtensionFailedToLoad(Arc<str>),
@@ -1214,7 +1213,9 @@ impl ExtensionStore {
 
         self.extension_index = new_index;
         cx.notify();
-        cx.emit(Event::ExtensionsUpdated);
+        ExtensionEvents::global(cx).update(cx, |this, cx| {
+            this.emit(extension::Event::ExtensionsUpdated, cx)
+        });
 
         cx.spawn(|this, mut cx| async move {
             cx.background_spawn({

crates/extension_host/src/extension_store_test.rs 🔗

@@ -780,6 +780,7 @@ fn init_test(cx: &mut TestAppContext) {
         let store = SettingsStore::test(cx);
         cx.set_global(store);
         release_channel::init(SemanticVersion::default(), cx);
+        extension::init(cx);
         theme::init(theme::LoadThemes::JustBase, cx);
         Project::init_settings(cx);
         ExtensionSettings::register(cx);

crates/extensions_ui/Cargo.toml 🔗

@@ -17,6 +17,7 @@ client.workspace = true
 collections.workspace = true
 db.workspace = true
 editor.workspace = true
+extension.workspace = true
 extension_host.workspace = true
 feature_flags.workspace = true
 fs.workspace = true

crates/extensions_ui/src/extensions_ui.rs 🔗

@@ -9,6 +9,7 @@ use std::{ops::Range, sync::Arc};
 use client::{ExtensionMetadata, ExtensionProvides};
 use collections::{BTreeMap, BTreeSet};
 use editor::{Editor, EditorElement, EditorStyle};
+use extension::ExtensionEvents;
 use extension_host::{ExtensionManifest, ExtensionOperation, ExtensionStore};
 use feature_flags::FeatureFlagAppExt as _;
 use fuzzy::{match_strings, StringMatchCandidate};
@@ -212,7 +213,7 @@ pub struct ExtensionsPage {
     query_editor: Entity<Editor>,
     query_contains_error: bool,
     provides_filter: Option<ExtensionProvides>,
-    _subscriptions: [gpui::Subscription; 2],
+    _subscriptions: Vec<gpui::Subscription>,
     extension_fetch_task: Option<Task<()>>,
     upsells: BTreeSet<Feature>,
 }
@@ -226,15 +227,12 @@ impl ExtensionsPage {
         cx.new(|cx| {
             let store = ExtensionStore::global(cx);
             let workspace_handle = workspace.weak_handle();
-            let subscriptions = [
+            let subscriptions = vec![
                 cx.observe(&store, |_: &mut Self, _, cx| cx.notify()),
                 cx.subscribe_in(
                     &store,
                     window,
                     move |this, _, event, window, cx| match event {
-                        extension_host::Event::ExtensionsUpdated => {
-                            this.fetch_extensions_debounced(cx)
-                        }
                         extension_host::Event::ExtensionInstalled(extension_id) => this
                             .on_extension_installed(
                                 workspace_handle.clone(),
@@ -245,6 +243,15 @@ impl ExtensionsPage {
                         _ => {}
                     },
                 ),
+                cx.subscribe_in(
+                    &ExtensionEvents::global(cx),
+                    window,
+                    move |this, _, event, _window, cx| match event {
+                        extension::Event::ExtensionsUpdated => {
+                            this.fetch_extensions_debounced(cx);
+                        }
+                    },
+                ),
             ];
 
             let query_editor = cx.new(|cx| {