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, FocusHandle, FocusableView,
 12    InteractiveElement, ParentElement, 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 focus_handle = editor.read(cx).focus_handle(cx);
114
115        let search_button = editor.is_singleton(cx).then(|| {
116            QuickActionBarButton::new(
117                "toggle buffer search",
118                IconName::MagnifyingGlass,
119                !self.buffer_search_bar.read(cx).is_dismissed(),
120                Box::new(buffer_search::Deploy::find()),
121                focus_handle.clone(),
122                "Buffer Search",
123                {
124                    let buffer_search_bar = self.buffer_search_bar.clone();
125                    move |_, cx| {
126                        buffer_search_bar.update(cx, |search_bar, cx| {
127                            search_bar.toggle(&buffer_search::Deploy::find(), cx)
128                        });
129                    }
130                },
131            )
132        });
133
134        let assistant_button = QuickActionBarButton::new(
135            "toggle inline assistant",
136            IconName::ZedAssistant,
137            false,
138            Box::new(InlineAssist::default()),
139            focus_handle.clone(),
140            "Inline Assist",
141            {
142                let workspace = self.workspace.clone();
143                move |_, cx| {
144                    if let Some(workspace) = workspace.upgrade() {
145                        workspace.update(cx, |workspace, cx| {
146                            AssistantPanel::inline_assist(workspace, &InlineAssist::default(), cx);
147                        });
148                    }
149                }
150            },
151        );
152
153        let editor_selections_dropdown = selection_menu_enabled.then(|| {
154            let focus = editor.focus_handle(cx);
155            PopoverMenu::new("editor-selections-dropdown")
156                .trigger(
157                    IconButton::new("toggle_editor_selections_icon", IconName::CursorIBeam)
158                        .shape(IconButtonShape::Square)
159                        .icon_size(IconSize::Small)
160                        .style(ButtonStyle::Subtle)
161                        .selected(self.toggle_selections_handle.is_deployed())
162                        .when(!self.toggle_selections_handle.is_deployed(), |this| {
163                            this.tooltip(|cx| Tooltip::text("Selection Controls", cx))
164                        }),
165                )
166                .with_handle(self.toggle_selections_handle.clone())
167                .anchor(AnchorCorner::TopRight)
168                .menu(move |cx| {
169                    let focus = focus.clone();
170                    let menu = ContextMenu::build(cx, move |menu, _| {
171                        menu.context(focus.clone())
172                            .action("Select All", Box::new(SelectAll))
173                            .action(
174                                "Select Next Occurrence",
175                                Box::new(SelectNext {
176                                    replace_newest: false,
177                                }),
178                            )
179                            .action("Expand Selection", Box::new(SelectLargerSyntaxNode))
180                            .action("Shrink Selection", Box::new(SelectSmallerSyntaxNode))
181                            .action("Add Cursor Above", Box::new(AddSelectionAbove))
182                            .action("Add Cursor Below", Box::new(AddSelectionBelow))
183                            .separator()
184                            .action("Go to Symbol", Box::new(ToggleOutline))
185                            .action("Go to Line/Column", Box::new(ToggleGoToLine))
186                            .separator()
187                            .action("Next Problem", Box::new(GoToDiagnostic))
188                            .action("Previous Problem", Box::new(GoToPrevDiagnostic))
189                            .separator()
190                            .action("Next Hunk", Box::new(GoToHunk))
191                            .action("Previous Hunk", Box::new(GoToPrevHunk))
192                            .separator()
193                            .action("Move Line Up", Box::new(MoveLineUp))
194                            .action("Move Line Down", Box::new(MoveLineDown))
195                            .action("Duplicate Selection", Box::new(DuplicateLineDown))
196                    });
197                    Some(menu)
198                })
199        });
200
201        let editor = editor.downgrade();
202        let editor_settings_dropdown = PopoverMenu::new("editor-settings")
203            .trigger(
204                IconButton::new("toggle_editor_settings_icon", IconName::Sliders)
205                    .shape(IconButtonShape::Square)
206                    .icon_size(IconSize::Small)
207                    .style(ButtonStyle::Subtle)
208                    .selected(self.toggle_settings_handle.is_deployed())
209                    .when(!self.toggle_settings_handle.is_deployed(), |this| {
210                        this.tooltip(|cx| Tooltip::text("Editor Controls", cx))
211                    }),
212            )
213            .anchor(AnchorCorner::TopRight)
214            .with_handle(self.toggle_settings_handle.clone())
215            .menu(move |cx| {
216                let menu = ContextMenu::build(cx, |mut menu, _| {
217                    if supports_inlay_hints {
218                        menu = menu.toggleable_entry(
219                            "Inlay Hints",
220                            inlay_hints_enabled,
221                            IconPosition::Start,
222                            Some(editor::actions::ToggleInlayHints.boxed_clone()),
223                            {
224                                let editor = editor.clone();
225                                move |cx| {
226                                    editor
227                                        .update(cx, |editor, cx| {
228                                            editor.toggle_inlay_hints(
229                                                &editor::actions::ToggleInlayHints,
230                                                cx,
231                                            );
232                                        })
233                                        .ok();
234                                }
235                            },
236                        );
237                    }
238
239                    menu = menu.toggleable_entry(
240                        "Inline Git Blame",
241                        git_blame_inline_enabled,
242                        IconPosition::Start,
243                        Some(editor::actions::ToggleGitBlameInline.boxed_clone()),
244                        {
245                            let editor = editor.clone();
246                            move |cx| {
247                                editor
248                                    .update(cx, |editor, cx| {
249                                        editor.toggle_git_blame_inline(
250                                            &editor::actions::ToggleGitBlameInline,
251                                            cx,
252                                        )
253                                    })
254                                    .ok();
255                            }
256                        },
257                    );
258
259                    menu = menu.toggleable_entry(
260                        "Selection Menu",
261                        selection_menu_enabled,
262                        IconPosition::Start,
263                        Some(editor::actions::ToggleSelectionMenu.boxed_clone()),
264                        {
265                            let editor = editor.clone();
266                            move |cx| {
267                                editor
268                                    .update(cx, |editor, cx| {
269                                        editor.toggle_selection_menu(
270                                            &editor::actions::ToggleSelectionMenu,
271                                            cx,
272                                        )
273                                    })
274                                    .ok();
275                            }
276                        },
277                    );
278
279                    menu = menu.toggleable_entry(
280                        "Auto Signature Help",
281                        auto_signature_help_enabled,
282                        IconPosition::Start,
283                        Some(editor::actions::ToggleAutoSignatureHelp.boxed_clone()),
284                        {
285                            let editor = editor.clone();
286                            move |cx| {
287                                editor
288                                    .update(cx, |editor, cx| {
289                                        editor.toggle_auto_signature_help_menu(
290                                            &editor::actions::ToggleAutoSignatureHelp,
291                                            cx,
292                                        );
293                                    })
294                                    .ok();
295                            }
296                        },
297                    );
298
299                    menu
300                });
301                Some(menu)
302            });
303
304        h_flex()
305            .id("quick action bar")
306            .gap(Spacing::Medium.rems(cx))
307            .children(self.render_repl_menu(cx))
308            .children(self.render_toggle_markdown_preview(self.workspace.clone(), cx))
309            .children(search_button)
310            .when(
311                AssistantSettings::get_global(cx).enabled
312                    && AssistantSettings::get_global(cx).button,
313                |bar| bar.child(assistant_button),
314            )
315            .children(editor_selections_dropdown)
316            .child(editor_settings_dropdown)
317    }
318}
319
320impl EventEmitter<ToolbarItemEvent> for QuickActionBar {}
321
322#[derive(IntoElement)]
323struct QuickActionBarButton {
324    id: ElementId,
325    icon: IconName,
326    toggled: bool,
327    action: Box<dyn Action>,
328    focus_handle: FocusHandle,
329    tooltip: SharedString,
330    on_click: Box<dyn Fn(&ClickEvent, &mut WindowContext)>,
331}
332
333impl QuickActionBarButton {
334    fn new(
335        id: impl Into<ElementId>,
336        icon: IconName,
337        toggled: bool,
338        action: Box<dyn Action>,
339        focus_handle: FocusHandle,
340        tooltip: impl Into<SharedString>,
341        on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
342    ) -> Self {
343        Self {
344            id: id.into(),
345            icon,
346            toggled,
347            action,
348            focus_handle,
349            tooltip: tooltip.into(),
350            on_click: Box::new(on_click),
351        }
352    }
353}
354
355impl RenderOnce for QuickActionBarButton {
356    fn render(self, _: &mut WindowContext) -> impl IntoElement {
357        let tooltip = self.tooltip.clone();
358        let action = self.action.boxed_clone();
359
360        IconButton::new(self.id.clone(), self.icon)
361            .shape(IconButtonShape::Square)
362            .icon_size(IconSize::Small)
363            .style(ButtonStyle::Subtle)
364            .selected(self.toggled)
365            .tooltip(move |cx| {
366                Tooltip::for_action_in(tooltip.clone(), &*action, &self.focus_handle, cx)
367            })
368            .on_click(move |event, cx| (self.on_click)(event, cx))
369    }
370}
371
372impl ToolbarItemView for QuickActionBar {
373    fn set_active_pane_item(
374        &mut self,
375        active_pane_item: Option<&dyn ItemHandle>,
376        cx: &mut ViewContext<Self>,
377    ) -> ToolbarItemLocation {
378        self.active_item = active_pane_item.map(ItemHandle::boxed_clone);
379        if let Some(active_item) = active_pane_item {
380            self._inlay_hints_enabled_subscription.take();
381
382            if let Some(editor) = active_item.downcast::<Editor>() {
383                let mut inlay_hints_enabled = editor.read(cx).inlay_hints_enabled();
384                let mut supports_inlay_hints = editor.read(cx).supports_inlay_hints(cx);
385                self._inlay_hints_enabled_subscription =
386                    Some(cx.observe(&editor, move |_, editor, cx| {
387                        let editor = editor.read(cx);
388                        let new_inlay_hints_enabled = editor.inlay_hints_enabled();
389                        let new_supports_inlay_hints = editor.supports_inlay_hints(cx);
390                        let should_notify = inlay_hints_enabled != new_inlay_hints_enabled
391                            || supports_inlay_hints != new_supports_inlay_hints;
392                        inlay_hints_enabled = new_inlay_hints_enabled;
393                        supports_inlay_hints = new_supports_inlay_hints;
394                        if should_notify {
395                            cx.notify()
396                        }
397                    }));
398            }
399        }
400        self.get_toolbar_item_location()
401    }
402}