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, InteractiveElement, ParentElement,
12 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 search_button = editor.is_singleton(cx).then(|| {
114 QuickActionBarButton::new(
115 "toggle buffer search",
116 IconName::MagnifyingGlass,
117 !self.buffer_search_bar.read(cx).is_dismissed(),
118 Box::new(buffer_search::Deploy::find()),
119 "Buffer Search",
120 {
121 let buffer_search_bar = self.buffer_search_bar.clone();
122 move |_, cx| {
123 buffer_search_bar.update(cx, |search_bar, cx| {
124 search_bar.toggle(&buffer_search::Deploy::find(), cx)
125 });
126 }
127 },
128 )
129 });
130
131 let assistant_button = QuickActionBarButton::new(
132 "toggle inline assistant",
133 IconName::ZedAssistant,
134 false,
135 Box::new(InlineAssist::default()),
136 "Inline Assist",
137 {
138 let workspace = self.workspace.clone();
139 move |_, cx| {
140 if let Some(workspace) = workspace.upgrade() {
141 workspace.update(cx, |workspace, cx| {
142 AssistantPanel::inline_assist(workspace, &InlineAssist::default(), cx);
143 });
144 }
145 }
146 },
147 );
148
149 let editor_selections_dropdown = selection_menu_enabled.then(|| {
150 let focus = editor.focus_handle(cx);
151 PopoverMenu::new("editor-selections-dropdown")
152 .trigger(
153 IconButton::new("toggle_editor_selections_icon", IconName::TextCursor)
154 .shape(IconButtonShape::Square)
155 .icon_size(IconSize::Small)
156 .style(ButtonStyle::Subtle)
157 .selected(self.toggle_selections_handle.is_deployed())
158 .when(!self.toggle_selections_handle.is_deployed(), |this| {
159 this.tooltip(|cx| Tooltip::text("Selection Controls", cx))
160 }),
161 )
162 .with_handle(self.toggle_selections_handle.clone())
163 .anchor(AnchorCorner::TopRight)
164 .menu(move |cx| {
165 let focus = focus.clone();
166 let menu = ContextMenu::build(cx, move |menu, _| {
167 menu.context(focus.clone())
168 .action("Select All", Box::new(SelectAll))
169 .action(
170 "Select Next Occurrence",
171 Box::new(SelectNext {
172 replace_newest: false,
173 }),
174 )
175 .action("Expand Selection", Box::new(SelectLargerSyntaxNode))
176 .action("Shrink Selection", Box::new(SelectSmallerSyntaxNode))
177 .action("Add Cursor Above", Box::new(AddSelectionAbove))
178 .action("Add Cursor Below", Box::new(AddSelectionBelow))
179 .separator()
180 .action("Go to Symbol", Box::new(ToggleOutline))
181 .action("Go to Line/Column", Box::new(ToggleGoToLine))
182 .separator()
183 .action("Next Problem", Box::new(GoToDiagnostic))
184 .action("Previous Problem", Box::new(GoToPrevDiagnostic))
185 .separator()
186 .action("Next Hunk", Box::new(GoToHunk))
187 .action("Previous Hunk", Box::new(GoToPrevHunk))
188 .separator()
189 .action("Move Line Up", Box::new(MoveLineUp))
190 .action("Move Line Down", Box::new(MoveLineDown))
191 .action("Duplicate Selection", Box::new(DuplicateLineDown))
192 });
193 Some(menu)
194 })
195 });
196
197 let editor = editor.downgrade();
198 let editor_settings_dropdown = PopoverMenu::new("editor-settings")
199 .trigger(
200 IconButton::new("toggle_editor_settings_icon", IconName::Sliders)
201 .shape(IconButtonShape::Square)
202 .icon_size(IconSize::Small)
203 .style(ButtonStyle::Subtle)
204 .selected(self.toggle_settings_handle.is_deployed())
205 .when(!self.toggle_settings_handle.is_deployed(), |this| {
206 this.tooltip(|cx| Tooltip::text("Editor Controls", cx))
207 }),
208 )
209 .anchor(AnchorCorner::TopRight)
210 .with_handle(self.toggle_settings_handle.clone())
211 .menu(move |cx| {
212 let menu = ContextMenu::build(cx, |mut menu, _| {
213 if supports_inlay_hints {
214 menu = menu.toggleable_entry(
215 "Inlay Hints",
216 inlay_hints_enabled,
217 IconPosition::Start,
218 Some(editor::actions::ToggleInlayHints.boxed_clone()),
219 {
220 let editor = editor.clone();
221 move |cx| {
222 editor
223 .update(cx, |editor, cx| {
224 editor.toggle_inlay_hints(
225 &editor::actions::ToggleInlayHints,
226 cx,
227 );
228 })
229 .ok();
230 }
231 },
232 );
233 }
234
235 menu = menu.toggleable_entry(
236 "Inline Git Blame",
237 git_blame_inline_enabled,
238 IconPosition::Start,
239 Some(editor::actions::ToggleGitBlameInline.boxed_clone()),
240 {
241 let editor = editor.clone();
242 move |cx| {
243 editor
244 .update(cx, |editor, cx| {
245 editor.toggle_git_blame_inline(
246 &editor::actions::ToggleGitBlameInline,
247 cx,
248 )
249 })
250 .ok();
251 }
252 },
253 );
254
255 menu = menu.toggleable_entry(
256 "Selection Menu",
257 selection_menu_enabled,
258 IconPosition::Start,
259 Some(editor::actions::ToggleSelectionMenu.boxed_clone()),
260 {
261 let editor = editor.clone();
262 move |cx| {
263 editor
264 .update(cx, |editor, cx| {
265 editor.toggle_selection_menu(
266 &editor::actions::ToggleSelectionMenu,
267 cx,
268 )
269 })
270 .ok();
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
284 .update(cx, |editor, cx| {
285 editor.toggle_auto_signature_help_menu(
286 &editor::actions::ToggleAutoSignatureHelp,
287 cx,
288 );
289 })
290 .ok();
291 }
292 },
293 );
294
295 menu
296 });
297 Some(menu)
298 });
299
300 h_flex()
301 .id("quick action bar")
302 .gap(Spacing::Medium.rems(cx))
303 .children(self.render_repl_menu(cx))
304 .children(self.render_toggle_markdown_preview(self.workspace.clone(), cx))
305 .children(search_button)
306 .when(
307 AssistantSettings::get_global(cx).enabled
308 && AssistantSettings::get_global(cx).button,
309 |bar| bar.child(assistant_button),
310 )
311 .children(editor_selections_dropdown)
312 .child(editor_settings_dropdown)
313 }
314}
315
316impl EventEmitter<ToolbarItemEvent> for QuickActionBar {}
317
318#[derive(IntoElement)]
319struct QuickActionBarButton {
320 id: ElementId,
321 icon: IconName,
322 toggled: bool,
323 action: Box<dyn Action>,
324 tooltip: SharedString,
325 on_click: Box<dyn Fn(&ClickEvent, &mut WindowContext)>,
326}
327
328impl QuickActionBarButton {
329 fn new(
330 id: impl Into<ElementId>,
331 icon: IconName,
332 toggled: bool,
333 action: Box<dyn Action>,
334 tooltip: impl Into<SharedString>,
335 on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
336 ) -> Self {
337 Self {
338 id: id.into(),
339 icon,
340 toggled,
341 action,
342 tooltip: tooltip.into(),
343 on_click: Box::new(on_click),
344 }
345 }
346}
347
348impl RenderOnce for QuickActionBarButton {
349 fn render(self, _: &mut WindowContext) -> impl IntoElement {
350 let tooltip = self.tooltip.clone();
351 let action = self.action.boxed_clone();
352
353 IconButton::new(self.id.clone(), self.icon)
354 .shape(IconButtonShape::Square)
355 .icon_size(IconSize::Small)
356 .style(ButtonStyle::Subtle)
357 .selected(self.toggled)
358 .tooltip(move |cx| Tooltip::for_action(tooltip.clone(), &*action, cx))
359 .on_click(move |event, cx| (self.on_click)(event, cx))
360 }
361}
362
363impl ToolbarItemView for QuickActionBar {
364 fn set_active_pane_item(
365 &mut self,
366 active_pane_item: Option<&dyn ItemHandle>,
367 cx: &mut ViewContext<Self>,
368 ) -> ToolbarItemLocation {
369 self.active_item = active_pane_item.map(ItemHandle::boxed_clone);
370 if let Some(active_item) = active_pane_item {
371 self._inlay_hints_enabled_subscription.take();
372
373 if let Some(editor) = active_item.downcast::<Editor>() {
374 let mut inlay_hints_enabled = editor.read(cx).inlay_hints_enabled();
375 let mut supports_inlay_hints = editor.read(cx).supports_inlay_hints(cx);
376 self._inlay_hints_enabled_subscription =
377 Some(cx.observe(&editor, move |_, editor, cx| {
378 let editor = editor.read(cx);
379 let new_inlay_hints_enabled = editor.inlay_hints_enabled();
380 let new_supports_inlay_hints = editor.supports_inlay_hints(cx);
381 let should_notify = inlay_hints_enabled != new_inlay_hints_enabled
382 || supports_inlay_hints != new_supports_inlay_hints;
383 inlay_hints_enabled = new_inlay_hints_enabled;
384 supports_inlay_hints = new_supports_inlay_hints;
385 if should_notify {
386 cx.notify()
387 }
388 }));
389 }
390 }
391 self.get_toolbar_item_location()
392 }
393}