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