keybindings.rs

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