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