keybindings.rs

  1use std::{fmt::Write as _, ops::Range};
  2
  3use db::anyhow::anyhow;
  4use gpui::{
  5    AppContext as _, Context, Entity, EventEmitter, FocusHandle, Focusable, Global, Subscription, actions, div,
  6};
  7
  8use ui::{
  9    ActiveTheme as _, App, BorrowAppContext, ParentElement as _, Render,
 10    SharedString, Styled as _, Window, prelude::*,
 11};
 12use workspace::{Item, SerializableItem, Workspace, register_serializable_item};
 13
 14use crate::{
 15    keybindings::persistence::KEYBINDING_EDITORS,
 16    ui_components::table::{Table, TableInteractionState},
 17};
 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 = KeymapEditor::new(window, 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    table_interaction_state: Entity<TableInteractionState>,
 57}
 58
 59impl EventEmitter<()> for KeymapEditor {}
 60
 61impl Focusable for KeymapEditor {
 62    fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
 63        self.focus_handle.clone()
 64    }
 65}
 66
 67impl KeymapEditor {
 68    fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
 69        let this = cx.new(|cx| {
 70            let focus_handle = cx.focus_handle();
 71
 72            let _keymap_subscription = cx.observe_global::<KeymapEventChannel>(|this, cx| {
 73                let key_bindings = Self::process_bindings(cx);
 74                this.processed_bindings = key_bindings;
 75            });
 76
 77            let table_interaction_state = TableInteractionState::new(window, cx);
 78
 79            Self {
 80                focus_handle: focus_handle.clone(),
 81                _keymap_subscription,
 82                processed_bindings: vec![],
 83                table_interaction_state,
 84            }
 85        });
 86        this
 87    }
 88
 89    fn process_bindings(cx: &mut Context<Self>) -> Vec<ProcessedKeybinding> {
 90        let key_bindings_ptr = cx.key_bindings();
 91        let lock = key_bindings_ptr.borrow();
 92        let key_bindings = lock.bindings();
 93
 94        let mut processed_bindings = Vec::new();
 95
 96        for key_binding in key_bindings {
 97            let mut keystroke_text = String::new();
 98            for keystroke in key_binding.keystrokes() {
 99                write!(&mut keystroke_text, "{} ", keystroke.unparse()).ok();
100            }
101            let keystroke_text = keystroke_text.trim().to_string();
102
103            let context = key_binding
104                .predicate()
105                .map(|predicate| predicate.to_string())
106                .unwrap_or_else(|| "<global>".to_string());
107
108            processed_bindings.push(ProcessedKeybinding {
109                keystroke_text: keystroke_text.into(),
110                action: key_binding.action().name().into(),
111                context: context.into(),
112            })
113        }
114        processed_bindings
115    }
116}
117
118struct ProcessedKeybinding {
119    keystroke_text: SharedString,
120    action: SharedString,
121    context: SharedString,
122}
123
124impl Item for KeymapEditor {
125    type Event = ();
126
127    fn tab_content_text(&self, _detail: usize, _cx: &App) -> ui::SharedString {
128        "Keymap Editor".into()
129    }
130}
131
132impl Render for KeymapEditor {
133    fn render(&mut self, _window: &mut Window, cx: &mut ui::Context<Self>) -> impl ui::IntoElement {
134        if self.processed_bindings.is_empty() {
135            self.processed_bindings = Self::process_bindings(cx);
136        }
137
138        // let scroll_track_size = px(16.);
139        // let h_scroll_offset = if self.vertical_scrollbar.show_scrollbar {
140        //     // magic number
141        //     px(3.)
142        // } else {
143        //     px(0.)
144        // };
145
146        let row_count = self.processed_bindings.len();
147
148        let theme = cx.theme();
149
150        div()
151            .size_full()
152            .bg(theme.colors().background)
153            .id("keymap-editor")
154            .track_focus(&self.focus_handle)
155            .child(
156                Table::uniform_list(
157                    "keymap-editor-table",
158                    row_count,
159                    cx.processor(|this, range: Range<usize>, window, cx| {
160                        range
161                            .map(|index| {
162                                let binding = &this.processed_bindings[index];
163                                let row = [
164                                    binding.action.clone(),
165                                    binding.keystroke_text.clone(),
166                                    binding.context.clone(),
167                                    // TODO: Add a source field
168                                    // binding.source.clone(),
169                                ];
170
171                                // fixme: pass through callback as a row_cx param
172                                let striped = false;
173
174                                crate::ui_components::table::render_row(
175                                    index, row, row_count, striped, cx,
176                                )
177                            })
178                            .collect()
179                    }),
180                )
181                .header(["Command", "Keystrokes", "Context"])
182                .interactable(&self.table_interaction_state),
183            )
184        // .child(
185        //     table
186        //         .h_full()
187        //         .v_flex()
188        //         .child(table.render_header(headers, cx))
189        //         .child(
190        //             div()
191        //                 .flex_grow()
192        //                 .w_full()
193        //                 .relative()
194        //                 .overflow_hidden()
195        //                 .child(
196        //                     uniform_list(
197        //                         "keybindings",
198        //                         row_count,
199        //                         cx.processor(move |this, range, _, cx| {}),
200        //                     )
201        //                     .size_full()
202        //                     .flex_grow()
203        //                     .track_scroll(self.scroll_handle.clone())
204        //                     .with_sizing_behavior(ListSizingBehavior::Auto)
205        //                     .with_horizontal_sizing_behavior(
206        //                         ListHorizontalSizingBehavior::Unconstrained,
207        //                     ),
208        //                 )
209        //                 .when(self.vertical_scrollbar.show_track, |this| {
210        //                     this.child(
211        //                         v_flex()
212        //                             .h_full()
213        //                             .flex_none()
214        //                             .w(scroll_track_size)
215        //                             .bg(cx.theme().colors().background)
216        //                             .child(
217        //                                 div()
218        //                                     .size_full()
219        //                                     .flex_1()
220        //                                     .border_l_1()
221        //                                     .border_color(cx.theme().colors().border),
222        //                             ),
223        //                     )
224        //                 })
225        //                 .when(self.vertical_scrollbar.show_scrollbar, |this| {
226        //                     this.child(self.render_vertical_scrollbar(cx))
227        //                 }),
228        //         )
229        //         .when(self.horizontal_scrollbar.show_track, |this| {
230        //             this.child(
231        //                 h_flex()
232        //                     .w_full()
233        //                     .h(scroll_track_size)
234        //                     .flex_none()
235        //                     .relative()
236        //                     .child(
237        //                         div()
238        //                             .w_full()
239        //                             .flex_1()
240        //                             // for some reason the horizontal scrollbar is 1px
241        //                             // taller than the vertical scrollbar??
242        //                             .h(scroll_track_size - px(1.))
243        //                             .bg(cx.theme().colors().background)
244        //                             .border_t_1()
245        //                             .border_color(cx.theme().colors().border),
246        //                     )
247        //                     .when(self.vertical_scrollbar.show_track, |this| {
248        //                         this.child(
249        //                             div()
250        //                                 .flex_none()
251        //                                 // -1px prevents a missing pixel between the two container borders
252        //                                 .w(scroll_track_size - px(1.))
253        //                                 .h_full(),
254        //                         )
255        //                         .child(
256        //                             // HACK: Fill the missing 1px 🥲
257        //                             div()
258        //                                 .absolute()
259        //                                 .right(scroll_track_size - px(1.))
260        //                                 .bottom(scroll_track_size - px(1.))
261        //                                 .size_px()
262        //                                 .bg(cx.theme().colors().border),
263        //                         )
264        //                     }),
265        //             )
266        //         })
267        //         .when(self.horizontal_scrollbar.show_scrollbar, |this| {
268        //             this.child(self.render_horizontal_scrollbar(h_scroll_offset, cx))
269        //         }),
270        // )
271    }
272}
273
274impl SerializableItem for KeymapEditor {
275    fn serialized_item_kind() -> &'static str {
276        "KeymapEditor"
277    }
278
279    fn cleanup(
280        workspace_id: workspace::WorkspaceId,
281        alive_items: Vec<workspace::ItemId>,
282        _window: &mut Window,
283        cx: &mut App,
284    ) -> gpui::Task<gpui::Result<()>> {
285        workspace::delete_unloaded_items(
286            alive_items,
287            workspace_id,
288            "keybinding_editors",
289            &KEYBINDING_EDITORS,
290            cx,
291        )
292    }
293
294    fn deserialize(
295        _project: gpui::Entity<project::Project>,
296        _workspace: gpui::WeakEntity<Workspace>,
297        workspace_id: workspace::WorkspaceId,
298        item_id: workspace::ItemId,
299        window: &mut Window,
300        cx: &mut App,
301    ) -> gpui::Task<gpui::Result<gpui::Entity<Self>>> {
302        window.spawn(cx, async move |cx| {
303            if KEYBINDING_EDITORS
304                .get_keybinding_editor(item_id, workspace_id)?
305                .is_some()
306            {
307                cx.update(KeymapEditor::new)
308            } else {
309                Err(anyhow!("No keybinding editor to deserialize"))
310            }
311        })
312    }
313
314    fn serialize(
315        &mut self,
316        workspace: &mut Workspace,
317        item_id: workspace::ItemId,
318        _closing: bool,
319        _window: &mut Window,
320        cx: &mut ui::Context<Self>,
321    ) -> Option<gpui::Task<gpui::Result<()>>> {
322        let Some(workspace_id) = workspace.database_id() else {
323            return None;
324        };
325        Some(cx.background_spawn(async move {
326            KEYBINDING_EDITORS
327                .save_keybinding_editor(item_id, workspace_id)
328                .await
329        }))
330    }
331
332    fn should_serialize(&self, _event: &Self::Event) -> bool {
333        false
334    }
335}
336
337mod persistence {
338    use db::{define_connection, query, sqlez_macros::sql};
339    use workspace::WorkspaceDb;
340
341    define_connection! {
342        pub static ref KEYBINDING_EDITORS: KeybindingEditorDb<WorkspaceDb> =
343            &[sql!(
344                CREATE TABLE keybinding_editors (
345                    workspace_id INTEGER,
346                    item_id INTEGER UNIQUE,
347
348                    PRIMARY KEY(workspace_id, item_id),
349                    FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
350                    ON DELETE CASCADE
351                ) STRICT;
352            )];
353    }
354
355    impl KeybindingEditorDb {
356        query! {
357            pub async fn save_keybinding_editor(
358                item_id: workspace::ItemId,
359                workspace_id: workspace::WorkspaceId
360            ) -> Result<()> {
361                INSERT OR REPLACE INTO keybinding_editors(item_id, workspace_id)
362                VALUES (?, ?)
363            }
364        }
365
366        query! {
367            pub fn get_keybinding_editor(
368                item_id: workspace::ItemId,
369                workspace_id: workspace::WorkspaceId
370            ) -> Result<Option<workspace::ItemId>> {
371                SELECT item_id
372                FROM keybinding_editors
373                WHERE item_id = ? AND workspace_id = ?
374            }
375        }
376    }
377}