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