extensions_ui: Add ability to filter extensions by category (#27005)

This PR adds the ability to filter the list of extensions by category:


https://github.com/user-attachments/assets/ea7b518e-4769-4e2e-8bbe-e75f9f01edf9

Release Notes:

- Added the ability to filter the list of extensions by category.
This commit is contained in:
Marshall Bowers 2025-03-18 13:59:58 -04:00 committed by GitHub
parent 628a61d929
commit cc36cd9768
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 90 additions and 58 deletions

2
Cargo.lock generated
View file

@ -4821,7 +4821,6 @@ dependencies = [
"db",
"editor",
"extension_host",
"feature_flags",
"fs",
"fuzzy",
"gpui",
@ -4834,6 +4833,7 @@ dependencies = [
"serde",
"settings",
"smallvec",
"strum",
"telemetry",
"theme",
"ui",

View file

@ -18,7 +18,6 @@ collections.workspace = true
db.workspace = true
editor.workspace = true
extension_host.workspace = true
feature_flags.workspace = true
fs.workspace = true
fuzzy.workspace = true
gpui.workspace = true
@ -31,6 +30,7 @@ semantic_version.workspace = true
serde.workspace = true
settings.workspace = true
smallvec.workspace = true
strum.workspace = true
telemetry.workspace = true
theme.workspace = true
ui.workspace = true

View file

@ -10,7 +10,6 @@ use client::{ExtensionMetadata, ExtensionProvides};
use collections::{BTreeMap, BTreeSet};
use editor::{Editor, EditorElement, EditorStyle};
use extension_host::{ExtensionManifest, ExtensionOperation, ExtensionStore};
use feature_flags::FeatureFlagAppExt as _;
use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{
actions, uniform_list, Action, App, ClipboardItem, Context, Entity, EventEmitter, Flatten,
@ -21,6 +20,7 @@ use num_format::{Locale, ToFormattedString};
use project::DirectoryLister;
use release_channel::ReleaseChannel;
use settings::Settings;
use strum::IntoEnumIterator as _;
use theme::ThemeSettings;
use ui::{prelude::*, CheckboxWithLabel, ContextMenu, PopoverMenu, ToggleButton, Tooltip};
use vim_mode_setting::VimModeSetting;
@ -127,6 +127,20 @@ pub fn init(cx: &mut App) {
.detach();
}
fn extension_provides_label(provides: ExtensionProvides) -> &'static str {
match provides {
ExtensionProvides::Themes => "Themes",
ExtensionProvides::IconThemes => "Icon Themes",
ExtensionProvides::Languages => "Languages",
ExtensionProvides::Grammars => "Grammars",
ExtensionProvides::LanguageServers => "Language Servers",
ExtensionProvides::ContextServers => "Context Servers",
ExtensionProvides::SlashCommands => "Slash Commands",
ExtensionProvides::IndexedDocsProviders => "Indexed Docs Providers",
ExtensionProvides::Snippets => "Snippets",
}
}
#[derive(Clone)]
pub enum ExtensionStatus {
NotInstalled,
@ -608,25 +622,6 @@ impl ExtensionsPage {
.provides
.iter()
.map(|provides| {
let label = match provides {
ExtensionProvides::Themes => "Themes",
ExtensionProvides::IconThemes => "Icon Themes",
ExtensionProvides::Languages => "Languages",
ExtensionProvides::Grammars => "Grammars",
ExtensionProvides::LanguageServers => {
"Language Servers"
}
ExtensionProvides::ContextServers => {
"Context Servers"
}
ExtensionProvides::SlashCommands => {
"Slash Commands"
}
ExtensionProvides::IndexedDocsProviders => {
"Indexed Docs Providers"
}
ExtensionProvides::Snippets => "Snippets",
};
div()
.bg(cx.theme().colors().element_background)
.px_0p5()
@ -634,7 +629,10 @@ impl ExtensionsPage {
.border_color(cx.theme().colors().border)
.rounded_sm()
.child(
Label::new(label).size(LabelSize::XSmall),
Label::new(extension_provides_label(
*provides,
))
.size(LabelSize::XSmall),
)
})
.collect::<Vec<_>>(),
@ -1140,6 +1138,53 @@ impl ExtensionsPage {
upsell.when(ix < upsells_count, |upsell| upsell.border_b_1())
}))
}
fn build_extension_provides_filter_menu(
&self,
window: &mut Window,
cx: &mut Context<Self>,
) -> Entity<ContextMenu> {
let this = cx.entity();
ContextMenu::build(window, cx, |mut menu, _window, _cx| {
menu = menu.header("Extension Category").toggleable_entry(
"All",
self.provides_filter.is_none(),
IconPosition::End,
None,
{
let this = this.clone();
move |_window, cx| {
this.update(cx, |this, cx| {
this.provides_filter = None;
this.refresh_search(cx);
});
}
},
);
for provides in ExtensionProvides::iter() {
let label = extension_provides_label(provides);
menu = menu.toggleable_entry(
label,
self.provides_filter == Some(provides),
IconPosition::End,
None,
{
let this = this.clone();
move |_window, cx| {
this.update(cx, |this, cx| {
this.provides_filter = Some(provides);
this.refresh_search(cx);
});
}
},
)
}
menu
})
}
}
impl Render for ExtensionsPage {
@ -1174,41 +1219,27 @@ impl Render for ExtensionsPage {
.w_full()
.gap_2()
.justify_between()
.child(
h_flex()
.gap_2()
.child(self.render_search(cx))
.map(|parent| {
// Note: Staff-only until this gets design input.
if !cx.is_staff() {
return parent;
}
parent.child(CheckboxWithLabel::new(
"icon-themes-filter",
Label::new("Icon themes"),
match self.provides_filter {
Some(ExtensionProvides::IconThemes) => {
ToggleState::Selected
}
_ => ToggleState::Unselected,
},
cx.listener(|this, checked, _window, cx| {
match checked {
ToggleState::Unselected
| ToggleState::Indeterminate => {
this.provides_filter = None
}
ToggleState::Selected => {
this.provides_filter =
Some(ExtensionProvides::IconThemes)
}
};
this.refresh_search(cx);
}),
))
}),
)
.child(h_flex().gap_2().child(self.render_search(cx)).child({
let this = cx.entity().clone();
PopoverMenu::new("extension-provides-filter")
.menu(move |window, cx| {
Some(this.update(cx, |this, cx| {
this.build_extension_provides_filter_menu(window, cx)
}))
})
.trigger_with_tooltip(
Button::new(
"extension-provides-filter-button",
self.provides_filter
.map(extension_provides_label)
.unwrap_or("All"),
)
.icon(IconName::Filter)
.icon_position(IconPosition::Start),
Tooltip::text("Filter extensions by category"),
)
.anchor(gpui::Corner::TopLeft)
}))
.child(
h_flex()
.child(

View file

@ -31,6 +31,7 @@ pub struct ExtensionApiManifest {
Deserialize,
EnumString,
strum::Display,
strum::EnumIter,
)]
#[serde(rename_all = "kebab-case")]
#[strum(serialize_all = "kebab-case")]