keybindings.rs

  1use std::fmt::Write as _;
  2
  3use db::anyhow::anyhow;
  4use gpui::{
  5    AnyElement, AppContext as _, Context, EventEmitter, FocusHandle, Focusable, FontWeight, Global,
  6    IntoElement, Length, ListHorizontalSizingBehavior, ListSizingBehavior, Subscription,
  7    UniformListScrollHandle, actions, div, px, uniform_list,
  8};
  9
 10use editor::ShowScrollbar;
 11use ui::{
 12    ActiveTheme as _, App, BorrowAppContext, ParentElement as _, Render, Scrollbar, ScrollbarState,
 13    SharedString, Styled as _, Window, prelude::*,
 14};
 15use workspace::{Item, SerializableItem, Workspace, register_serializable_item};
 16
 17use crate::keybindings::persistence::KEYBINDING_EDITORS;
 18
 19actions!(zed, [OpenKeymapEditor]);
 20
 21pub fn init(cx: &mut App) {
 22    let keymap_event_channel = KeymapEventChannel::new();
 23    cx.set_global(keymap_event_channel);
 24
 25    cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
 26        workspace.register_action(|workspace, _: &OpenKeymapEditor, window, cx| {
 27            let open_keymap_editor = cx.new(|cx| KeymapEditor::new(cx));
 28            workspace.add_item_to_center(Box::new(open_keymap_editor), window, cx);
 29        });
 30    })
 31    .detach();
 32
 33    register_serializable_item::<KeymapEditor>(cx);
 34}
 35
 36pub struct KeymapEventChannel {}
 37
 38impl Global for KeymapEventChannel {}
 39
 40impl KeymapEventChannel {
 41    fn new() -> Self {
 42        Self {}
 43    }
 44
 45    pub fn trigger_keymap_changed(cx: &mut App) {
 46        cx.update_global(|_event_channel: &mut Self, _| {
 47            /* triggers observers in KeymapEditors */
 48        });
 49    }
 50}
 51
 52struct KeymapEditor {
 53    focus_handle: FocusHandle,
 54    _keymap_subscription: Subscription,
 55    processed_bindings: Vec<ProcessedKeybinding>,
 56    scroll_handle: UniformListScrollHandle,
 57    vertical_scrollbar_state: ScrollbarState,
 58    show_vertical_scrollbar: bool,
 59}
 60
 61impl EventEmitter<()> for KeymapEditor {}
 62
 63impl Focusable for KeymapEditor {
 64    fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
 65        self.focus_handle.clone()
 66    }
 67}
 68
 69impl KeymapEditor {
 70    fn new(cx: &mut gpui::Context<Self>) -> Self {
 71        let focus_handle = cx.focus_handle();
 72
 73        let _keymap_subscription = cx.observe_global::<KeymapEventChannel>(|this, cx| {
 74            let key_bindings = Self::process_bindings(cx);
 75            this.processed_bindings = key_bindings;
 76        });
 77        let scroll_handle = UniformListScrollHandle::new();
 78        let vertical_scrollbar_state = ScrollbarState::new(scroll_handle.clone());
 79        let mut this = Self {
 80            focus_handle: focus_handle.clone(),
 81            _keymap_subscription,
 82            processed_bindings: vec![],
 83            scroll_handle,
 84            vertical_scrollbar_state,
 85            show_vertical_scrollbar: false,
 86        };
 87
 88        this.update_scrollbar_visibility(cx);
 89        this
 90    }
 91
 92    fn process_bindings(cx: &mut Context<Self>) -> Vec<ProcessedKeybinding> {
 93        let key_bindings_ptr = cx.key_bindings();
 94        let lock = key_bindings_ptr.borrow();
 95        let key_bindings = lock.bindings();
 96
 97        let mut processed_bindings = Vec::new();
 98
 99        for key_binding in key_bindings {
100            let mut keystroke_text = String::new();
101            for keystroke in key_binding.keystrokes() {
102                write!(&mut keystroke_text, "{} ", keystroke.unparse()).ok();
103            }
104            let keystroke_text = keystroke_text.trim().to_string();
105
106            let context = key_binding
107                .predicate()
108                .map(|predicate| predicate.to_string())
109                .unwrap_or_else(|| "<global>".to_string());
110
111            processed_bindings.push(ProcessedKeybinding {
112                keystroke_text: keystroke_text.into(),
113                action: key_binding.action().name().into(),
114                context: context.into(),
115            })
116        }
117        processed_bindings
118    }
119
120    fn update_scrollbar_visibility(&mut self, cx: &mut Context<Self>) {
121        use editor::EditorSettings;
122        use settings::Settings;
123
124        let show_setting = EditorSettings::get_global(cx).scrollbar.show;
125
126        self.show_vertical_scrollbar = match show_setting {
127            ShowScrollbar::Auto | ShowScrollbar::System | ShowScrollbar::Always => true,
128            ShowScrollbar::Never => false,
129        };
130
131        cx.notify();
132    }
133
134    fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> impl IntoElement {
135        div()
136            .id("keymap-editor-vertical-scroll")
137            .occlude()
138            .flex_none()
139            .h_full()
140            .cursor_default()
141            .absolute()
142            .right_0()
143            .top_0()
144            .bottom_0()
145            .w(px(12.))
146            .on_mouse_move(cx.listener(|_, _, _, cx| {
147                cx.notify();
148                cx.stop_propagation()
149            }))
150            .on_hover(|_, _, cx| {
151                cx.stop_propagation();
152            })
153            .on_any_mouse_down(|_, _, cx| {
154                cx.stop_propagation();
155            })
156            .on_scroll_wheel(cx.listener(|_, _, _, cx| {
157                cx.notify();
158            }))
159            .children(Scrollbar::vertical(self.vertical_scrollbar_state.clone()))
160    }
161}
162
163struct ProcessedKeybinding {
164    keystroke_text: SharedString,
165    action: SharedString,
166    context: SharedString,
167}
168
169impl Item for KeymapEditor {
170    type Event = ();
171
172    fn tab_content_text(&self, _detail: usize, _cx: &App) -> ui::SharedString {
173        "Keymap Editor".into()
174    }
175}
176
177impl Render for KeymapEditor {
178    fn render(&mut self, _window: &mut Window, cx: &mut ui::Context<Self>) -> impl ui::IntoElement {
179        if self.processed_bindings.is_empty() {
180            self.processed_bindings = Self::process_bindings(cx);
181            self.update_scrollbar_visibility(cx);
182        }
183
184        let table = Table::new(self.processed_bindings.len());
185
186        let theme = cx.theme();
187        let headers = ["Command", "Keystrokes", "Context"].map(Into::into);
188
189        div()
190            .size_full()
191            .bg(theme.colors().background)
192            .track_focus(&self.focus_handle)
193            .child(
194                table
195                    .render()
196                    .h_full()
197                    .v_flex()
198                    .child(table.render_header(headers, cx))
199                    .child(
200                        div()
201                            .flex_grow()
202                            .w_full()
203                            .relative()
204                            .child(
205                                uniform_list(
206                                    cx.entity(),
207                                    "keybindings",
208                                    table.row_count,
209                                    move |this, range, _, cx| {
210                                        range
211                                            .map(|index| {
212                                                let binding = &this.processed_bindings[index];
213                                                let row = [
214                                                    binding.action.clone(),
215                                                    binding.keystroke_text.clone(),
216                                                    binding.context.clone(),
217                                                    // TODO: Add a source field
218                                                    // string_cell(keybinding.source().to_string()),
219                                                ]
220                                                .map(string_cell);
221
222                                                table.render_row(index, row, cx)
223                                            })
224                                            .collect()
225                                    },
226                                )
227                                .size_full()
228                                .flex_grow()
229                                .track_scroll(self.scroll_handle.clone())
230                                .with_sizing_behavior(ListSizingBehavior::Auto)
231                                .with_horizontal_sizing_behavior(
232                                    ListHorizontalSizingBehavior::Unconstrained,
233                                ),
234                            )
235                            .when(self.show_vertical_scrollbar, |this| {
236                                this.child(self.render_vertical_scrollbar(cx))
237                            }),
238                    ),
239            )
240    }
241}
242
243/// A table component
244#[derive(Clone, Copy)]
245pub struct Table<const COLS: usize> {
246    striped: bool,
247    width: Length,
248    row_count: usize,
249}
250
251impl<const COLS: usize> Table<COLS> {
252    /// Create a new table with a column count equal to the
253    /// number of headers provided.
254    pub fn new(row_count: usize) -> Self {
255        Table {
256            striped: false,
257            width: Length::Auto,
258            row_count,
259        }
260    }
261
262    /// Enables row striping.
263    pub fn striped(mut self) -> Self {
264        self.striped = true;
265        self
266    }
267
268    /// Sets the width of the table.
269    pub fn width(mut self, width: impl Into<Length>) -> Self {
270        self.width = width.into();
271        self
272    }
273
274    fn base_cell_style(cx: &App) -> Div {
275        div()
276            .px_1p5()
277            .flex_1()
278            .justify_start()
279            .text_ui(cx)
280            .whitespace_nowrap()
281            .text_ellipsis()
282            .overflow_hidden()
283    }
284
285    pub fn render_row(&self, row_index: usize, items: [TableCell; COLS], cx: &App) -> AnyElement {
286        let is_last = row_index == self.row_count - 1;
287        let bg = if row_index % 2 == 1 && self.striped {
288            Some(cx.theme().colors().text.opacity(0.05))
289        } else {
290            None
291        };
292        div()
293            .w_full()
294            .flex()
295            .flex_row()
296            .items_center()
297            .justify_between()
298            .px_1p5()
299            .py_1()
300            .when_some(bg, |row, bg| row.bg(bg))
301            .when(!is_last, |row| {
302                row.border_b_1().border_color(cx.theme().colors().border)
303            })
304            .children(items.into_iter().map(|cell| match cell {
305                TableCell::String(s) => Self::base_cell_style(cx).child(s),
306                TableCell::Element(e) => Self::base_cell_style(cx).child(e),
307            }))
308            .into_any_element()
309    }
310
311    fn render_header(&self, headers: [SharedString; COLS], cx: &mut App) -> impl IntoElement {
312        div()
313            .flex()
314            .flex_row()
315            .items_center()
316            .justify_between()
317            .w_full()
318            .p_2()
319            .border_b_1()
320            .border_color(cx.theme().colors().border)
321            .children(headers.into_iter().map(|h| {
322                Self::base_cell_style(cx)
323                    .font_weight(FontWeight::SEMIBOLD)
324                    .child(h.clone())
325            }))
326    }
327
328    fn render(&self) -> Div {
329        div().w(self.width).overflow_hidden()
330    }
331}
332
333/// Represents a cell in a table.
334pub enum TableCell {
335    /// A cell containing a string value.
336    String(SharedString),
337    /// A cell containing a UI element.
338    Element(AnyElement),
339}
340
341/// Creates a `TableCell` containing a string value.
342pub fn string_cell(s: impl Into<SharedString>) -> TableCell {
343    TableCell::String(s.into())
344}
345
346/// Creates a `TableCell` containing an element.
347pub fn element_cell(e: impl Into<AnyElement>) -> TableCell {
348    TableCell::Element(e.into())
349}
350
351impl<E> From<E> for TableCell
352where
353    E: Into<SharedString>,
354{
355    fn from(e: E) -> Self {
356        TableCell::String(e.into())
357    }
358}
359
360impl SerializableItem for KeymapEditor {
361    fn serialized_item_kind() -> &'static str {
362        "KeymapEditor"
363    }
364
365    fn cleanup(
366        workspace_id: workspace::WorkspaceId,
367        alive_items: Vec<workspace::ItemId>,
368        _window: &mut Window,
369        cx: &mut App,
370    ) -> gpui::Task<gpui::Result<()>> {
371        workspace::delete_unloaded_items(
372            alive_items,
373            workspace_id,
374            "keybinding_editors",
375            &KEYBINDING_EDITORS,
376            cx,
377        )
378    }
379
380    fn deserialize(
381        _project: gpui::Entity<project::Project>,
382        _workspace: gpui::WeakEntity<Workspace>,
383        workspace_id: workspace::WorkspaceId,
384        item_id: workspace::ItemId,
385        _window: &mut Window,
386        cx: &mut App,
387    ) -> gpui::Task<gpui::Result<gpui::Entity<Self>>> {
388        cx.spawn(async move |cx| {
389            if KEYBINDING_EDITORS
390                .get_keybinding_editor(item_id, workspace_id)?
391                .is_some()
392            {
393                cx.new(|cx| KeymapEditor::new(cx))
394            } else {
395                Err(anyhow!("No keybinding editor to deserialize"))
396            }
397        })
398    }
399
400    fn serialize(
401        &mut self,
402        workspace: &mut Workspace,
403        item_id: workspace::ItemId,
404        _closing: bool,
405        _window: &mut Window,
406        cx: &mut ui::Context<Self>,
407    ) -> Option<gpui::Task<gpui::Result<()>>> {
408        let Some(workspace_id) = workspace.database_id() else {
409            return None;
410        };
411        Some(cx.background_spawn(async move {
412            KEYBINDING_EDITORS
413                .save_keybinding_editor(item_id, workspace_id)
414                .await
415        }))
416    }
417
418    fn should_serialize(&self, _event: &Self::Event) -> bool {
419        false
420    }
421}
422
423mod persistence {
424    use db::{define_connection, query, sqlez_macros::sql};
425    use workspace::WorkspaceDb;
426
427    define_connection! {
428        pub static ref KEYBINDING_EDITORS: KeybindingEditorDb<WorkspaceDb> =
429            &[sql!(
430                CREATE TABLE keybinding_editors (
431                    workspace_id INTEGER,
432                    item_id INTEGER UNIQUE,
433
434                    PRIMARY KEY(workspace_id, item_id),
435                    FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
436                    ON DELETE CASCADE
437                ) STRICT;
438            )];
439    }
440
441    impl KeybindingEditorDb {
442        query! {
443            pub async fn save_keybinding_editor(
444                item_id: workspace::ItemId,
445                workspace_id: workspace::WorkspaceId
446            ) -> Result<()> {
447                INSERT OR REPLACE INTO keybinding_editors(item_id, workspace_id)
448                VALUES (?, ?)
449            }
450        }
451
452        query! {
453            pub fn get_keybinding_editor(
454                item_id: workspace::ItemId,
455                workspace_id: workspace::WorkspaceId
456            ) -> Result<Option<workspace::ItemId>> {
457                SELECT item_id
458                FROM keybinding_editors
459                WHERE item_id = ? AND workspace_id = ?
460            }
461        }
462    }
463}