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