1use std::collections::HashSet;
2use std::sync::atomic::{AtomicUsize, Ordering};
3use std::sync::OnceLock;
4
5use db::kvp::KEY_VALUE_STORE;
6use gpui::{AppContext, Empty, EntityId, EventEmitter};
7use ui::{prelude::*, ButtonLike, IconButtonShape, Tooltip};
8use workspace::item::ItemHandle;
9use workspace::{ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
10
11pub struct MultibufferHint {
12 shown_on: HashSet<EntityId>,
13 active_item: Option<Box<dyn ItemHandle>>,
14}
15
16const NUMBER_OF_HINTS: usize = 10;
17
18const SHOWN_COUNT_KEY: &str = "MULTIBUFFER_HINT_SHOWN_COUNT";
19
20impl MultibufferHint {
21 pub fn new() -> Self {
22 Self {
23 shown_on: Default::default(),
24 active_item: None,
25 }
26 }
27}
28
29impl MultibufferHint {
30 fn counter() -> &'static AtomicUsize {
31 static SHOWN_COUNT: OnceLock<AtomicUsize> = OnceLock::new();
32 SHOWN_COUNT.get_or_init(|| {
33 let value: usize = KEY_VALUE_STORE
34 .read_kvp(SHOWN_COUNT_KEY)
35 .ok()
36 .flatten()
37 .and_then(|v| v.parse().ok())
38 .unwrap_or(0);
39
40 AtomicUsize::new(value)
41 })
42 }
43
44 fn shown_count() -> usize {
45 Self::counter().load(Ordering::Relaxed)
46 }
47
48 fn increment_count(cx: &mut AppContext) {
49 Self::set_count(Self::shown_count() + 1, cx)
50 }
51
52 pub(crate) fn set_count(count: usize, cx: &mut AppContext) {
53 Self::counter().store(count, Ordering::Relaxed);
54
55 db::write_and_log(cx, move || {
56 KEY_VALUE_STORE.write_kvp(SHOWN_COUNT_KEY.to_string(), format!("{}", count))
57 });
58 }
59
60 fn dismiss(&mut self, cx: &mut AppContext) {
61 Self::set_count(NUMBER_OF_HINTS, cx)
62 }
63}
64
65impl EventEmitter<ToolbarItemEvent> for MultibufferHint {}
66
67impl ToolbarItemView for MultibufferHint {
68 fn set_active_pane_item(
69 &mut self,
70 active_pane_item: Option<&dyn ItemHandle>,
71 cx: &mut ViewContext<Self>,
72 ) -> ToolbarItemLocation {
73 if Self::shown_count() > NUMBER_OF_HINTS {
74 return ToolbarItemLocation::Hidden;
75 }
76
77 let Some(active_pane_item) = active_pane_item else {
78 return ToolbarItemLocation::Hidden;
79 };
80
81 if active_pane_item.is_singleton(cx) {
82 return ToolbarItemLocation::Hidden;
83 }
84
85 if self.shown_on.insert(active_pane_item.item_id()) {
86 Self::increment_count(cx)
87 }
88
89 self.active_item = Some(active_pane_item.boxed_clone());
90 ToolbarItemLocation::Secondary
91 }
92}
93
94impl Render for MultibufferHint {
95 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
96 let Some(active_item) = self.active_item.as_ref() else {
97 return Empty.into_any_element();
98 };
99
100 if active_item.breadcrumbs(cx.theme(), cx).is_none() {
101 return Empty.into_any_element();
102 }
103
104 h_flex()
105 .px_2()
106 .justify_between()
107 .bg(cx.theme().status().info_background)
108 .rounded_md()
109 .child(
110 h_flex()
111 .gap_2()
112 .child(Label::new(
113 "Edit and save files directly in the results multibuffer!",
114 ))
115 .child(
116 ButtonLike::new("open_docs")
117 .style(ButtonStyle::Transparent)
118 .child(
119 h_flex()
120 .gap_1()
121 .child(Label::new("Read more…"))
122 .child(Icon::new(IconName::ArrowUpRight).size(IconSize::Small)),
123 )
124 .on_click(move |_event, cx| {
125 cx.open_url("https://zed.dev/docs/multibuffers")
126 }),
127 ),
128 )
129 .child(
130 IconButton::new("dismiss", IconName::Close)
131 .style(ButtonStyle::Transparent)
132 .shape(IconButtonShape::Square)
133 .icon_size(IconSize::Small)
134 .on_click(cx.listener(|this, _event, cx| {
135 this.dismiss(cx);
136 cx.emit(ToolbarItemEvent::ChangeLocation(
137 ToolbarItemLocation::Hidden,
138 ))
139 }))
140 .tooltip(move |cx| Tooltip::text("Dismiss this hint", cx)),
141 )
142 .into_any_element()
143 }
144}