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, ScrollStrategy,
  8    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}
 65
 66impl EventEmitter<()> for KeymapEditor {}
 67
 68impl Focusable for KeymapEditor {
 69    fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
 70        self.focus_handle.clone()
 71    }
 72}
 73
 74impl KeymapEditor {
 75    fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
 76        let focus_handle = cx.focus_handle();
 77
 78        let _keymap_subscription =
 79            cx.observe_global::<KeymapEventChannel>(Self::update_keybindings);
 80        let table_interaction_state = TableInteractionState::new(window, cx);
 81
 82        let filter_editor = cx.new(|cx| {
 83            let mut editor = Editor::single_line(window, cx);
 84            editor.set_placeholder_text("Filter action names...", cx);
 85            editor
 86        });
 87
 88        cx.subscribe(&filter_editor, |this, _, e: &EditorEvent, cx| {
 89            if !matches!(e, EditorEvent::BufferEdited) {
 90                return;
 91            }
 92
 93            this.update_matches(cx);
 94        })
 95        .detach();
 96
 97        let mut this = Self {
 98            keybindings: vec![],
 99            string_match_candidates: Arc::new(vec![]),
100            matches: vec![],
101            focus_handle: focus_handle.clone(),
102            _keymap_subscription,
103            table_interaction_state,
104            filter_editor,
105        };
106
107        this.update_keybindings(cx);
108
109        this
110    }
111
112    fn update_matches(&mut self, cx: &mut Context<Self>) {
113        let query = self.filter_editor.read(cx).text(cx);
114        let string_match_candidates = self.string_match_candidates.clone();
115        let executor = cx.background_executor().clone();
116        let keybind_count = self.keybindings.len();
117        let query = command_palette::normalize_action_query(&query);
118        let fuzzy_match = cx.background_spawn(async move {
119            fuzzy::match_strings(
120                &string_match_candidates,
121                &query,
122                true,
123                true,
124                keybind_count,
125                &Default::default(),
126                executor,
127            )
128            .await
129        });
130
131        cx.spawn(async move |this, cx| {
132            let matches = fuzzy_match.await;
133            this.update(cx, |this, cx| {
134                this.table_interaction_state.update(cx, |this, _cx| {
135                    this.scroll_handle.scroll_to_item(0, ScrollStrategy::Top);
136                });
137                this.matches = matches;
138                cx.notify();
139            })
140        })
141        .detach();
142    }
143
144    fn process_bindings(
145        cx: &mut Context<Self>,
146    ) -> (Vec<ProcessedKeybinding>, Vec<StringMatchCandidate>) {
147        let key_bindings_ptr = cx.key_bindings();
148        let lock = key_bindings_ptr.borrow();
149        let key_bindings = lock.bindings();
150
151        let mut processed_bindings = Vec::new();
152        let mut string_match_candidates = Vec::new();
153
154        for key_binding in key_bindings {
155            let mut keystroke_text = String::new();
156            for keystroke in key_binding.keystrokes() {
157                write!(&mut keystroke_text, "{} ", keystroke.unparse()).ok();
158            }
159            let keystroke_text = keystroke_text.trim().to_string();
160
161            let context = key_binding
162                .predicate()
163                .map(|predicate| predicate.to_string())
164                .unwrap_or_else(|| "<global>".to_string());
165
166            let source = key_binding
167                .meta()
168                .map(|meta| settings::KeybindSource::from_meta(meta).name().into());
169
170            let action_name = key_binding.action().name();
171
172            let index = processed_bindings.len();
173            let string_match_candidate = StringMatchCandidate::new(index, &action_name);
174            processed_bindings.push(ProcessedKeybinding {
175                keystroke_text: keystroke_text.into(),
176                action: action_name.into(),
177                context: context.into(),
178                source,
179            });
180            string_match_candidates.push(string_match_candidate);
181        }
182        (processed_bindings, string_match_candidates)
183    }
184
185    fn update_keybindings(self: &mut KeymapEditor, cx: &mut Context<KeymapEditor>) {
186        let (key_bindings, string_match_candidates) = Self::process_bindings(cx);
187        self.keybindings = key_bindings;
188        self.string_match_candidates = Arc::new(string_match_candidates);
189        self.matches = self
190            .string_match_candidates
191            .iter()
192            .enumerate()
193            .map(|(ix, candidate)| StringMatch {
194                candidate_id: ix,
195                score: 0.0,
196                positions: vec![],
197                string: candidate.string.clone(),
198            })
199            .collect();
200
201        self.update_matches(cx);
202        cx.notify();
203    }
204}
205
206#[derive(Clone)]
207struct ProcessedKeybinding {
208    keystroke_text: SharedString,
209    action: SharedString,
210    context: SharedString,
211    source: Option<SharedString>,
212}
213
214impl Item for KeymapEditor {
215    type Event = ();
216
217    fn tab_content_text(&self, _detail: usize, _cx: &App) -> ui::SharedString {
218        "Keymap Editor".into()
219    }
220}
221
222impl Render for KeymapEditor {
223    fn render(&mut self, _window: &mut Window, cx: &mut ui::Context<Self>) -> impl ui::IntoElement {
224        let row_count = self.matches.len();
225        let theme = cx.theme();
226
227        div()
228            .size_full()
229            .bg(theme.colors().background)
230            .id("keymap-editor")
231            .track_focus(&self.focus_handle)
232            .px_4()
233            .v_flex()
234            .pb_4()
235            .child(
236                h_flex()
237                    .w_full()
238                    .h_12()
239                    .px_4()
240                    .my_4()
241                    .border_2()
242                    .border_color(theme.colors().border)
243                    .child(self.filter_editor.clone()),
244            )
245            .child(
246                Table::new()
247                    .interactable(&self.table_interaction_state)
248                    .striped()
249                    .column_widths([rems(24.), rems(16.), rems(32.), rems(8.)])
250                    .header(["Command", "Keystrokes", "Context", "Source"])
251                    .uniform_list(
252                        "keymap-editor-table",
253                        row_count,
254                        cx.processor(move |this, range: Range<usize>, _window, _cx| {
255                            range
256                                .filter_map(|index| {
257                                    let candidate_id = this.matches.get(index)?.candidate_id;
258                                    let binding = &this.keybindings[candidate_id];
259                                    Some(
260                                        [
261                                            binding.action.clone(),
262                                            binding.keystroke_text.clone(),
263                                            binding.context.clone(),
264                                            binding.source.clone().unwrap_or_default(),
265                                        ]
266                                        .map(IntoElement::into_any_element),
267                                    )
268                                })
269                                .collect()
270                        }),
271                    ),
272            )
273    }
274}
275
276impl SerializableItem for KeymapEditor {
277    fn serialized_item_kind() -> &'static str {
278        "KeymapEditor"
279    }
280
281    fn cleanup(
282        workspace_id: workspace::WorkspaceId,
283        alive_items: Vec<workspace::ItemId>,
284        _window: &mut Window,
285        cx: &mut App,
286    ) -> gpui::Task<gpui::Result<()>> {
287        workspace::delete_unloaded_items(
288            alive_items,
289            workspace_id,
290            "keybinding_editors",
291            &KEYBINDING_EDITORS,
292            cx,
293        )
294    }
295
296    fn deserialize(
297        _project: gpui::Entity<project::Project>,
298        _workspace: gpui::WeakEntity<Workspace>,
299        workspace_id: workspace::WorkspaceId,
300        item_id: workspace::ItemId,
301        window: &mut Window,
302        cx: &mut App,
303    ) -> gpui::Task<gpui::Result<gpui::Entity<Self>>> {
304        window.spawn(cx, async move |cx| {
305            if KEYBINDING_EDITORS
306                .get_keybinding_editor(item_id, workspace_id)?
307                .is_some()
308            {
309                cx.update(|window, cx| cx.new(|cx| KeymapEditor::new(window, cx)))
310            } else {
311                Err(anyhow!("No keybinding editor to deserialize"))
312            }
313        })
314    }
315
316    fn serialize(
317        &mut self,
318        workspace: &mut Workspace,
319        item_id: workspace::ItemId,
320        _closing: bool,
321        _window: &mut Window,
322        cx: &mut ui::Context<Self>,
323    ) -> Option<gpui::Task<gpui::Result<()>>> {
324        let workspace_id = workspace.database_id()?;
325        Some(cx.background_spawn(async move {
326            KEYBINDING_EDITORS
327                .save_keybinding_editor(item_id, workspace_id)
328                .await
329        }))
330    }
331
332    fn should_serialize(&self, _event: &Self::Event) -> bool {
333        false
334    }
335}
336
337mod persistence {
338    use db::{define_connection, query, sqlez_macros::sql};
339    use workspace::WorkspaceDb;
340
341    define_connection! {
342        pub static ref KEYBINDING_EDITORS: KeybindingEditorDb<WorkspaceDb> =
343            &[sql!(
344                CREATE TABLE keybinding_editors (
345                    workspace_id INTEGER,
346                    item_id INTEGER UNIQUE,
347
348                    PRIMARY KEY(workspace_id, item_id),
349                    FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
350                    ON DELETE CASCADE
351                ) STRICT;
352            )];
353    }
354
355    impl KeybindingEditorDb {
356        query! {
357            pub async fn save_keybinding_editor(
358                item_id: workspace::ItemId,
359                workspace_id: workspace::WorkspaceId
360            ) -> Result<()> {
361                INSERT OR REPLACE INTO keybinding_editors(item_id, workspace_id)
362                VALUES (?, ?)
363            }
364        }
365
366        query! {
367            pub fn get_keybinding_editor(
368                item_id: workspace::ItemId,
369                workspace_id: workspace::WorkspaceId
370            ) -> Result<Option<workspace::ItemId>> {
371                SELECT item_id
372                FROM keybinding_editors
373                WHERE item_id = ? AND workspace_id = ?
374            }
375        }
376    }
377}