multibuffer_hint.rs

  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}