diff --git a/Cargo.lock b/Cargo.lock index 651ca95a2ae87b..06a48f8de738eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4490,6 +4490,7 @@ dependencies = [ "db", "editor", "extension_host", + "feature_flags", "fs", "fuzzy", "gpui", diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index 35a58af2e4ca2c..69c26d44a46b61 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -8,8 +8,9 @@ mod extension_store_test; use anyhow::{anyhow, bail, Context as _, Result}; use async_compression::futures::bufread::GzipDecoder; use async_tar::Archive; +use client::ExtensionProvides; 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}; pub use extension::ExtensionManifest; use extension::{ @@ -464,6 +465,7 @@ impl ExtensionStore { pub fn fetch_extensions( &self, search: Option<&str>, + provides_filter: Option<&BTreeSet>, cx: &mut Context, ) -> Task>> { let version = CURRENT_SCHEMA_VERSION.to_string(); @@ -472,6 +474,17 @@ impl ExtensionStore { query.push(("filter", search)); } + let provides_filter = provides_filter.map(|provides_filter| { + provides_filter + .iter() + .map(|provides| provides.to_string()) + .collect::>() + .join(",") + }); + if let Some(provides_filter) = provides_filter.as_deref() { + query.push(("provides", provides_filter)); + } + self.fetch_extensions_from_api("/extensions", &query, cx) } diff --git a/crates/extensions_ui/Cargo.toml b/crates/extensions_ui/Cargo.toml index 1df235c82209b5..afdb3bf0a359d9 100644 --- a/crates/extensions_ui/Cargo.toml +++ b/crates/extensions_ui/Cargo.toml @@ -18,6 +18,7 @@ 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 diff --git a/crates/extensions_ui/src/extensions_ui.rs b/crates/extensions_ui/src/extensions_ui.rs index 6d9cfa3f4b353b..afca5616db7f0d 100644 --- a/crates/extensions_ui/src/extensions_ui.rs +++ b/crates/extensions_ui/src/extensions_ui.rs @@ -10,6 +10,7 @@ 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, @@ -210,6 +211,7 @@ pub struct ExtensionsPage { filtered_remote_extension_indices: Vec, query_editor: Entity, query_contains_error: bool, + provides_filter: Option, _subscriptions: [gpui::Subscription; 2], extension_fetch_task: Option>, upsells: BTreeSet, @@ -261,12 +263,13 @@ impl ExtensionsPage { filtered_remote_extension_indices: Vec::new(), remote_extension_entries: Vec::new(), query_contains_error: false, + provides_filter: None, extension_fetch_task: None, _subscriptions: subscriptions, query_editor, upsells: BTreeSet::default(), }; - this.fetch_extensions(None, cx); + this.fetch_extensions(None, None, cx); this }) } @@ -363,7 +366,12 @@ impl ExtensionsPage { cx.notify(); } - fn fetch_extensions(&mut self, search: Option, cx: &mut Context) { + fn fetch_extensions( + &mut self, + search: Option, + provides_filter: Option>, + cx: &mut Context, + ) { self.is_fetching_extensions = true; cx.notify(); @@ -374,7 +382,7 @@ impl ExtensionsPage { }); 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 { @@ -953,11 +961,15 @@ impl ExtensionsPage { ) { if let editor::EditorEvent::Edited { .. } = event { self.query_contains_error = false; - self.fetch_extensions_debounced(cx); - self.refresh_feature_upsells(cx); + self.refresh_search(cx); } } + fn refresh_search(&mut self, cx: &mut Context) { + self.fetch_extensions_debounced(cx); + self.refresh_feature_upsells(cx); + } + fn fetch_extensions_debounced(&mut self, cx: &mut Context) { self.extension_fetch_task = Some(cx.spawn(|this, mut cx| async move { let search = this @@ -978,7 +990,7 @@ impl ExtensionsPage { }; this.update(&mut cx, |this, cx| { - this.fetch_extensions(search, cx); + this.fetch_extensions(search, Some(BTreeSet::from_iter(this.provides_filter)), cx); }) .ok(); })); @@ -1162,7 +1174,41 @@ impl Render for ExtensionsPage { .w_full() .gap_2() .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( h_flex() .child( diff --git a/crates/rpc/src/extension.rs b/crates/rpc/src/extension.rs index f1dcdc28d66925..e8dd22b1bbae97 100644 --- a/crates/rpc/src/extension.rs +++ b/crates/rpc/src/extension.rs @@ -19,7 +19,18 @@ pub struct ExtensionApiManifest { } #[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")] #[strum(serialize_all = "kebab-case")]