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