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::MagicWand,
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                                    Some(editor::actions::ToggleInlayHints.boxed_clone()),
224                                    {
225                                        let editor = editor.clone();
226                                        move |cx| {
227                                            editor.update(cx, |editor, cx| {
228                                                editor.toggle_inlay_hints(
229                                                    &editor::actions::ToggleInlayHints,
230                                                    cx,
231                                                );
232                                            });
233                                        }
234                                    },
235                                );
236                            }
237
238                            menu = menu.toggleable_entry(
239                                "Inline Git Blame",
240                                git_blame_inline_enabled,
241                                Some(editor::actions::ToggleGitBlameInline.boxed_clone()),
242                                {
243                                    let editor = editor.clone();
244                                    move |cx| {
245                                        editor.update(cx, |editor, cx| {
246                                            editor.toggle_git_blame_inline(
247                                                &editor::actions::ToggleGitBlameInline,
248                                                cx,
249                                            )
250                                        });
251                                    }
252                                },
253                            );
254
255                            menu = menu.toggleable_entry(
256                                "Selection Menu",
257                                selection_menu_enabled,
258                                Some(editor::actions::ToggleSelectionMenu.boxed_clone()),
259                                {
260                                    let editor = editor.clone();
261                                    move |cx| {
262                                        editor.update(cx, |editor, cx| {
263                                            editor.toggle_selection_menu(
264                                                &editor::actions::ToggleSelectionMenu,
265                                                cx,
266                                            )
267                                        });
268                                    }
269                                },
270                            );
271
272                            menu = menu.toggleable_entry(
273                                "Auto Signature Help",
274                                auto_signature_help_enabled,
275                                Some(editor::actions::ToggleAutoSignatureHelp.boxed_clone()),
276                                {
277                                    let editor = editor.clone();
278                                    move |cx| {
279                                        editor.update(cx, |editor, cx| {
280                                            editor.toggle_auto_signature_help_menu(
281                                                &editor::actions::ToggleAutoSignatureHelp,
282                                                cx,
283                                            );
284                                        });
285                                    }
286                                },
287                            );
288
289                            menu
290                        });
291                        cx.subscribe(&menu, |quick_action_bar, _, _: &DismissEvent, _cx| {
292                            quick_action_bar.toggle_settings_menu = None;
293                        })
294                        .detach();
295                        quick_action_bar.toggle_settings_menu = Some(menu);
296                    })
297                })
298                .when(self.toggle_settings_menu.is_none(), |this| {
299                    this.tooltip(|cx| Tooltip::text("Editor Controls", cx))
300                });
301
302        h_flex()
303            .id("quick action bar")
304            .gap(Spacing::Medium.rems(cx))
305            .children(self.render_repl_menu(cx))
306            .children(self.render_toggle_markdown_preview(self.workspace.clone(), cx))
307            .children(search_button)
308            .when(
309                AssistantSettings::get_global(cx).enabled
310                    && AssistantSettings::get_global(cx).button,
311                |bar| bar.child(assistant_button),
312            )
313            .children(editor_selections_dropdown)
314            .child(editor_settings_dropdown)
315            .when_some(self.repl_menu.as_ref(), |el, repl_menu| {
316                el.child(Self::render_menu_overlay(repl_menu))
317            })
318            .when_some(
319                self.toggle_settings_menu.as_ref(),
320                |el, toggle_settings_menu| {
321                    el.child(Self::render_menu_overlay(toggle_settings_menu))
322                },
323            )
324            .when_some(
325                self.toggle_selections_menu.as_ref(),
326                |el, toggle_selections_menu| {
327                    el.child(Self::render_menu_overlay(toggle_selections_menu))
328                },
329            )
330    }
331}
332
333impl EventEmitter<ToolbarItemEvent> for QuickActionBar {}
334
335#[derive(IntoElement)]
336struct QuickActionBarButton {
337    id: ElementId,
338    icon: IconName,
339    toggled: bool,
340    action: Box<dyn Action>,
341    tooltip: SharedString,
342    on_click: Box<dyn Fn(&ClickEvent, &mut WindowContext)>,
343}
344
345impl QuickActionBarButton {
346    fn new(
347        id: impl Into<ElementId>,
348        icon: IconName,
349        toggled: bool,
350        action: Box<dyn Action>,
351        tooltip: impl Into<SharedString>,
352        on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
353    ) -> Self {
354        Self {
355            id: id.into(),
356            icon,
357            toggled,
358            action,
359            tooltip: tooltip.into(),
360            on_click: Box::new(on_click),
361        }
362    }
363}
364
365impl RenderOnce for QuickActionBarButton {
366    fn render(self, _: &mut WindowContext) -> impl IntoElement {
367        let tooltip = self.tooltip.clone();
368        let action = self.action.boxed_clone();
369
370        IconButton::new(self.id.clone(), self.icon)
371            .shape(IconButtonShape::Square)
372            .icon_size(IconSize::Small)
373            .style(ButtonStyle::Subtle)
374            .selected(self.toggled)
375            .tooltip(move |cx| Tooltip::for_action(tooltip.clone(), &*action, cx))
376            .on_click(move |event, cx| (self.on_click)(event, cx))
377    }
378}
379
380impl ToolbarItemView for QuickActionBar {
381    fn set_active_pane_item(
382        &mut self,
383        active_pane_item: Option<&dyn ItemHandle>,
384        cx: &mut ViewContext<Self>,
385    ) -> ToolbarItemLocation {
386        self.active_item = active_pane_item.map(ItemHandle::boxed_clone);
387        if let Some(active_item) = active_pane_item {
388            self._inlay_hints_enabled_subscription.take();
389
390            if let Some(editor) = active_item.downcast::<Editor>() {
391                let mut inlay_hints_enabled = editor.read(cx).inlay_hints_enabled();
392                let mut supports_inlay_hints = editor.read(cx).supports_inlay_hints(cx);
393                self._inlay_hints_enabled_subscription =
394                    Some(cx.observe(&editor, move |_, editor, cx| {
395                        let editor = editor.read(cx);
396                        let new_inlay_hints_enabled = editor.inlay_hints_enabled();
397                        let new_supports_inlay_hints = editor.supports_inlay_hints(cx);
398                        let should_notify = inlay_hints_enabled != new_inlay_hints_enabled
399                            || supports_inlay_hints != new_supports_inlay_hints;
400                        inlay_hints_enabled = new_inlay_hints_enabled;
401                        supports_inlay_hints = new_supports_inlay_hints;
402                        if should_notify {
403                            cx.notify()
404                        }
405                    }));
406            }
407        }
408        self.get_toolbar_item_location()
409    }
410}