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 = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"client",
|
"client",
|
||||||
|
"collections",
|
||||||
"db",
|
"db",
|
||||||
"editor",
|
"editor",
|
||||||
"extension",
|
"extension",
|
||||||
|
@ -4006,6 +4007,7 @@ dependencies = [
|
||||||
"theme_selector",
|
"theme_selector",
|
||||||
"ui",
|
"ui",
|
||||||
"util",
|
"util",
|
||||||
|
"vim",
|
||||||
"workspace",
|
"workspace",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ test-support = []
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
client.workspace = true
|
client.workspace = true
|
||||||
|
collections.workspace = true
|
||||||
db.workspace = true
|
db.workspace = true
|
||||||
editor.workspace = true
|
editor.workspace = true
|
||||||
extension.workspace = true
|
extension.workspace = true
|
||||||
|
@ -36,6 +37,7 @@ theme.workspace = true
|
||||||
theme_selector.workspace = true
|
theme_selector.workspace = true
|
||||||
ui.workspace = true
|
ui.workspace = true
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
|
vim.workspace = true
|
||||||
workspace.workspace = true
|
workspace.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
mod extension_card;
|
mod extension_card;
|
||||||
|
mod feature_upsell;
|
||||||
|
|
||||||
pub use extension_card::*;
|
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_suggest;
|
||||||
mod extension_version_selector;
|
mod extension_version_selector;
|
||||||
|
|
||||||
use crate::components::ExtensionCard;
|
use std::ops::DerefMut;
|
||||||
use crate::extension_version_selector::{
|
use std::sync::OnceLock;
|
||||||
ExtensionVersionSelector, ExtensionVersionSelectorDelegate,
|
use std::time::Duration;
|
||||||
};
|
use std::{ops::Range, sync::Arc};
|
||||||
|
|
||||||
use client::telemetry::Telemetry;
|
use client::telemetry::Telemetry;
|
||||||
use client::ExtensionMetadata;
|
use client::ExtensionMetadata;
|
||||||
|
use collections::{BTreeMap, BTreeSet};
|
||||||
use editor::{Editor, EditorElement, EditorStyle};
|
use editor::{Editor, EditorElement, EditorStyle};
|
||||||
use extension::{ExtensionManifest, ExtensionOperation, ExtensionStore};
|
use extension::{ExtensionManifest, ExtensionOperation, ExtensionStore};
|
||||||
use fuzzy::{match_strings, StringMatchCandidate};
|
use fuzzy::{match_strings, StringMatchCandidate};
|
||||||
|
@ -19,17 +21,20 @@ use gpui::{
|
||||||
use num_format::{Locale, ToFormattedString};
|
use num_format::{Locale, ToFormattedString};
|
||||||
use release_channel::ReleaseChannel;
|
use release_channel::ReleaseChannel;
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use std::ops::DerefMut;
|
|
||||||
use std::time::Duration;
|
|
||||||
use std::{ops::Range, sync::Arc};
|
|
||||||
use theme::ThemeSettings;
|
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::TabContentParams;
|
||||||
use workspace::{
|
use workspace::{
|
||||||
item::{Item, ItemEvent},
|
item::{Item, ItemEvent},
|
||||||
Workspace, WorkspaceId,
|
Workspace, WorkspaceId,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::components::{ExtensionCard, FeatureUpsell};
|
||||||
|
use crate::extension_version_selector::{
|
||||||
|
ExtensionVersionSelector, ExtensionVersionSelectorDelegate,
|
||||||
|
};
|
||||||
|
|
||||||
actions!(zed, [Extensions, InstallDevExtension]);
|
actions!(zed, [Extensions, InstallDevExtension]);
|
||||||
|
|
||||||
pub fn init(cx: &mut AppContext) {
|
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 {
|
pub struct ExtensionsPage {
|
||||||
workspace: WeakView<Workspace>,
|
workspace: WeakView<Workspace>,
|
||||||
list: UniformListScrollHandle,
|
list: UniformListScrollHandle,
|
||||||
|
@ -135,6 +164,7 @@ pub struct ExtensionsPage {
|
||||||
query_contains_error: bool,
|
query_contains_error: bool,
|
||||||
_subscriptions: [gpui::Subscription; 2],
|
_subscriptions: [gpui::Subscription; 2],
|
||||||
extension_fetch_task: Option<Task<()>>,
|
extension_fetch_task: Option<Task<()>>,
|
||||||
|
upsells: BTreeSet<Feature>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExtensionsPage {
|
impl ExtensionsPage {
|
||||||
|
@ -173,6 +203,7 @@ impl ExtensionsPage {
|
||||||
extension_fetch_task: None,
|
extension_fetch_task: None,
|
||||||
_subscriptions: subscriptions,
|
_subscriptions: subscriptions,
|
||||||
query_editor,
|
query_editor,
|
||||||
|
upsells: BTreeSet::default(),
|
||||||
};
|
};
|
||||||
this.fetch_extensions(None, cx);
|
this.fetch_extensions(None, cx);
|
||||||
this
|
this
|
||||||
|
@ -792,6 +823,7 @@ 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.fetch_extensions_debounced(cx);
|
||||||
|
self.refresh_feature_upsells(cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -863,6 +895,91 @@ impl ExtensionsPage {
|
||||||
|
|
||||||
Label::new(message)
|
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 {
|
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| {
|
.child(v_flex().px_4().size_full().overflow_y_hidden().map(|this| {
|
||||||
let mut count = self.filtered_remote_extension_indices.len();
|
let mut count = self.filtered_remote_extension_indices.len();
|
||||||
if self.filter.include_dev_extensions() {
|
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.
|
/// Sets the border color of the element.
|
||||||
#visibility fn border_color<C>(mut self, border_color: C) -> Self
|
#visibility fn border_color<C>(mut self, border_color: C) -> Self
|
||||||
where
|
where
|
||||||
C: Into<Hsla>,
|
C: Into<gpui::Hsla>,
|
||||||
Self: Sized,
|
Self: Sized,
|
||||||
{
|
{
|
||||||
self.style().border_color = Some(border_color.into());
|
self.style().border_color = Some(border_color.into());
|
||||||
|
|
Loading…
Reference in a new issue