1use std::collections::HashSet;
2use std::sync::OnceLock;
3use std::sync::atomic::{AtomicUsize, Ordering};
4
5use db::kvp::KeyValueStore;
6use gpui::{App, EntityId, EventEmitter, Subscription};
7use ui::{IconButtonShape, Tooltip, prelude::*};
8use workspace::item::{ItemBufferKind, ItemEvent, ItemHandle};
9use workspace::{ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
10
11pub struct MultibufferHint {
12 shown_on: HashSet<EntityId>,
13 active_item: Option<Box<dyn ItemHandle>>,
14 subscription: Option<Subscription>,
15}
16
17const NUMBER_OF_HINTS: usize = 10;
18
19const SHOWN_COUNT_KEY: &str = "MULTIBUFFER_HINT_SHOWN_COUNT";
20
21impl Default for MultibufferHint {
22 fn default() -> Self {
23 Self::new()
24 }
25}
26
27impl MultibufferHint {
28 pub fn new() -> Self {
29 Self {
30 shown_on: Default::default(),
31 active_item: None,
32 subscription: None,
33 }
34 }
35}
36
37impl MultibufferHint {
38 fn counter(cx: &App) -> &'static AtomicUsize {
39 static SHOWN_COUNT: OnceLock<AtomicUsize> = OnceLock::new();
40 SHOWN_COUNT.get_or_init(|| {
41 let value: usize = KeyValueStore::global(cx)
42 .read_kvp(SHOWN_COUNT_KEY)
43 .ok()
44 .flatten()
45 .and_then(|v| v.parse().ok())
46 .unwrap_or(0);
47
48 AtomicUsize::new(value)
49 })
50 }
51
52 fn shown_count(cx: &App) -> usize {
53 Self::counter(cx).load(Ordering::Relaxed)
54 }
55
56 fn increment_count(cx: &mut App) {
57 Self::set_count(Self::shown_count(cx) + 1, cx)
58 }
59
60 pub(crate) fn set_count(count: usize, cx: &mut App) {
61 Self::counter(cx).store(count, Ordering::Relaxed);
62
63 let kvp = KeyValueStore::global(cx);
64 db::write_and_log(cx, move || async move {
65 kvp.write_kvp(SHOWN_COUNT_KEY.to_string(), format!("{}", count))
66 .await
67 });
68 }
69
70 fn dismiss(&mut self, cx: &mut App) {
71 Self::set_count(NUMBER_OF_HINTS, cx)
72 }
73
74 /// Determines the toolbar location for this [`MultibufferHint`].
75 fn determine_toolbar_location(&mut self, cx: &mut Context<Self>) -> ToolbarItemLocation {
76 if Self::shown_count(cx) >= NUMBER_OF_HINTS {
77 return ToolbarItemLocation::Hidden;
78 }
79
80 let Some(active_pane_item) = self.active_item.as_ref() else {
81 return ToolbarItemLocation::Hidden;
82 };
83
84 if active_pane_item.buffer_kind(cx) == ItemBufferKind::Singleton
85 || active_pane_item.breadcrumbs(cx).is_none()
86 || !active_pane_item.can_save(cx)
87 {
88 return ToolbarItemLocation::Hidden;
89 }
90
91 if self.shown_on.insert(active_pane_item.item_id()) {
92 Self::increment_count(cx);
93 }
94
95 ToolbarItemLocation::Secondary
96 }
97}
98
99impl EventEmitter<ToolbarItemEvent> for MultibufferHint {}
100
101impl ToolbarItemView for MultibufferHint {
102 fn set_active_pane_item(
103 &mut self,
104 active_pane_item: Option<&dyn ItemHandle>,
105 window: &mut Window,
106 cx: &mut Context<Self>,
107 ) -> ToolbarItemLocation {
108 cx.notify();
109 self.active_item = active_pane_item.map(|item| item.boxed_clone());
110
111 let Some(active_pane_item) = active_pane_item else {
112 return ToolbarItemLocation::Hidden;
113 };
114
115 let this = cx.entity().downgrade();
116 self.subscription = Some(active_pane_item.subscribe_to_item_events(
117 window,
118 cx,
119 Box::new(move |event, _, cx| {
120 if let ItemEvent::UpdateBreadcrumbs = event {
121 this.update(cx, |this, cx| {
122 cx.notify();
123 let location = this.determine_toolbar_location(cx);
124 cx.emit(ToolbarItemEvent::ChangeLocation(location))
125 })
126 .ok();
127 }
128 }),
129 ));
130
131 self.determine_toolbar_location(cx)
132 }
133}
134
135impl Render for MultibufferHint {
136 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
137 h_flex()
138 .px_2()
139 .py_0p5()
140 .justify_between()
141 .bg(cx.theme().status().info_background.opacity(0.5))
142 .border_1()
143 .border_color(cx.theme().colors().border_variant)
144 .rounded_sm()
145 .overflow_hidden()
146 .child(
147 h_flex()
148 .gap_0p5()
149 .child(
150 h_flex()
151 .gap_2()
152 .child(
153 Icon::new(IconName::Info)
154 .size(IconSize::XSmall)
155 .color(Color::Muted),
156 )
157 .child(Label::new(
158 "Edit and save files directly in the results multibuffer!",
159 )),
160 )
161 .child(
162 Button::new("open_docs", "Learn More")
163 .end_icon(
164 Icon::new(IconName::ArrowUpRight)
165 .size(IconSize::Small)
166 .color(Color::Muted),
167 )
168 .on_click(move |_event, _, cx| {
169 cx.open_url("https://zed.dev/docs/multibuffers")
170 }),
171 ),
172 )
173 .child(
174 IconButton::new("dismiss", IconName::Close)
175 .shape(IconButtonShape::Square)
176 .icon_size(IconSize::Small)
177 .on_click(cx.listener(|this, _event, _, cx| {
178 this.dismiss(cx);
179 cx.emit(ToolbarItemEvent::ChangeLocation(
180 ToolbarItemLocation::Hidden,
181 ))
182 }))
183 .tooltip(Tooltip::text("Dismiss Hint")),
184 )
185 .into_any_element()
186 }
187}