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}