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.selected_index.take();
137                this.scroll_to_item(0, ScrollStrategy::Top, cx);
138                this.matches = matches;
139                cx.notify();
140            })
141        })
142        .detach();
143    }
144
145    fn process_bindings(
146        cx: &mut Context<Self>,
147    ) -> (Vec<ProcessedKeybinding>, Vec<StringMatchCandidate>) {
148        let key_bindings_ptr = cx.key_bindings();
149        let lock = key_bindings_ptr.borrow();
150        let key_bindings = lock.bindings();
151
152        let mut processed_bindings = Vec::new();
153        let mut string_match_candidates = Vec::new();
154
155        for key_binding in key_bindings {
156            let mut keystroke_text = String::new();
157            for keystroke in key_binding.keystrokes() {
158                write!(&mut keystroke_text, "{} ", keystroke.unparse()).ok();
159            }
160            let keystroke_text = keystroke_text.trim().to_string();
161
162            let context = key_binding
163                .predicate()
164                .map(|predicate| predicate.to_string())
165                .unwrap_or_else(|| "<global>".to_string());
166
167            let source = key_binding
168                .meta()
169                .map(|meta| settings::KeybindSource::from_meta(meta).name().into());
170
171            let action_name = key_binding.action().name();
172
173            let index = processed_bindings.len();
174            let string_match_candidate = StringMatchCandidate::new(index, &action_name);
175            processed_bindings.push(ProcessedKeybinding {
176                keystroke_text: keystroke_text.into(),
177                action: action_name.into(),
178                context: context.into(),
179                source,
180            });
181            string_match_candidates.push(string_match_candidate);
182        }
183        (processed_bindings, string_match_candidates)
184    }
185
186    fn update_keybindings(self: &mut KeymapEditor, cx: &mut Context<KeymapEditor>) {
187        let (key_bindings, string_match_candidates) = Self::process_bindings(cx);
188        self.keybindings = key_bindings;
189        self.string_match_candidates = Arc::new(string_match_candidates);
190        self.matches = self
191            .string_match_candidates
192            .iter()
193            .enumerate()
194            .map(|(ix, candidate)| StringMatch {
195                candidate_id: ix,
196                score: 0.0,
197                positions: vec![],
198                string: candidate.string.clone(),
199            })
200            .collect();
201
202        self.update_matches(cx);
203        cx.notify();
204    }
205
206    fn dispatch_context(&self, _window: &Window, _cx: &Context<Self>) -> KeyContext {
207        let mut dispatch_context = KeyContext::new_with_defaults();
208        dispatch_context.add("KeymapEditor");
209        dispatch_context.add("BufferSearchBar");
210        dispatch_context.add("menu");
211
212        // todo! track key context in keybind edit modal
213        // let identifier = if self.keymap_editor.focus_handle(cx).is_focused(window) {
214        //     "editing"
215        // } else {
216        //     "not_editing"
217        // };
218        // dispatch_context.add(identifier);
219
220        dispatch_context
221    }
222
223    fn scroll_to_item(&self, index: usize, strategy: ScrollStrategy, cx: &mut App) {
224        let index = usize::min(index, self.matches.len().saturating_sub(1));
225        self.table_interaction_state.update(cx, |this, _cx| {
226            this.scroll_handle.scroll_to_item(index, strategy);
227        });
228    }
229
230    fn select_next(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) {
231        if let Some(selected) = self.selected_index {
232            let selected = selected + 1;
233            if selected >= self.matches.len() {
234                self.select_last(&Default::default(), window, cx);
235            } else {
236                self.selected_index = Some(selected);
237                self.scroll_to_item(selected, ScrollStrategy::Center, cx);
238                cx.notify();
239            }
240        } else {
241            self.select_first(&Default::default(), window, cx);
242        }
243    }
244
245    fn select_previous(
246        &mut self,
247        _: &menu::SelectPrevious,
248        window: &mut Window,
249        cx: &mut Context<Self>,
250    ) {
251        if let Some(selected) = self.selected_index {
252            if selected == 0 {
253                return;
254            }
255
256            let selected = selected - 1;
257
258            if selected >= self.matches.len() {
259                self.select_last(&Default::default(), window, cx);
260            } else {
261                self.selected_index = Some(selected);
262                self.scroll_to_item(selected, ScrollStrategy::Center, cx);
263                cx.notify();
264            }
265        } else {
266            self.select_last(&Default::default(), window, cx);
267        }
268    }
269
270    fn select_first(
271        &mut self,
272        _: &menu::SelectFirst,
273        _window: &mut Window,
274        cx: &mut Context<Self>,
275    ) {
276        if self.matches.get(0).is_some() {
277            self.selected_index = Some(0);
278            self.scroll_to_item(0, ScrollStrategy::Center, cx);
279            cx.notify();
280        }
281    }
282
283    fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
284        if self.matches.last().is_some() {
285            let index = self.matches.len() - 1;
286            self.selected_index = Some(index);
287            self.scroll_to_item(index, ScrollStrategy::Center, cx);
288            cx.notify();
289        }
290    }
291
292    fn focus_search(
293        &mut self,
294        _: &search::FocusSearch,
295        window: &mut Window,
296        cx: &mut Context<Self>,
297    ) {
298        if !self
299            .filter_editor
300            .focus_handle(cx)
301            .contains_focused(window, cx)
302        {
303            window.focus(&self.filter_editor.focus_handle(cx));
304        } else {
305            self.filter_editor.update(cx, |editor, cx| {
306                editor.select_all(&Default::default(), window, cx);
307            });
308        }
309        self.selected_index.take();
310    }
311}
312
313#[derive(Clone)]
314struct ProcessedKeybinding {
315    keystroke_text: SharedString,
316    action: SharedString,
317    context: SharedString,
318    source: Option<SharedString>,
319}
320
321impl Item for KeymapEditor {
322    type Event = ();
323
324    fn tab_content_text(&self, _detail: usize, _cx: &App) -> ui::SharedString {
325        "Keymap Editor".into()
326    }
327}
328
329impl Render for KeymapEditor {
330    fn render(&mut self, window: &mut Window, cx: &mut ui::Context<Self>) -> impl ui::IntoElement {
331        let row_count = self.matches.len();
332        let theme = cx.theme();
333
334        div()
335            .key_context(self.dispatch_context(window, cx))
336            .on_action(cx.listener(Self::select_next))
337            .on_action(cx.listener(Self::select_previous))
338            .on_action(cx.listener(Self::select_first))
339            .on_action(cx.listener(Self::select_last))
340            .on_action(cx.listener(Self::focus_search))
341            .size_full()
342            .bg(theme.colors().background)
343            .id("keymap-editor")
344            .track_focus(&self.focus_handle)
345            .px_4()
346            .v_flex()
347            .pb_4()
348            .child(
349                h_flex()
350                    .w_full()
351                    .h_12()
352                    .px_4()
353                    .my_4()
354                    .border_2()
355                    .border_color(theme.colors().border)
356                    .child(self.filter_editor.clone()),
357            )
358            .child(
359                Table::new()
360                    .interactable(&self.table_interaction_state)
361                    .striped()
362                    .column_widths([rems(24.), rems(16.), rems(32.), rems(8.)])
363                    .header(["Command", "Keystrokes", "Context", "Source"])
364                    .selected_item_index(self.selected_index.clone())
365                    .on_click_row(cx.processor(|this, row_index, _window, _cx| {
366                        this.selected_index = Some(row_index);
367                    }))
368                    .uniform_list(
369                        "keymap-editor-table",
370                        row_count,
371                        cx.processor(move |this, range: Range<usize>, _window, _cx| {
372                            range
373                                .filter_map(|index| {
374                                    let candidate_id = this.matches.get(index)?.candidate_id;
375                                    let binding = &this.keybindings[candidate_id];
376                                    Some(
377                                        [
378                                            binding.action.clone(),
379                                            binding.keystroke_text.clone(),
380                                            binding.context.clone(),
381                                            binding.source.clone().unwrap_or_default(),
382                                        ]
383                                        .map(IntoElement::into_any_element),
384                                    )
385                                })
386                                .collect()
387                        }),
388                    ),
389            )
390    }
391}
392
393impl SerializableItem for KeymapEditor {
394    fn serialized_item_kind() -> &'static str {
395        "KeymapEditor"
396    }
397
398    fn cleanup(
399        workspace_id: workspace::WorkspaceId,
400        alive_items: Vec<workspace::ItemId>,
401        _window: &mut Window,
402        cx: &mut App,
403    ) -> gpui::Task<gpui::Result<()>> {
404        workspace::delete_unloaded_items(
405            alive_items,
406            workspace_id,
407            "keybinding_editors",
408            &KEYBINDING_EDITORS,
409            cx,
410        )
411    }
412
413    fn deserialize(
414        _project: gpui::Entity<project::Project>,
415        _workspace: gpui::WeakEntity<Workspace>,
416        workspace_id: workspace::WorkspaceId,
417        item_id: workspace::ItemId,
418        window: &mut Window,
419        cx: &mut App,
420    ) -> gpui::Task<gpui::Result<gpui::Entity<Self>>> {
421        window.spawn(cx, async move |cx| {
422            if KEYBINDING_EDITORS
423                .get_keybinding_editor(item_id, workspace_id)?
424                .is_some()
425            {
426                cx.update(|window, cx| cx.new(|cx| KeymapEditor::new(window, cx)))
427            } else {
428                Err(anyhow!("No keybinding editor to deserialize"))
429            }
430        })
431    }
432
433    fn serialize(
434        &mut self,
435        workspace: &mut Workspace,
436        item_id: workspace::ItemId,
437        _closing: bool,
438        _window: &mut Window,
439        cx: &mut ui::Context<Self>,
440    ) -> Option<gpui::Task<gpui::Result<()>>> {
441        let workspace_id = workspace.database_id()?;
442        Some(cx.background_spawn(async move {
443            KEYBINDING_EDITORS
444                .save_keybinding_editor(item_id, workspace_id)
445                .await
446        }))
447    }
448
449    fn should_serialize(&self, _event: &Self::Event) -> bool {
450        false
451    }
452}
453
454mod persistence {
455    use db::{define_connection, query, sqlez_macros::sql};
456    use workspace::WorkspaceDb;
457
458    define_connection! {
459        pub static ref KEYBINDING_EDITORS: KeybindingEditorDb<WorkspaceDb> =
460            &[sql!(
461                CREATE TABLE keybinding_editors (
462                    workspace_id INTEGER,
463                    item_id INTEGER UNIQUE,
464
465                    PRIMARY KEY(workspace_id, item_id),
466                    FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
467                    ON DELETE CASCADE
468                ) STRICT;
469            )];
470    }
471
472    impl KeybindingEditorDb {
473        query! {
474            pub async fn save_keybinding_editor(
475                item_id: workspace::ItemId,
476                workspace_id: workspace::WorkspaceId
477            ) -> Result<()> {
478                INSERT OR REPLACE INTO keybinding_editors(item_id, workspace_id)
479                VALUES (?, ?)
480            }
481        }
482
483        query! {
484            pub fn get_keybinding_editor(
485                item_id: workspace::ItemId,
486                workspace_id: workspace::WorkspaceId
487            ) -> Result<Option<workspace::ItemId>> {
488                SELECT item_id
489                FROM keybinding_editors
490                WHERE item_id = ? AND workspace_id = ?
491            }
492        }
493    }
494}