Upsell built-in features on the extensions page (#14516)

This PR extends the extensions page with support for upselling built-in
Zed features when certain keywords are searched for.

This should help inform users about features that Zed has out-of-the-box
when they go looking for them as extensions.

For example, when someone searches "vim":

<img width="1341" alt="Screenshot 2024-07-15 at 4 58 44 PM"
src="https://github.com/user-attachments/assets/b256d07a-559a-43c2-b491-3eca5bff436e">

Here are more examples of what the upsells can look like:

<img width="1341" alt="Screenshot 2024-07-15 at 4 54 39 PM"
src="https://github.com/user-attachments/assets/1f453132-ac14-4884-afc4-7c12db47ad1d">

Release Notes:

- Added banners for built-in Zed features when corresponding keywords
are used in the extension search.
This commit is contained in:
Marshall Bowers 2024-07-15 17:10:01 -04:00 committed by GitHub
parent d7a25c1696
commit 2ae1a472e4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 205 additions and 9 deletions

2
Cargo.lock generated
View File

@ -3987,6 +3987,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"client",
"collections",
"db",
"editor",
"extension",
@ -4006,6 +4007,7 @@ dependencies = [
"theme_selector",
"ui",
"util",
"vim",
"workspace",
]

View File

@ -17,6 +17,7 @@ test-support = []
[dependencies]
anyhow.workspace = true
client.workspace = true
collections.workspace = true
db.workspace = true
editor.workspace = true
extension.workspace = true
@ -36,6 +37,7 @@ theme.workspace = true
theme_selector.workspace = true
ui.workspace = true
util.workspace = true
vim.workspace = true
workspace.workspace = true
[dev-dependencies]

View File

@ -1,3 +1,5 @@
mod extension_card;
mod feature_upsell;
pub use extension_card::*;
pub use feature_upsell::*;

View File

@ -0,0 +1,72 @@
use gpui::{AnyElement, Div, StyleRefinement};
use smallvec::SmallVec;
use ui::{prelude::*, ButtonLike};
#[derive(IntoElement)]
pub struct FeatureUpsell {
base: Div,
text: SharedString,
docs_url: Option<SharedString>,
children: SmallVec<[AnyElement; 2]>,
}
impl FeatureUpsell {
pub fn new(text: impl Into<SharedString>) -> Self {
Self {
base: h_flex(),
text: text.into(),
docs_url: None,
children: SmallVec::new(),
}
}
pub fn docs_url(mut self, docs_url: impl Into<SharedString>) -> Self {
self.docs_url = Some(docs_url.into());
self
}
}
impl ParentElement for FeatureUpsell {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements)
}
}
// Style methods.
impl FeatureUpsell {
fn style(&mut self) -> &mut StyleRefinement {
self.base.style()
}
gpui::border_style_methods!({
visibility: pub
});
}
impl RenderOnce for FeatureUpsell {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
self.base
.p_4()
.justify_between()
.border_color(cx.theme().colors().border)
.child(v_flex().overflow_hidden().child(Label::new(self.text)))
.child(h_flex().gap_2().children(self.children).when_some(
self.docs_url,
|el, docs_url| {
el.child(
ButtonLike::new("open_docs")
.child(
h_flex()
.gap_2()
.child(Label::new("View docs"))
.child(Icon::new(IconName::ArrowUpRight)),
)
.on_click({
let docs_url = docs_url.clone();
move |_event, cx| cx.open_url(&docs_url)
}),
)
},
))
}
}

View File

