mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-28 09:54:33 +00:00
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:
parent
d7a25c1696
commit
2ae1a472e4
6 changed files with 205 additions and 9 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -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",
|
||||
]
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
mod extension_card;
|
||||
mod feature_upsell;
|
||||
|
||||
pub use extension_card::*;
|
||||
pub use feature_upsell::*;
|
||||
|
|
72
crates/extensions_ui/src/components/feature_upsell.rs
Normal file
72
crates/extensions_ui/src/components/feature_upsell.rs
Normal 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)
|
||||
}),
|
||||
)
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -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());
|
||||
|
|
Loading…
Reference in a new issue