keybindings.rs

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