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