quick_action_bar.rs

  1use assistant::assistant_settings::AssistantSettings;
  2use assistant::AssistantPanel;
  3use editor::actions::{
  4    AddSelectionAbove, AddSelectionBelow, DuplicateLineDown, GoToDiagnostic, GoToHunk,
  5    GoToPrevDiagnostic, GoToPrevHunk, MoveLineDown, MoveLineUp, SelectAll, SelectLargerSyntaxNode,
  6    SelectNext, SelectSmallerSyntaxNode, ToggleGoToLine, ToggleOutline,
  7};
  8use editor::{Editor, EditorSettings};
  9
 10use gpui::{
 11    Action, AnchorCorner, ClickEvent, ElementId, EventEmitter, InteractiveElement, ParentElement,
 12    Render, Styled, Subscription, View, ViewContext, WeakView,
 13};
 14use search::{buffer_search, BufferSearchBar};
 15use settings::{Settings, SettingsStore};
 16use ui::{
 17    prelude::*, ButtonStyle, ContextMenu, IconButton, IconButtonShape, IconName, IconSize,
 18    PopoverMenu, PopoverMenuHandle, Tooltip,
 19};
 20use workspace::{
 21    item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
 22};
 23use zed_actions::InlineAssist;
 24
 25mod repl_menu;
 26mod toggle_markdown_preview;
 27
 28pub struct QuickActionBar {
 29    _inlay_hints_enabled_subscription: Option<Subscription>,
 30    active_item: Option<Box<dyn ItemHandle>>,
 31    buffer_search_bar: View<BufferSearchBar>,
 32    show: bool,
 33    toggle_selections_handle: PopoverMenuHandle<ContextMenu>,
 34    toggle_settings_handle: PopoverMenuHandle<ContextMenu>,
 35    workspace: WeakView<Workspace>,
 36}
 37
 38impl QuickActionBar {
 39    pub fn new(
 40        buffer_search_bar: View<BufferSearchBar>,
 41        workspace: &Workspace,
 42        cx: &mut ViewContext<Self>,
 43    ) -> Self {
 44        let mut this = Self {
 45            _inlay_hints_enabled_subscription: None,
 46            active_item: None,
 47            buffer_search_bar,
 48            show: true,
 49            toggle_selections_handle: Default::default(),
 50            toggle_settings_handle: Default::default(),
 51            workspace: workspace.weak_handle(),
 52        };
 53        this.apply_settings(cx);
 54        cx.observe_global::<SettingsStore>(|this, cx| this.apply_settings(cx))
 55            .detach();
 56        this
 57    }
 58
 59    fn active_editor(&self) -> Option<View<Editor>> {
 60        self.active_item
 61            .as_ref()
 62            .and_then(|item| item.downcast::<Editor>())
 63    }
 64
 65    fn apply_settings(&mut self, cx: &mut ViewContext<Self>) {
 66        let new_show = EditorSettings::get_global(cx).toolbar.quick_actions;
 67        if new_show != self.show {
 68            self.show = new_show;
 69            cx.emit(ToolbarItemEvent::ChangeLocation(
 70                self.get_toolbar_item_location(),
 71            ));
 72        }
 73    }
 74
 75    fn get_toolbar_item_location(&self) -> ToolbarItemLocation {
 76        if self.show && self.active_editor().is_some() {
 77            ToolbarItemLocation::PrimaryRight
 78        } else {
 79            ToolbarItemLocation::Hidden
 80        }
 81    }
 82}
 83
 84impl Render for QuickActionBar {
 85    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
 86        let Some(editor) = self.active_editor() else {
 87            return div().id("empty quick action bar");
 88        };
 89
 90        let (
 91            selection_menu_enabled,
 92            inlay_hints_enabled,
 93            supports_inlay_hints,
 94            git_blame_inline_enabled,
 95            auto_signature_help_enabled,
 96        ) = {
 97            let editor = editor.read(cx);
 98            let selection_menu_enabled = editor.selection_menu_enabled(cx);
 99            let inlay_hints_enabled = editor.inlay_hints_enabled();
100            let supports_inlay_hints = editor.supports_inlay_hints(cx);
101            let git_blame_inline_enabled = editor.git_blame_inline_enabled();
102            let auto_signature_help_enabled = editor.auto_signature_help_enabled(cx);
103
104            (
105                selection_menu_enabled,
106                inlay_hints_enabled,
107                supports_inlay_hints,
108                git_blame_inline_enabled,
109                auto_signature_help_enabled,
110            )
111        };
112
113        let search_button = editor.is_singleton(cx).then(|| {
114            QuickActionBarButton::new(
115                "toggle buffer search",
116                IconName::MagnifyingGlass,
117                !self.buffer_search_bar.read(cx).is_dismissed(),
118                Box::new(buffer_search::Deploy::find()),
119                "Buffer Search",
120                {
121                    let buffer_search_bar = self.buffer_search_bar.clone();
122                    move |_, cx| {
123                        buffer_search_bar.update(cx, |search_bar, cx| {
124                            search_bar.toggle(&buffer_search::Deploy::find(), cx)
125                        });
126                    }
127                },
128            )
129        });
130
131        let assistant_button = QuickActionBarButton::new(
132            "toggle inline assistant",
133            IconName::ZedAssistant,
134            false,
135            Box::new(InlineAssist::default()),
136            "Inline Assist",
137            {
138                let workspace = self.workspace.clone();
139                move |_, cx| {
140                    if let Some(workspace) = workspace.upgrade() {
141                        workspace.update(cx, |workspace, cx| {
142                            AssistantPanel::inline_assist(workspace, &InlineAssist::default(), cx);
143                        });
144                    }
145                }
146            },
147        );
148
149        let editor_selections_dropdown = selection_menu_enabled.then(|| {
150            let focus = editor.focus_handle(cx);
151            PopoverMenu::new("editor-selections-dropdown")
152                .trigger(
153                    IconButton::new("toggle_editor_selections_icon", IconName::TextCursor)
154                        .shape(IconButtonShape::Square)
155                        .icon_size(IconSize::Small)
156                        .style(ButtonStyle::Subtle)
157                        .selected(self.toggle_selections_handle.is_deployed())
158                        .when(!self.toggle_selections_handle.is_deployed(), |this| {
159                            this.tooltip(|cx| Tooltip::text("Selection Controls", cx))
160                        }),
161                )
162                .with_handle(self.toggle_selections_handle.clone())
163                .anchor(AnchorCorner::TopRight)
164                .menu(move |cx| {
165                    let focus = focus.clone();
166                    let menu = ContextMenu::build(cx, move |menu, _| {
167                        menu.context(focus.clone())
168                            .action("Select All", Box::new(SelectAll))
169                            .action(
170                                "Select Next Occurrence",
171                                Box::new(SelectNext {
172                                    replace_newest: false,
173                                }),
174                            )
175                            .action("Expand Selection", Box::new(SelectLargerSyntaxNode))
176                            .action("Shrink Selection", Box::new(SelectSmallerSyntaxNode))
177                            .action("Add Cursor Above", Box::new(AddSelectionAbove))
178                            .action("Add Cursor Below", Box::new(AddSelectionBelow))
179                            .separator()
180                            .action("Go to Symbol", Box::new(ToggleOutline))
181                            .action("Go to Line/Column", Box::new(ToggleGoToLine))
182                            .separator()
183                            .action("Next Problem", Box::new(GoToDiagnostic))
184                            .action("Previous Problem", Box::new(GoToPrevDiagnostic))
185                            .separator()
186                            .action("Next Hunk", Box::new(GoToHunk))
187                            .action("Previous Hunk", Box::new(GoToPrevHunk))
188                            .separator()
189                            .action("Move Line Up", Box::new(MoveLineUp))
190                            .action("Move Line Down", Box::new(MoveLineDown))
191                            .action("Duplicate Selection", Box::new(DuplicateLineDown))
192                    });
193                    Some(menu)
194                })
195        });
196
197        let editor = editor.downgrade();
198        let editor_settings_dropdown = PopoverMenu::new("editor-settings")
199            .trigger(
200                IconButton::new("toggle_editor_settings_icon", IconName::Sliders)
201                    .shape(IconButtonShape::Square)
202                    .icon_size(IconSize::Small)
203                    .style(ButtonStyle::Subtle)
204                    .selected(self.toggle_settings_handle.is_deployed())
205                    .when(!self.toggle_settings_handle.is_deployed(), |this| {
206                        this.tooltip(|cx| Tooltip::text("Editor Controls", cx))
207                    }),
208            )
209            .anchor(AnchorCorner::TopRight)
210            .with_handle(self.toggle_settings_handle.clone())
211            .menu(move |cx| {
212                let menu = ContextMenu::build(cx, |mut menu, _| {
213                    if supports_inlay_hints {
214                        menu = menu.toggleable_entry(
215                            "Inlay Hints",
216                            inlay_hints_enabled,
217                            IconPosition::Start,
218                            Some(editor::actions::ToggleInlayHints.boxed_clone()),
219                            {
220                                let editor = editor.clone();
221                                move |cx| {
222                                    editor
223                                        .update(cx, |editor, cx| {
224                                            editor.toggle_inlay_hints(
225                                                &editor::actions::ToggleInlayHints,
226                                                cx,
227                                            );
228                                        })
229                                        .ok();
230                                }
231                            },
232                        );
233                    }
234
235                    menu = menu.toggleable_entry(
236                        "Inline Git Blame",
237                        git_blame_inline_enabled,
238                        IconPosition::Start,
239                        Some(editor::actions::ToggleGitBlameInline.boxed_clone()),
240                        {
241                            let editor = editor.clone();
242                            move |cx| {
243                                editor
244                                    .update(cx, |editor, cx| {
245                                        editor.toggle_git_blame_inline(
246                                            &editor::actions::ToggleGitBlameInline,
247                                            cx,
248                                        )
249                                    })
250                                    .ok();
251                            }
252                        },
253                    );
254
255                    menu = menu.toggleable_entry(
256                        "Selection Menu",
257                        selection_menu_enabled,
258                        IconPosition::Start,
259                        Some(editor::actions::ToggleSelectionMenu.boxed_clone()),
260                        {
261                            let editor = editor.clone();
262                            move |cx| {
263                                editor
264                                    .update(cx, |editor, cx| {
265                                        editor.toggle_selection_menu(
266                                            &editor::actions::ToggleSelectionMenu,
267                                            cx,
268                                        )
269                                    })
270                                    .ok();
271                            }
272                        },
273                    );
274
275                    menu = menu.toggleable_entry(
276                        "Auto Signature Help",
277                        auto_signature_help_enabled,
278                        IconPosition::Start,
279                        Some(editor::actions::ToggleAutoSignatureHelp.boxed_clone()),
280                        {
281                            let editor = editor.clone();
282                            move |cx| {
283                                editor
284                                    .update(cx, |editor, cx| {
285                                        editor.toggle_auto_signature_help_menu(
286                                            &editor::actions::ToggleAutoSignatureHelp,
287                                            cx,
288                                        );
289                                    })
290                                    .ok();
291                            }
292                        },
293                    );
294
295                    menu
296                });
297                Some(menu)
298            });
299
300        h_flex()
301            .id("quick action bar")
302            .gap(Spacing::Medium.rems(cx))
303            .children(self.render_repl_menu(cx))
304            .children(self.render_toggle_markdown_preview(self.workspace.clone(), cx))
305            .children(search_button)
306            .when(
307                AssistantSettings::get_global(cx).enabled
308                    && AssistantSettings::get_global(cx).button,
309                |bar| bar.child(assistant_button),
310            )
311            .children(editor_selections_dropdown)
312            .child(editor_settings_dropdown)
313    }
314}
315
316impl EventEmitter<ToolbarItemEvent> for QuickActionBar {}
317
318#[derive(IntoElement)]
319struct QuickActionBarButton {
320    id: ElementId,
321    icon: IconName,
322    toggled: bool,
323    action: Box<dyn Action>,
324    tooltip: SharedString,
325    on_click: Box<dyn Fn(&ClickEvent, &mut WindowContext)>,
326}
327
328impl QuickActionBarButton {
329    fn new(
330        id: impl Into<ElementId>,
331        icon: IconName,
332        toggled: bool,
333        action: Box<dyn Action>,
334        tooltip: impl Into<SharedString>,
335        on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
336    ) -> Self {
337        Self {
338            id: id.into(),
339            icon,
340            toggled,
341            action,
342            tooltip: tooltip.into(),
343            on_click: Box::new(on_click),
344        }
345    }
346}
347
348impl RenderOnce for QuickActionBarButton {
349    fn render(self, _: &mut WindowContext) -> impl IntoElement {
350        let tooltip = self.tooltip.clone();
351        let action = self.action.boxed_clone();
352
353        IconButton::new(self.id.clone(), self.icon)
354            .shape(IconButtonShape::Square)
355            .icon_size(IconSize::Small)
356            .style(ButtonStyle::Subtle)
357            .selected(self.toggled)
358            .tooltip(move |cx| Tooltip::for_action(tooltip.clone(), &*action, cx))
359            .on_click(move |event, cx| (self.on_click)(event, cx))
360    }
361}
362
363impl ToolbarItemView for QuickActionBar {
364    fn set_active_pane_item(
365        &mut self,
366        active_pane_item: Option<&dyn ItemHandle>,
367        cx: &mut ViewContext<Self>,
368    ) -> ToolbarItemLocation {
369        self.active_item = active_pane_item.map(ItemHandle::boxed_clone);
370        if let Some(active_item) = active_pane_item {
371            self._inlay_hints_enabled_subscription.take();
372
373            if let Some(editor) = active_item.downcast::<Editor>() {
374                let mut inlay_hints_enabled = editor.read(cx).inlay_hints_enabled();
375                let mut supports_inlay_hints = editor.read(cx).supports_inlay_hints(cx);
376                self._inlay_hints_enabled_subscription =
377                    Some(cx.observe(&editor, move |_, editor, cx| {
378                        let editor = editor.read(cx);
379                        let new_inlay_hints_enabled = editor.inlay_hints_enabled();
380                        let new_supports_inlay_hints = editor.supports_inlay_hints(cx);
381                        let should_notify = inlay_hints_enabled != new_inlay_hints_enabled
382                            || supports_inlay_hints != new_supports_inlay_hints;
383                        inlay_hints_enabled = new_inlay_hints_enabled;
384                        supports_inlay_hints = new_supports_inlay_hints;
385                        if should_notify {
386                            cx.notify()
387                        }
388                    }));
389            }
390        }
391        self.get_toolbar_item_location()
392    }
393}