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

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
This commit is contained in:
Marshall Bowers 2025-03-19 13:26:46 -04:00 committed by GitHub
parent d51cd15e4d
commit d722067000
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 107 additions and 33 deletions

View file

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

View file

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

View file

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

View file

@ -683,7 +683,10 @@ impl TitleBar {
"Icon Themes…", "Icon Themes…",
zed_actions::icon_theme_selector::Toggle::default().boxed_clone(), 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() .separator()
.link( .link(
"Book Onboarding", "Book Onboarding",
@ -730,7 +733,10 @@ impl TitleBar {
"Icon Themes…", "Icon Themes…",
zed_actions::icon_theme_selector::Toggle::default().boxed_clone(), 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() .separator()
.link( .link(
"Book Onboarding", "Book Onboarding",

View file

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

View file

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

View file

@ -35,12 +35,32 @@ actions!(
Quit, Quit,
OpenKeymap, OpenKeymap,
About, About,
Extensions,
OpenLicenses, OpenLicenses,
OpenTelemetryLog, 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)] #[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema)]
pub struct DecreaseBufferFontSize { pub struct DecreaseBufferFontSize {
#[serde(default)] #[serde(default)]
@ -80,6 +100,7 @@ pub struct ResetUiFontSize {
impl_actions!( impl_actions!(
zed, zed,
[ [
Extensions,
DecreaseBufferFontSize, DecreaseBufferFontSize,
IncreaseBufferFontSize, IncreaseBufferFontSize,
ResetBufferFontSize, ResetBufferFontSize,