multibuffer_hint.rs

  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}