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            processed_bindings.push(ProcessedKeybinding {
110                keystroke_text: keystroke_text.into(),
111                action: key_binding.action().name().into(),
112                context: context.into(),
113            })
114        }
115        processed_bindings
116    }
117}
118
119struct ProcessedKeybinding {
120    keystroke_text: SharedString,
121    action: SharedString,
122    context: SharedString,
123}
124
125impl Item for KeymapEditor {
126    type Event = ();
127
128    fn tab_content_text(&self, _detail: usize, _cx: &App) -> ui::SharedString {
129        "Keymap Editor".into()
130    }
131}
132
133impl Render for KeymapEditor {
134    fn render(&mut self, _window: &mut Window, cx: &mut ui::Context<Self>) -> impl ui::IntoElement {
135        if self.processed_bindings.is_empty() {
136            self.processed_bindings = Self::process_bindings(cx);
137        }
138
139        let row_count = self.processed_bindings.len();
140
141        let theme = cx.theme();
142
143        div()
144            .size_full()
145            .bg(theme.colors().background)
146            .id("keymap-editor")
147            .track_focus(&self.focus_handle)
148            .child(
149                Table::new()
150                    .interactable(&self.table_interaction_state)
151                    .header(["Command", "Keystrokes", "Context"])
152                    .uniform_list(
153                        "keymap-editor-table",
154                        row_count,
155                        cx.processor(move |this, range: Range<usize>, _window, cx| {
156                            range
157                                .map(|index| {
158                                    let binding = &this.processed_bindings[index];
159                                    let row = [
160                                        binding.action.clone(),
161                                        binding.keystroke_text.clone(),
162                                        binding.context.clone(),
163                                        // TODO: Add a source field
164                                        // binding.source.clone(),
165                                    ];
166
167                                    // fixme: pass through callback as a row_cx param
168                                    let striped = false;
169
170                                    crate::ui_components::table::render_row(
171                                        index, row, row_count, striped, cx,
172                                    )
173                                })
174                                .collect()
175                        }),
176                    ),
177            )
178    }
179}
180
181impl SerializableItem for KeymapEditor {
182    fn serialized_item_kind() -> &'static str {
183        "KeymapEditor"
184    }
185
186    fn cleanup(
187        workspace_id: workspace::WorkspaceId,
188        alive_items: Vec<workspace::ItemId>,
189        _window: &mut Window,
190        cx: &mut App,
191    ) -> gpui::Task<gpui::Result<()>> {
192        workspace::delete_unloaded_items(
193            alive_items,
194            workspace_id,
195            "keybinding_editors",
196            &KEYBINDING_EDITORS,
197            cx,
198        )
199    }
200
201    fn deserialize(
202        _project: gpui::Entity<project::Project>,
203        _workspace: gpui::WeakEntity<Workspace>,
204        workspace_id: workspace::WorkspaceId,
205        item_id: workspace::ItemId,
206        window: &mut Window,
207        cx: &mut App,
208    ) -> gpui::Task<gpui::Result<gpui::Entity<Self>>> {
209        window.spawn(cx, async move |cx| {
210            if KEYBINDING_EDITORS
211                .get_keybinding_editor(item_id, workspace_id)?
212                .is_some()
213            {
214                cx.update(KeymapEditor::new)
215            } else {
216                Err(anyhow!("No keybinding editor to deserialize"))
217            }
218        })
219    }
220
221    fn serialize(
222        &mut self,
223        workspace: &mut Workspace,
224        item_id: workspace::ItemId,
225        _closing: bool,
226        _window: &mut Window,
227        cx: &mut ui::Context<Self>,
228    ) -> Option<gpui::Task<gpui::Result<()>>> {
229        let Some(workspace_id) = workspace.database_id() else {
230            return None;
231        };
232        Some(cx.background_spawn(async move {
233            KEYBINDING_EDITORS
234                .save_keybinding_editor(item_id, workspace_id)
235                .await
236        }))
237    }
238
239    fn should_serialize(&self, _event: &Self::Event) -> bool {
240        false
241    }
242}
243
244mod persistence {
245    use db::{define_connection, query, sqlez_macros::sql};
246    use workspace::WorkspaceDb;
247
248    define_connection! {
249        pub static ref KEYBINDING_EDITORS: KeybindingEditorDb<WorkspaceDb> =
250            &[sql!(
251                CREATE TABLE keybinding_editors (
252                    workspace_id INTEGER,
253                    item_id INTEGER UNIQUE,
254
255                    PRIMARY KEY(workspace_id, item_id),
256                    FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
257                    ON DELETE CASCADE
258                ) STRICT;
259            )];
260    }
261
262    impl KeybindingEditorDb {
263        query! {
264            pub async fn save_keybinding_editor(
265                item_id: workspace::ItemId,
266                workspace_id: workspace::WorkspaceId
267            ) -> Result<()> {
268                INSERT OR REPLACE INTO keybinding_editors(item_id, workspace_id)
269                VALUES (?, ?)
270            }
271        }
272
273        query! {
274            pub fn get_keybinding_editor(
275                item_id: workspace::ItemId,
276                workspace_id: workspace::WorkspaceId
277            ) -> Result<Option<workspace::ItemId>> {
278                SELECT item_id
279                FROM keybinding_editors
280                WHERE item_id = ? AND workspace_id = ?
281            }
282        }
283    }
284}