quick_action_bar.rs

  1use assistant::assistant_settings::AssistantSettings;
  2use assistant::{AssistantPanel, InlineAssist};
  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    anchored, deferred, Action, AnchorCorner, ClickEvent, DismissEvent, ElementId, EventEmitter,
 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, Tooltip,
 18};
 19use workspace::{
 20    item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
 21};
 22
 23mod repl_menu;
 24mod toggle_markdown_preview;
 25
 26pub struct QuickActionBar {
 27    _inlay_hints_enabled_subscription: Option<Subscription>,
 28    active_item: Option<Box<dyn ItemHandle>>,
 29    buffer_search_bar: View<BufferSearchBar>,
 30    repl_menu: Option<View<ContextMenu>>,
 31    show: bool,
 32    toggle_selections_menu: Option<View<ContextMenu>>,
 33    toggle_settings_menu: Option<View<ContextMenu>>,
 34    workspace: WeakView<Workspace>,
 35}
 36
 37impl QuickActionBar {
 38    pub fn new(
 39        buffer_search_bar: View<BufferSearchBar>,
 40        workspace: &Workspace,
 41        cx: &mut ViewContext<Self>,
 42    ) -> Self {
 43        let mut this = Self {
 44            _inlay_hints_enabled_subscription: None,
 45            active_item: None,
 46            buffer_search_bar,
 47            repl_menu: None,
 48            show: true,
 49            toggle_selections_menu: None,
 50            toggle_settings_menu: None,
 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    fn render_menu_overlay(menu: &View<ContextMenu>) -> Div {
 84        div().absolute().bottom_0().right_0().size_0().child(
 85            deferred(
 86                anchored()
 87                    .anchor(AnchorCorner::TopRight)
 88                    .child(menu.clone()),
 89            )
 90            .with_priority(1),
 91        )
 92    }
 93}
 94
 95impl Render for QuickActionBar {
 96    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
 97        let Some(editor) = self.active_editor() else {
 98            return div().id("empty quick action bar");
 99        };
100
101        let (
102            selection_menu_enabled,
103            inlay_hints_enabled,
104            supports_inlay_hints,
105            git_blame_inline_enabled,
106            auto_signature_help_enabled,
107        ) = {
108            let editor = editor.read(cx);
109            let selection_menu_enabled = editor.selection_menu_enabled(cx);
110            let inlay_hints_enabled = editor.inlay_hints_enabled();
111            let supports_inlay_hints = editor.supports_inlay_hints(cx);
112            let git_blame_inline_enabled = editor.git_blame_inline_enabled();
113            let auto_signature_help_enabled = editor.auto_signature_help_enabled(cx);
114
115            (
116                selection_menu_enabled,
117                inlay_hints_enabled,
118                supports_inlay_hints,
119                git_blame_inline_enabled,
120                auto_signature_help_enabled,
121            )
122        };
123
124        let search_button = editor.is_singleton(cx).then(|| {
125            QuickActionBarButton::new(
126                "toggle buffer search",
127                IconName::MagnifyingGlass,
128                !self.buffer_search_bar.read(cx).is_dismissed(),
129                Box::new(buffer_search::Deploy::find()),
130                "Buffer Search",
131                {
132                    let buffer_search_bar = self.buffer_search_bar.clone();
133                    move |_, cx| {
134                        buffer_search_bar.update(cx, |search_bar, cx| {
135                            search_bar.toggle(&buffer_search::Deploy::find(), cx)
136                        });
137                    }
138                },
139            )
140        });
141
142        let assistant_button = QuickActionBarButton::new(
143            "toggle inline assistant",
144            IconName::ZedAssistant,
145            false,
146            Box::new(InlineAssist::default()),
147            "Inline Assist",
148            {
149                let workspace = self.workspace.clone();
150                move |_, cx| {
151                    if let Some(workspace) = workspace.upgrade() {
152                        workspace.update(cx, |workspace, cx| {
153                            AssistantPanel::inline_assist(workspace, &InlineAssist::default(), cx);
154                        });
155                    }
156                }
157            },
158        );
159
160        let editor_selections_dropdown = selection_menu_enabled.then(|| {
161            IconButton::new("toggle_editor_selections_icon", IconName::TextCursor)
162                .shape(IconButtonShape::Square)
163                .icon_size(IconSize::Small)
164                .style(ButtonStyle::Subtle)
165                .selected(self.toggle_selections_menu.is_some())
166                .on_click({
167                    let focus = editor.focus_handle(cx);
168                    cx.listener(move |quick_action_bar, _, 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                        cx.subscribe(&menu, |quick_action_bar, _, _: &DismissEvent, _cx| {
198                            quick_action_bar.toggle_selections_menu = None;
199                        })
200                        .detach();
201                        quick_action_bar.toggle_selections_menu = Some(menu);
202                    })
203                })
204                .when(self.toggle_selections_menu.is_none(), |this| {
205                    this.tooltip(|cx| Tooltip::text("Selection Controls", cx))
206                })
207        });
208
209        let editor_settings_dropdown =
210            IconButton::new("toggle_editor_settings_icon", IconName::Sliders)
211                .shape(IconButtonShape::Square)
212                .icon_size(IconSize::Small)
213                .style(ButtonStyle::Subtle)
214                .selected(self.toggle_settings_menu.is_some())
215                .on_click({
216                    let editor = editor.clone();
217                    cx.listener(move |quick_action_bar, _, cx| {
218                        let menu = ContextMenu::build(cx, |mut menu, _| {
219                            if supports_inlay_hints {
220                                menu = menu.toggleable_entry(
221                                    "Inlay Hints",
222                                    inlay_hints_enabled,
223                                    IconPosition::Start,
224                                    Some(editor::actions::ToggleInlayHints.boxed_clone()),
225                                    {
226                                        let editor = editor.clone();
227                                        move |cx| {
228                                            editor.update(cx, |editor, cx| {
229                                                editor.toggle_inlay_hints(
230                                                    &editor::actions::ToggleInlayHints,
231                                                    cx,
232                                                );
233                                            });
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.update(cx, |editor, cx| {
248                                            editor.toggle_git_blame_inline(
249                                                &editor::actions::ToggleGitBlameInline,
250                                                cx,
251                                            )
252                                        });
253                                    }
254                                },
255                            );
256
257                            menu = menu.toggleable_entry(
258                                "Selection Menu",
259                                selection_menu_enabled,
260                                IconPosition::Start,
261                                Some(editor::actions::ToggleSelectionMenu.boxed_clone()),
262                                {
263                                    let editor = editor.clone();
264                                    move |cx| {
265                                        editor.update(cx, |editor, cx| {
266                                            editor.toggle_selection_menu(
267                                                &editor::actions::ToggleSelectionMenu,
268                                                cx,
269                                            )
270                                        });
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.update(cx, |editor, cx| {
284                                            editor.toggle_auto_signature_help_menu(
285                                                &editor::actions::ToggleAutoSignatureHelp,
286                                                cx,
287                                            );
288                                        });
289                                    }
290                                },
291                            );
292
293                            menu
294                        });
295                        cx.subscribe(&menu, |quick_action_bar, _, _: &DismissEvent, _cx| {
296                            quick_action_bar.toggle_settings_menu = None;
297                        })
298                        .detach();
299                        quick_action_bar.toggle_settings_menu = Some(menu);
300                    })
301                })
302                .when(self.toggle_settings_menu.is_none(), |this| {
303                    this.tooltip(|cx| Tooltip::text("Editor Controls", cx))
304                });
305
306        h_flex()
307            .id("quick action bar")
308            .gap(Spacing::Medium.rems(cx))
309            .children(self.render_repl_menu(cx))
310            .children(self.render_toggle_markdown_preview(self.workspace.clone(), cx))
311            .children(search_button)
312            .when(
313                AssistantSettings::get_global(cx).enabled
314                    && AssistantSettings::get_global(cx).button,
315                |bar| bar.child(assistant_button),
316            )
317            .children(editor_selections_dropdown)
318            .child(editor_settings_dropdown)
319            .when_some(self.repl_menu.as_ref(), |el, repl_menu| {
320                el.child(Self::render_menu_overlay(repl_menu))
321            })
322            .when_some(
323                self.toggle_settings_menu.as_ref(),
324                |el, toggle_settings_menu| {
325                    el.child(Self::render_menu_overlay(toggle_settings_menu))
326                },
327            )
328            .when_some(
329                self.toggle_selections_menu.as_ref(),
330                |el, toggle_selections_menu| {
331                    el.child(Self::render_menu_overlay(toggle_selections_menu))
332                },
333            )
334    }
335}
336
337impl EventEmitter<ToolbarItemEvent> for QuickActionBar {}
338
339#[derive(IntoElement)]
340struct QuickActionBarButton {
341    id: ElementId,
342    icon: IconName,
343    toggled: bool,
344    action: Box<dyn Action>,
345    tooltip: SharedString,
346    on_click: Box<dyn Fn(&ClickEvent, &mut WindowContext)>,
347}
348
349impl QuickActionBarButton {
350    fn new(
351        id: impl Into<ElementId>,
352        icon: IconName,
353        toggled: bool,
354        action: Box<dyn Action>,
355        tooltip: impl Into<SharedString>,
356        on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
357    ) -> Self {
358        Self {
359            id: id.into(),
360            icon,
361            toggled,
362            action,
363            tooltip: tooltip.into(),
364            on_click: Box::new(on_click),
365        }
366    }
367}
368
369impl RenderOnce for QuickActionBarButton {
370    fn render(self, _: &mut WindowContext) -> impl IntoElement {
371        let tooltip = self.tooltip.clone();
372        let action = self.action.boxed_clone();
373
374        IconButton::new(self.id.clone(), self.icon)
375            .shape(IconButtonShape::Square)
376            .icon_size(IconSize::Small)
377            .style(ButtonStyle::Subtle)
378            .selected(self.toggled)
379            .tooltip(move |cx| Tooltip::for_action(tooltip.clone(), &*action, cx))
380            .on_click(move |event, cx| (self.on_click)(event, cx))
381    }
382}
383
384impl ToolbarItemView for QuickActionBar {
385    fn set_active_pane_item(
386        &mut self,
387        active_pane_item: Option<&dyn ItemHandle>,
388        cx: &mut ViewContext<Self>,
389    ) -> ToolbarItemLocation {
390        self.active_item = active_pane_item.map(ItemHandle::boxed_clone);
391        if let Some(active_item) = active_pane_item {
392            self._inlay_hints_enabled_subscription.take();
393
394            if let Some(editor) = active_item.downcast::<Editor>() {
395                let mut inlay_hints_enabled = editor.read(cx).inlay_hints_enabled();
396                let mut supports_inlay_hints = editor.read(cx).supports_inlay_hints(cx);
397                self._inlay_hints_enabled_subscription =
398                    Some(cx.observe(&editor, move |_, editor, cx| {
399                        let editor = editor.read(cx);
400                        let new_inlay_hints_enabled = editor.inlay_hints_enabled();
401                        let new_supports_inlay_hints = editor.supports_inlay_hints(cx);
402                        let should_notify = inlay_hints_enabled != new_inlay_hints_enabled
403                            || supports_inlay_hints != new_supports_inlay_hints;
404                        inlay_hints_enabled = new_inlay_hints_enabled;
405                        supports_inlay_hints = new_supports_inlay_hints;
406                        if should_notify {
407                            cx.notify()
408                        }
409                    }));
410            }
411        }
412        self.get_toolbar_item_location()
413    }
414}