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 SerializableItem for KeymapEditor {
115    fn serialized_item_kind() -> &'static str {
116        "KeymapEditor"
117    }
118
119    fn cleanup(
120        workspace_id: workspace::WorkspaceId,
121        alive_items: Vec<workspace::ItemId>,
122        _window: &mut Window,
123        cx: &mut App,
124    ) -> gpui::Task<gpui::Result<()>> {
125        workspace::delete_unloaded_items(
126            alive_items,
127            workspace_id,
128            "keybinding_editors",
129            &KEYBINDING_EDITORS,
130            cx,
131        )
132    }
133
134    fn deserialize(
135        _project: gpui::Entity<project::Project>,
136        _workspace: gpui::WeakEntity<Workspace>,
137        workspace_id: workspace::WorkspaceId,
138        item_id: workspace::ItemId,
139        _window: &mut Window,
140        cx: &mut App,
141    ) -> gpui::Task<gpui::Result<gpui::Entity<Self>>> {
142        cx.spawn(async move |cx| {
143            if KEYBINDING_EDITORS
144                .get_keybinding_editor(item_id, workspace_id)?
145                .is_some()
146            {
147                cx.new(|cx| KeymapEditor::new(cx))
148            } else {
149                Err(anyhow!("No keybinding editor to deserialize"))
150            }
151        })
152    }
153
154    fn serialize(
155        &mut self,
156        workspace: &mut Workspace,
157        item_id: workspace::ItemId,
158        _closing: bool,
159        _window: &mut Window,
160        cx: &mut ui::Context<Self>,
161    ) -> Option<gpui::Task<gpui::Result<()>>> {
162        let Some(workspace_id) = workspace.database_id() else {
163            return None;
164        };
165        Some(cx.background_spawn(async move {
166            KEYBINDING_EDITORS
167                .save_keybinding_editor(item_id, workspace_id)
168                .await
169        }))
170    }
171
172    fn should_serialize(&self, _event: &Self::Event) -> bool {
173        false
174    }
175}
176
177impl Item for KeymapEditor {
178    type Event = ();
179
180    fn tab_content_text(&self, _detail: usize, _cx: &App) -> ui::SharedString {
181        "Keymap Editor".into()
182    }
183}
184
185impl Render for KeymapEditor {
186    fn render(&mut self, _window: &mut Window, cx: &mut ui::Context<Self>) -> impl ui::IntoElement {
187        dbg!("rendering");
188        if self.processed_bindings.is_empty() {
189            self.processed_bindings = Self::process_bindings(cx);
190        }
191
192        let table = Table::new(self.processed_bindings.len());
193
194        let theme = cx.theme();
195        let headers = ["Command", "Keystrokes", "Context"].map(Into::into);
196
197        div().size_full().bg(theme.colors().background).child(
198            table
199                .render()
200                .h_full()
201                .child(table.render_header(headers, cx))
202                .child(
203                    uniform_list(
204                        cx.entity(),
205                        "keybindings",
206                        table.row_count,
207                        move |this, range, _, cx| {
208                            return range
209                                .map(|index| {
210                                    table.render_row(
211                                        index,
212                                        [
213                                            string_cell(
214                                                this.processed_bindings[index].action.clone(),
215                                            ),
216                                            string_cell(
217                                                this.processed_bindings[index]
218                                                    .keystroke_text
219                                                    .clone(),
220                                            ),
221                                            string_cell(
222                                                this.processed_bindings[index].context.clone(),
223                                            ),
224                                            // TODO: Add a source field
225                                            // string_cell(keybinding.source().to_string()),
226                                        ],
227                                        cx,
228                                    )
229                                })
230                                .collect();
231                        },
232                    )
233                    .size_full()
234                    .flex_grow()
235                    .with_sizing_behavior(ListSizingBehavior::Auto)
236                    .with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained),
237                ),
238        )
239    }
240}
241
242/// A table component
243#[derive(Clone, Copy)]
244pub struct Table<const COLS: usize> {
245    striped: bool,
246    width: Length,
247    row_count: usize,
248}
249
250impl<const COLS: usize> Table<COLS> {
251    /// Create a new table with a column count equal to the
252    /// number of headers provided.
253    pub fn new(row_count: usize) -> Self {
254        Table {
255            striped: false,
256            width: Length::Auto,
257            row_count,
258        }
259    }
260
261    /// Enables row striping.
262    pub fn striped(mut self) -> Self {
263        self.striped = true;
264        self
265    }
266
267    /// Sets the width of the table.
268    pub fn width(mut self, width: impl Into<Length>) -> Self {
269        self.width = width.into();
270        self
271    }
272
273    fn base_cell_style(cx: &App) -> Div {
274        div()
275            .px_1p5()
276            .flex_1()
277            .justify_start()
278            .text_ui(cx)
279            .whitespace_nowrap()
280            .text_ellipsis()
281            .overflow_hidden()
282    }
283
284    pub fn render_row(&self, row_index: usize, items: [TableCell; COLS], cx: &App) -> AnyElement {
285        let is_last = row_index == self.row_count - 1;
286        let bg = if row_index % 2 == 1 && self.striped {
287            Some(cx.theme().colors().text.opacity(0.05))
288        } else {
289            None
290        };
291        div()
292            .w_full()
293            .flex()
294            .flex_row()
295            .items_center()
296            .justify_between()
297            .px_1p5()
298            .py_1()
299            .when_some(bg, |row, bg| row.bg(bg))
300            .when(!is_last, |row| {
301                row.border_b_1().border_color(cx.theme().colors().border)
302            })
303            .children(items.into_iter().map(|cell| match cell {
304                TableCell::String(s) => Self::base_cell_style(cx).child(s),
305                TableCell::Element(e) => Self::base_cell_style(cx).child(e),
306            }))
307            .into_any_element()
308    }
309
310    fn render_header(&self, headers: [SharedString; COLS], cx: &mut App) -> impl IntoElement {
311        div()
312            .flex()
313            .flex_row()
314            .items_center()
315            .justify_between()
316            .w_full()
317            .p_2()
318            .border_b_1()
319            .border_color(cx.theme().colors().border)
320            .children(headers.into_iter().map(|h| {
321                Self::base_cell_style(cx)
322                    .font_weight(FontWeight::SEMIBOLD)
323                    .child(h.clone())
324            }))
325    }
326
327    fn render(&self) -> Div {
328        div().w(self.width).overflow_hidden()
329    }
330}
331
332/// Represents a cell in a table.
333pub enum TableCell {
334    /// A cell containing a string value.
335    String(SharedString),
336    /// A cell containing a UI element.
337    Element(AnyElement),
338}
339
340/// Creates a `TableCell` containing a string value.
341pub fn string_cell(s: impl Into<SharedString>) -> TableCell {
342    TableCell::String(s.into())
343}
344
345/// Creates a `TableCell` containing an element.
346pub fn element_cell(e: impl Into<AnyElement>) -> TableCell {
347    TableCell::Element(e.into())
348}
349
350impl<E> From<E> for TableCell
351where
352    E: Into<SharedString>,
353{
354    fn from(e: E) -> Self {
355        TableCell::String(e.into())
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}