@ -2,12 +2,14 @@ mod components;
mod extension_suggest;
mod extension_version_selector;
use crate::components::ExtensionCard;
use crate::extension_version_selector::{
ExtensionVersionSelector, ExtensionVersionSelectorDelegate,
};
use std::ops::DerefMut;
use std::sync::OnceLock;
use std::time::Duration;
use std::{ops::Range, sync::Arc};
use client::telemetry::Telemetry;
use client::ExtensionMetadata;
use collections::{BTreeMap, BTreeSet};
use editor::{Editor, EditorElement, EditorStyle};
use extension::{ExtensionManifest, ExtensionOperation, ExtensionStore};
use fuzzy::{match_strings, StringMatchCandidate};
@ -19,17 +21,20 @@ use gpui::{
use num_format::{Locale, ToFormattedString};
use release_channel::ReleaseChannel;
use settings::Settings;
use std::ops::DerefMut;
use std::time::Duration;
use std::{ops::Range, sync::Arc};
use theme::ThemeSettings;
use ui::{prelude::*, ContextMenu, PopoverMenu, ToggleButton, Tooltip};
use ui::{prelude::*, CheckboxWithLabel, ContextMenu, PopoverMenu, ToggleButton, Tooltip};
use vim::VimModeSetting;
use workspace::item::TabContentParams;
use workspace::{
item::{Item, ItemEvent},
Workspace, WorkspaceId,
};
use crate::components::{ExtensionCard, FeatureUpsell};
use crate::extension_version_selector::{
ExtensionVersionSelector, ExtensionVersionSelectorDelegate,
};
actions!(zed, [Extensions, InstallDevExtension]);
pub fn init(cx: &mut AppContext) {
@ -122,6 +127,30 @@ impl ExtensionFilter {
}
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
enum Feature {
Git,
Vim,
LanguageC,
LanguageCpp,
LanguagePython,
LanguageRust,
}
fn keywords_by_feature() -> &'static BTreeMap<Feature, Vec<&'static str>> {
static KEYWORDS_BY_FEATURE: OnceLock<BTreeMap<Feature, Vec<&'static str>>> = OnceLock::new();
KEYWORDS_BY_FEATURE.get_or_init(|| {
BTreeMap::from_iter([
(Feature::Git, vec!["git"]),
(Feature::Vim, vec!["vim"]),
(Feature::LanguageC, vec!["c", "clang"]),
(Feature::LanguageCpp, vec!["c++", "cpp", "clang"]),
(Feature::LanguagePython, vec!["python", "py"]),
(Feature::LanguageRust, vec!["rust", "rs"]),
])
})
}
pub struct ExtensionsPage {
workspace: WeakView<Workspace>,
list: UniformListScrollHandle,
@ -135,6 +164,7 @@ pub struct ExtensionsPage {
query_contains_error: bool,
_subscriptions: [gpui::Subscription; 2],
extension_fetch_task: Option<Task<()>>,
upsells: BTreeSet<Feature>,
}
impl ExtensionsPage {
@ -173,6 +203,7 @@ impl ExtensionsPage {
extension_fetch_task: None,
_subscriptions: subscriptions,
query_editor,
upsells: BTreeSet::default(),
};
this.fetch_extensions(None, cx);
this
@ -792,6 +823,7 @@ impl ExtensionsPage {
if let editor::EditorEvent::Edited { .. } = event {
self.query_contains_error = false;
self.fetch_extensions_debounced(cx);
self.refresh_feature_upsells(cx);
}
}
@ -863,6 +895,91 @@ impl ExtensionsPage {
Label::new(message)
}
fn update_settings<T: Settings>(
&mut self,
selection: &Selection,
cx: &mut ViewContext<Self>,
callback: impl 'static + Send + Fn(&mut T::FileContent, bool),
) {
if let Some(workspace) = self.workspace.upgrade() {
let fs = workspace.read(cx).app_state().fs.clone();
let selection = *selection;
settings::update_settings_file::<T>(fs, cx, move |settings| {
let value = match selection {
Selection::Unselected => false,
Selection::Selected => true,
_ => return,
};
callback(settings, value)
});
}
}
fn refresh_feature_upsells(&mut self, cx: &mut ViewContext<Self>) {
let Some(search) = self.search_query(cx) else {
self.upsells.clear();
return;
};
let search = search.to_lowercase();
let search_terms = search
.split_whitespace()
.map(|term| term.trim())
.collect::<Vec<_>>();
for (feature, keywords) in keywords_by_feature() {
if keywords
.iter()
.any(|keyword| search_terms.contains(keyword))
{
self.upsells.insert(*feature);
} else {
self.upsells.remove(&feature);
}
}
}
fn render_feature_upsells(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let upsells_count = self.upsells.len();
v_flex().children(self.upsells.iter().enumerate().map(|(ix, feature)| {
let upsell = match feature {
Feature::Git => FeatureUpsell::new("Zed comes with basic Git support for diffs and branches. More Git features are coming in the future."),
Feature::Vim => FeatureUpsell::new("Vim support is built-in to Zed!")
.docs_url("https://zed.dev/docs/vim")
.child(CheckboxWithLabel::new(
"enable-vim",
Label::new("Enable vim mode"),
if VimModeSetting::get_global(cx).0 {
ui::Selection::Selected
} else {
ui::Selection::Unselected
},
cx.listener(move |this, selection, cx| {
this.telemetry
.report_app_event("extensions: toggle vim".to_string());
this.update_settings::<VimModeSetting>(
selection,
cx,
|setting, value| *setting = Some(value),
);
}),
)),
Feature::LanguageC => FeatureUpsell::new("C support is built-in to Zed!")
.docs_url("https://zed.dev/docs/languages/c"),
Feature::LanguageCpp => FeatureUpsell::new("C++ support is built-in to Zed!")
.docs_url("https://zed.dev/docs/languages/cpp"),
Feature::LanguagePython => FeatureUpsell::new("Python support is built-in to Zed!")
.docs_url("https://zed.dev/docs/languages/python"),
Feature::LanguageRust => FeatureUpsell::new("Rust support is built-in to Zed!")
.docs_url("https://zed.dev/docs/languages/rust"),
};
upsell.when(ix < upsells_count, |upsell| upsell.border_b_1())
}))
}
}
impl Render for ExtensionsPage {
@ -945,6 +1062,7 @@ impl Render for ExtensionsPage {
),
),
)
.child(self.render_feature_upsells(cx))
.child(v_flex().px_4().size_full().overflow_y_hidden().map(|this| {
let mut count = self.filtered_remote_extension_indices.len();
if self.filter.include_dev_extensions() {

View File

@ -353,7 +353,7 @@ pub fn border_style_methods(input: TokenStream) -> TokenStream {
/// Sets the border color of the element.
#visibility fn border_color<C>(mut self, border_color: C) -> Self
where
C: Into<Hsla>,
C: Into<gpui::Hsla>,
Self: Sized,
{
self.style().border_color = Some(border_color.into());