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
23pub struct QuickActionBar {
24 buffer_search_bar: View<BufferSearchBar>,
25 toggle_settings_menu: Option<View<ContextMenu>>,
26 toggle_selections_menu: Option<View<ContextMenu>>,
27 active_item: Option<Box<dyn ItemHandle>>,
28 _inlay_hints_enabled_subscription: Option<Subscription>,
29 workspace: WeakView<Workspace>,
30 show: bool,
31}
32
33impl QuickActionBar {
34 pub fn new(
35 buffer_search_bar: View<BufferSearchBar>,
36 workspace: &Workspace,
37 cx: &mut ViewContext<Self>,
38 ) -> Self {
39 let mut this = Self {
40 buffer_search_bar,
41 toggle_settings_menu: None,
42 toggle_selections_menu: None,
43 active_item: None,
44 _inlay_hints_enabled_subscription: None,
45 workspace: workspace.weak_handle(),
46 show: true,
47 };
48 this.apply_settings(cx);
49 cx.observe_global::<SettingsStore>(|this, cx| this.apply_settings(cx))
50 .detach();
51 this
52 }
53
54 fn active_editor(&self) -> Option<View<Editor>> {
55 self.active_item
56 .as_ref()
57 .and_then(|item| item.downcast::<Editor>())
58 }
59
60 fn apply_settings(&mut self, cx: &mut ViewContext<Self>) {
61 let new_show = EditorSettings::get_global(cx).toolbar.quick_actions;
62 if new_show != self.show {
63 self.show = new_show;
64 cx.emit(ToolbarItemEvent::ChangeLocation(
65 self.get_toolbar_item_location(),
66 ));
67 }
68 }
69
70 fn get_toolbar_item_location(&self) -> ToolbarItemLocation {
71 if self.show && self.active_editor().is_some() {
72 ToolbarItemLocation::PrimaryRight
73 } else {
74 ToolbarItemLocation::Hidden
75 }
76 }
77
78 fn render_menu_overlay(menu: &View<ContextMenu>) -> Div {
79 div().absolute().bottom_0().right_0().size_0().child(
80 deferred(
81 anchored()
82 .anchor(AnchorCorner::TopRight)
83 .child(menu.clone()),
84 )
85 .with_priority(1),
86 )
87 }
88}
89
90impl Render for QuickActionBar {
91 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
92 let Some(editor) = self.active_editor() else {
93 return div().id("empty quick action bar");
94 };
95
96 let (
97 selection_menu_enabled,
98 inlay_hints_enabled,
99 supports_inlay_hints,
100 git_blame_inline_enabled,
101 ) = {
102 let editor = editor.read(cx);
103 let selection_menu_enabled = editor.selection_menu_enabled(cx);
104 let inlay_hints_enabled = editor.inlay_hints_enabled();
105 let supports_inlay_hints = editor.supports_inlay_hints(cx);
106 let git_blame_inline_enabled = editor.git_blame_inline_enabled();
107
108 (
109 selection_menu_enabled,
110 inlay_hints_enabled,
111 supports_inlay_hints,
112 git_blame_inline_enabled,
113 )
114 };
115
116 let search_button = editor.is_singleton(cx).then(|| {
117 QuickActionBarButton::new(
118 "toggle buffer search",
119 IconName::MagnifyingGlass,
120 !self.buffer_search_bar.read(cx).is_dismissed(),
121 Box::new(buffer_search::Deploy::find()),
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::MagicWand,
137 false,
138 Box::new(InlineAssist),
139 "Inline Assist",
140 {
141 let workspace = self.workspace.clone();
142 move |_, cx| {
143 if let Some(workspace) = workspace.upgrade() {
144 workspace.update(cx, |workspace, cx| {
145 AssistantPanel::inline_assist(workspace, &InlineAssist, cx);
146 });
147 }
148 }
149 },
150 );
151
152 let editor_selections_dropdown = selection_menu_enabled.then(|| {
153 IconButton::new("toggle_editor_selections_icon", IconName::TextCursor)
154 .size(ButtonSize::Compact)
155 .icon_size(IconSize::Small)
156 .style(ButtonStyle::Subtle)
157 .selected(self.toggle_selections_menu.is_some())
158 .on_click({
159 let focus = editor.focus_handle(cx);
160 cx.listener(move |quick_action_bar, _, cx| {
161 let focus = focus.clone();
162 let menu = ContextMenu::build(cx, move |menu, _| {
163 menu.context(focus.clone())
164 .action("Select All", Box::new(SelectAll))
165 .action(
166 "Select Next Occurrence",
167 Box::new(SelectNext {
168 replace_newest: false,
169 }),
170 )
171 .action("Expand Selection", Box::new(SelectLargerSyntaxNode))
172 .action("Shrink Selection", Box::new(SelectSmallerSyntaxNode))
173 .action("Add Cursor Above", Box::new(AddSelectionAbove))
174 .action("Add Cursor Below", Box::new(AddSelectionBelow))
175 .separator()
176 .action("Go to Symbol", Box::new(ToggleOutline))
177 .action("Go to Line/Column", Box::new(ToggleGoToLine))
178 .separator()
179 .action("Next Problem", Box::new(GoToDiagnostic))
180 .action("Previous Problem", Box::new(GoToPrevDiagnostic))
181 .separator()
182 .action("Next Hunk", Box::new(GoToHunk))
183 .action("Previous Hunk", Box::new(GoToPrevHunk))
184 .separator()
185 .action("Move Line Up", Box::new(MoveLineUp))
186 .action("Move Line Down", Box::new(MoveLineDown))
187 .action("Duplicate Selection", Box::new(DuplicateLineDown))
188 });
189 cx.subscribe(&menu, |quick_action_bar, _, _: &DismissEvent, _cx| {
190 quick_action_bar.toggle_selections_menu = None;
191 })
192 .detach();
193 quick_action_bar.toggle_selections_menu = Some(menu);
194 })
195 })
196 .when(self.toggle_selections_menu.is_none(), |this| {
197 this.tooltip(|cx| Tooltip::text("Selection Controls", cx))
198 })
199 });
200
201 let editor_settings_dropdown =
202 IconButton::new("toggle_editor_settings_icon", IconName::Sliders)
203 .size(ButtonSize::Compact)
204 .icon_size(IconSize::Small)
205 .style(ButtonStyle::Subtle)
206 .selected(self.toggle_settings_menu.is_some())
207 .on_click({
208 let editor = editor.clone();
209 cx.listener(move |quick_action_bar, _, cx| {
210 let menu = ContextMenu::build(cx, |mut menu, _| {
211 if supports_inlay_hints {
212 menu = menu.toggleable_entry(
213 "Show Inlay Hints",
214 inlay_hints_enabled,
215 Some(editor::actions::ToggleInlayHints.boxed_clone()),
216 {
217 let editor = editor.clone();
218 move |cx| {
219 editor.update(cx, |editor, cx| {
220 editor.toggle_inlay_hints(
221 &editor::actions::ToggleInlayHints,
222 cx,
223 );
224 });
225 }
226 },
227 );
228 }
229
230 menu = menu.toggleable_entry(
231 "Show Git Blame Inline",
232 git_blame_inline_enabled,
233 Some(editor::actions::ToggleGitBlameInline.boxed_clone()),
234 {
235 let editor = editor.clone();
236 move |cx| {
237 editor.update(cx, |editor, cx| {
238 editor.toggle_git_blame_inline(
239 &editor::actions::ToggleGitBlameInline,
240 cx,
241 )
242 });
243 }
244 },
245 );
246
247 menu = menu.toggleable_entry(
248 "Show Selection Menu",
249 selection_menu_enabled,
250 Some(editor::actions::ToggleSelectionMenu.boxed_clone()),
251 {
252 let editor = editor.clone();
253 move |cx| {
254 editor.update(cx, |editor, cx| {
255 editor.toggle_selection_menu(
256 &editor::actions::ToggleSelectionMenu,
257 cx,
258 )
259 });
260 }
261 },
262 );
263
264 menu
265 });
266 cx.subscribe(&menu, |quick_action_bar, _, _: &DismissEvent, _cx| {
267 quick_action_bar.toggle_settings_menu = None;
268 })
269 .detach();
270 quick_action_bar.toggle_settings_menu = Some(menu);
271 })
272 })
273 .when(self.toggle_settings_menu.is_none(), |this| {
274 this.tooltip(|cx| Tooltip::text("Editor Controls", cx))
275 });
276
277 h_flex()
278 .id("quick action bar")
279 .gap_3()
280 .child(
281 h_flex()
282 .gap_1p5()
283 .children(search_button)
284 .children(editor_selections_dropdown)
285 .when(AssistantSettings::get_global(cx).button, |bar| {
286 bar.child(assistant_button)
287 }),
288 )
289 .child(editor_settings_dropdown)
290 .when_some(
291 self.toggle_settings_menu.as_ref(),
292 |el, toggle_settings_menu| {
293 el.child(Self::render_menu_overlay(toggle_settings_menu))
294 },
295 )
296 .when_some(
297 self.toggle_selections_menu.as_ref(),
298 |el, toggle_selections_menu| {
299 el.child(Self::render_menu_overlay(toggle_selections_menu))
300 },
301 )
302 }
303}
304
305impl EventEmitter<ToolbarItemEvent> for QuickActionBar {}
306
307#[derive(IntoElement)]
308struct QuickActionBarButton {
309 id: ElementId,
310 icon: IconName,
311 toggled: bool,
312 action: Box<dyn Action>,
313 tooltip: SharedString,
314 on_click: Box<dyn Fn(&ClickEvent, &mut WindowContext)>,
315}
316
317impl QuickActionBarButton {
318 fn new(
319 id: impl Into<ElementId>,
320 icon: IconName,
321 toggled: bool,
322 action: Box<dyn Action>,
323 tooltip: impl Into<SharedString>,
324 on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
325 ) -> Self {
326 Self {
327 id: id.into(),
328 icon,
329 toggled,
330 action,
331 tooltip: tooltip.into(),
332 on_click: Box::new(on_click),
333 }
334 }
335}
336
337impl RenderOnce for QuickActionBarButton {
338 fn render(self, _: &mut WindowContext) -> impl IntoElement {
339 let tooltip = self.tooltip.clone();
340 let action = self.action.boxed_clone();
341
342 IconButton::new(self.id.clone(), self.icon)
343 .size(ButtonSize::Compact)
344 .icon_size(IconSize::Small)
345 .style(ButtonStyle::Subtle)
346 .selected(self.toggled)
347 .tooltip(move |cx| Tooltip::for_action(tooltip.clone(), &*action, cx))
348 .on_click(move |event, cx| (self.on_click)(event, cx))
349 }
350}
351
352impl ToolbarItemView for QuickActionBar {
353 fn set_active_pane_item(
354 &mut self,
355 active_pane_item: Option<&dyn ItemHandle>,
356 cx: &mut ViewContext<Self>,
357 ) -> ToolbarItemLocation {
358 self.active_item = active_pane_item.map(ItemHandle::boxed_clone);
359 if let Some(active_item) = active_pane_item {
360 self._inlay_hints_enabled_subscription.take();
361
362 if let Some(editor) = active_item.downcast::<Editor>() {
363 let mut inlay_hints_enabled = editor.read(cx).inlay_hints_enabled();
364 let mut supports_inlay_hints = editor.read(cx).supports_inlay_hints(cx);
365 self._inlay_hints_enabled_subscription =
366 Some(cx.observe(&editor, move |_, editor, cx| {
367 let editor = editor.read(cx);
368 let new_inlay_hints_enabled = editor.inlay_hints_enabled();
369 let new_supports_inlay_hints = editor.supports_inlay_hints(cx);
370 let should_notify = inlay_hints_enabled != new_inlay_hints_enabled
371 || supports_inlay_hints != new_supports_inlay_hints;
372 inlay_hints_enabled = new_inlay_hints_enabled;
373 supports_inlay_hints = new_supports_inlay_hints;
374 if should_notify {
375 cx.notify()
376 }
377 }));
378 }
379 }
380 self.get_toolbar_item_location()
381 }
382}