1use std::{ops::Range, sync::Arc};
2
3use collections::HashSet;
4use db::anyhow::anyhow;
5use editor::{Editor, EditorEvent};
6use feature_flags::FeatureFlagViewExt;
7use fs::Fs;
8use fuzzy::{StringMatch, StringMatchCandidate};
9use gpui::{
10 AppContext as _, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
11 FontWeight, Global, KeyContext, Keystroke, ModifiersChangedEvent, ScrollStrategy, Subscription,
12 WeakEntity, actions, div,
13};
14use settings::KeybindSource;
15use util::ResultExt;
16
17use ui::{
18 ActiveTheme as _, App, BorrowAppContext, ParentElement as _, Render, SharedString, Styled as _,
19 Window, prelude::*,
20};
21use workspace::{Item, ModalView, SerializableItem, Workspace, register_serializable_item};
22
23use crate::{
24 SettingsUiFeatureFlag,
25 keybindings::persistence::KEYBINDING_EDITORS,
26 ui_components::table::{Table, TableInteractionState},
27};
28
29actions!(zed, [OpenKeymapEditor]);
30
31pub fn init(cx: &mut App) {
32 let keymap_event_channel = KeymapEventChannel::new();
33 cx.set_global(keymap_event_channel);
34
35 cx.on_action(|_: &OpenKeymapEditor, cx| {
36 workspace::with_active_or_new_workspace(cx, move |workspace, window, cx| {
37 let existing = workspace
38 .active_pane()
39 .read(cx)
40 .items()
41 .find_map(|item| item.downcast::<KeymapEditor>());
42
43 if let Some(existing) = existing {
44 workspace.activate_item(&existing, true, true, window, cx);
45 } else {
46 let keymap_editor =
47 cx.new(|cx| KeymapEditor::new(workspace.weak_handle(), window, cx));
48 workspace.add_item_to_active_pane(Box::new(keymap_editor), None, true, window, cx);
49 }
50 });
51 });
52
53 cx.observe_new(|_workspace: &mut Workspace, window, cx| {
54 let Some(window) = window else { return };
55
56 let keymap_ui_actions = [std::any::TypeId::of::<OpenKeymapEditor>()];
57
58 command_palette_hooks::CommandPaletteFilter::update_global(cx, |filter, _cx| {
59 filter.hide_action_types(&keymap_ui_actions);
60 });
61
62 cx.observe_flag::<SettingsUiFeatureFlag, _>(
63 window,
64 move |is_enabled, _workspace, _, cx| {
65 if is_enabled {
66 command_palette_hooks::CommandPaletteFilter::update_global(
67 cx,
68 |filter, _cx| {
69 filter.show_action_types(keymap_ui_actions.iter());
70 },
71 );
72 } else {
73 command_palette_hooks::CommandPaletteFilter::update_global(
74 cx,
75 |filter, _cx| {
76 filter.hide_action_types(&keymap_ui_actions);
77 },
78 );
79 }
80 },
81 )
82 .detach();
83 })
84 .detach();
85
86 register_serializable_item::<KeymapEditor>(cx);
87}
88
89pub struct KeymapEventChannel {}
90
91impl Global for KeymapEventChannel {}
92
93impl KeymapEventChannel {
94 fn new() -> Self {
95 Self {}
96 }
97
98 pub fn trigger_keymap_changed(cx: &mut App) {
99 let Some(_event_channel) = cx.try_global::<Self>() else {
100 // don't panic if no global defined. This usually happens in tests
101 return;
102 };
103 cx.update_global(|_event_channel: &mut Self, _| {
104 /* triggers observers in KeymapEditors */
105 });
106 }
107}
108
109struct KeymapEditor {
110 workspace: WeakEntity<Workspace>,
111 focus_handle: FocusHandle,
112 _keymap_subscription: Subscription,
113 keybindings: Vec<ProcessedKeybinding>,
114 // corresponds 1 to 1 with keybindings
115 string_match_candidates: Arc<Vec<StringMatchCandidate>>,
116 matches: Vec<StringMatch>,
117 table_interaction_state: Entity<TableInteractionState>,
118 filter_editor: Entity<Editor>,
119 selected_index: Option<usize>,
120}
121
122impl EventEmitter<()> for KeymapEditor {}
123
124impl Focusable for KeymapEditor {
125 fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
126 return self.filter_editor.focus_handle(cx);
127 }
128}
129
130impl KeymapEditor {
131 fn new(workspace: WeakEntity<Workspace>, window: &mut Window, cx: &mut Context<Self>) -> Self {
132 let focus_handle = cx.focus_handle();
133
134 let _keymap_subscription =
135 cx.observe_global::<KeymapEventChannel>(Self::update_keybindings);
136 let table_interaction_state = TableInteractionState::new(window, cx);
137
138 let filter_editor = cx.new(|cx| {
139 let mut editor = Editor::single_line(window, cx);
140 editor.set_placeholder_text("Filter action names...", cx);
141 editor
142 });
143
144 cx.subscribe(&filter_editor, |this, _, e: &EditorEvent, cx| {
145 if !matches!(e, EditorEvent::BufferEdited) {
146 return;
147 }
148
149 this.update_matches(cx);
150 })
151 .detach();
152
153 let mut this = Self {
154 workspace,
155 keybindings: vec![],
156 string_match_candidates: Arc::new(vec![]),
157 matches: vec![],
158 focus_handle: focus_handle.clone(),
159 _keymap_subscription,
160 table_interaction_state,
161 filter_editor,
162 selected_index: None,
163 };
164
165 this.update_keybindings(cx);
166
167 this
168 }
169
170 fn update_matches(&mut self, cx: &mut Context<Self>) {
171 let query = self.filter_editor.read(cx).text(cx);
172 let string_match_candidates = self.string_match_candidates.clone();
173 let executor = cx.background_executor().clone();
174 let keybind_count = self.keybindings.len();
175 let query = command_palette::normalize_action_query(&query);
176 let fuzzy_match = cx.background_spawn(async move {
177 fuzzy::match_strings(
178 &string_match_candidates,
179 &query,
180 true,
181 true,
182 keybind_count,
183 &Default::default(),
184 executor,
185 )
186 .await
187 });
188
189 cx.spawn(async move |this, cx| {
190 let matches = fuzzy_match.await;
191 this.update(cx, |this, cx| {
192 this.selected_index.take();
193 this.scroll_to_item(0, ScrollStrategy::Top, cx);
194 this.matches = matches;
195 cx.notify();
196 })
197 })
198 .detach();
199 }
200
201 fn process_bindings(
202 cx: &mut Context<Self>,
203 ) -> (Vec<ProcessedKeybinding>, Vec<StringMatchCandidate>) {
204 let key_bindings_ptr = cx.key_bindings();
205 let lock = key_bindings_ptr.borrow();
206 let key_bindings = lock.bindings();
207 let mut unmapped_action_names = HashSet::from_iter(cx.all_action_names());
208
209 let mut processed_bindings = Vec::new();
210 let mut string_match_candidates = Vec::new();
211
212 for key_binding in key_bindings {
213 let source = key_binding.meta().map(settings::KeybindSource::from_meta);
214
215 let keystroke_text = ui::text_for_keystrokes(key_binding.keystrokes(), cx);
216 let ui_key_binding = Some(
217 ui::KeyBinding::new(key_binding.clone(), cx)
218 .vim_mode(source == Some(settings::KeybindSource::Vim)),
219 );
220
221 let context = key_binding
222 .predicate()
223 .map(|predicate| predicate.to_string())
224 .unwrap_or_else(|| "<global>".to_string());
225
226 let source = source.map(|source| (source, source.name().into()));
227
228 let action_name = key_binding.action().name();
229 unmapped_action_names.remove(&action_name);
230
231 let index = processed_bindings.len();
232 let string_match_candidate = StringMatchCandidate::new(index, &action_name);
233 processed_bindings.push(ProcessedKeybinding {
234 keystroke_text: keystroke_text.into(),
235 ui_key_binding,
236 action: action_name.into(),
237 action_input: key_binding.action_input(),
238 context: context.into(),
239 source,
240 });
241 string_match_candidates.push(string_match_candidate);
242 }
243
244 let empty = SharedString::new_static("");
245 for action_name in unmapped_action_names.into_iter() {
246 let index = processed_bindings.len();
247 let string_match_candidate = StringMatchCandidate::new(index, &action_name);
248 processed_bindings.push(ProcessedKeybinding {
249 keystroke_text: empty.clone(),
250 ui_key_binding: None,
251 action: (*action_name).into(),
252 action_input: None,
253 context: empty.clone(),
254 source: None,
255 });
256 string_match_candidates.push(string_match_candidate);
257 }
258
259 (processed_bindings, string_match_candidates)
260 }
261
262 fn update_keybindings(self: &mut KeymapEditor, cx: &mut Context<KeymapEditor>) {
263 let (key_bindings, string_match_candidates) = Self::process_bindings(cx);
264 self.keybindings = key_bindings;
265 self.string_match_candidates = Arc::new(string_match_candidates);
266 self.matches = self
267 .string_match_candidates
268 .iter()
269 .enumerate()
270 .map(|(ix, candidate)| StringMatch {
271 candidate_id: ix,
272 score: 0.0,
273 positions: vec![],
274 string: candidate.string.clone(),
275 })
276 .collect();
277
278 self.update_matches(cx);
279 cx.notify();
280 }
281
282 fn dispatch_context(&self, _window: &Window, _cx: &Context<Self>) -> KeyContext {
283 let mut dispatch_context = KeyContext::new_with_defaults();
284 dispatch_context.add("KeymapEditor");
285 dispatch_context.add("menu");
286
287 dispatch_context
288 }
289
290 fn scroll_to_item(&self, index: usize, strategy: ScrollStrategy, cx: &mut App) {
291 let index = usize::min(index, self.matches.len().saturating_sub(1));
292 self.table_interaction_state.update(cx, |this, _cx| {
293 this.scroll_handle.scroll_to_item(index, strategy);
294 });
295 }
296
297 fn select_next(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) {
298 if let Some(selected) = self.selected_index {
299 let selected = selected + 1;
300 if selected >= self.matches.len() {
301 self.select_last(&Default::default(), window, cx);
302 } else {
303 self.selected_index = Some(selected);
304 self.scroll_to_item(selected, ScrollStrategy::Center, cx);
305 cx.notify();
306 }
307 } else {
308 self.select_first(&Default::default(), window, cx);
309 }
310 }
311
312 fn select_previous(
313 &mut self,
314 _: &menu::SelectPrevious,
315 window: &mut Window,
316 cx: &mut Context<Self>,
317 ) {
318 if let Some(selected) = self.selected_index {
319 if selected == 0 {
320 return;
321 }
322
323 let selected = selected - 1;
324
325 if selected >= self.matches.len() {
326 self.select_last(&Default::default(), window, cx);
327 } else {
328 self.selected_index = Some(selected);
329 self.scroll_to_item(selected, ScrollStrategy::Center, cx);
330 cx.notify();
331 }
332 } else {
333 self.select_last(&Default::default(), window, cx);
334 }
335 }
336
337 fn select_first(
338 &mut self,
339 _: &menu::SelectFirst,
340 _window: &mut Window,
341 cx: &mut Context<Self>,
342 ) {
343 if self.matches.get(0).is_some() {
344 self.selected_index = Some(0);
345 self.scroll_to_item(0, ScrollStrategy::Center, cx);
346 cx.notify();
347 }
348 }
349
350 fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
351 if self.matches.last().is_some() {
352 let index = self.matches.len() - 1;
353 self.selected_index = Some(index);
354 self.scroll_to_item(index, ScrollStrategy::Center, cx);
355 cx.notify();
356 }
357 }
358
359 fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
360 let Some(index) = self.selected_index else {
361 return;
362 };
363 let keybind = self.keybindings[self.matches[index].candidate_id].clone();
364
365 self.edit_keybinding(keybind, window, cx);
366 }
367
368 fn edit_keybinding(
369 &mut self,
370 keybind: ProcessedKeybinding,
371 window: &mut Window,
372 cx: &mut Context<Self>,
373 ) {
374 self.workspace
375 .update(cx, |workspace, cx| {
376 let fs = workspace.app_state().fs.clone();
377 workspace.toggle_modal(window, cx, |window, cx| {
378 let modal = KeybindingEditorModal::new(keybind, fs, window, cx);
379 window.focus(&modal.focus_handle(cx));
380 modal
381 });
382 })
383 .log_err();
384 }
385
386 fn focus_search(
387 &mut self,
388 _: &search::FocusSearch,
389 window: &mut Window,
390 cx: &mut Context<Self>,
391 ) {
392 if !self
393 .filter_editor
394 .focus_handle(cx)
395 .contains_focused(window, cx)
396 {
397 window.focus(&self.filter_editor.focus_handle(cx));
398 } else {
399 self.filter_editor.update(cx, |editor, cx| {
400 editor.select_all(&Default::default(), window, cx);
401 });
402 }
403 self.selected_index.take();
404 }
405}
406
407#[derive(Clone)]
408struct ProcessedKeybinding {
409 keystroke_text: SharedString,
410 ui_key_binding: Option<ui::KeyBinding>,
411 action: SharedString,
412 action_input: Option<SharedString>,
413 context: SharedString,
414 source: Option<(KeybindSource, SharedString)>,
415}
416
417impl Item for KeymapEditor {
418 type Event = ();
419
420 fn tab_content_text(&self, _detail: usize, _cx: &App) -> ui::SharedString {
421 "Keymap Editor".into()
422 }
423}
424
425impl Render for KeymapEditor {
426 fn render(&mut self, window: &mut Window, cx: &mut ui::Context<Self>) -> impl ui::IntoElement {
427 let row_count = self.matches.len();
428 let theme = cx.theme();
429
430 div()
431 .key_context(self.dispatch_context(window, cx))
432 .on_action(cx.listener(Self::select_next))
433 .on_action(cx.listener(Self::select_previous))
434 .on_action(cx.listener(Self::select_first))
435 .on_action(cx.listener(Self::select_last))
436 .on_action(cx.listener(Self::focus_search))
437 .on_action(cx.listener(Self::confirm))
438 .size_full()
439 .bg(theme.colors().editor_background)
440 .id("keymap-editor")
441 .track_focus(&self.focus_handle)
442 .px_4()
443 .v_flex()
444 .pb_4()
445 .child(
446 h_flex()
447 .key_context({
448 let mut context = KeyContext::new_with_defaults();
449 context.add("BufferSearchBar");
450 context
451 })
452 .w_full()
453 .h_12()
454 .px_4()
455 .my_4()
456 .border_2()
457 .border_color(theme.colors().border)
458 .child(self.filter_editor.clone()),
459 )
460 .child(
461 Table::new()
462 .interactable(&self.table_interaction_state)
463 .striped()
464 .column_widths([rems(24.), rems(16.), rems(32.), rems(8.)])
465 .header(["Command", "Keystrokes", "Context", "Source"])
466 .selected_item_index(self.selected_index)
467 .on_click_row(cx.processor(|this, row_index, _window, _cx| {
468 this.selected_index = Some(row_index);
469 }))
470 .uniform_list(
471 "keymap-editor-table",
472 row_count,
473 cx.processor(move |this, range: Range<usize>, _window, _cx| {
474 range
475 .filter_map(|index| {
476 let candidate_id = this.matches.get(index)?.candidate_id;
477 let binding = &this.keybindings[candidate_id];
478 let action = h_flex()
479 .items_start()
480 .gap_1()
481 .child(binding.action.clone())
482 .when_some(
483 binding.action_input.clone(),
484 |this, binding_input| this.child(binding_input),
485 );
486 let keystrokes = binding.ui_key_binding.clone().map_or(
487 binding.keystroke_text.clone().into_any_element(),
488 IntoElement::into_any_element,
489 );
490 let context = binding.context.clone();
491 let source = binding
492 .source
493 .clone()
494 .map(|(_source, name)| name)
495 .unwrap_or_default();
496 Some([
497 action.into_any_element(),
498 keystrokes,
499 context.into_any_element(),
500 source.into_any_element(),
501 ])
502 })
503 .collect()
504 }),
505 ),
506 )
507 }
508}
509
510struct KeybindingEditorModal {
511 editing_keybind: ProcessedKeybinding,
512 keybind_editor: Entity<KeybindInput>,
513 fs: Arc<dyn Fs>,
514 error: Option<String>,
515}
516
517impl ModalView for KeybindingEditorModal {}
518
519impl EventEmitter<DismissEvent> for KeybindingEditorModal {}
520
521impl Focusable for KeybindingEditorModal {
522 fn focus_handle(&self, cx: &App) -> FocusHandle {
523 self.keybind_editor.focus_handle(cx)
524 }
525}
526
527impl KeybindingEditorModal {
528 pub fn new(
529 editing_keybind: ProcessedKeybinding,
530 fs: Arc<dyn Fs>,
531 _window: &mut Window,
532 cx: &mut App,
533 ) -> Self {
534 let keybind_editor = cx.new(KeybindInput::new);
535 Self {
536 editing_keybind,
537 fs,
538 keybind_editor,
539 error: None,
540 }
541 }
542}
543
544impl Render for KeybindingEditorModal {
545 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
546 let theme = cx.theme().colors();
547 return v_flex()
548 .gap_4()
549 .w(rems(36.))
550 .child(
551 v_flex()
552 .items_center()
553 .text_center()
554 .bg(theme.background)
555 .border_color(theme.border)
556 .border_2()
557 .px_4()
558 .py_2()
559 .w_full()
560 .child(
561 div()
562 .text_lg()
563 .font_weight(FontWeight::BOLD)
564 .child("Input desired keybinding, then hit save"),
565 )
566 .child(
567 h_flex()
568 .w_full()
569 .child(self.keybind_editor.clone())
570 .child(
571 IconButton::new("backspace-btn", ui::IconName::Backspace).on_click(
572 cx.listener(|this, _event, _window, cx| {
573 this.keybind_editor.update(cx, |editor, cx| {
574 editor.keystrokes.pop();
575 cx.notify();
576 })
577 }),
578 ),
579 )
580 .child(IconButton::new("clear-btn", ui::IconName::Eraser).on_click(
581 cx.listener(|this, _event, _window, cx| {
582 this.keybind_editor.update(cx, |editor, cx| {
583 editor.keystrokes.clear();
584 cx.notify();
585 })
586 }),
587 )),
588 )
589 .child(
590 h_flex().w_full().items_center().justify_center().child(
591 Button::new("save-btn", "Save")
592 .label_size(LabelSize::Large)
593 .on_click(cx.listener(|this, _event, _window, cx| {
594 let existing_keybind = this.editing_keybind.clone();
595 let fs = this.fs.clone();
596 let new_keystrokes = this
597 .keybind_editor
598 .read_with(cx, |editor, _| editor.keystrokes.clone());
599 if new_keystrokes.is_empty() {
600 this.error = Some("Keystrokes cannot be empty".to_string());
601 cx.notify();
602 return;
603 }
604 let tab_size =
605 cx.global::<settings::SettingsStore>().json_tab_size();
606 cx.spawn(async move |this, cx| {
607 if let Err(err) = save_keybinding_update(
608 existing_keybind,
609 &new_keystrokes,
610 &fs,
611 tab_size,
612 )
613 .await
614 {
615 this.update(cx, |this, cx| {
616 this.error = Some(err);
617 cx.notify();
618 })
619 .log_err();
620 }
621 })
622 .detach();
623 })),
624 ),
625 ),
626 )
627 .when_some(self.error.clone(), |this, error| {
628 this.child(
629 div()
630 .bg(theme.background)
631 .border_color(theme.border)
632 .border_2()
633 .rounded_md()
634 .child(error),
635 )
636 });
637 }
638}
639
640async fn save_keybinding_update(
641 existing: ProcessedKeybinding,
642 new_keystrokes: &[Keystroke],
643 fs: &Arc<dyn Fs>,
644 tab_size: usize,
645) -> Result<(), String> {
646 let keymap_contents = settings::KeymapFile::load_keymap_file(fs)
647 .await
648 .map_err(|err| format!("Failed to load keymap file: {}", err))?;
649 let existing_keystrokes = existing
650 .ui_key_binding
651 .as_ref()
652 .map(|keybinding| keybinding.key_binding.keystrokes())
653 .unwrap_or_default();
654 let operation = if existing.ui_key_binding.is_some() {
655 settings::KeybindUpdateOperation::Replace {
656 target: settings::KeybindUpdateTarget {
657 context: Some(existing.context.as_ref()).filter(|context| !context.is_empty()),
658 keystrokes: existing_keystrokes,
659 action_name: &existing.action,
660 use_key_equivalents: false,
661 input: existing.action_input.as_ref().map(|input| input.as_ref()),
662 },
663 target_source: existing
664 .source
665 .map(|(source, _name)| source)
666 .unwrap_or(KeybindSource::User),
667 source: settings::KeybindUpdateTarget {
668 context: Some(existing.context.as_ref()).filter(|context| !context.is_empty()),
669 keystrokes: new_keystrokes,
670 action_name: &existing.action,
671 use_key_equivalents: false,
672 input: existing.action_input.as_ref().map(|input| input.as_ref()),
673 },
674 }
675 } else {
676 return Err(
677 "Not Implemented: Creating new bindings from unbound actions is not supported yet."
678 .to_string(),
679 );
680 };
681 let updated_keymap_contents =
682 settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size)
683 .map_err(|err| format!("Failed to update keybinding: {}", err))?;
684 fs.atomic_write(paths::keymap_file().clone(), updated_keymap_contents)
685 .await
686 .map_err(|err| format!("Failed to write keymap file: {}", err))?;
687 Ok(())
688}
689
690struct KeybindInput {
691 keystrokes: Vec<Keystroke>,
692 focus_handle: FocusHandle,
693}
694
695impl KeybindInput {
696 fn new(cx: &mut Context<Self>) -> Self {
697 let focus_handle = cx.focus_handle();
698 Self {
699 keystrokes: Vec::new(),
700 focus_handle,
701 }
702 }
703
704 fn on_modifiers_changed(
705 &mut self,
706 event: &ModifiersChangedEvent,
707 _window: &mut Window,
708 cx: &mut Context<Self>,
709 ) {
710 if let Some(last) = self.keystrokes.last_mut()
711 && last.key.is_empty()
712 {
713 if !event.modifiers.modified() {
714 self.keystrokes.pop();
715 } else {
716 last.modifiers = event.modifiers;
717 }
718 } else {
719 self.keystrokes.push(Keystroke {
720 modifiers: event.modifiers,
721 key: "".to_string(),
722 key_char: None,
723 });
724 }
725 cx.stop_propagation();
726 cx.notify();
727 }
728
729 fn on_key_down(
730 &mut self,
731 event: &gpui::KeyDownEvent,
732 _window: &mut Window,
733 cx: &mut Context<Self>,
734 ) {
735 if event.is_held {
736 return;
737 }
738 if let Some(last) = self.keystrokes.last_mut()
739 && last.key.is_empty()
740 {
741 *last = event.keystroke.clone();
742 } else {
743 self.keystrokes.push(event.keystroke.clone());
744 }
745 cx.stop_propagation();
746 cx.notify();
747 }
748
749 fn on_key_up(
750 &mut self,
751 event: &gpui::KeyUpEvent,
752 _window: &mut Window,
753 cx: &mut Context<Self>,
754 ) {
755 if let Some(last) = self.keystrokes.last_mut()
756 && !last.key.is_empty()
757 && last.modifiers == event.keystroke.modifiers
758 {
759 self.keystrokes.push(Keystroke {
760 modifiers: event.keystroke.modifiers,
761 key: "".to_string(),
762 key_char: None,
763 });
764 }
765 cx.stop_propagation();
766 cx.notify();
767 }
768}
769
770impl Focusable for KeybindInput {
771 fn focus_handle(&self, _cx: &App) -> FocusHandle {
772 self.focus_handle.clone()
773 }
774}
775
776impl Render for KeybindInput {
777 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
778 let colors = cx.theme().colors();
779 return div()
780 .track_focus(&self.focus_handle)
781 .on_modifiers_changed(cx.listener(Self::on_modifiers_changed))
782 .on_key_down(cx.listener(Self::on_key_down))
783 .on_key_up(cx.listener(Self::on_key_up))
784 .focus(|mut style| {
785 style.border_color = Some(colors.border_focused);
786 style
787 })
788 .h_12()
789 .w_full()
790 .bg(colors.editor_background)
791 .border_2()
792 .border_color(colors.border)
793 .p_4()
794 .flex_row()
795 .text_center()
796 .justify_center()
797 .child(ui::text_for_keystrokes(&self.keystrokes, cx));
798 }
799}
800
801impl SerializableItem for KeymapEditor {
802 fn serialized_item_kind() -> &'static str {
803 "KeymapEditor"
804 }
805
806 fn cleanup(
807 workspace_id: workspace::WorkspaceId,
808 alive_items: Vec<workspace::ItemId>,
809 _window: &mut Window,
810 cx: &mut App,
811 ) -> gpui::Task<gpui::Result<()>> {
812 workspace::delete_unloaded_items(
813 alive_items,
814 workspace_id,
815 "keybinding_editors",
816 &KEYBINDING_EDITORS,
817 cx,
818 )
819 }
820
821 fn deserialize(
822 _project: Entity<project::Project>,
823 workspace: WeakEntity<Workspace>,
824 workspace_id: workspace::WorkspaceId,
825 item_id: workspace::ItemId,
826 window: &mut Window,
827 cx: &mut App,
828 ) -> gpui::Task<gpui::Result<Entity<Self>>> {
829 window.spawn(cx, async move |cx| {
830 if KEYBINDING_EDITORS
831 .get_keybinding_editor(item_id, workspace_id)?
832 .is_some()
833 {
834 cx.update(|window, cx| cx.new(|cx| KeymapEditor::new(workspace, window, cx)))
835 } else {
836 Err(anyhow!("No keybinding editor to deserialize"))
837 }
838 })
839 }
840
841 fn serialize(
842 &mut self,
843 workspace: &mut Workspace,
844 item_id: workspace::ItemId,
845 _closing: bool,
846 _window: &mut Window,
847 cx: &mut ui::Context<Self>,
848 ) -> Option<gpui::Task<gpui::Result<()>>> {
849 let workspace_id = workspace.database_id()?;
850 Some(cx.background_spawn(async move {
851 KEYBINDING_EDITORS
852 .save_keybinding_editor(item_id, workspace_id)
853 .await
854 }))
855 }
856
857 fn should_serialize(&self, _event: &Self::Event) -> bool {
858 false
859 }
860}
861
862mod persistence {
863 use db::{define_connection, query, sqlez_macros::sql};
864 use workspace::WorkspaceDb;
865
866 define_connection! {
867 pub static ref KEYBINDING_EDITORS: KeybindingEditorDb<WorkspaceDb> =
868 &[sql!(
869 CREATE TABLE keybinding_editors (
870 workspace_id INTEGER,
871 item_id INTEGER UNIQUE,
872
873 PRIMARY KEY(workspace_id, item_id),
874 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
875 ON DELETE CASCADE
876 ) STRICT;
877 )];
878 }
879
880 impl KeybindingEditorDb {
881 query! {
882 pub async fn save_keybinding_editor(
883 item_id: workspace::ItemId,
884 workspace_id: workspace::WorkspaceId
885 ) -> Result<()> {
886 INSERT OR REPLACE INTO keybinding_editors(item_id, workspace_id)
887 VALUES (?, ?)
888 }
889 }
890
891 query! {
892 pub fn get_keybinding_editor(
893 item_id: workspace::ItemId,
894 workspace_id: workspace::WorkspaceId
895 ) -> Result<Option<workspace::ItemId>> {
896 SELECT item_id
897 FROM keybinding_editors
898 WHERE item_id = ? AND workspace_id = ?
899 }
900 }
901 }
902}