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:
parent
628a61d929
commit
cc36cd9768
4 changed files with 90 additions and 58 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -4821,7 +4821,6 @@ dependencies = [
|
||||||
"db",
|
"db",
|
||||||
"editor",
|
"editor",
|
||||||
"extension_host",
|
"extension_host",
|
||||||
"feature_flags",
|
|
||||||
"fs",
|
"fs",
|
||||||
"fuzzy",
|
"fuzzy",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
@ -4834,6 +4833,7 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"settings",
|
"settings",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
|
"strum",
|
||||||
"telemetry",
|
"telemetry",
|
||||||
"theme",
|
"theme",
|
||||||
"ui",
|
"ui",
|
||||||
|
|
|
@ -18,7 +18,6 @@ collections.workspace = true
|
||||||
db.workspace = true
|
db.workspace = true
|
||||||
editor.workspace = true
|
editor.workspace = true
|
||||||
extension_host.workspace = true
|
extension_host.workspace = true
|
||||||
feature_flags.workspace = true
|
|
||||||
fs.workspace = true
|
fs.workspace = true
|
||||||
fuzzy.workspace = true
|
fuzzy.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
|
@ -31,6 +30,7 @@ semantic_version.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
settings.workspace = true
|
settings.workspace = true
|
||||||
smallvec.workspace = true
|
smallvec.workspace = true
|
||||||
|
strum.workspace = true
|
||||||
telemetry.workspace = true
|
telemetry.workspace = true
|
||||||
theme.workspace = true
|
theme.workspace = true
|
||||||
ui.workspace = true
|
ui.workspace = true
|
||||||
|
|
|
@ -10,7 +10,6 @@ use client::{ExtensionMetadata, ExtensionProvides};
|
||||||
use collections::{BTreeMap, BTreeSet};
|
use collections::{BTreeMap, BTreeSet};
|
||||||
use editor::{Editor, EditorElement, EditorStyle};
|
use editor::{Editor, EditorElement, EditorStyle};
|
||||||
use extension_host::{ExtensionManifest, ExtensionOperation, ExtensionStore};
|
use extension_host::{ExtensionManifest, ExtensionOperation, ExtensionStore};
|
||||||
use feature_flags::FeatureFlagAppExt as _;
|
|
||||||
use fuzzy::{match_strings, StringMatchCandidate};
|
use fuzzy::{match_strings, StringMatchCandidate};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, uniform_list, Action, App, ClipboardItem, Context, Entity, EventEmitter, Flatten,
|
actions, uniform_list, Action, App, ClipboardItem, Context, Entity, EventEmitter, Flatten,
|
||||||
|
@ -21,6 +20,7 @@ use num_format::{Locale, ToFormattedString};
|
||||||
use project::DirectoryLister;
|
use project::DirectoryLister;
|
||||||
use release_channel::ReleaseChannel;
|
use release_channel::ReleaseChannel;
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
|
use strum::IntoEnumIterator as _;
|
||||||
use theme::ThemeSettings;
|
use theme::ThemeSettings;
|
||||||
use ui::{prelude::*, CheckboxWithLabel, ContextMenu, PopoverMenu, ToggleButton, Tooltip};
|
use ui::{prelude::*, CheckboxWithLabel, ContextMenu, PopoverMenu, ToggleButton, Tooltip};
|
||||||
use vim_mode_setting::VimModeSetting;
|
use vim_mode_setting::VimModeSetting;
|
||||||
|
@ -127,6 +127,20 @@ pub fn init(cx: &mut App) {
|
||||||
.detach();
|
.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)]
|
#[derive(Clone)]
|
||||||
pub enum ExtensionStatus {
|
pub enum ExtensionStatus {
|
||||||
NotInstalled,
|
NotInstalled,
|
||||||
|
@ -608,25 +622,6 @@ impl ExtensionsPage {
|
||||||
.provides
|
.provides
|
||||||
.iter()
|
.iter()
|
||||||
.map(|provides| {
|
.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()
|
div()
|
||||||
.bg(cx.theme().colors().element_background)
|
.bg(cx.theme().colors().element_background)
|
||||||
.px_0p5()
|
.px_0p5()
|
||||||
|
@ -634,7 +629,10 @@ impl ExtensionsPage {
|
||||||
.border_color(cx.theme().colors().border)
|
.border_color(cx.theme().colors().border)
|
||||||
.rounded_sm()
|
.rounded_sm()
|
||||||
.child(
|
.child(
|
||||||
Label::new(label).size(LabelSize::XSmall),
|
Label::new(extension_provides_label(
|
||||||
|
*provides,
|
||||||
|
))
|
||||||
|
.size(LabelSize::XSmall),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
|
@ -1140,6 +1138,53 @@ impl ExtensionsPage {
|
||||||
upsell.when(ix < upsells_count, |upsell| upsell.border_b_1())
|
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 {
|
impl Render for ExtensionsPage {
|
||||||
|
@ -1174,41 +1219,27 @@ impl Render for ExtensionsPage {
|
||||||
.w_full()
|
.w_full()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.child(
|
.child(h_flex().gap_2().child(self.render_search(cx)).child({
|
||||||
h_flex()
|
let this = cx.entity().clone();
|
||||||
.gap_2()
|
PopoverMenu::new("extension-provides-filter")
|
||||||
.child(self.render_search(cx))
|
.menu(move |window, cx| {
|
||||||
.map(|parent| {
|
Some(this.update(cx, |this, cx| {
|
||||||
// Note: Staff-only until this gets design input.
|
this.build_extension_provides_filter_menu(window, cx)
|
||||||
if !cx.is_staff() {
|
}))
|
||||||
return parent;
|
})
|
||||||
}
|
.trigger_with_tooltip(
|
||||||
|
Button::new(
|
||||||
parent.child(CheckboxWithLabel::new(
|
"extension-provides-filter-button",
|
||||||
"icon-themes-filter",
|
self.provides_filter
|
||||||
Label::new("Icon themes"),
|
.map(extension_provides_label)
|
||||||
match self.provides_filter {
|
.unwrap_or("All"),
|
||||||
Some(ExtensionProvides::IconThemes) => {
|
)
|
||||||
ToggleState::Selected
|
.icon(IconName::Filter)
|
||||||
}
|
.icon_position(IconPosition::Start),
|
||||||
_ => ToggleState::Unselected,
|
Tooltip::text("Filter extensions by category"),
|
||||||
},
|
)
|
||||||
cx.listener(|this, checked, _window, cx| {
|
.anchor(gpui::Corner::TopLeft)
|
||||||
match checked {
|
}))
|
||||||
ToggleState::Unselected
|
|
||||||
| ToggleState::Indeterminate => {
|
|
||||||
this.provides_filter = None
|
|
||||||
}
|
|
||||||
ToggleState::Selected => {
|
|
||||||
this.provides_filter =
|
|
||||||
Some(ExtensionProvides::IconThemes)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
this.refresh_search(cx);
|
|
||||||
}),
|
|
||||||
))
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.child(
|
.child(
|
||||||
|
|
|
@ -31,6 +31,7 @@ pub struct ExtensionApiManifest {
|
||||||
Deserialize,
|
Deserialize,
|
||||||
EnumString,
|
EnumString,
|
||||||
strum::Display,
|
strum::Display,
|
||||||
|
strum::EnumIter,
|
||||||
)]
|
)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
#[strum(serialize_all = "kebab-case")]
|
#[strum(serialize_all = "kebab-case")]
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue