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}