1use std::{fmt::Write as _, ops::Range, time::Duration};
2
3use db::anyhow::anyhow;
4use gpui::{
5 AnyElement, AppContext as _, Axis, Context, Entity, EventEmitter, FocusHandle, Focusable,
6 FontWeight, Global, IntoElement, Length, ListHorizontalSizingBehavior, ListSizingBehavior,
7 MouseButton, Subscription, Task, UniformListScrollHandle, actions, div, px, uniform_list,
8};
9
10use editor::{EditorSettings, ShowScrollbar, scroll::ScrollbarAutoHide};
11use settings::Settings as _;
12use ui::{
13 ActiveTheme as _, App, BorrowAppContext, ParentElement as _, Render, Scrollbar, ScrollbarState,
14 SharedString, Styled as _, Window, prelude::*,
15};
16use workspace::{Item, SerializableItem, Workspace, register_serializable_item};
17
18use crate::{
19 keybindings::persistence::KEYBINDING_EDITORS,
20 ui_components::table::{Table, TableInteractionState},
21};
22
23actions!(zed, [OpenKeymapEditor]);
24
25pub fn init(cx: &mut App) {
26 let keymap_event_channel = KeymapEventChannel::new();
27 cx.set_global(keymap_event_channel);
28
29 cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
30 workspace.register_action(|workspace, _: &OpenKeymapEditor, window, cx| {
31 let open_keymap_editor = KeymapEditor::new(window, cx);
32 workspace.add_item_to_center(Box::new(open_keymap_editor), window, cx);
33 });
34 })
35 .detach();
36
37 register_serializable_item::<KeymapEditor>(cx);
38}
39
40pub struct KeymapEventChannel {}
41
42impl Global for KeymapEventChannel {}
43
44impl KeymapEventChannel {
45 fn new() -> Self {
46 Self {}
47 }
48
49 pub fn trigger_keymap_changed(cx: &mut App) {
50 cx.update_global(|_event_channel: &mut Self, _| {
51 /* triggers observers in KeymapEditors */
52 });
53 }
54}
55
56struct KeymapEditor {
57 focus_handle: FocusHandle,
58 _keymap_subscription: Subscription,
59 processed_bindings: Vec<ProcessedKeybinding>,
60 table_interaction_state: Entity<TableInteractionState>,
61 scroll_handle: UniformListScrollHandle,
62 horizontal_scrollbar: ScrollbarProperties,
63 vertical_scrollbar: ScrollbarProperties,
64}
65
66impl EventEmitter<()> for KeymapEditor {}
67
68impl Focusable for KeymapEditor {
69 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
70 self.focus_handle.clone()
71 }
72}
73
74impl KeymapEditor {
75 fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
76 let this = cx.new(|cx| {
77 let focus_handle = cx.focus_handle();
78
79 let _keymap_subscription = cx.observe_global::<KeymapEventChannel>(|this, cx| {
80 let key_bindings = Self::process_bindings(cx);
81 this.processed_bindings = key_bindings;
82 });
83
84 let table_interaction_state = TableInteractionState::new(window, cx);
85
86 let scroll_handle = UniformListScrollHandle::new();
87 let vertical_scrollbar = ScrollbarProperties {
88 axis: Axis::Vertical,
89 state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()),
90 show_scrollbar: false,
91 show_track: false,
92 auto_hide: false,
93 hide_task: None,
94 };
95
96 let horizontal_scrollbar = ScrollbarProperties {
97 axis: Axis::Horizontal,
98 state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()),
99 show_scrollbar: false,
100 show_track: false,
101 auto_hide: false,
102 hide_task: None,
103 };
104
105 let mut this = Self {
106 focus_handle: focus_handle.clone(),
107 _keymap_subscription,
108 processed_bindings: vec![],
109 scroll_handle,
110 horizontal_scrollbar,
111 vertical_scrollbar,
112 table_interaction_state,
113 };
114
115 this.update_scrollbar_visibility(cx);
116 this
117 });
118 this
119 }
120
121 fn process_bindings(cx: &mut Context<Self>) -> Vec<ProcessedKeybinding> {
122 let key_bindings_ptr = cx.key_bindings();
123 let lock = key_bindings_ptr.borrow();
124 let key_bindings = lock.bindings();
125
126 let mut processed_bindings = Vec::new();
127
128 for key_binding in key_bindings {
129 let mut keystroke_text = String::new();
130 for keystroke in key_binding.keystrokes() {
131 write!(&mut keystroke_text, "{} ", keystroke.unparse()).ok();
132 }
133 let keystroke_text = keystroke_text.trim().to_string();
134
135 let context = key_binding
136 .predicate()
137 .map(|predicate| predicate.to_string())
138 .unwrap_or_else(|| "<global>".to_string());
139
140 processed_bindings.push(ProcessedKeybinding {
141 keystroke_text: keystroke_text.into(),
142 action: key_binding.action().name().into(),
143 context: context.into(),
144 })
145 }
146 processed_bindings
147 }
148
149 fn update_scrollbar_visibility(&mut self, cx: &mut Context<Self>) {
150 let show_setting = EditorSettings::get_global(cx).scrollbar.show;
151
152 let scroll_handle = self.scroll_handle.0.borrow();
153
154 let autohide = |show: ShowScrollbar, cx: &mut Context<Self>| match show {
155 ShowScrollbar::Auto => true,
156 ShowScrollbar::System => cx
157 .try_global::<ScrollbarAutoHide>()
158 .map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0),
159 ShowScrollbar::Always => false,
160 ShowScrollbar::Never => false,
161 };
162
163 let longest_item_width = scroll_handle.last_item_size.and_then(|size| {
164 (size.contents.width > size.item.width).then_some(size.contents.width)
165 });
166
167 // is there an item long enough that we should show a horizontal scrollbar?
168 let item_wider_than_container = if let Some(longest_item_width) = longest_item_width {
169 longest_item_width > px(scroll_handle.base_handle.bounds().size.width.0)
170 } else {
171 true
172 };
173
174 let show_scrollbar = match show_setting {
175 ShowScrollbar::Auto | ShowScrollbar::System | ShowScrollbar::Always => true,
176 ShowScrollbar::Never => false,
177 };
178 let show_vertical = show_scrollbar;
179
180 let show_horizontal = item_wider_than_container && show_scrollbar;
181
182 let show_horizontal_track =
183 show_horizontal && matches!(show_setting, ShowScrollbar::Always);
184
185 // TODO: we probably should hide the scroll track when the list doesn't need to scroll
186 let show_vertical_track = show_vertical && matches!(show_setting, ShowScrollbar::Always);
187
188 self.vertical_scrollbar = ScrollbarProperties {
189 axis: self.vertical_scrollbar.axis,
190 state: self.vertical_scrollbar.state.clone(),
191 show_scrollbar: show_vertical,
192 show_track: show_vertical_track,
193 auto_hide: autohide(show_setting, cx),
194 hide_task: None,
195 };
196
197 self.horizontal_scrollbar = ScrollbarProperties {
198 axis: self.horizontal_scrollbar.axis,
199 state: self.horizontal_scrollbar.state.clone(),
200 show_scrollbar: show_horizontal,
201 show_track: show_horizontal_track,
202 auto_hide: autohide(show_setting, cx),
203 hide_task: None,
204 };
205
206 cx.notify();
207 }
208
209 fn hide_scrollbars(&mut self, window: &mut Window, cx: &mut Context<Self>) {
210 self.horizontal_scrollbar.hide(window, cx);
211 self.vertical_scrollbar.hide(window, cx);
212 }
213
214 fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> impl IntoElement {
215 div()
216 .id("keymap-editor-vertical-scroll")
217 .occlude()
218 .flex_none()
219 .h_full()
220 .cursor_default()
221 .absolute()
222 .right_0()
223 .top_0()
224 .bottom_0()
225 .w(px(12.))
226 .on_mouse_move(cx.listener(|_, _, _, cx| {
227 cx.notify();
228 cx.stop_propagation()
229 }))
230 .on_hover(|_, _, cx| {
231 cx.stop_propagation();
232 })
233 .on_mouse_up(
234 MouseButton::Left,
235 cx.listener(|this, _, window, cx| {
236 if !this.vertical_scrollbar.state.is_dragging()
237 && !this.focus_handle.contains_focused(window, cx)
238 {
239 this.vertical_scrollbar.hide(window, cx);
240 cx.notify();
241 }
242
243 cx.stop_propagation();
244 }),
245 )
246 .on_any_mouse_down(|_, _, cx| {
247 cx.stop_propagation();
248 })
249 .on_scroll_wheel(cx.listener(|_, _, _, cx| {
250 cx.notify();
251 }))
252 .children(Scrollbar::vertical(self.vertical_scrollbar.state.clone()))
253 }
254
255 /// Renders the horizontal scrollbar.
256 ///
257 /// The right offset is used to determine how far to the right the
258 /// scrollbar should extend to, useful for ensuring it doesn't collide
259 /// with the vertical scrollbar when visible.
260 fn render_horizontal_scrollbar(
261 &self,
262 right_offset: Pixels,
263 cx: &mut Context<Self>,
264 ) -> impl IntoElement {
265 div()
266 .id("keymap-editor-horizontal-scroll")
267 .occlude()
268 .flex_none()
269 .w_full()
270 .cursor_default()
271 .absolute()
272 .bottom_neg_px()
273 .left_0()
274 .right_0()
275 .pr(right_offset)
276 .on_mouse_move(cx.listener(|_, _, _, cx| {
277 cx.notify();
278 cx.stop_propagation()
279 }))
280 .on_hover(|_, _, cx| {
281 cx.stop_propagation();
282 })
283 .on_any_mouse_down(|_, _, cx| {
284 cx.stop_propagation();
285 })
286 .on_mouse_up(
287 MouseButton::Left,
288 cx.listener(|this, _, window, cx| {
289 if !this.horizontal_scrollbar.state.is_dragging()
290 && !this.focus_handle.contains_focused(window, cx)
291 {
292 this.horizontal_scrollbar.hide(window, cx);
293 cx.notify();
294 }
295
296 cx.stop_propagation();
297 }),
298 )
299 .on_scroll_wheel(cx.listener(|_, _, _, cx| {
300 cx.notify();
301 }))
302 .children(Scrollbar::horizontal(
303 // percentage as f32..end_offset as f32,
304 self.horizontal_scrollbar.state.clone(),
305 ))
306 }
307}
308
309// computed state related to how to render scrollbars
310// one per axis
311// on render we just read this off the keymap editor
312// we update it when
313// - settings change
314// - on focus in, on focus out, on hover, etc.
315#[derive(Debug)]
316struct ScrollbarProperties {
317 axis: Axis,
318 show_scrollbar: bool,
319 show_track: bool,
320 auto_hide: bool,
321 hide_task: Option<Task<()>>,
322 state: ScrollbarState,
323}
324
325impl ScrollbarProperties {
326 // Shows the scrollbar and cancels any pending hide task
327 fn show(&mut self, cx: &mut Context<KeymapEditor>) {
328 if !self.auto_hide {
329 return;
330 }
331 self.show_scrollbar = true;
332 self.hide_task.take();
333 cx.notify();
334 }
335
336 fn hide(&mut self, window: &mut Window, cx: &mut Context<KeymapEditor>) {
337 const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
338
339 if !self.auto_hide {
340 return;
341 }
342
343 let axis = self.axis;
344 self.hide_task = Some(cx.spawn_in(window, async move |keymap_editor, cx| {
345 cx.background_executor()
346 .timer(SCROLLBAR_SHOW_INTERVAL)
347 .await;
348
349 if let Some(keymap_editor) = keymap_editor.upgrade() {
350 keymap_editor
351 .update(cx, |keymap_editor, cx| {
352 match axis {
353 Axis::Vertical => {
354 keymap_editor.vertical_scrollbar.show_scrollbar = false
355 }
356 Axis::Horizontal => {
357 keymap_editor.horizontal_scrollbar.show_scrollbar = false
358 }
359 }
360 cx.notify();
361 })
362 .ok();
363 }
364 }));
365 }
366}
367
368struct ProcessedKeybinding {
369 keystroke_text: SharedString,
370 action: SharedString,
371 context: SharedString,
372}
373
374impl Item for KeymapEditor {
375 type Event = ();
376
377 fn tab_content_text(&self, _detail: usize, _cx: &App) -> ui::SharedString {
378 "Keymap Editor".into()
379 }
380}
381
382impl Render for KeymapEditor {
383 fn render(&mut self, _window: &mut Window, cx: &mut ui::Context<Self>) -> impl ui::IntoElement {
384 if self.processed_bindings.is_empty() {
385 self.processed_bindings = Self::process_bindings(cx);
386 }
387
388 let scroll_track_size = px(16.);
389 let h_scroll_offset = if self.vertical_scrollbar.show_scrollbar {
390 // magic number
391 px(3.)
392 } else {
393 px(0.)
394 };
395
396 let row_count = self.processed_bindings.len();
397
398 let theme = cx.theme();
399
400 div()
401 .size_full()
402 .bg(theme.colors().background)
403 .id("keymap-editor")
404 .track_focus(&self.focus_handle)
405 .on_hover(cx.listener(move |this, hovered, window, cx| {
406 if *hovered {
407 this.horizontal_scrollbar.show(cx);
408 this.vertical_scrollbar.show(cx);
409 cx.notify();
410 } else if !this.focus_handle.contains_focused(window, cx) {
411 this.hide_scrollbars(window, cx);
412 }
413 }))
414 .child(
415 Table::uniform_list(
416 "keymap-editor-table",
417 row_count,
418 cx.processor(|this, range: Range<usize>, window, cx| {
419 range
420 .map(|index| {
421 let binding = &this.processed_bindings[index];
422 let row = [
423 binding.action.clone(),
424 binding.keystroke_text.clone(),
425 binding.context.clone(),
426 // TODO: Add a source field
427 // binding.source.clone(),
428 ];
429
430 // fixme: pass through callback as a row_cx param
431 let striped = false;
432
433 crate::ui_components::table::render_row(
434 index, row, row_count, striped, cx,
435 )
436 })
437 .collect()
438 }),
439 )
440 .header(["Command", "Keystrokes", "Context"])
441 .interactable(&self.table_interaction_state),
442 )
443 // .child(
444 // table
445 // .h_full()
446 // .v_flex()
447 // .child(table.render_header(headers, cx))
448 // .child(
449 // div()
450 // .flex_grow()
451 // .w_full()
452 // .relative()
453 // .overflow_hidden()
454 // .child(
455 // uniform_list(
456 // "keybindings",
457 // row_count,
458 // cx.processor(move |this, range, _, cx| {}),
459 // )
460 // .size_full()
461 // .flex_grow()
462 // .track_scroll(self.scroll_handle.clone())
463 // .with_sizing_behavior(ListSizingBehavior::Auto)
464 // .with_horizontal_sizing_behavior(
465 // ListHorizontalSizingBehavior::Unconstrained,
466 // ),
467 // )
468 // .when(self.vertical_scrollbar.show_track, |this| {
469 // this.child(
470 // v_flex()
471 // .h_full()
472 // .flex_none()
473 // .w(scroll_track_size)
474 // .bg(cx.theme().colors().background)
475 // .child(
476 // div()
477 // .size_full()
478 // .flex_1()
479 // .border_l_1()
480 // .border_color(cx.theme().colors().border),
481 // ),
482 // )
483 // })
484 // .when(self.vertical_scrollbar.show_scrollbar, |this| {
485 // this.child(self.render_vertical_scrollbar(cx))
486 // }),
487 // )
488 // .when(self.horizontal_scrollbar.show_track, |this| {
489 // this.child(
490 // h_flex()
491 // .w_full()
492 // .h(scroll_track_size)
493 // .flex_none()
494 // .relative()
495 // .child(
496 // div()
497 // .w_full()
498 // .flex_1()
499 // // for some reason the horizontal scrollbar is 1px
500 // // taller than the vertical scrollbar??
501 // .h(scroll_track_size - px(1.))
502 // .bg(cx.theme().colors().background)
503 // .border_t_1()
504 // .border_color(cx.theme().colors().border),
505 // )
506 // .when(self.vertical_scrollbar.show_track, |this| {
507 // this.child(
508 // div()
509 // .flex_none()
510 // // -1px prevents a missing pixel between the two container borders
511 // .w(scroll_track_size - px(1.))
512 // .h_full(),
513 // )
514 // .child(
515 // // HACK: Fill the missing 1px 🥲
516 // div()
517 // .absolute()
518 // .right(scroll_track_size - px(1.))
519 // .bottom(scroll_track_size - px(1.))
520 // .size_px()
521 // .bg(cx.theme().colors().border),
522 // )
523 // }),
524 // )
525 // })
526 // .when(self.horizontal_scrollbar.show_scrollbar, |this| {
527 // this.child(self.render_horizontal_scrollbar(h_scroll_offset, cx))
528 // }),
529 // )
530 }
531}
532
533impl SerializableItem for KeymapEditor {
534 fn serialized_item_kind() -> &'static str {
535 "KeymapEditor"
536 }
537
538 fn cleanup(
539 workspace_id: workspace::WorkspaceId,
540 alive_items: Vec<workspace::ItemId>,
541 _window: &mut Window,
542 cx: &mut App,
543 ) -> gpui::Task<gpui::Result<()>> {
544 workspace::delete_unloaded_items(
545 alive_items,
546 workspace_id,
547 "keybinding_editors",
548 &KEYBINDING_EDITORS,
549 cx,
550 )
551 }
552
553 fn deserialize(
554 _project: gpui::Entity<project::Project>,
555 _workspace: gpui::WeakEntity<Workspace>,
556 workspace_id: workspace::WorkspaceId,
557 item_id: workspace::ItemId,
558 window: &mut Window,
559 cx: &mut App,
560 ) -> gpui::Task<gpui::Result<gpui::Entity<Self>>> {
561 window.spawn(cx, async move |cx| {
562 if KEYBINDING_EDITORS
563 .get_keybinding_editor(item_id, workspace_id)?
564 .is_some()
565 {
566 cx.update(|window, cx| KeymapEditor::new(window, cx))
567 } else {
568 Err(anyhow!("No keybinding editor to deserialize"))
569 }
570 })
571 }
572
573 fn serialize(
574 &mut self,
575 workspace: &mut Workspace,
576 item_id: workspace::ItemId,
577 _closing: bool,
578 _window: &mut Window,
579 cx: &mut ui::Context<Self>,
580 ) -> Option<gpui::Task<gpui::Result<()>>> {
581 let Some(workspace_id) = workspace.database_id() else {
582 return None;
583 };
584 Some(cx.background_spawn(async move {
585 KEYBINDING_EDITORS
586 .save_keybinding_editor(item_id, workspace_id)
587 .await
588 }))
589 }
590
591 fn should_serialize(&self, _event: &Self::Event) -> bool {
592 false
593 }
594}
595
596mod persistence {
597 use db::{define_connection, query, sqlez_macros::sql};
598 use workspace::WorkspaceDb;
599
600 define_connection! {
601 pub static ref KEYBINDING_EDITORS: KeybindingEditorDb<WorkspaceDb> =
602 &[sql!(
603 CREATE TABLE keybinding_editors (
604 workspace_id INTEGER,
605 item_id INTEGER UNIQUE,
606
607 PRIMARY KEY(workspace_id, item_id),
608 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
609 ON DELETE CASCADE
610 ) STRICT;
611 )];
612 }
613
614 impl KeybindingEditorDb {
615 query! {
616 pub async fn save_keybinding_editor(
617 item_id: workspace::ItemId,
618 workspace_id: workspace::WorkspaceId
619 ) -> Result<()> {
620 INSERT OR REPLACE INTO keybinding_editors(item_id, workspace_id)
621 VALUES (?, ?)
622 }
623 }
624
625 query! {
626 pub fn get_keybinding_editor(
627 item_id: workspace::ItemId,
628 workspace_id: workspace::WorkspaceId
629 ) -> Result<Option<workspace::ItemId>> {
630 SELECT item_id
631 FROM keybinding_editors
632 WHERE item_id = ? AND workspace_id = ?
633 }
634 }
635 }
636}