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 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}