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                action_input: key_binding.action_input(),
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 scroll_to_item(&self, index: usize, strategy: ScrollStrategy, cx: &mut App) {
225        let index = usize::min(index, self.matches.len().saturating_sub(1));
226        self.table_interaction_state.update(cx, |this, _cx| {
227            this.scroll_handle.scroll_to_item(index, strategy);
228        });
229    }
230
231    fn select_next(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) {
232        if let Some(selected) = self.selected_index {
233            let selected = selected + 1;
234            if selected >= self.matches.len() {
235                self.select_last(&Default::default(), window, cx);
236            } else {
237                self.selected_index = Some(selected);
238                self.scroll_to_item(selected, ScrollStrategy::Center, cx);
239                cx.notify();
240            }
241        } else {
242            self.select_first(&Default::default(), window, cx);
243        }
244    }
245
246    fn select_previous(
247        &mut self,
248        _: &menu::SelectPrevious,
249        window: &mut Window,
250        cx: &mut Context<Self>,
251    ) {
252        if let Some(selected) = self.selected_index {
253            if selected == 0 {
254                return;
255            }
256
257            let selected = selected - 1;
258
259            if selected >= self.matches.len() {
260                self.select_last(&Default::default(), window, cx);
261            } else {
262                self.selected_index = Some(selected);
263                self.scroll_to_item(selected, ScrollStrategy::Center, cx);
264                cx.notify();
265            }
266        } else {
267            self.select_last(&Default::default(), window, cx);
268        }
269    }
270
271    fn select_first(
272        &mut self,
273        _: &menu::SelectFirst,
274        _window: &mut Window,
275        cx: &mut Context<Self>,
276    ) {
277        if self.matches.get(0).is_some() {
278            self.selected_index = Some(0);
279            self.scroll_to_item(0, ScrollStrategy::Center, cx);
280            cx.notify();
281        }
282    }
283
284    fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
285        if self.matches.last().is_some() {
286            let index = self.matches.len() - 1;
287            self.selected_index = Some(index);
288            self.scroll_to_item(index, ScrollStrategy::Center, cx);
289            cx.notify();
290        }
291    }
292
293    fn focus_search(
294        &mut self,
295        _: &search::FocusSearch,
296        window: &mut Window,
297        cx: &mut Context<Self>,
298    ) {
299        if !self
300            .filter_editor
301            .focus_handle(cx)
302            .contains_focused(window, cx)
303        {
304            window.focus(&self.filter_editor.focus_handle(cx));
305        } else {
306            self.filter_editor.update(cx, |editor, cx| {
307                editor.select_all(&Default::default(), window, cx);
308            });
309        }
310        self.selected_index.take();
311    }
312}
313
314#[derive(Clone)]
315struct ProcessedKeybinding {
316    keystroke_text: SharedString,
317    action: SharedString,
318    action_input: Option<SharedString>,
319    context: SharedString,
320    source: Option<SharedString>,
321}
322
323impl Item for KeymapEditor {
324    type Event = ();
325
326    fn tab_content_text(&self, _detail: usize, _cx: &App) -> ui::SharedString {
327        "Keymap Editor".into()
328    }
329}
330
331impl Render for KeymapEditor {
332    fn render(&mut self, window: &mut Window, cx: &mut ui::Context<Self>) -> impl ui::IntoElement {
333        let row_count = self.matches.len();
334        let theme = cx.theme();
335
336        div()
337            .key_context(self.dispatch_context(window, cx))
338            .on_action(cx.listener(Self::select_next))
339            .on_action(cx.listener(Self::select_previous))
340            .on_action(cx.listener(Self::select_first))
341            .on_action(cx.listener(Self::select_last))
342            .on_action(cx.listener(Self::focus_search))
343            .size_full()
344            .bg(theme.colors().editor_background)
345            .id("keymap-editor")
346            .track_focus(&self.focus_handle)
347            .px_4()
348            .v_flex()
349            .pb_4()
350            .child(
351                h_flex()
352                    .w_full()
353                    .h_12()
354                    .px_4()
355                    .my_4()
356                    .border_2()
357                    .border_color(theme.colors().border)
358                    .child(self.filter_editor.clone()),
359            )
360            .child(
361                Table::new()
362                    .interactable(&self.table_interaction_state)
363                    .striped()
364                    .column_widths([rems(24.), rems(16.), rems(32.), rems(8.)])
365                    .header(["Command", "Keystrokes", "Context", "Source"])
366                    .selected_item_index(self.selected_index.clone())
367                    .on_click_row(cx.processor(|this, row_index, _window, _cx| {
368                        this.selected_index = Some(row_index);
369                    }))
370                    .uniform_list(
371                        "keymap-editor-table",
372                        row_count,
373                        cx.processor(move |this, range: Range<usize>, _window, _cx| {
374                            range
375                                .filter_map(|index| {
376                                    let candidate_id = this.matches.get(index)?.candidate_id;
377                                    let binding = &this.keybindings[candidate_id];
378                                    let action = h_flex()
379                                        .items_start()
380                                        .gap_1()
381                                        .child(binding.action.clone())
382                                        .when_some(
383                                            binding.action_input.clone(),
384                                            |this, binding_input| this.child(binding_input),
385                                        );
386                                    let keystrokes = binding.keystroke_text.clone();
387                                    let context = binding.context.clone();
388                                    let source = binding.source.clone().unwrap_or_default();
389                                    Some([
390                                        action.into_any_element(),
391                                        keystrokes.into_any_element(),
392                                        context.into_any_element(),
393                                        source.into_any_element(),
394                                    ])
395                                })
396                                .collect()
397                        }),
398                    ),
399            )
400    }
401}
402
403impl SerializableItem for KeymapEditor {
404    fn serialized_item_kind() -> &'static str {
405        "KeymapEditor"
406    }
407
408    fn cleanup(
409        workspace_id: workspace::WorkspaceId,
410        alive_items: Vec<workspace::ItemId>,
411        _window: &mut Window,
412        cx: &mut App,
413    ) -> gpui::Task<gpui::Result<()>> {
414        workspace::delete_unloaded_items(
415            alive_items,
416            workspace_id,
417            "keybinding_editors",
418            &KEYBINDING_EDITORS,
419            cx,
420        )
421    }
422
423    fn deserialize(
424        _project: gpui::Entity<project::Project>,
425        _workspace: gpui::WeakEntity<Workspace>,
426        workspace_id: workspace::WorkspaceId,
427        item_id: workspace::ItemId,
428        window: &mut Window,
429        cx: &mut App,
430    ) -> gpui::Task<gpui::Result<gpui::Entity<Self>>> {
431        window.spawn(cx, async move |cx| {
432            if KEYBINDING_EDITORS
433                .get_keybinding_editor(item_id, workspace_id)?
434                .is_some()
435            {
436                cx.update(|window, cx| cx.new(|cx| KeymapEditor::new(window, cx)))
437            } else {
438                Err(anyhow!("No keybinding editor to deserialize"))
439            }
440        })
441    }
442
443    fn serialize(
444        &mut self,
445        workspace: &mut Workspace,
446        item_id: workspace::ItemId,
447        _closing: bool,
448        _window: &mut Window,
449        cx: &mut ui::Context<Self>,
450    ) -> Option<gpui::Task<gpui::Result<()>>> {
451        let workspace_id = workspace.database_id()?;
452        Some(cx.background_spawn(async move {
453            KEYBINDING_EDITORS
454                .save_keybinding_editor(item_id, workspace_id)
455                .await
456        }))
457    }
458
459    fn should_serialize(&self, _event: &Self::Event) -> bool {
460        false
461    }
462}
463
464mod persistence {
465    use db::{define_connection, query, sqlez_macros::sql};
466    use workspace::WorkspaceDb;
467
468    define_connection! {
469        pub static ref KEYBINDING_EDITORS: KeybindingEditorDb<WorkspaceDb> =
470            &[sql!(
471                CREATE TABLE keybinding_editors (
472                    workspace_id INTEGER,
473                    item_id INTEGER UNIQUE,
474
475                    PRIMARY KEY(workspace_id, item_id),
476                    FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
477                    ON DELETE CASCADE
478                ) STRICT;
479            )];
480    }
481
482    impl KeybindingEditorDb {
483        query! {
484            pub async fn save_keybinding_editor(
485                item_id: workspace::ItemId,
486                workspace_id: workspace::WorkspaceId
487            ) -> Result<()> {
488                INSERT OR REPLACE INTO keybinding_editors(item_id, workspace_id)
489                VALUES (?, ?)
490            }
491        }
492
493        query! {
494            pub fn get_keybinding_editor(
495                item_id: workspace::ItemId,
496                workspace_id: workspace::WorkspaceId
497            ) -> Result<Option<workspace::ItemId>> {
498                SELECT item_id
499                FROM keybinding_editors
500                WHERE item_id = ? AND workspace_id = ?
501            }
502        }
503    }
504}