keybindings.rs

  1use std::{fmt::Write as _, ops::Range, sync::Arc};
  2
  3use db::anyhow::anyhow;
  4use editor::{Editor, EditorEvent};
  5use fuzzy::{StringMatch, StringMatchCandidate};
  6use gpui::{
  7    AppContext as _, Context, Entity, EventEmitter, FocusHandle, Focusable, Global, KeyContext,
  8    ScrollStrategy, Subscription, actions, div,
  9};
 10
 11use ui::{
 12    ActiveTheme as _, App, BorrowAppContext, ParentElement as _, Render, SharedString, Styled as _,
 13    Window, prelude::*,
 14};
 15use workspace::{Item, SerializableItem, Workspace, register_serializable_item};
 16
 17use crate::{
 18    keybindings::persistence::KEYBINDING_EDITORS,
 19    ui_components::table::{Table, TableInteractionState},
 20};
 21
 22actions!(zed, [OpenKeymapEditor]);
 23
 24pub fn init(cx: &mut App) {
 25    let keymap_event_channel = KeymapEventChannel::new();
 26    cx.set_global(keymap_event_channel);
 27
 28    cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
 29        workspace.register_action(|workspace, _: &OpenKeymapEditor, window, cx| {
 30            let open_keymap_editor = cx.new(|cx| KeymapEditor::new(window, cx));
 31            workspace.add_item_to_center(Box::new(open_keymap_editor), window, cx);
 32        });
 33    })
 34    .detach();
 35
 36    register_serializable_item::<KeymapEditor>(cx);
 37}
 38
 39pub struct KeymapEventChannel {}
 40
 41impl Global for KeymapEventChannel {}
 42
 43impl KeymapEventChannel {
 44    fn new() -> Self {
 45        Self {}
 46    }
 47
 48    pub fn trigger_keymap_changed(cx: &mut App) {
 49        cx.update_global(|_event_channel: &mut Self, _| {
 50            /* triggers observers in KeymapEditors */
 51        });
 52    }
 53}
 54
 55struct KeymapEditor {
 56    focus_handle: FocusHandle,
 57    _keymap_subscription: Subscription,
 58    keybindings: Vec<ProcessedKeybinding>,
 59    // corresponds 1 to 1 with keybindings
 60    string_match_candidates: Arc<Vec<StringMatchCandidate>>,
 61    matches: Vec<StringMatch>,
 62    table_interaction_state: Entity<TableInteractionState>,
 63    filter_editor: Entity<Editor>,
 64    selected_index: Option<usize>,
 65}
 66
 67impl EventEmitter<()> for KeymapEditor {}
 68
 69impl Focusable for KeymapEditor {
 70    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
 71        return self.filter_editor.focus_handle(cx);
 72    }
 73}
 74
 75impl KeymapEditor {
 76    fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
 77        let focus_handle = cx.focus_handle();
 78
 79        let _keymap_subscription =
 80            cx.observe_global::<KeymapEventChannel>(Self::update_keybindings);
 81        let table_interaction_state = TableInteractionState::new(window, cx);
 82
 83        let filter_editor = cx.new(|cx| {
 84            let mut editor = Editor::single_line(window, cx);
 85            editor.set_placeholder_text("Filter action names...", cx);
 86            editor
 87        });
 88
 89        cx.subscribe(&filter_editor, |this, _, e: &EditorEvent, cx| {
 90            if !matches!(e, EditorEvent::BufferEdited) {
 91                return;
 92            }
 93
 94            this.update_matches(cx);
 95        })
 96        .detach();
 97
 98        let mut this = Self {
 99            keybindings: vec![],
100            string_match_candidates: Arc::new(vec![]),
101            matches: vec![],
102            focus_handle: focus_handle.clone(),
103            _keymap_subscription,
104            table_interaction_state,
105            filter_editor,
106            selected_index: None,
107        };
108
109        this.update_keybindings(cx);
110
111        this
112    }
113
114    fn update_matches(&mut self, cx: &mut Context<Self>) {
115        let query = self.filter_editor.read(cx).text(cx);
116        let string_match_candidates = self.string_match_candidates.clone();
117        let executor = cx.background_executor().clone();
118        let keybind_count = self.keybindings.len();
119        let query = command_palette::normalize_action_query(&query);
120        let fuzzy_match = cx.background_spawn(async move {
121            fuzzy::match_strings(
122                &string_match_candidates,
123                &query,
124                true,
125                true,
126                keybind_count,
127                &Default::default(),
128                executor,
129            )
130            .await
131        });
132
133        cx.spawn(async move |this, cx| {
134            let matches = fuzzy_match.await;
135            this.update(cx, |this, cx| {
136                this.table_interaction_state.update(cx, |this, _cx| {
137                    this.scroll_handle.scroll_to_item(0, ScrollStrategy::Top);
138                });
139                this.matches = matches;
140                cx.notify();
141            })
142        })
143        .detach();
144    }
145
146    fn process_bindings(
147        cx: &mut Context<Self>,
148    ) -> (Vec<ProcessedKeybinding>, Vec<StringMatchCandidate>) {
149        let key_bindings_ptr = cx.key_bindings();
150        let lock = key_bindings_ptr.borrow();
151        let key_bindings = lock.bindings();
152
153        let mut processed_bindings = Vec::new();
154        let mut string_match_candidates = Vec::new();
155
156        for key_binding in key_bindings {
157            let mut keystroke_text = String::new();
158            for keystroke in key_binding.keystrokes() {
159                write!(&mut keystroke_text, "{} ", keystroke.unparse()).ok();
160            }
161            let keystroke_text = keystroke_text.trim().to_string();
162
163            let context = key_binding
164                .predicate()
165                .map(|predicate| predicate.to_string())
166                .unwrap_or_else(|| "<global>".to_string());
167
168            let source = key_binding
169                .meta()
170                .map(|meta| settings::KeybindSource::from_meta(meta).name().into());
171
172            let action_name = key_binding.action().name();
173
174            let index = processed_bindings.len();
175            let string_match_candidate = StringMatchCandidate::new(index, &action_name);
176            processed_bindings.push(ProcessedKeybinding {
177                keystroke_text: keystroke_text.into(),
178                action: action_name.into(),
179                context: context.into(),
180                source,
181            });
182            string_match_candidates.push(string_match_candidate);
183        }
184        (processed_bindings, string_match_candidates)
185    }
186
187    fn update_keybindings(self: &mut KeymapEditor, cx: &mut Context<KeymapEditor>) {
188        let (key_bindings, string_match_candidates) = Self::process_bindings(cx);
189        self.keybindings = key_bindings;
190        self.string_match_candidates = Arc::new(string_match_candidates);
191        self.matches = self
192            .string_match_candidates
193            .iter()
194            .enumerate()
195            .map(|(ix, candidate)| StringMatch {
196                candidate_id: ix,
197                score: 0.0,
198                positions: vec![],
199                string: candidate.string.clone(),
200            })
201            .collect();
202
203        self.update_matches(cx);
204        cx.notify();
205    }
206
207    fn dispatch_context(&self, _window: &Window, _cx: &Context<Self>) -> KeyContext {
208        let mut dispatch_context = KeyContext::new_with_defaults();
209        dispatch_context.add("KeymapEditor");
210        dispatch_context.add("BufferSearchBar");
211        dispatch_context.add("menu");
212
213        // todo! track key context in keybind edit modal
214        // let identifier = if self.keymap_editor.focus_handle(cx).is_focused(window) {
215        //     "editing"
216        // } else {
217        //     "not_editing"
218        // };
219        // dispatch_context.add(identifier);
220
221        dispatch_context
222    }
223
224    fn select_next(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) {
225        if let Some(selected) = &mut self.selected_index {
226            *selected += 1;
227            if *selected >= self.matches.len() {
228                self.select_last(&Default::default(), window, cx);
229            } else {
230                cx.notify();
231            }
232        } else {
233            self.select_first(&Default::default(), window, cx);
234        }
235    }
236
237    fn select_previous(
238        &mut self,
239        _: &menu::SelectPrevious,
240        window: &mut Window,
241        cx: &mut Context<Self>,
242    ) {
243        if let Some(selected) = &mut self.selected_index {
244            *selected = selected.saturating_sub(1);
245            if *selected == 0 {
246                self.select_first(&Default::default(), window, cx);
247            } else if *selected >= self.matches.len() {
248                self.select_last(&Default::default(), window, cx);
249            } else {
250                cx.notify();
251            }
252        } else {
253            self.select_last(&Default::default(), window, cx);
254        }
255    }
256
257    fn select_first(&mut self, _: &menu::SelectFirst, window: &mut Window, cx: &mut Context<Self>) {
258        if self.matches.get(0).is_some() {
259            self.selected_index = Some(0);
260            cx.notify();
261        }
262    }
263
264    fn select_last(&mut self, _: &menu::SelectLast, _: &mut Window, cx: &mut Context<Self>) {
265        if self.matches.last().is_some() {
266            self.selected_index = Some(self.matches.len() - 1);
267            cx.notify();
268        }
269    }
270
271    fn focus_search(
272        &mut self,
273        _: &search::FocusSearch,
274        window: &mut Window,
275        cx: &mut Context<Self>,
276    ) {
277        if !self
278            .filter_editor
279            .focus_handle(cx)
280            .contains_focused(window, cx)
281        {
282            window.focus(&self.filter_editor.focus_handle(cx));
283        }
284    }
285}
286
287#[derive(Clone)]
288struct ProcessedKeybinding {
289    keystroke_text: SharedString,
290    action: SharedString,
291    context: SharedString,
292    source: Option<SharedString>,
293}
294
295impl Item for KeymapEditor {
296    type Event = ();
297
298    fn tab_content_text(&self, _detail: usize, _cx: &App) -> ui::SharedString {
299        "Keymap Editor".into()
300    }
301}
302
303impl Render for KeymapEditor {
304    fn render(&mut self, window: &mut Window, cx: &mut ui::Context<Self>) -> impl ui::IntoElement {
305        let row_count = self.matches.len();
306        let theme = cx.theme();
307
308        div()
309            .key_context(self.dispatch_context(window, cx))
310            .on_action(cx.listener(Self::select_next))
311            .on_action(cx.listener(Self::select_previous))
312            .on_action(cx.listener(Self::select_first))
313            .on_action(cx.listener(Self::select_last))
314            .on_action(cx.listener(Self::focus_search))
315            .size_full()
316            .bg(theme.colors().background)
317            .id("keymap-editor")
318            .track_focus(&self.focus_handle)
319            .px_4()
320            .v_flex()
321            .pb_4()
322            .child(
323                h_flex()
324                    .w_full()
325                    .h_12()
326                    .px_4()
327                    .my_4()
328                    .border_2()
329                    .border_color(theme.colors().border)
330                    .child(self.filter_editor.clone()),
331            )
332            .child(
333                Table::new()
334                    .interactable(&self.table_interaction_state)
335                    .striped()
336                    .column_widths([rems(24.), rems(16.), rems(32.), rems(8.)])
337                    .header(["Command", "Keystrokes", "Context", "Source"])
338                    .selected_item_index(self.selected_index.clone())
339                    .on_click_row(cx.processor(|this, row_index, _window, _cx| {
340                        this.selected_index = Some(row_index);
341                    }))
342                    .uniform_list(
343                        "keymap-editor-table",
344                        row_count,
345                        cx.processor(move |this, range: Range<usize>, _window, _cx| {
346                            range
347                                .filter_map(|index| {
348                                    let candidate_id = this.matches.get(index)?.candidate_id;
349                                    let binding = &this.keybindings[candidate_id];
350                                    Some(
351                                        [
352                                            binding.action.clone(),
353                                            binding.keystroke_text.clone(),
354                                            binding.context.clone(),
355                                            binding.source.clone().unwrap_or_default(),
356                                        ]
357                                        .map(IntoElement::into_any_element),
358                                    )
359                                })
360                                .collect()
361                        }),
362                    ),
363            )
364    }
365}
366
367impl SerializableItem for KeymapEditor {
368    fn serialized_item_kind() -> &'static str {
369        "KeymapEditor"
370    }
371
372    fn cleanup(
373        workspace_id: workspace::WorkspaceId,
374        alive_items: Vec<workspace::ItemId>,
375        _window: &mut Window,
376        cx: &mut App,
377    ) -> gpui::Task<gpui::Result<()>> {
378        workspace::delete_unloaded_items(
379            alive_items,
380            workspace_id,
381            "keybinding_editors",
382            &KEYBINDING_EDITORS,
383            cx,
384        )
385    }
386
387    fn deserialize(
388        _project: gpui::Entity<project::Project>,
389        _workspace: gpui::WeakEntity<Workspace>,
390        workspace_id: workspace::WorkspaceId,
391        item_id: workspace::ItemId,
392        window: &mut Window,
393        cx: &mut App,
394    ) -> gpui::Task<gpui::Result<gpui::Entity<Self>>> {
395        window.spawn(cx, async move |cx| {
396            if KEYBINDING_EDITORS
397                .get_keybinding_editor(item_id, workspace_id)?
398                .is_some()
399            {
400                cx.update(|window, cx| cx.new(|cx| KeymapEditor::new(window, cx)))
401            } else {
402                Err(anyhow!("No keybinding editor to deserialize"))
403            }
404        })
405    }
406
407    fn serialize(
408        &mut self,
409        workspace: &mut Workspace,
410        item_id: workspace::ItemId,
411        _closing: bool,
412        _window: &mut Window,
413        cx: &mut ui::Context<Self>,
414    ) -> Option<gpui::Task<gpui::Result<()>>> {
415        let workspace_id = workspace.database_id()?;
416        Some(cx.background_spawn(async move {
417            KEYBINDING_EDITORS
418                .save_keybinding_editor(item_id, workspace_id)
419                .await
420        }))
421    }
422
423    fn should_serialize(&self, _event: &Self::Event) -> bool {
424        false
425    }
426}
427
428mod persistence {
429    use db::{define_connection, query, sqlez_macros::sql};
430    use workspace::WorkspaceDb;
431
432    define_connection! {
433        pub static ref KEYBINDING_EDITORS: KeybindingEditorDb<WorkspaceDb> =
434            &[sql!(
435                CREATE TABLE keybinding_editors (
436                    workspace_id INTEGER,
437                    item_id INTEGER UNIQUE,
438
439                    PRIMARY KEY(workspace_id, item_id),
440                    FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
441                    ON DELETE CASCADE
442                ) STRICT;
443            )];
444    }
445
446    impl KeybindingEditorDb {
447        query! {
448            pub async fn save_keybinding_editor(
449                item_id: workspace::ItemId,
450                workspace_id: workspace::WorkspaceId
451            ) -> Result<()> {
452                INSERT OR REPLACE INTO keybinding_editors(item_id, workspace_id)
453                VALUES (?, ?)
454            }
455        }
456
457        query! {
458            pub fn get_keybinding_editor(
459                item_id: workspace::ItemId,
460                workspace_id: workspace::WorkspaceId
461            ) -> Result<Option<workspace::ItemId>> {
462                SELECT item_id
463                FROM keybinding_editors
464                WHERE item_id = ? AND workspace_id = ?
465            }
466        }
467    }
468}