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            .child(self.filter_editor.clone())
233            .child(
234                Table::new()
235                    .interactable(&self.table_interaction_state)
236                    .striped()
237                    .column_widths([rems(24.), rems(16.), rems(32.), rems(8.)])
238                    .header(["Command", "Keystrokes", "Context", "Source"])
239                    .uniform_list(
240                        "keymap-editor-table",
241                        row_count,
242                        cx.processor(move |this, range: Range<usize>, _window, _cx| {
243                            range
244                                .filter_map(|index| {
245                                    let candidate_id = this.matches.get(index)?.candidate_id;
246                                    let binding = &this.keybindings[candidate_id];
247                                    Some(
248                                        [
249                                            binding.action.clone(),
250                                            binding.keystroke_text.clone(),
251                                            binding.context.clone(),
252                                            binding.source.clone().unwrap_or_default(),
253                                        ]
254                                        .map(IntoElement::into_any_element),
255                                    )
256                                })
257                                .collect()
258                        }),
259                    ),
260            )
261    }
262}
263
264impl SerializableItem for KeymapEditor {
265    fn serialized_item_kind() -> &'static str {
266        "KeymapEditor"
267    }
268
269    fn cleanup(
270        workspace_id: workspace::WorkspaceId,
271        alive_items: Vec<workspace::ItemId>,
272        _window: &mut Window,
273        cx: &mut App,
274    ) -> gpui::Task<gpui::Result<()>> {
275        workspace::delete_unloaded_items(
276            alive_items,
277            workspace_id,
278            "keybinding_editors",
279            &KEYBINDING_EDITORS,
280            cx,
281        )
282    }
283
284    fn deserialize(
285        _project: gpui::Entity<project::Project>,
286        _workspace: gpui::WeakEntity<Workspace>,
287        workspace_id: workspace::WorkspaceId,
288        item_id: workspace::ItemId,
289        window: &mut Window,
290        cx: &mut App,
291    ) -> gpui::Task<gpui::Result<gpui::Entity<Self>>> {
292        window.spawn(cx, async move |cx| {
293            if KEYBINDING_EDITORS
294                .get_keybinding_editor(item_id, workspace_id)?
295                .is_some()
296            {
297                cx.update(|window, cx| cx.new(|cx| KeymapEditor::new(window, cx)))
298            } else {
299                Err(anyhow!("No keybinding editor to deserialize"))
300            }
301        })
302    }
303
304    fn serialize(
305        &mut self,
306        workspace: &mut Workspace,
307        item_id: workspace::ItemId,
308        _closing: bool,
309        _window: &mut Window,
310        cx: &mut ui::Context<Self>,
311    ) -> Option<gpui::Task<gpui::Result<()>>> {
312        let workspace_id = workspace.database_id()?;
313        Some(cx.background_spawn(async move {
314            KEYBINDING_EDITORS
315                .save_keybinding_editor(item_id, workspace_id)
316                .await
317        }))
318    }
319
320    fn should_serialize(&self, _event: &Self::Event) -> bool {
321        false
322    }
323}
324
325mod persistence {
326    use db::{define_connection, query, sqlez_macros::sql};
327    use workspace::WorkspaceDb;
328
329    define_connection! {
330        pub static ref KEYBINDING_EDITORS: KeybindingEditorDb<WorkspaceDb> =
331            &[sql!(
332                CREATE TABLE keybinding_editors (
333                    workspace_id INTEGER,
334                    item_id INTEGER UNIQUE,
335
336                    PRIMARY KEY(workspace_id, item_id),
337                    FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
338                    ON DELETE CASCADE
339                ) STRICT;
340            )];
341    }
342
343    impl KeybindingEditorDb {
344        query! {
345            pub async fn save_keybinding_editor(
346                item_id: workspace::ItemId,
347                workspace_id: workspace::WorkspaceId
348            ) -> Result<()> {
349                INSERT OR REPLACE INTO keybinding_editors(item_id, workspace_id)
350                VALUES (?, ?)
351            }
352        }
353
354        query! {
355            pub fn get_keybinding_editor(
356                item_id: workspace::ItemId,
357                workspace_id: workspace::WorkspaceId
358            ) -> Result<Option<workspace::ItemId>> {
359                SELECT item_id
360                FROM keybinding_editors
361                WHERE item_id = ? AND workspace_id = ?
362            }
363        }
364    }
365}