1use assistant::assistant_settings::AssistantSettings;
2use assistant::AssistantPanel;
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 Action, AnchorCorner, ClickEvent, ElementId, EventEmitter, FocusHandle, FocusableView,
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,
18 PopoverMenu, PopoverMenuHandle, Tooltip,
19};
20use workspace::{
21 item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
22};
23use zed_actions::InlineAssist;
24
25mod repl_menu;
26mod toggle_markdown_preview;
27
28pub struct QuickActionBar {
29 _inlay_hints_enabled_subscription: Option<Subscription>,
30 active_item: Option<Box<dyn ItemHandle>>,
31 buffer_search_bar: View<BufferSearchBar>,
32 show: bool,
33 toggle_selections_handle: PopoverMenuHandle<ContextMenu>,
34 toggle_settings_handle: PopoverMenuHandle<ContextMenu>,
35 workspace: WeakView<Workspace>,
36}
37
38impl QuickActionBar {
39 pub fn new(
40 buffer_search_bar: View<BufferSearchBar>,
41 workspace: &Workspace,
42 cx: &mut ViewContext<Self>,
43 ) -> Self {
44 let mut this = Self {
45 _inlay_hints_enabled_subscription: None,
46 active_item: None,
47 buffer_search_bar,
48 show: true,
49 toggle_selections_handle: Default::default(),
50 toggle_settings_handle: Default::default(),
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
84impl Render for QuickActionBar {
85 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
86 let Some(editor) = self.active_editor() else {
87 return div().id("empty quick action bar");
88 };
89
90 let (
91 selection_menu_enabled,
92 inlay_hints_enabled,
93 supports_inlay_hints,
94 git_blame_inline_enabled,
95 auto_signature_help_enabled,
96 ) = {
97 let editor = editor.read(cx);
98 let selection_menu_enabled = editor.selection_menu_enabled(cx);
99 let inlay_hints_enabled = editor.inlay_hints_enabled();
100 let supports_inlay_hints = editor.supports_inlay_hints(cx);
101 let git_blame_inline_enabled = editor.git_blame_inline_enabled();
102 let auto_signature_help_enabled = editor.auto_signature_help_enabled(cx);
103
104 (
105 selection_menu_enabled,
106 inlay_hints_enabled,
107 supports_inlay_hints,
108 git_blame_inline_enabled,
109 auto_signature_help_enabled,
110 )
111 };
112
113 let focus_handle = editor.read(cx).focus_handle(cx);
114
115 let search_button = editor.is_singleton(cx).then(|| {
116 QuickActionBarButton::new(
117 "toggle buffer search",
118 IconName::MagnifyingGlass,
119 !self.buffer_search_bar.read(cx).is_dismissed(),
120 Box::new(buffer_search::Deploy::find()),
121 focus_handle.clone(),
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::ZedAssistant,
137 false,
138 Box::new(InlineAssist::default()),
139 focus_handle.clone(),
140 "Inline Assist",
141 {
142 let workspace = self.workspace.clone();
143 move |_, cx| {
144 if let Some(workspace) = workspace.upgrade() {
145 workspace.update(cx, |workspace, cx| {
146 AssistantPanel::inline_assist(workspace, &InlineAssist::default(), cx);
147 });
148 }
149 }
150 },
151 );
152
153 let editor_selections_dropdown = selection_menu_enabled.then(|| {
154 let focus = editor.focus_handle(cx);
155 PopoverMenu::new("editor-selections-dropdown")
156 .trigger(
157 IconButton::new("toggle_editor_selections_icon", IconName::CursorIBeam)
158 .shape(IconButtonShape::Square)
159 .icon_size(IconSize::Small)
160 .style(ButtonStyle::Subtle)
161 .selected(self.toggle_selections_handle.is_deployed())
162 .when(!self.toggle_selections_handle.is_deployed(), |this| {
163 this.tooltip(|cx| Tooltip::text("Selection Controls", cx))
164 }),
165 )
166 .with_handle(self.toggle_selections_handle.clone())
167 .anchor(AnchorCorner::TopRight)
168 .menu(move |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 Some(menu)
198 })
199 });
200
201 let editor = editor.downgrade();
202 let editor_settings_dropdown = PopoverMenu::new("editor-settings")
203 .trigger(
204 IconButton::new("toggle_editor_settings_icon", IconName::Sliders)
205 .shape(IconButtonShape::Square)
206 .icon_size(IconSize::Small)
207 .style(ButtonStyle::Subtle)
208 .selected(self.toggle_settings_handle.is_deployed())
209 .when(!self.toggle_settings_handle.is_deployed(), |this| {
210 this.tooltip(|cx| Tooltip::text("Editor Controls", cx))
211 }),
212 )
213 .anchor(AnchorCorner::TopRight)
214 .with_handle(self.toggle_settings_handle.clone())
215 .menu(move |cx| {
216 let menu = ContextMenu::build(cx, |mut menu, _| {
217 if supports_inlay_hints {
218 menu = menu.toggleable_entry(
219 "Inlay Hints",
220 inlay_hints_enabled,
221 IconPosition::Start,
222 Some(editor::actions::ToggleInlayHints.boxed_clone()),
223 {
224 let editor = editor.clone();
225 move |cx| {
226 editor
227 .update(cx, |editor, cx| {
228 editor.toggle_inlay_hints(
229 &editor::actions::ToggleInlayHints,
230 cx,
231 );
232 })
233 .ok();
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
248 .update(cx, |editor, cx| {
249 editor.toggle_git_blame_inline(
250 &editor::actions::ToggleGitBlameInline,
251 cx,
252 )
253 })
254 .ok();
255 }
256 },
257 );
258
259 menu = menu.toggleable_entry(
260 "Selection Menu",
261 selection_menu_enabled,
262 IconPosition::Start,
263 Some(editor::actions::ToggleSelectionMenu.boxed_clone()),
264 {
265 let editor = editor.clone();
266 move |cx| {
267 editor
268 .update(cx, |editor, cx| {
269 editor.toggle_selection_menu(
270 &editor::actions::ToggleSelectionMenu,
271 cx,
272 )
273 })
274 .ok();
275 }
276 },
277 );
278
279 menu = menu.toggleable_entry(
280 "Auto Signature Help",
281 auto_signature_help_enabled,
282 IconPosition::Start,
283 Some(editor::actions::ToggleAutoSignatureHelp.boxed_clone()),
284 {
285 let editor = editor.clone();
286 move |cx| {
287 editor
288 .update(cx, |editor, cx| {
289 editor.toggle_auto_signature_help_menu(
290 &editor::actions::ToggleAutoSignatureHelp,
291 cx,
292 );
293 })
294 .ok();
295 }
296 },
297 );
298
299 menu
300 });
301 Some(menu)
302 });
303
304 h_flex()
305 .id("quick action bar")
306 .gap(Spacing::Medium.rems(cx))
307 .children(self.render_repl_menu(cx))
308 .children(self.render_toggle_markdown_preview(self.workspace.clone(), cx))
309 .children(search_button)
310 .when(
311 AssistantSettings::get_global(cx).enabled
312 && AssistantSettings::get_global(cx).button,
313 |bar| bar.child(assistant_button),
314 )
315 .children(editor_selections_dropdown)
316 .child(editor_settings_dropdown)
317 }
318}
319
320impl EventEmitter<ToolbarItemEvent> for QuickActionBar {}
321
322#[derive(IntoElement)]
323struct QuickActionBarButton {
324 id: ElementId,
325 icon: IconName,
326 toggled: bool,
327 action: Box<dyn Action>,
328 focus_handle: FocusHandle,
329 tooltip: SharedString,
330 on_click: Box<dyn Fn(&ClickEvent, &mut WindowContext)>,
331}
332
333impl QuickActionBarButton {
334 fn new(
335 id: impl Into<ElementId>,
336 icon: IconName,
337 toggled: bool,
338 action: Box<dyn Action>,
339 focus_handle: FocusHandle,
340 tooltip: impl Into<SharedString>,
341 on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
342 ) -> Self {
343 Self {
344 id: id.into(),
345 icon,
346 toggled,
347 action,
348 focus_handle,
349 tooltip: tooltip.into(),
350 on_click: Box::new(on_click),
351 }
352 }
353}
354
355impl RenderOnce for QuickActionBarButton {
356 fn render(self, _: &mut WindowContext) -> impl IntoElement {
357 let tooltip = self.tooltip.clone();
358 let action = self.action.boxed_clone();
359
360 IconButton::new(self.id.clone(), self.icon)
361 .shape(IconButtonShape::Square)
362 .icon_size(IconSize::Small)
363 .style(ButtonStyle::Subtle)
364 .selected(self.toggled)
365 .tooltip(move |cx| {
366 Tooltip::for_action_in(tooltip.clone(), &*action, &self.focus_handle, cx)
367 })
368 .on_click(move |event, cx| (self.on_click)(event, cx))
369 }
370}
371
372impl ToolbarItemView for QuickActionBar {
373 fn set_active_pane_item(
374 &mut self,
375 active_pane_item: Option<&dyn ItemHandle>,
376 cx: &mut ViewContext<Self>,
377 ) -> ToolbarItemLocation {
378 self.active_item = active_pane_item.map(ItemHandle::boxed_clone);
379 if let Some(active_item) = active_pane_item {
380 self._inlay_hints_enabled_subscription.take();
381
382 if let Some(editor) = active_item.downcast::<Editor>() {
383 let mut inlay_hints_enabled = editor.read(cx).inlay_hints_enabled();
384 let mut supports_inlay_hints = editor.read(cx).supports_inlay_hints(cx);
385 self._inlay_hints_enabled_subscription =
386 Some(cx.observe(&editor, move |_, editor, cx| {
387 let editor = editor.read(cx);
388 let new_inlay_hints_enabled = editor.inlay_hints_enabled();
389 let new_supports_inlay_hints = editor.supports_inlay_hints(cx);
390 let should_notify = inlay_hints_enabled != new_inlay_hints_enabled
391 || supports_inlay_hints != new_supports_inlay_hints;
392 inlay_hints_enabled = new_inlay_hints_enabled;
393 supports_inlay_hints = new_supports_inlay_hints;
394 if should_notify {
395 cx.notify()
396 }
397 }));
398 }
399 }
400 self.get_toolbar_item_location()
401 }
402}