Add support for applying theme after extension is installed (#9529)

Jason Lee and Marshall Bowers created

Release Notes:

- Added support for opening the theme selector with installed themes
after installing an extension containing themes.
([#9228](https://github.com/zed-industries/zed/issues/9228)).

<img width="1315" alt="Screenshot 2024-03-20 at 11 00 35 AM"
src="https://github.com/zed-industries/zed/assets/1486634/593389b3-eade-4bce-ae17-25c02a074f21">

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>

Change summary

Cargo.lock                                   |  2 +
crates/collab_ui/src/collab_titlebar_item.rs |  4 +-
crates/extension/src/extension_store.rs      | 33 ++++++++++++++++-
crates/extensions_ui/Cargo.toml              |  1 
crates/extensions_ui/src/extensions_ui.rs    | 34 +++++++++++++++++-
crates/theme_selector/Cargo.toml             |  1 
crates/theme_selector/src/theme_selector.rs  | 41 +++++++++++++++++----
crates/zed/src/zed/app_menus.rs              |  2 
8 files changed, 102 insertions(+), 16 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -3528,6 +3528,7 @@ dependencies = [
  "settings",
  "smallvec",
  "theme",
+ "theme_selector",
  "ui",
  "util",
  "workspace",
@@ -9619,6 +9620,7 @@ dependencies = [
  "gpui",
  "log",
  "picker",
+ "serde",
  "settings",
  "theme",
  "ui",

crates/collab_ui/src/collab_titlebar_item.rs 🔗

@@ -689,7 +689,7 @@ impl CollabTitlebarItem {
                     ContextMenu::build(cx, |menu, _| {
                         menu.action("Settings", zed_actions::OpenSettings.boxed_clone())
                             .action("Extensions", extensions_ui::Extensions.boxed_clone())
-                            .action("Themes...", theme_selector::Toggle.boxed_clone())
+                            .action("Themes...", theme_selector::Toggle::default().boxed_clone())
                             .separator()
                             .action("Sign Out", client::SignOut.boxed_clone())
                     })
@@ -713,7 +713,7 @@ impl CollabTitlebarItem {
                     ContextMenu::build(cx, |menu, _| {
                         menu.action("Settings", zed_actions::OpenSettings.boxed_clone())
                             .action("Extensions", extensions_ui::Extensions.boxed_clone())
-                            .action("Themes...", theme_selector::Toggle.boxed_clone())
+                            .action("Themes...", theme_selector::Toggle::default().boxed_clone())
                     })
                     .into()
                 })

crates/extension/src/extension_store.rs 🔗

@@ -92,16 +92,18 @@ pub enum ExtensionStatus {
     Removing,
 }
 
+#[derive(Clone, Copy)]
 enum ExtensionOperation {
     Upgrade,
     Install,
     Remove,
 }
 
-#[derive(Copy, Clone)]
+#[derive(Clone)]
 pub enum Event {
     ExtensionsUpdated,
     StartedReloading,
+    ExtensionInstalled(Arc<str>),
 }
 
 impl EventEmitter<Event> for ExtensionStore {}
@@ -330,6 +332,7 @@ impl ExtensionStore {
             .unbounded_send(modified_extension)
             .expect("reload task exited");
         cx.emit(Event::StartedReloading);
+
         async move {
             rx.await.ok();
         }
@@ -358,6 +361,17 @@ impl ExtensionStore {
             .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))
+    }
+
     pub fn fetch_extensions(
         &self,
         search: Option<&str>,
@@ -441,8 +455,21 @@ impl ExtensionStore {
             archive
                 .unpack(extensions_dir.join(extension_id.as_ref()))
                 .await?;
-            this.update(&mut cx, |this, cx| this.reload(Some(extension_id), cx))?
-                .await;
+            this.update(&mut cx, |this, cx| {
+                this.reload(Some(extension_id.clone()), cx)
+            })?
+            .await;
+
+            match operation {
+                ExtensionOperation::Install => {
+                    this.update(&mut cx, |_, cx| {
+                        cx.emit(Event::ExtensionInstalled(extension_id));
+                    })
+                    .ok();
+                }
+                _ => {}
+            }
+
             anyhow::Ok(())
         })
         .detach_and_log_err(cx);

crates/extensions_ui/Cargo.toml 🔗

@@ -28,6 +28,7 @@ serde.workspace = true
 settings.workspace = true
 smallvec.workspace = true
 theme.workspace = true
+theme_selector.workspace = true
 ui.workspace = true
 util.workspace = true
 workspace.workspace = true

crates/extensions_ui/src/extensions_ui.rs 🔗

@@ -9,7 +9,7 @@ use fuzzy::{match_strings, StringMatchCandidate};
 use gpui::{
     actions, canvas, uniform_list, AnyElement, AppContext, EventEmitter, FocusableView, FontStyle,
     FontWeight, InteractiveElement, KeyContext, ParentElement, Render, Styled, Task, TextStyle,
-    UniformListScrollHandle, View, ViewContext, VisualContext, WhiteSpace, WindowContext,
+    UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WhiteSpace, WindowContext,
 };
 use settings::Settings;
 use std::ops::DerefMut;
@@ -100,10 +100,14 @@ impl ExtensionsPage {
     pub fn new(workspace: &Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
         cx.new_view(|cx: &mut ViewContext<Self>| {
             let store = ExtensionStore::global(cx);
+            let workspace_handle = workspace.weak_handle();
             let subscriptions = [
                 cx.observe(&store, |_, _, cx| cx.notify()),
-                cx.subscribe(&store, |this, _, event, cx| match event {
+                cx.subscribe(&store, move |this, _, event, cx| match event {
                     extension::Event::ExtensionsUpdated => this.fetch_extensions_debounced(cx),
+                    extension::Event::ExtensionInstalled(extension_id) => {
+                        this.on_extension_installed(workspace_handle.clone(), extension_id, cx)
+                    }
                     _ => {}
                 }),
             ];
@@ -133,6 +137,32 @@ impl ExtensionsPage {
         })
     }
 
+    fn on_extension_installed(
+        &mut self,
+        workspace: WeakView<Workspace>,
+        extension_id: &str,
+        cx: &mut ViewContext<Self>,
+    ) {
+        let extension_store = ExtensionStore::global(cx).read(cx);
+        let themes = extension_store
+            .extension_themes(extension_id)
+            .map(|name| name.to_string())
+            .collect::<Vec<_>>();
+        if !themes.is_empty() {
+            workspace
+                .update(cx, |workspace, cx| {
+                    theme_selector::toggle(
+                        workspace,
+                        &theme_selector::Toggle {
+                            themes_filter: Some(themes),
+                        },
+                        cx,
+                    )
+                })
+                .ok();
+        }
+    }
+
     fn filter_extension_entries(&mut self, cx: &mut ViewContext<Self>) {
         let extension_store = ExtensionStore::global(cx).read(cx);
 

crates/theme_selector/Cargo.toml 🔗

@@ -20,6 +20,7 @@ fuzzy.workspace = true
 gpui.workspace = true
 log.workspace = true
 picker.workspace = true
+serde.workspace = true
 settings.workspace = true
 theme.workspace = true
 ui.workspace = true

crates/theme_selector/src/theme_selector.rs 🔗

@@ -3,10 +3,11 @@ use feature_flags::FeatureFlagAppExt;
 use fs::Fs;
 use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
 use gpui::{
-    actions, AppContext, DismissEvent, EventEmitter, FocusableView, Render, View, ViewContext,
-    VisualContext, WeakView,
+    actions, impl_actions, AppContext, DismissEvent, EventEmitter, FocusableView, Render, View,
+    ViewContext, VisualContext, WeakView,
 };
 use picker::{Picker, PickerDelegate};
+use serde::Deserialize;
 use settings::{update_settings_file, SettingsStore};
 use std::sync::Arc;
 use theme::{
@@ -16,7 +17,14 @@ use ui::{prelude::*, v_flex, ListItem, ListItemSpacing};
 use util::ResultExt;
 use workspace::{ui::HighlightedLabel, ModalView, Workspace};
 
-actions!(theme_selector, [Toggle, Reload]);
+#[derive(PartialEq, Clone, Default, Debug, Deserialize)]
+pub struct Toggle {
+    /// A list of theme names to filter the theme selector down to.
+    pub themes_filter: Option<Vec<String>>,
+}
+
+impl_actions!(theme_selector, [Toggle]);
+actions!(theme_selector, [Reload]);
 
 pub fn init(cx: &mut AppContext) {
     cx.observe_new_views(
@@ -27,14 +35,18 @@ pub fn init(cx: &mut AppContext) {
     .detach();
 }
 
-pub fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
+pub fn toggle(workspace: &mut Workspace, toggle: &Toggle, cx: &mut ViewContext<Workspace>) {
     let fs = workspace.app_state().fs.clone();
     let telemetry = workspace.client().telemetry().clone();
     workspace.toggle_modal(cx, |cx| {
-        ThemeSelector::new(
-            ThemeSelectorDelegate::new(cx.view().downgrade(), fs, telemetry, cx),
+        let delegate = ThemeSelectorDelegate::new(
+            cx.view().downgrade(),
+            fs,
+            telemetry,
+            toggle.themes_filter.as_ref(),
             cx,
-        )
+        );
+        ThemeSelector::new(delegate, cx)
     });
 }
 
@@ -81,13 +93,25 @@ impl ThemeSelectorDelegate {
         weak_view: WeakView<ThemeSelector>,
         fs: Arc<dyn Fs>,
         telemetry: Arc<Telemetry>,
+        themes_filter: Option<&Vec<String>>,
         cx: &mut ViewContext<ThemeSelector>,
     ) -> Self {
         let original_theme = cx.theme().clone();
 
         let staff_mode = cx.is_staff();
         let registry = ThemeRegistry::global(cx);
-        let mut themes = registry.list(staff_mode);
+        let mut themes = registry
+            .list(staff_mode)
+            .into_iter()
+            .filter(|meta| {
+                if let Some(theme_filter) = themes_filter {
+                    theme_filter.contains(&meta.name.to_string())
+                } else {
+                    true
+                }
+            })
+            .collect::<Vec<_>>();
+
         themes.sort_unstable_by(|a, b| {
             a.appearance
                 .is_light()
@@ -113,6 +137,7 @@ impl ThemeSelectorDelegate {
             telemetry,
             view: weak_view,
         };
+
         this.select_if_matching(&original_theme.name);
         this
     }

crates/zed/src/zed/app_menus.rs 🔗

@@ -20,7 +20,7 @@ pub fn app_menus() -> Vec<Menu<'static>> {
                         MenuItem::action("Open Default Settings", super::OpenDefaultSettings),
                         MenuItem::action("Open Default Key Bindings", super::OpenDefaultKeymap),
                         MenuItem::action("Open Local Settings", super::OpenLocalSettings),
-                        MenuItem::action("Select Theme...", theme_selector::Toggle),
+                        MenuItem::action("Select Theme...", theme_selector::Toggle::default()),
                     ],
                 }),
                 MenuItem::action("Extensions", extensions_ui::Extensions),