1use std::collections::HashSet;
2use std::sync::atomic::{AtomicUsize, Ordering};
3use std::sync::OnceLock;
4
5use db::kvp::KEY_VALUE_STORE;
6use gpui::{AppContext, EntityId, EventEmitter, Subscription};
7use ui::{prelude::*, ButtonLike, IconButtonShape, Tooltip};
8use workspace::item::{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() -> &'static AtomicUsize {
39 static SHOWN_COUNT: OnceLock<AtomicUsize> = OnceLock::new();
40 SHOWN_COUNT.get_or_init(|| {
41 let value: usize = KEY_VALUE_STORE
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() -> usize {
53 Self::counter().load(Ordering::Relaxed)
54 }
55
56 fn increment_count(cx: &mut AppContext) {
57 Self::set_count(Self::shown_count() + 1, cx)
58 }
59
60 pub(crate) fn set_count(count: usize, cx: &mut AppContext) {
61 Self::counter().store(count, Ordering::Relaxed);
62
63 db::write_and_log(cx, move || {
64 KEY_VALUE_STORE.write_kvp(SHOWN_COUNT_KEY.to_string(), format!("{}", count))
65 });
66 }
67
68 fn dismiss(&mut self, cx: &mut AppContext) {
69 Self::set_count(NUMBER_OF_HINTS, cx)
70 }
71
72 /// Determines the toolbar location for this [`MultibufferHint`].
73 fn determine_toolbar_location(&mut self, cx: &mut ViewContext<Self>) -> ToolbarItemLocation {
74 if Self::shown_count() >= NUMBER_OF_HINTS {
75 return ToolbarItemLocation::Hidden;
76 }
77
78 let Some(active_pane_item) = self.active_item.as_ref() else {
79 return ToolbarItemLocation::Hidden;
80 };
81
82 if active_pane_item.is_singleton(cx)
83 || active_pane_item.breadcrumbs(cx.theme(), cx).is_none()
84 {
85 return ToolbarItemLocation::Hidden;
86 }
87
88 if self.shown_on.insert(active_pane_item.item_id()) {
89 Self::increment_count(cx);
90 }
91
92 ToolbarItemLocation::Secondary
93 }
94}
95
96impl EventEmitter<ToolbarItemEvent> for MultibufferHint {}
97
98impl ToolbarItemView for MultibufferHint {
99 fn set_active_pane_item(
100 &mut self,
101 active_pane_item: Option<&dyn ItemHandle>,
102 cx: &mut ViewContext<Self>,
103 ) -> ToolbarItemLocation {
104 cx.notify();
105 self.active_item = active_pane_item.map(|item| item.boxed_clone());
106
107 let Some(active_pane_item) = active_pane_item else {
108 return ToolbarItemLocation::Hidden;
109 };
110
111 let this = cx.view().downgrade();
112 self.subscription = Some(active_pane_item.subscribe_to_item_events(
113 cx,
114 Box::new(move |event, cx| {
115 if let ItemEvent::UpdateBreadcrumbs = event {
116 this.update(cx, |this, cx| {
117 cx.notify();
118 let location = this.determine_toolbar_location(cx);
119 cx.emit(ToolbarItemEvent::ChangeLocation(location))
120 })
121 .ok();
122 }
123 }),
124 ));
125
126 self.determine_toolbar_location(cx)
127 }
128}
129
130impl Render for MultibufferHint {
131 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
132 h_flex()
133 .px_2()
134 .justify_between()
135 .bg(cx.theme().status().info_background)
136 .rounded_md()
137 .child(
138 h_flex()
139 .gap_2()
140 .child(Label::new(
141 "Edit and save files directly in the results multibuffer!",
142 ))
143 .child(
144 ButtonLike::new("open_docs")
145 .style(ButtonStyle::Transparent)
146 .child(
147 h_flex()
148 .gap_1()
149 .child(Label::new("Read more…"))
150 .child(Icon::new(IconName::ArrowUpRight).size(IconSize::Small)),
151 )
152 .on_click(move |_event, cx| {
153 cx.open_url("https://zed.dev/docs/multibuffers")
154 }),
155 ),
156 )
157 .child(
158 IconButton::new("dismiss", IconName::Close)
159 .style(ButtonStyle::Transparent)
160 .shape(IconButtonShape::Square)
161 .icon_size(IconSize::Small)
162 .on_click(cx.listener(|this, _event, cx| {
163 this.dismiss(cx);
164 cx.emit(ToolbarItemEvent::ChangeLocation(
165 ToolbarItemLocation::Hidden,
166 ))
167 }))
168 .tooltip(move |cx| Tooltip::text("Dismiss this hint", cx)),
169 )
170 .into_any_element()
171 }
172}