1use std::{fmt::Write as _, ops::Range, sync::Arc};
2
3use collections::HashSet;
4use db::anyhow::anyhow;
5use editor::{Editor, EditorEvent};
6use fuzzy::{StringMatch, StringMatchCandidate};
7use gpui::{
8 AppContext as _, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
9 FontWeight, Global, KeyContext, ScrollStrategy, Subscription, WeakEntity, actions, div,
10};
11use util::ResultExt;
12
13use ui::{
14 ActiveTheme as _, App, BorrowAppContext, ParentElement as _, Render, SharedString, Styled as _,
15 Window, prelude::*,
16};
17use workspace::{Item, ModalView, SerializableItem, Workspace, register_serializable_item};
18
19use crate::{
20 keybindings::persistence::KEYBINDING_EDITORS,
21 ui_components::table::{Table, TableInteractionState},
22};
23
24actions!(zed, [OpenKeymapEditor]);
25
26pub fn init(cx: &mut App) {
27 let keymap_event_channel = KeymapEventChannel::new();
28 cx.set_global(keymap_event_channel);
29
30 cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
31 workspace.register_action(|workspace, _: &OpenKeymapEditor, window, cx| {
32 let open_keymap_editor =
33 cx.new(|cx| KeymapEditor::new(workspace.weak_handle(), window, cx));
34 workspace.add_item_to_center(Box::new(open_keymap_editor), window, cx);
35 });
36 })
37 .detach();
38
39 register_serializable_item::<KeymapEditor>(cx);
40}
41
42pub struct KeymapEventChannel {}
43
44impl Global for KeymapEventChannel {}
45
46impl KeymapEventChannel {
47 fn new() -> Self {
48 Self {}
49 }
50
51 pub fn trigger_keymap_changed(cx: &mut App) {
52 cx.update_global(|_event_channel: &mut Self, _| {
53 /* triggers observers in KeymapEditors */
54 });
55 }
56}
57
58struct KeymapEditor {
59 workspace: WeakEntity<Workspace>,
60 focus_handle: FocusHandle,
61 _keymap_subscription: Subscription,
62 keybindings: Vec<ProcessedKeybinding>,
63 // corresponds 1 to 1 with keybindings
64 string_match_candidates: Arc<Vec<StringMatchCandidate>>,
65 matches: Vec<StringMatch>,
66 table_interaction_state: Entity<TableInteractionState>,
67 filter_editor: Entity<Editor>,
68 selected_index: Option<usize>,
69}
70
71impl EventEmitter<()> for KeymapEditor {}
72
73impl Focusable for KeymapEditor {
74 fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
75 return self.filter_editor.focus_handle(cx);
76 }
77}
78
79impl KeymapEditor {
80 fn new(workspace: WeakEntity<Workspace>, window: &mut Window, cx: &mut Context<Self>) -> Self {
81 let focus_handle = cx.focus_handle();
82
83 let _keymap_subscription =
84 cx.observe_global::<KeymapEventChannel>(Self::update_keybindings);
85 let table_interaction_state = TableInteractionState::new(window, cx);
86
87 let filter_editor = cx.new(|cx| {
88 let mut editor = Editor::single_line(window, cx);
89 editor.set_placeholder_text("Filter action names...", cx);
90 editor
91 });
92
93 cx.subscribe(&filter_editor, |this, _, e: &EditorEvent, cx| {
94 if !matches!(e, EditorEvent::BufferEdited) {
95 return;
96 }
97
98 this.update_matches(cx);
99 })
100 .detach();
101
102 let mut this = Self {
103 workspace,
104 keybindings: vec![],
105 string_match_candidates: Arc::new(vec![]),
106 matches: vec![],
107 focus_handle: focus_handle.clone(),
108 _keymap_subscription,
109 table_interaction_state,
110 filter_editor,
111 selected_index: None,
112 };
113
114 this.update_keybindings(cx);
115
116 this
117 }
118
119 fn update_matches(&mut self, cx: &mut Context<Self>) {
120 let query = self.filter_editor.read(cx).text(cx);
121 let string_match_candidates = self.string_match_candidates.clone();
122 let executor = cx.background_executor().clone();
123 let keybind_count = self.keybindings.len();
124 let query = command_palette::normalize_action_query(&query);
125 let fuzzy_match = cx.background_spawn(async move {
126 fuzzy::match_strings(
127 &string_match_candidates,
128 &query,
129 true,
130 true,
131 keybind_count,
132 &Default::default(),
133 executor,
134 )
135 .await
136 });
137
138 cx.spawn(async move |this, cx| {
139 let matches = fuzzy_match.await;
140 this.update(cx, |this, cx| {
141 this.selected_index.take();
142 this.scroll_to_item(0, ScrollStrategy::Top, cx);
143 this.matches = matches;
144 cx.notify();
145 })
146 })
147 .detach();
148 }
149
150 fn process_bindings(
151 cx: &mut Context<Self>,
152 ) -> (Vec<ProcessedKeybinding>, Vec<StringMatchCandidate>) {
153 let key_bindings_ptr = cx.key_bindings();
154 let lock = key_bindings_ptr.borrow();
155 let key_bindings = lock.bindings();
156 let mut unmapped_action_names = HashSet::from_iter(cx.all_action_names());
157
158 let mut processed_bindings = Vec::new();
159 let mut string_match_candidates = Vec::new();
160
161 for key_binding in key_bindings {
162 let mut keystroke_text = String::new();
163 for keystroke in key_binding.keystrokes() {
164 write!(&mut keystroke_text, "{} ", keystroke.unparse()).ok();
165 }
166 let keystroke_text = keystroke_text.trim().to_string();
167
168 let context = key_binding
169 .predicate()
170 .map(|predicate| predicate.to_string())
171 .unwrap_or_else(|| "<global>".to_string());
172
173 let source = key_binding
174 .meta()
175 .map(|meta| settings::KeybindSource::from_meta(meta).name().into());
176
177 let action_name = key_binding.action().name();
178 unmapped_action_names.remove(&action_name);
179
180 let index = processed_bindings.len();
181 let string_match_candidate = StringMatchCandidate::new(index, &action_name);
182 processed_bindings.push(ProcessedKeybinding {
183 keystroke_text: keystroke_text.into(),
184 action: action_name.into(),
185 action_input: key_binding.action_input(),
186 context: context.into(),
187 source,
188 });
189 string_match_candidates.push(string_match_candidate);
190 }
191
192 let empty = SharedString::new_static("");
193 for action_name in unmapped_action_names.into_iter() {
194 let index = processed_bindings.len();
195 let string_match_candidate = StringMatchCandidate::new(index, &action_name);
196 processed_bindings.push(ProcessedKeybinding {
197 keystroke_text: empty.clone(),
198 action: (*action_name).into(),
199 action_input: None,
200 context: empty.clone(),
201 source: None,
202 });
203 string_match_candidates.push(string_match_candidate);
204 }
205
206 (processed_bindings, string_match_candidates)
207 }
208
209 fn update_keybindings(self: &mut KeymapEditor, cx: &mut Context<KeymapEditor>) {
210 let (key_bindings, string_match_candidates) = Self::process_bindings(cx);
211 self.keybindings = key_bindings;
212 self.string_match_candidates = Arc::new(string_match_candidates);
213 self.matches = self
214 .string_match_candidates
215 .iter()
216 .enumerate()
217 .map(|(ix, candidate)| StringMatch {
218 candidate_id: ix,
219 score: 0.0,
220 positions: vec![],
221 string: candidate.string.clone(),
222 })
223 .collect();
224
225 self.update_matches(cx);
226 cx.notify();
227 }
228
229 fn dispatch_context(&self, _window: &Window, _cx: &Context<Self>) -> KeyContext {
230 let mut dispatch_context = KeyContext::new_with_defaults();
231 dispatch_context.add("KeymapEditor");
232 dispatch_context.add("menu");
233
234 // todo! track key context in keybind edit modal
235 // let identifier = if self.keymap_editor.focus_handle(cx).is_focused(window) {
236 // "editing"
237 // } else {
238 // "not_editing"
239 // };
240 // dispatch_context.add(identifier);
241
242 dispatch_context
243 }
244
245 fn scroll_to_item(&self, index: usize, strategy: ScrollStrategy, cx: &mut App) {
246 let index = usize::min(index, self.matches.len().saturating_sub(1));
247 self.table_interaction_state.update(cx, |this, _cx| {
248 this.scroll_handle.scroll_to_item(index, strategy);
249 });
250 }
251
252 fn select_next(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) {
253 if let Some(selected) = self.selected_index {
254 let selected = selected + 1;
255 if selected >= self.matches.len() {
256 self.select_last(&Default::default(), window, cx);
257 } else {
258 self.selected_index = Some(selected);
259 self.scroll_to_item(selected, ScrollStrategy::Center, cx);
260 cx.notify();
261 }
262 } else {
263 self.select_first(&Default::default(), window, cx);
264 }
265 }
266
267 fn select_previous(
268 &mut self,
269 _: &menu::SelectPrevious,
270 window: &mut Window,
271 cx: &mut Context<Self>,
272 ) {
273 if let Some(selected) = self.selected_index {
274 if selected == 0 {
275 return;
276 }
277
278 let selected = selected - 1;
279
280 if selected >= self.matches.len() {
281 self.select_last(&Default::default(), window, cx);
282 } else {
283 self.selected_index = Some(selected);
284 self.scroll_to_item(selected, ScrollStrategy::Center, cx);
285 cx.notify();
286 }
287 } else {
288 self.select_last(&Default::default(), window, cx);
289 }
290 }
291
292 fn select_first(
293 &mut self,
294 _: &menu::SelectFirst,
295 _window: &mut Window,
296 cx: &mut Context<Self>,
297 ) {
298 if self.matches.get(0).is_some() {
299 self.selected_index = Some(0);
300 self.scroll_to_item(0, ScrollStrategy::Center, cx);
301 cx.notify();
302 }
303 }
304
305 fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
306 if self.matches.last().is_some() {
307 let index = self.matches.len() - 1;
308 self.selected_index = Some(index);
309 self.scroll_to_item(index, ScrollStrategy::Center, cx);
310 cx.notify();
311 }
312 }
313
314 fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
315 let Some(index) = self.selected_index else {
316 return;
317 };
318 let keybind = self.keybindings[self.matches[index].candidate_id].clone();
319
320 self.edit_keybinding(keybind, window, cx);
321 }
322
323 fn edit_keybinding(
324 &mut self,
325 keybind: ProcessedKeybinding,
326 window: &mut Window,
327 cx: &mut Context<Self>,
328 ) {
329 // todo! how to map keybinds to how to update/edit them
330 _ = keybind;
331 self.workspace
332 .update(cx, |workspace, cx| {
333 workspace.toggle_modal(window, cx, |window, cx| {
334 let modal = KeybindingEditorModal::new(window, cx);
335 window.focus(&modal.focus_handle(cx));
336 modal
337 });
338 })
339 .log_err();
340 }
341
342 fn focus_search(
343 &mut self,
344 _: &search::FocusSearch,
345 window: &mut Window,
346 cx: &mut Context<Self>,
347 ) {
348 if !self
349 .filter_editor
350 .focus_handle(cx)
351 .contains_focused(window, cx)
352 {
353 window.focus(&self.filter_editor.focus_handle(cx));
354 } else {
355 self.filter_editor.update(cx, |editor, cx| {
356 editor.select_all(&Default::default(), window, cx);
357 });
358 }
359 self.selected_index.take();
360 }
361}
362
363#[derive(Clone)]
364struct ProcessedKeybinding {
365 keystroke_text: SharedString,
366 action: SharedString,
367 action_input: Option<SharedString>,
368 context: SharedString,
369 source: Option<SharedString>,
370}
371
372impl Item for KeymapEditor {
373 type Event = ();
374
375 fn tab_content_text(&self, _detail: usize, _cx: &App) -> ui::SharedString {
376 "Keymap Editor".into()
377 }
378}
379
380impl Render for KeymapEditor {
381 fn render(&mut self, window: &mut Window, cx: &mut ui::Context<Self>) -> impl ui::IntoElement {
382 let row_count = self.matches.len();
383 let theme = cx.theme();
384
385 div()
386 .key_context(self.dispatch_context(window, cx))
387 .on_action(cx.listener(Self::select_next))
388 .on_action(cx.listener(Self::select_previous))
389 .on_action(cx.listener(Self::select_first))
390 .on_action(cx.listener(Self::select_last))
391 .on_action(cx.listener(Self::focus_search))
392 .on_action(cx.listener(Self::confirm))
393 .size_full()
394 .bg(theme.colors().editor_background)
395 .id("keymap-editor")
396 .track_focus(&self.focus_handle)
397 .px_4()
398 .v_flex()
399 .pb_4()
400 .child(
401 h_flex()
402 .key_context({
403 let mut context = KeyContext::new_with_defaults();
404 context.add("BufferSearchBar");
405 context
406 })
407 .w_full()
408 .h_12()
409 .px_4()
410 .my_4()
411 .border_2()
412 .border_color(theme.colors().border)
413 .child(self.filter_editor.clone()),
414 )
415 .child(
416 Table::new()
417 .interactable(&self.table_interaction_state)
418 .striped()
419 .column_widths([rems(24.), rems(16.), rems(32.), rems(8.)])
420 .header(["Command", "Keystrokes", "Context", "Source"])
421 .selected_item_index(self.selected_index.clone())
422 .on_click_row(cx.processor(|this, row_index, _window, _cx| {
423 this.selected_index = Some(row_index);
424 }))
425 .uniform_list(
426 "keymap-editor-table",
427 row_count,
428 cx.processor(move |this, range: Range<usize>, _window, _cx| {
429 range
430 .filter_map(|index| {
431 let candidate_id = this.matches.get(index)?.candidate_id;
432 let binding = &this.keybindings[candidate_id];
433 let action = h_flex()
434 .items_start()
435 .gap_1()
436 .child(binding.action.clone())
437 .when_some(
438 binding.action_input.clone(),
439 |this, binding_input| this.child(binding_input),
440 );
441 let keystrokes = binding.keystroke_text.clone();
442 let context = binding.context.clone();
443 let source = binding.source.clone().unwrap_or_default();
444 Some([
445 action.into_any_element(),
446 keystrokes.into_any_element(),
447 context.into_any_element(),
448 source.into_any_element(),
449 ])
450 })
451 .collect()
452 }),
453 ),
454 )
455 }
456}
457
458struct KeybindingEditorModal {
459 keybind_editor: Entity<Editor>,
460}
461
462impl ModalView for KeybindingEditorModal {}
463
464impl EventEmitter<DismissEvent> for KeybindingEditorModal {}
465
466impl Focusable for KeybindingEditorModal {
467 fn focus_handle(&self, cx: &App) -> FocusHandle {
468 self.keybind_editor.focus_handle(cx)
469 }
470}
471
472impl KeybindingEditorModal {
473 pub fn new(window: &mut Window, cx: &mut App) -> Self {
474 let keybind_editor = cx.new(|cx| {
475 let editor = Editor::single_line(window, cx);
476 editor
477 });
478 Self { keybind_editor }
479 }
480}
481
482impl Render for KeybindingEditorModal {
483 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
484 let theme = cx.theme().colors();
485 return v_flex()
486 .items_center()
487 .text_center()
488 .bg(theme.background)
489 .border_color(theme.border)
490 .border_2()
491 .px_4()
492 .py_2()
493 .w(rems(36.))
494 .child(div().text_lg().font_weight(FontWeight::BOLD).child(
495 // todo! better text
496 "Input desired keybinding, then hit Enter to save",
497 ))
498 .child(
499 h_flex()
500 .w_full()
501 .h_12()
502 .px_4()
503 .my_4()
504 .border_2()
505 .border_color(theme.border)
506 .child(self.keybind_editor.clone()),
507 );
508 }
509}
510
511impl SerializableItem for KeymapEditor {
512 fn serialized_item_kind() -> &'static str {
513 "KeymapEditor"
514 }
515
516 fn cleanup(
517 workspace_id: workspace::WorkspaceId,
518 alive_items: Vec<workspace::ItemId>,
519 _window: &mut Window,
520 cx: &mut App,
521 ) -> gpui::Task<gpui::Result<()>> {
522 workspace::delete_unloaded_items(
523 alive_items,
524 workspace_id,
525 "keybinding_editors",
526 &KEYBINDING_EDITORS,
527 cx,
528 )
529 }
530
531 fn deserialize(
532 _project: Entity<project::Project>,
533 workspace: WeakEntity<Workspace>,
534 workspace_id: workspace::WorkspaceId,
535 item_id: workspace::ItemId,
536 window: &mut Window,
537 cx: &mut App,
538 ) -> gpui::Task<gpui::Result<Entity<Self>>> {
539 window.spawn(cx, async move |cx| {
540 if KEYBINDING_EDITORS
541 .get_keybinding_editor(item_id, workspace_id)?
542 .is_some()
543 {
544 cx.update(|window, cx| cx.new(|cx| KeymapEditor::new(workspace, window, cx)))
545 } else {
546 Err(anyhow!("No keybinding editor to deserialize"))
547 }
548 })
549 }
550
551 fn serialize(
552 &mut self,
553 workspace: &mut Workspace,
554 item_id: workspace::ItemId,
555 _closing: bool,
556 _window: &mut Window,
557 cx: &mut ui::Context<Self>,
558 ) -> Option<gpui::Task<gpui::Result<()>>> {
559 let workspace_id = workspace.database_id()?;
560 Some(cx.background_spawn(async move {
561 KEYBINDING_EDITORS
562 .save_keybinding_editor(item_id, workspace_id)
563 .await
564 }))
565 }
566
567 fn should_serialize(&self, _event: &Self::Event) -> bool {
568 false
569 }
570}
571
572mod persistence {
573 use db::{define_connection, query, sqlez_macros::sql};
574 use workspace::WorkspaceDb;
575
576 define_connection! {
577 pub static ref KEYBINDING_EDITORS: KeybindingEditorDb<WorkspaceDb> =
578 &[sql!(
579 CREATE TABLE keybinding_editors (
580 workspace_id INTEGER,
581 item_id INTEGER UNIQUE,
582
583 PRIMARY KEY(workspace_id, item_id),
584 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
585 ON DELETE CASCADE
586 ) STRICT;
587 )];
588 }
589
590 impl KeybindingEditorDb {
591 query! {
592 pub async fn save_keybinding_editor(
593 item_id: workspace::ItemId,
594 workspace_id: workspace::WorkspaceId
595 ) -> Result<()> {
596 INSERT OR REPLACE INTO keybinding_editors(item_id, workspace_id)
597 VALUES (?, ?)
598 }
599 }
600
601 query! {
602 pub fn get_keybinding_editor(
603 item_id: workspace::ItemId,
604 workspace_id: workspace::WorkspaceId
605 ) -> Result<Option<workspace::ItemId>> {
606 SELECT item_id
607 FROM keybinding_editors
608 WHERE item_id = ? AND workspace_id = ?
609 }
610 }
611 }
612}