extensions_ui: Add ability to open the extensions view with a pre-selected filter (#27093)

Marshall Bowers created

This PR adds the ability to open the extensions view via the `zed:
extensions` action with a pre-selected filter.

The "Install Themes" and "Install Icon Themes" buttons in their
respective selectors take advantage of this to set the filter when
opening the view:


https://github.com/user-attachments/assets/2e345c0f-418a-47b6-811e-cabae6c616d1

Release Notes:

- N/A

Change summary

crates/extensions_ui/src/extensions_ui.rs        | 87 ++++++++++++-----
crates/theme_selector/src/icon_theme_selector.rs |  9 +
crates/theme_selector/src/theme_selector.rs      |  9 +
crates/title_bar/src/title_bar.rs                | 10 +
crates/welcome/src/welcome.rs                    |  2 
crates/zed/src/zed/app_menus.rs                  |  2 
crates/zed_actions/src/lib.rs                    | 23 ++++
7 files changed, 108 insertions(+), 34 deletions(-)

Detailed changes

crates/extensions_ui/src/extensions_ui.rs πŸ”—

@@ -28,6 +28,7 @@ use workspace::{
     item::{Item, ItemEvent},
     Workspace, WorkspaceId,
 };
+use zed_actions::ExtensionCategoryFilter;
 
 use crate::components::{ExtensionCard, FeatureUpsell};
 use crate::extension_version_selector::{
@@ -42,26 +43,53 @@ pub fn init(cx: &mut App) {
             return;
         };
         workspace
-            .register_action(move |workspace, _: &zed_actions::Extensions, window, cx| {
-                let existing = workspace
-                    .active_pane()
-                    .read(cx)
-                    .items()
-                    .find_map(|item| item.downcast::<ExtensionsPage>());
-
-                if let Some(existing) = existing {
-                    workspace.activate_item(&existing, true, true, window, cx);
-                } else {
-                    let extensions_page = ExtensionsPage::new(workspace, window, cx);
-                    workspace.add_item_to_active_pane(
-                        Box::new(extensions_page),
-                        None,
-                        true,
-                        window,
-                        cx,
-                    )
-                }
-            })
+            .register_action(
+                move |workspace, action: &zed_actions::Extensions, window, cx| {
+                    let provides_filter = action.category_filter.map(|category| match category {
+                        ExtensionCategoryFilter::Themes => ExtensionProvides::Themes,
+                        ExtensionCategoryFilter::IconThemes => ExtensionProvides::IconThemes,
+                        ExtensionCategoryFilter::Languages => ExtensionProvides::Languages,
+                        ExtensionCategoryFilter::Grammars => ExtensionProvides::Grammars,
+                        ExtensionCategoryFilter::LanguageServers => {
+                            ExtensionProvides::LanguageServers
+                        }
+                        ExtensionCategoryFilter::ContextServers => {
+                            ExtensionProvides::ContextServers
+                        }
+                        ExtensionCategoryFilter::SlashCommands => ExtensionProvides::SlashCommands,
+                        ExtensionCategoryFilter::IndexedDocsProviders => {
+                            ExtensionProvides::IndexedDocsProviders
+                        }
+                        ExtensionCategoryFilter::Snippets => ExtensionProvides::Snippets,
+                    });
+
+                    let existing = workspace
+                        .active_pane()
+                        .read(cx)
+                        .items()
+                        .find_map(|item| item.downcast::<ExtensionsPage>());
+
+                    if let Some(existing) = existing {
+                        if provides_filter.is_some() {
+                            existing.update(cx, |extensions_page, cx| {
+                                extensions_page.change_provides_filter(provides_filter, cx);
+                            });
+                        }
+
+                        workspace.activate_item(&existing, true, true, window, cx);
+                    } else {
+                        let extensions_page =
+                            ExtensionsPage::new(workspace, provides_filter, window, cx);
+                        workspace.add_item_to_active_pane(
+                            Box::new(extensions_page),
+                            None,
+                            true,
+                            window,
+                            cx,
+                        )
+                    }
+                },
+            )
             .register_action(move |workspace, _: &InstallDevExtension, window, cx| {
                 let store = ExtensionStore::global(cx);
                 let prompt = workspace.prompt_for_open_path(
@@ -234,6 +262,7 @@ pub struct ExtensionsPage {
 impl ExtensionsPage {
     pub fn new(
         workspace: &Workspace,
+        provides_filter: Option<ExtensionProvides>,
         window: &mut Window,
         cx: &mut Context<Workspace>,
     ) -> Entity<Self> {
@@ -277,13 +306,13 @@ impl ExtensionsPage {
                 filtered_remote_extension_indices: Vec::new(),
                 remote_extension_entries: Vec::new(),
                 query_contains_error: false,
-                provides_filter: None,
+                provides_filter,
                 extension_fetch_task: None,
                 _subscriptions: subscriptions,
                 query_editor,
                 upsells: BTreeSet::default(),
             };
-            this.fetch_extensions(None, None, cx);
+            this.fetch_extensions(None, Some(BTreeSet::from_iter(this.provides_filter)), cx);
             this
         })
     }
@@ -968,6 +997,15 @@ impl ExtensionsPage {
         self.refresh_feature_upsells(cx);
     }
 
+    pub fn change_provides_filter(
+        &mut self,
+        provides_filter: Option<ExtensionProvides>,
+        cx: &mut Context<Self>,
+    ) {
+        self.provides_filter = provides_filter;
+        self.refresh_search(cx);
+    }
+
     fn fetch_extensions_debounced(&mut self, cx: &mut Context<ExtensionsPage>) {
         self.extension_fetch_task = Some(cx.spawn(async move |this, cx| {
             let search = this
@@ -1155,8 +1193,7 @@ impl ExtensionsPage {
                     let this = this.clone();
                     move |_window, cx| {
                         this.update(cx, |this, cx| {
-                            this.provides_filter = None;
-                            this.refresh_search(cx);
+                            this.change_provides_filter(None, cx);
                         });
                     }
                 },
@@ -1174,8 +1211,8 @@ impl ExtensionsPage {
                         let this = this.clone();
                         move |_window, cx| {
                             this.update(cx, |this, cx| {
+                                this.change_provides_filter(Some(provides), cx);
                                 this.provides_filter = Some(provides);
-                                this.refresh_search(cx);
                             });
                         }
                     },

crates/theme_selector/src/icon_theme_selector.rs πŸ”—

@@ -11,7 +11,7 @@ use theme::{Appearance, IconTheme, ThemeMeta, ThemeRegistry, ThemeSettings};
 use ui::{prelude::*, v_flex, ListItem, ListItemSpacing};
 use util::ResultExt;
 use workspace::{ui::HighlightedLabel, ModalView};
-use zed_actions::Extensions;
+use zed_actions::{ExtensionCategoryFilter, Extensions};
 
 pub(crate) struct IconThemeSelector {
     picker: Entity<Picker<IconThemeSelectorDelegate>>,
@@ -301,7 +301,12 @@ impl PickerDelegate for IconThemeSelectorDelegate {
                 .child(
                     Button::new("more-icon-themes", "Install Icon Themes").on_click(
                         move |_event, window, cx| {
-                            window.dispatch_action(Box::new(Extensions), cx);
+                            window.dispatch_action(
+                                Box::new(Extensions {
+                                    category_filter: Some(ExtensionCategoryFilter::IconThemes),
+                                }),
+                                cx,
+                            );
                         },
                     ),
                 )

crates/theme_selector/src/theme_selector.rs πŸ”—

@@ -13,7 +13,7 @@ use theme::{Appearance, Theme, ThemeMeta, ThemeRegistry, ThemeSettings};
 use ui::{prelude::*, v_flex, ListItem, ListItemSpacing};
 use util::ResultExt;
 use workspace::{ui::HighlightedLabel, ModalView, Workspace};
-use zed_actions::Extensions;
+use zed_actions::{ExtensionCategoryFilter, Extensions};
 
 use crate::icon_theme_selector::{IconThemeSelector, IconThemeSelectorDelegate};
 
@@ -349,7 +349,12 @@ impl PickerDelegate for ThemeSelectorDelegate {
                 .child(
                     Button::new("more-themes", "Install Themes").on_click(cx.listener({
                         move |_, _, window, cx| {
-                            window.dispatch_action(Box::new(Extensions), cx);
+                            window.dispatch_action(
+                                Box::new(Extensions {
+                                    category_filter: Some(ExtensionCategoryFilter::Themes),
+                                }),
+                                cx,
+                            );
                         }
                     })),
                 )

crates/title_bar/src/title_bar.rs πŸ”—

@@ -683,7 +683,10 @@ impl TitleBar {
                             "Icon Themes…",
                             zed_actions::icon_theme_selector::Toggle::default().boxed_clone(),
                         )
-                        .action("Extensions", zed_actions::Extensions.boxed_clone())
+                        .action(
+                            "Extensions",
+                            zed_actions::Extensions::default().boxed_clone(),
+                        )
                         .separator()
                         .link(
                             "Book Onboarding",
@@ -730,7 +733,10 @@ impl TitleBar {
                                 "Icon Themes…",
                                 zed_actions::icon_theme_selector::Toggle::default().boxed_clone(),
                             )
-                            .action("Extensions", zed_actions::Extensions.boxed_clone())
+                            .action(
+                                "Extensions",
+                                zed_actions::Extensions::default().boxed_clone(),
+                            )
                             .separator()
                             .link(
                                 "Book Onboarding",

crates/welcome/src/welcome.rs πŸ”—

@@ -248,7 +248,7 @@ impl Render for WelcomePage {
                                             .on_click(cx.listener(|_, _, window, cx| {
                                                 telemetry::event!("Welcome Extensions Page Opened");
                                                 window.dispatch_action(Box::new(
-                                                    zed_actions::Extensions,
+                                                    zed_actions::Extensions::default(),
                                                 ), cx);
                                             })),
                                     )

crates/zed/src/zed/app_menus.rs πŸ”—

@@ -35,7 +35,7 @@ pub fn app_menus() -> Vec<Menu> {
                     items: vec![],
                 }),
                 MenuItem::separator(),
-                MenuItem::action("Extensions", zed_actions::Extensions),
+                MenuItem::action("Extensions", zed_actions::Extensions::default()),
                 MenuItem::action("Install CLI", install_cli::Install),
                 MenuItem::separator(),
                 #[cfg(target_os = "macos")]

crates/zed_actions/src/lib.rs πŸ”—

@@ -35,12 +35,32 @@ actions!(
         Quit,
         OpenKeymap,
         About,
-        Extensions,
         OpenLicenses,
         OpenTelemetryLog,
     ]
 );
 
+#[derive(PartialEq, Clone, Copy, Debug, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum ExtensionCategoryFilter {
+    Themes,
+    IconThemes,
+    Languages,
+    Grammars,
+    LanguageServers,
+    ContextServers,
+    SlashCommands,
+    IndexedDocsProviders,
+    Snippets,
+}
+
+#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema)]
+pub struct Extensions {
+    /// Filters the extensions page down to extensions that are in the specified category.
+    #[serde(default)]
+    pub category_filter: Option<ExtensionCategoryFilter>,
+}
+
 #[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema)]
 pub struct DecreaseBufferFontSize {
     #[serde(default)]
@@ -80,6 +100,7 @@ pub struct ResetUiFontSize {
 impl_actions!(
     zed,
     [
+        Extensions,
         DecreaseBufferFontSize,
         IncreaseBufferFontSize,
         ResetBufferFontSize,