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,
  6    actions, div,
  7};
  8
  9use ui::{
 10    ActiveTheme as _, App, BorrowAppContext, ParentElement as _, Render, SharedString, Styled as _,
 11    Window, prelude::*,
 12};
 13use workspace::{Item, SerializableItem, Workspace, register_serializable_item};
 14
 15use crate::{
 16    keybindings::persistence::KEYBINDING_EDITORS,
 17    ui_components::table::{Table, TableInteractionState},
 18};
 19
 20actions!(zed, [OpenKeymapEditor]);
 21
 22pub fn init(cx: &mut App) {
 23    let keymap_event_channel = KeymapEventChannel::new();
 24    cx.set_global(keymap_event_channel);
 25
 26    cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
 27        workspace.register_action(|workspace, _: &OpenKeymapEditor, window, cx| {
 28            let open_keymap_editor = KeymapEditor::new(window, cx);
 29            workspace.add_item_to_center(Box::new(open_keymap_editor), window, cx);
 30        });
 31    })
 32    .detach();
 33
 34    register_serializable_item::<KeymapEditor>(cx);
 35}
 36
 37pub struct KeymapEventChannel {}
 38
 39impl Global for KeymapEventChannel {}
 40
 41impl KeymapEventChannel {
 42    fn new() -> Self {
 43        Self {}
 44    }
 45
 46    pub fn trigger_keymap_changed(cx: &mut App) {
 47        cx.update_global(|_event_channel: &mut Self, _| {
 48            /* triggers observers in KeymapEditors */
 49        });
 50    }
 51}
 52
 53struct KeymapEditor {
 54    focus_handle: FocusHandle,
 55    _keymap_subscription: Subscription,
 56    processed_bindings: Vec<ProcessedKeybinding>,
 57    table_interaction_state: Entity<TableInteractionState>,
 58}
 59
 60impl EventEmitter<()> for KeymapEditor {}
 61
 62impl Focusable for KeymapEditor {
 63    fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
 64        self.focus_handle.clone()
 65    }
 66}
 67
 68impl KeymapEditor {
 69    fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
 70        let this = cx.new(|cx| {
 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
 78            let table_interaction_state = TableInteractionState::new(window, cx);
 79
 80            Self {
 81                focus_handle: focus_handle.clone(),
 82                _keymap_subscription,
 83                processed_bindings: vec![],
 84                table_interaction_state,
 85            }
 86        });
 87        this
 88    }
 89
 90    fn process_bindings(cx: &mut Context<Self>) -> Vec<ProcessedKeybinding> {
 91        let key_bindings_ptr = cx.key_bindings();
 92        let lock = key_bindings_ptr.borrow();
 93        let key_bindings = lock.bindings();
 94
 95        let mut processed_bindings = Vec::new();
 96
 97        for key_binding in key_bindings {
 98            let mut keystroke_text = String::new();
 99            for keystroke in key_binding.keystrokes() {
100                write!(&mut keystroke_text, "{} ", keystroke.unparse()).ok();
101            }
102            let keystroke_text = keystroke_text.trim().to_string();
103
104            let context = key_binding
105                .predicate()
106                .map(|predicate| predicate.to_string())
107                .unwrap_or_else(|| "<global>".to_string());
108
109            let source = key_binding
110                .meta()
111                .map(|meta| settings::KeybindSource::from_meta(meta).name().into());
112
113            processed_bindings.push(ProcessedKeybinding {
114                keystroke_text: keystroke_text.into(),
115                action: key_binding.action().name().into(),
116                context: context.into(),
117                source,
118            })
119        }
120        processed_bindings
121    }
122}
123
124struct ProcessedKeybinding {
125    keystroke_text: SharedString,
126    action: SharedString,
127    context: SharedString,
128    source: Option<SharedString>,
129}
130
131impl Item for KeymapEditor {
132    type Event = ();
133
134    fn tab_content_text(&self, _detail: usize, _cx: &App) -> ui::SharedString {
135        "Keymap Editor".into()
136    }
137}
138
139impl Render for KeymapEditor {
140    fn render(&mut self, _window: &mut Window, cx: &mut ui::Context<Self>) -> impl ui::IntoElement {
141        if self.processed_bindings.is_empty() {
142            self.processed_bindings = Self::process_bindings(cx);
143        }
144
145        let row_count = self.processed_bindings.len();
146
147        let theme = cx.theme();
148
149        div()
150            .size_full()
151            .bg(theme.colors().background)
152            .id("keymap-editor")
153            .track_focus(&self.focus_handle)
154            .child(
155                Table::new()
156                    .interactable(&self.table_interaction_state)
157                    .header(["Command", "Keystrokes", "Context", "Source"])
158                    .uniform_list(
159                        "keymap-editor-table",
160                        row_count,
161                        cx.processor(move |this, range: Range<usize>, _window, cx| {
162                            range
163                                .map(|index| {
164                                    let binding = &this.processed_bindings[index];
165                                    let row = [
166                                        binding.action.clone(),
167                                        binding.keystroke_text.clone(),
168                                        binding.context.clone(),
169                                        binding.source.clone().unwrap_or_default(),
170                                    ];
171
172                                    // fixme: pass through callback as a row_cx param
173                                    let striped = false;
174
175                                    crate::ui_components::table::render_row(
176                                        index, row, row_count, striped, cx,
177                                    )
178                                })
179                                .collect()
180                        }),
181                    ),
182            )
183    }
184}
185
186impl SerializableItem for KeymapEditor {
187    fn serialized_item_kind() -> &'static str {
188        "KeymapEditor"
189    }
190
191    fn cleanup(
192        workspace_id: workspace::WorkspaceId,
193        alive_items: Vec<workspace::ItemId>,
194        _window: &mut Window,
195        cx: &mut App,
196    ) -> gpui::Task<gpui::Result<()>> {
197        workspace::delete_unloaded_items(
198            alive_items,
199            workspace_id,
200            "keybinding_editors",
201            &KEYBINDING_EDITORS,
202            cx,
203        )
204    }
205
206    fn deserialize(
207        _project: gpui::Entity<project::Project>,
208        _workspace: gpui::WeakEntity<Workspace>,
209        workspace_id: workspace::WorkspaceId,
210        item_id: workspace::ItemId,
211        window: &mut Window,
212        cx: &mut App,
213    ) -> gpui::Task<gpui::Result<gpui::Entity<Self>>> {
214        window.spawn(cx, async move |cx| {
215            if KEYBINDING_EDITORS
216                .get_keybinding_editor(item_id, workspace_id)?
217                .is_some()
218            {
219                cx.update(KeymapEditor::new)
220            } else {
221                Err(anyhow!("No keybinding editor to deserialize"))
222            }
223        })
224    }
225
226    fn serialize(
227        &mut self,
228        workspace: &mut Workspace,
229        item_id: workspace::ItemId,
230        _closing: bool,
231        _window: &mut Window,
232        cx: &mut ui::Context<Self>,
233    ) -> Option<gpui::Task<gpui::Result<()>>> {
234        let Some(workspace_id) = workspace.database_id() else {
235            return None;
236        };
237        Some(cx.background_spawn(async move {
238            KEYBINDING_EDITORS
239                .save_keybinding_editor(item_id, workspace_id)
240                .await
241        }))
242    }
243
244    fn should_serialize(&self, _event: &Self::Event) -> bool {
245        false
246    }
247}
248
249mod persistence {
250    use db::{define_connection, query, sqlez_macros::sql};
251    use workspace::WorkspaceDb;
252
253    define_connection! {
254        pub static ref KEYBINDING_EDITORS: KeybindingEditorDb<WorkspaceDb> =
255            &[sql!(
256                CREATE TABLE keybinding_editors (
257                    workspace_id INTEGER,
258                    item_id INTEGER UNIQUE,
259
260                    PRIMARY KEY(workspace_id, item_id),
261                    FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
262                    ON DELETE CASCADE
263                ) STRICT;
264            )];
265    }
266
267    impl KeybindingEditorDb {
268        query! {
269            pub async fn save_keybinding_editor(
270                item_id: workspace::ItemId,
271                workspace_id: workspace::WorkspaceId
272            ) -> Result<()> {
273                INSERT OR REPLACE INTO keybinding_editors(item_id, workspace_id)
274                VALUES (?, ?)
275            }
276        }
277
278        query! {
279            pub fn get_keybinding_editor(
280                item_id: workspace::ItemId,
281                workspace_id: workspace::WorkspaceId
282            ) -> Result<Option<workspace::ItemId>> {
283                SELECT item_id
284                FROM keybinding_editors
285                WHERE item_id = ? AND workspace_id = ?
286            }
287        }
288    }
289}