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