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