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 = dbg!(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        dbg!(&query);
119        let fuzzy_match = cx.background_spawn(async move {
120            fuzzy::match_strings(
121                &string_match_candidates,
122                &query,
123                true,
124                true,
125                keybind_count,
126                &Default::default(),
127                executor,
128            )
129            .await
130        });
131
132        cx.spawn(async move |this, cx| {
133            let matches = fuzzy_match.await;
134            dbg!(&matches);
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
208#[derive(Clone)]
209struct ProcessedKeybinding {
210    keystroke_text: SharedString,
211    action: SharedString,
212    context: SharedString,
213    source: Option<SharedString>,
214}
215
216impl Item for KeymapEditor {
217    type Event = ();
218
219    fn tab_content_text(&self, _detail: usize, _cx: &App) -> ui::SharedString {
220        "Keymap Editor".into()
221    }
222}
223
224impl Render for KeymapEditor {
225    fn render(&mut self, _window: &mut Window, cx: &mut ui::Context<Self>) -> impl ui::IntoElement {
226        let row_count = self.matches.len();
227        let theme = cx.theme();
228
229        dbg!(&self.matches);
230
231        div()
232            .size_full()
233            .bg(theme.colors().background)
234            .id("keymap-editor")
235            .track_focus(&self.focus_handle)
236            .child(self.filter_editor.clone())
237            .child(
238                Table::new()
239                    .interactable(&self.table_interaction_state)
240                    .striped()
241                    .column_widths([rems(24.), rems(16.), rems(32.), rems(8.)])
242                    .header(["Command", "Keystrokes", "Context", "Source"])
243                    .uniform_list(
244                        "keymap-editor-table",
245                        row_count,
246                        cx.processor(move |this, range: Range<usize>, _window, _cx| {
247                            range
248                                .filter_map(|index| {
249                                    dbg!(index);
250                                    let candidate_id = this.matches.get(index)?.candidate_id;
251                                    let binding = &this.keybindings[candidate_id];
252                                    Some(
253                                        [
254                                            binding.action.clone(),
255                                            binding.keystroke_text.clone(),
256                                            binding.context.clone(),
257                                            binding.source.clone().unwrap_or_default(),
258                                        ]
259                                        .map(IntoElement::into_any_element),
260                                    )
261                                })
262                                .collect()
263                        }),
264                    ),
265            )
266    }
267}
268
269impl SerializableItem for KeymapEditor {
270    fn serialized_item_kind() -> &'static str {
271        "KeymapEditor"
272    }
273
274    fn cleanup(
275        workspace_id: workspace::WorkspaceId,
276        alive_items: Vec<workspace::ItemId>,
277        _window: &mut Window,
278        cx: &mut App,
279    ) -> gpui::Task<gpui::Result<()>> {
280        workspace::delete_unloaded_items(
281            alive_items,
282            workspace_id,
283            "keybinding_editors",
284            &KEYBINDING_EDITORS,
285            cx,
286        )
287    }
288
289    fn deserialize(
290        _project: gpui::Entity<project::Project>,
291        _workspace: gpui::WeakEntity<Workspace>,
292        workspace_id: workspace::WorkspaceId,
293        item_id: workspace::ItemId,
294        window: &mut Window,
295        cx: &mut App,
296    ) -> gpui::Task<gpui::Result<gpui::Entity<Self>>> {
297        window.spawn(cx, async move |cx| {
298            if KEYBINDING_EDITORS
299                .get_keybinding_editor(item_id, workspace_id)?
300                .is_some()
301            {
302                cx.update(|window, cx| cx.new(|cx| KeymapEditor::new(window, cx)))
303            } else {
304                Err(anyhow!("No keybinding editor to deserialize"))
305            }
306        })
307    }
308
309    fn serialize(
310        &mut self,
311        workspace: &mut Workspace,
312        item_id: workspace::ItemId,
313        _closing: bool,
314        _window: &mut Window,
315        cx: &mut ui::Context<Self>,
316    ) -> Option<gpui::Task<gpui::Result<()>>> {
317        let workspace_id = workspace.database_id()?;
318        Some(cx.background_spawn(async move {
319            KEYBINDING_EDITORS
320                .save_keybinding_editor(item_id, workspace_id)
321                .await
322        }))
323    }
324
325    fn should_serialize(&self, _event: &Self::Event) -> bool {
326        false
327    }
328}
329
330mod persistence {
331    use db::{define_connection, query, sqlez_macros::sql};
332    use workspace::WorkspaceDb;
333
334    define_connection! {
335        pub static ref KEYBINDING_EDITORS: KeybindingEditorDb<WorkspaceDb> =
336            &[sql!(
337                CREATE TABLE keybinding_editors (
338                    workspace_id INTEGER,
339                    item_id INTEGER UNIQUE,
340
341                    PRIMARY KEY(workspace_id, item_id),
342                    FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
343                    ON DELETE CASCADE
344                ) STRICT;
345            )];
346    }
347
348    impl KeybindingEditorDb {
349        query! {
350            pub async fn save_keybinding_editor(
351                item_id: workspace::ItemId,
352                workspace_id: workspace::WorkspaceId
353            ) -> Result<()> {
354                INSERT OR REPLACE INTO keybinding_editors(item_id, workspace_id)
355                VALUES (?, ?)
356            }
357        }
358
359        query! {
360            pub fn get_keybinding_editor(
361                item_id: workspace::ItemId,
362                workspace_id: workspace::WorkspaceId
363            ) -> Result<Option<workspace::ItemId>> {
364                SELECT item_id
365                FROM keybinding_editors
366                WHERE item_id = ? AND workspace_id = ?
367            }
368        }
369    }
370}