keybindings.rs

  1use std::{cell::RefCell, fmt::Write as _, rc::Rc};
  2
  3use db::anyhow::anyhow;
  4use gpui::{
  5    AppContext as _, Context, Entity, EventEmitter, FocusHandle, Focusable, Global, Keymap,
  6    Subscription, Task, actions,
  7};
  8use ui::{
  9    ActiveTheme as _, App, BorrowAppContext, ParentElement as _, Render, SharedString, Styled as _,
 10    Table, Window, div, string_cell,
 11};
 12use workspace::{Item, SerializableItem, Workspace, register_serializable_item};
 13
 14use crate::keybindings::persistence::KEYBINDING_EDITORS;
 15
 16actions!(zed, [OpenKeymapEditor]);
 17
 18pub fn init(cx: &mut App) {
 19    let keymap_event_channel = KeymapEventChannel::new();
 20    cx.set_global(keymap_event_channel);
 21
 22    cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
 23        workspace.register_action(|workspace, _: &OpenKeymapEditor, window, cx| {
 24            let open_keymap_editor = cx.new(|cx| KeymapEditor::new(cx));
 25            workspace.add_item_to_center(Box::new(open_keymap_editor), window, cx);
 26        });
 27    })
 28    .detach();
 29
 30    register_serializable_item::<KeymapEditor>(cx);
 31}
 32
 33enum KeymapEvent {
 34    KeymapChanged,
 35}
 36
 37pub struct KeymapEventChannel {}
 38
 39impl EventEmitter<KeymapEvent> for KeymapEventChannel {}
 40impl Global for KeymapEventChannel {}
 41
 42impl KeymapEventChannel {
 43    fn new() -> Self {
 44        Self {}
 45    }
 46
 47    pub fn trigger_keymap_changed(cx: &mut App) {
 48        cx.update_global(|_event_channel: &mut Self, _| {
 49            dbg!("updating global");
 50            *_event_channel = Self::new();
 51        });
 52    }
 53}
 54
 55struct KeymapEditor {
 56    focus_handle: FocusHandle,
 57    _keymap_subscription: Subscription,
 58    processed_bindings: Vec<ProcessedKeybinding>,
 59}
 60
 61impl EventEmitter<()> for KeymapEditor {}
 62
 63impl Focusable for KeymapEditor {
 64    fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
 65        self.focus_handle.clone()
 66    }
 67}
 68
 69impl KeymapEditor {
 70    fn new(cx: &mut gpui::Context<Self>) -> Self {
 71        let _keymap_subscription = cx.observe_global::<KeymapEventChannel>(|this, cx| {
 72            let key_bindings = Self::process_bindings(cx);
 73            this.processed_bindings = key_bindings;
 74        });
 75        Self {
 76            focus_handle: cx.focus_handle(),
 77            _keymap_subscription,
 78            processed_bindings: vec![],
 79        }
 80    }
 81
 82    fn process_bindings(cx: &mut Context<Self>) -> Vec<ProcessedKeybinding> {
 83        let key_bindings_ptr = cx.key_bindings();
 84        let lock = key_bindings_ptr.borrow();
 85        let key_bindings = lock.bindings();
 86
 87        let mut processed_bindings = Vec::new();
 88
 89        for key_binding in key_bindings {
 90            let mut keystroke_text = String::new();
 91            for keystroke in key_binding.keystrokes() {
 92                write!(&mut keystroke_text, "{} ", keystroke.unparse()).ok();
 93            }
 94            let keystroke_text = keystroke_text.trim().to_string();
 95
 96            let context = key_binding
 97                .predicate()
 98                .map(|predicate| predicate.to_string())
 99                .unwrap_or_else(|| "<global>".to_string());
100
101            processed_bindings.push(ProcessedKeybinding {
102                keystroke_text: keystroke_text.into(),
103                action: key_binding.action().name().into(),
104                context: context.into(),
105            })
106        }
107        processed_bindings
108    }
109}
110
111struct ProcessedKeybinding {
112    keystroke_text: SharedString,
113    action: SharedString,
114    context: SharedString,
115}
116
117impl SerializableItem for KeymapEditor {
118    fn serialized_item_kind() -> &'static str {
119        "KeymapEditor"
120    }
121
122    fn cleanup(
123        workspace_id: workspace::WorkspaceId,
124        alive_items: Vec<workspace::ItemId>,
125        _window: &mut Window,
126        cx: &mut App,
127    ) -> gpui::Task<gpui::Result<()>> {
128        workspace::delete_unloaded_items(
129            alive_items,
130            workspace_id,
131            "keybinding_editors",
132            &KEYBINDING_EDITORS,
133            cx,
134        )
135    }
136
137    fn deserialize(
138        _project: gpui::Entity<project::Project>,
139        _workspace: gpui::WeakEntity<Workspace>,
140        workspace_id: workspace::WorkspaceId,
141        item_id: workspace::ItemId,
142        _window: &mut Window,
143        cx: &mut App,
144    ) -> gpui::Task<gpui::Result<gpui::Entity<Self>>> {
145        cx.spawn(async move |cx| {
146            if KEYBINDING_EDITORS
147                .get_keybinding_editor(item_id, workspace_id)?
148                .is_some()
149            {
150                cx.new(|cx| KeymapEditor::new(cx))
151            } else {
152                Err(anyhow!("No keybinding editor to deserialize"))
153            }
154        })
155    }
156
157    fn serialize(
158        &mut self,
159        workspace: &mut Workspace,
160        item_id: workspace::ItemId,
161        _closing: bool,
162        _window: &mut Window,
163        cx: &mut ui::Context<Self>,
164    ) -> Option<gpui::Task<gpui::Result<()>>> {
165        let Some(workspace_id) = workspace.database_id() else {
166            return None;
167        };
168        Some(cx.background_spawn(async move {
169            KEYBINDING_EDITORS
170                .save_keybinding_editor(item_id, workspace_id)
171                .await
172        }))
173    }
174
175    fn should_serialize(&self, _event: &Self::Event) -> bool {
176        false
177    }
178}
179
180impl Item for KeymapEditor {
181    type Event = ();
182
183    fn tab_content_text(&self, _detail: usize, _cx: &App) -> ui::SharedString {
184        "Keymap Editor".into()
185    }
186}
187
188impl Render for KeymapEditor {
189    fn render(&mut self, _window: &mut Window, cx: &mut ui::Context<Self>) -> impl ui::IntoElement {
190        dbg!("rendering");
191        if self.processed_bindings.is_empty() {
192            self.processed_bindings = Self::process_bindings(cx);
193        }
194
195        let mut table = Table::new(vec!["Command", "Keystrokes", "Context"]);
196        for key_binding in &self.processed_bindings {
197            table = table.row(vec![
198                string_cell(key_binding.action.clone()),
199                string_cell(key_binding.keystroke_text.clone()),
200                string_cell(key_binding.context.clone()),
201                // TODO: Add a source field
202                // string_cell(keybinding.source().to_string()),
203            ]);
204        }
205
206        let theme = cx.theme();
207
208        div()
209            .size_full()
210            .bg(theme.colors().background)
211            .child(table.striped())
212    }
213}
214
215mod persistence {
216    use db::{define_connection, query, sqlez_macros::sql};
217    use workspace::WorkspaceDb;
218
219    define_connection! {
220        pub static ref KEYBINDING_EDITORS: KeybindingEditorDb<WorkspaceDb> =
221            &[sql!(
222                CREATE TABLE keybinding_editors (
223                    workspace_id INTEGER,
224                    item_id INTEGER UNIQUE,
225
226                    PRIMARY KEY(workspace_id, item_id),
227                    FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
228                    ON DELETE CASCADE
229                ) STRICT;
230            )];
231    }
232
233    impl KeybindingEditorDb {
234        query! {
235            pub async fn save_keybinding_editor(
236                item_id: workspace::ItemId,
237                workspace_id: workspace::WorkspaceId
238            ) -> Result<()> {
239                INSERT OR REPLACE INTO keybinding_editors(item_id, workspace_id)
240                VALUES (?, ?)
241            }
242        }
243
244        query! {
245            pub fn get_keybinding_editor(
246                item_id: workspace::ItemId,
247                workspace_id: workspace::WorkspaceId
248            ) -> Result<Option<workspace::ItemId>> {
249                SELECT item_id
250                FROM keybinding_editors
251                WHERE item_id = ? AND workspace_id = ?
252            }
253        }
254    }
255}