extensions_ui: Add general structure for filtering extensions by what they provide (#24325)

This PR adds the general structure for filtering the extensions list by
what the extensions provide.

Currently flagged for Zed staff until we get some design direction on
how best to present the filter.

Release Notes:

- N/A
This commit is contained in:
Marshall Bowers 2025-02-05 19:09:37 -05:00 committed by GitHub
parent d81a4ec7ec
commit 4e5b11a0a7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 81 additions and 9 deletions

1
Cargo.lock generated
View file

@ -4490,6 +4490,7 @@ dependencies = [
"db", "db",
"editor", "editor",
"extension_host", "extension_host",
"feature_flags",
"fs", "fs",
"fuzzy", "fuzzy",
"gpui", "gpui",

View file

@ -8,8 +8,9 @@ mod extension_store_test;
use anyhow::{anyhow, bail, Context as _, Result}; use anyhow::{anyhow, bail, Context as _, Result};
use async_compression::futures::bufread::GzipDecoder; use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive; use async_tar::Archive;
use client::ExtensionProvides;
use client::{proto, telemetry::Telemetry, Client, ExtensionMetadata, GetExtensionsResponse}; use client::{proto, telemetry::Telemetry, Client, ExtensionMetadata, GetExtensionsResponse};
use collections::{btree_map, BTreeMap, HashMap, HashSet}; use collections::{btree_map, BTreeMap, BTreeSet, HashMap, HashSet};
use extension::extension_builder::{CompileExtensionOptions, ExtensionBuilder}; use extension::extension_builder::{CompileExtensionOptions, ExtensionBuilder};
pub use extension::ExtensionManifest; pub use extension::ExtensionManifest;
use extension::{ use extension::{
@ -464,6 +465,7 @@ impl ExtensionStore {
pub fn fetch_extensions( pub fn fetch_extensions(
&self, &self,
search: Option<&str>, search: Option<&str>,
provides_filter: Option<&BTreeSet<ExtensionProvides>>,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Task<Result<Vec<ExtensionMetadata>>> { ) -> Task<Result<Vec<ExtensionMetadata>>> {
let version = CURRENT_SCHEMA_VERSION.to_string(); let version = CURRENT_SCHEMA_VERSION.to_string();
@ -472,6 +474,17 @@ impl ExtensionStore {
query.push(("filter", search)); query.push(("filter", search));
} }
let provides_filter = provides_filter.map(|provides_filter| {
provides_filter
.iter()
.map(|provides| provides.to_string())
.collect::<Vec<_>>()
.join(",")
});
if let Some(provides_filter) = provides_filter.as_deref() {
query.push(("provides", provides_filter));
}
self.fetch_extensions_from_api("/extensions", &query, cx) self.fetch_extensions_from_api("/extensions", &query, cx)
} }

View file

@ -18,6 +18,7 @@ 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

View file

@ -10,6 +10,7 @@ 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,
@ -210,6 +211,7 @@ pub struct ExtensionsPage {
filtered_remote_extension_indices: Vec<usize>, filtered_remote_extension_indices: Vec<usize>,
query_editor: Entity<Editor>, query_editor: Entity<Editor>,
query_contains_error: bool, query_contains_error: bool,
provides_filter: Option<ExtensionProvides>,
_subscriptions: [gpui::Subscription; 2], _subscriptions: [gpui::Subscription; 2],
extension_fetch_task: Option<Task<()>>, extension_fetch_task: Option<Task<()>>,
upsells: BTreeSet<Feature>, upsells: BTreeSet<Feature>,
@ -261,12 +263,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,
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, cx); this.fetch_extensions(None, None, cx);
this this
}) })
} }
@ -363,7 +366,12 @@ impl ExtensionsPage {
cx.notify(); cx.notify();
} }
fn fetch_extensions(&mut self, search: Option<String>, cx: &mut Context<Self>) { fn fetch_extensions(
&mut self,
search: Option<String>,
provides_filter: Option<BTreeSet<ExtensionProvides>>,
cx: &mut Context<Self>,
) {
self.is_fetching_extensions = true; self.is_fetching_extensions = true;
cx.notify(); cx.notify();
@ -374,7 +382,7 @@ impl ExtensionsPage {
}); });
let remote_extensions = extension_store.update(cx, |store, cx| { let remote_extensions = extension_store.update(cx, |store, cx| {
store.fetch_extensions(search.as_deref(), cx) store.fetch_extensions(search.as_deref(), provides_filter.as_ref(), cx)
}); });
cx.spawn(move |this, mut cx| async move { cx.spawn(move |this, mut cx| async move {
@ -953,11 +961,15 @@ impl ExtensionsPage {
) { ) {
if let editor::EditorEvent::Edited { .. } = event { if let editor::EditorEvent::Edited { .. } = event {
self.query_contains_error = false; self.query_contains_error = false;
self.fetch_extensions_debounced(cx); self.refresh_search(cx);
self.refresh_feature_upsells(cx);
} }
} }
fn refresh_search(&mut self, cx: &mut Context<Self>) {
self.fetch_extensions_debounced(cx);
self.refresh_feature_upsells(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(|this, mut cx| async move { self.extension_fetch_task = Some(cx.spawn(|this, mut cx| async move {
let search = this let search = this
@ -978,7 +990,7 @@ impl ExtensionsPage {
}; };
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
this.fetch_extensions(search, cx); this.fetch_extensions(search, Some(BTreeSet::from_iter(this.provides_filter)), cx);
}) })
.ok(); .ok();
})); }));
@ -1162,7 +1174,41 @@ impl Render for ExtensionsPage {
.w_full() .w_full()
.gap_2() .gap_2()
.justify_between() .justify_between()
.child(h_flex().child(self.render_search(cx))) .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( .child(
h_flex() h_flex()
.child( .child(

View file

@ -19,7 +19,18 @@ pub struct ExtensionApiManifest {
} }
#[derive( #[derive(
Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize, EnumString, Debug,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Clone,
Copy,
Serialize,
Deserialize,
EnumString,
strum::Display,
)] )]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
#[strum(serialize_all = "kebab-case")] #[strum(serialize_all = "kebab-case")]