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