1use chrono::Utc;
2use feature_flags::{FeatureFlagAppExt as _, PredictEditsFeatureFlag};
3use gpui::Subscription;
4use language::language_settings::{all_language_settings, InlineCompletionProvider};
5use settings::SettingsStore;
6use ui::{prelude::*, ButtonLike, Tooltip};
7use util::ResultExt;
8
9/// Prompts the user to try Zed's Edit Prediction feature
10pub struct ZedPredictBanner {
11 dismissed: bool,
12 _subscription: Subscription,
13}
14
15impl ZedPredictBanner {
16 pub fn new(cx: &mut Context<Self>) -> Self {
17 Self {
18 dismissed: get_dismissed(),
19 _subscription: cx.observe_global::<SettingsStore>(Self::handle_settings_changed),
20 }
21 }
22
23 fn should_show(&self, cx: &mut App) -> bool {
24 if !cx.has_flag::<PredictEditsFeatureFlag>() || self.dismissed {
25 return false;
26 }
27
28 let provider = all_language_settings(None, cx).inline_completions.provider;
29
30 match provider {
31 InlineCompletionProvider::None
32 | InlineCompletionProvider::Copilot
33 | InlineCompletionProvider::Supermaven => true,
34 InlineCompletionProvider::Zed => false,
35 }
36 }
37
38 fn handle_settings_changed(&mut self, cx: &mut Context<Self>) {
39 if self.dismissed {
40 return;
41 }
42
43 let provider = all_language_settings(None, cx).inline_completions.provider;
44
45 match provider {
46 InlineCompletionProvider::None
47 | InlineCompletionProvider::Copilot
48 | InlineCompletionProvider::Supermaven => {}
49 InlineCompletionProvider::Zed => {
50 self.dismiss(cx);
51 }
52 }
53 }
54
55 fn dismiss(&mut self, cx: &mut Context<Self>) {
56 persist_dismissed(cx);
57 self.dismissed = true;
58 cx.notify();
59 }
60}
61
62const DISMISSED_AT_KEY: &str = "zed_predict_banner_dismissed_at";
63
64pub(crate) fn get_dismissed() -> bool {
65 db::kvp::KEY_VALUE_STORE
66 .read_kvp(DISMISSED_AT_KEY)
67 .log_err()
68 .map_or(false, |dismissed| dismissed.is_some())
69}
70
71pub(crate) fn persist_dismissed(cx: &mut App) {
72 cx.spawn(|_| {
73 let time = Utc::now().to_rfc3339();
74 db::kvp::KEY_VALUE_STORE.write_kvp(DISMISSED_AT_KEY.into(), time)
75 })
76 .detach_and_log_err(cx);
77}
78
79impl Render for ZedPredictBanner {
80 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
81 if !self.should_show(cx) {
82 return div();
83 }
84
85 let border_color = cx.theme().colors().editor_foreground.opacity(0.3);
86 let banner = h_flex()
87 .rounded_md()
88 .border_1()
89 .border_color(border_color)
90 .child(
91 ButtonLike::new("try-zed-predict")
92 .child(
93 h_flex()
94 .h_full()
95 .items_center()
96 .gap_1p5()
97 .child(Icon::new(IconName::ZedPredict).size(IconSize::Small))
98 .child(
99 h_flex()
100 .gap_0p5()
101 .child(
102 Label::new("Introducing:")
103 .size(LabelSize::Small)
104 .color(Color::Muted),
105 )
106 .child(Label::new("Edit Prediction").size(LabelSize::Small)),
107 ),
108 )
109 .on_click(|_, window, cx| {
110 window.dispatch_action(Box::new(zed_actions::OpenZedPredictOnboarding), cx)
111 }),
112 )
113 .child(
114 div().border_l_1().border_color(border_color).child(
115 IconButton::new("close", IconName::Close)
116 .icon_size(IconSize::Indicator)
117 .on_click(cx.listener(|this, _, _window, cx| this.dismiss(cx)))
118 .tooltip(|window, cx| {
119 Tooltip::with_meta(
120 "Close Announcement Banner",
121 None,
122 "It won't show again for this feature",
123 window,
124 cx,
125 )
126 }),
127 ),
128 );
129
130 div().pr_2().child(banner)
131 }
132}