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 Global, KeyContext, Keystroke, ModifiersChangedEvent, ScrollStrategy, StyledText, Subscription,
12 WeakEntity, actions, div, transparent_black,
13};
14use language::{Language, LanguageConfig};
15use settings::KeybindSource;
16
17use util::ResultExt;
18
19use ui::{
20 ActiveTheme as _, App, BorrowAppContext, ContextMenu, ParentElement as _, Render, SharedString,
21 Styled as _, Tooltip, Window, prelude::*, right_click_menu,
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!(
32 zed,
33 [
34 /// Opens the keymap editor.
35 OpenKeymapEditor
36 ]
37);
38
39const KEYMAP_EDITOR_NAMESPACE: &'static str = "keymap_editor";
40actions!(
41 keymap_editor,
42 [
43 /// Edits the selected key binding.
44 EditBinding,
45 /// Copies the action name to clipboard.
46 CopyAction,
47 /// Copies the context predicate to clipboard.
48 CopyContext
49 ]
50);
51
52pub fn init(cx: &mut App) {
53 let keymap_event_channel = KeymapEventChannel::new();
54 cx.set_global(keymap_event_channel);
55
56 cx.on_action(|_: &OpenKeymapEditor, cx| {
57 workspace::with_active_or_new_workspace(cx, move |workspace, window, cx| {
58 let existing = workspace
59 .active_pane()
60 .read(cx)
61 .items()
62 .find_map(|item| item.downcast::<KeymapEditor>());
63
64 if let Some(existing) = existing {
65 workspace.activate_item(&existing, true, true, window, cx);
66 } else {
67 let keymap_editor =
68 cx.new(|cx| KeymapEditor::new(workspace.weak_handle(), window, cx));
69 workspace.add_item_to_active_pane(Box::new(keymap_editor), None, true, window, cx);
70 }
71 });
72 });
73
74 cx.observe_new(|_workspace: &mut Workspace, window, cx| {
75 let Some(window) = window else { return };
76
77 let keymap_ui_actions = [std::any::TypeId::of::<OpenKeymapEditor>()];
78
79 command_palette_hooks::CommandPaletteFilter::update_global(cx, |filter, _cx| {
80 filter.hide_action_types(&keymap_ui_actions);
81 filter.hide_namespace(KEYMAP_EDITOR_NAMESPACE);
82 });
83
84 cx.observe_flag::<SettingsUiFeatureFlag, _>(
85 window,
86 move |is_enabled, _workspace, _, cx| {
87 if is_enabled {
88 command_palette_hooks::CommandPaletteFilter::update_global(
89 cx,
90 |filter, _cx| {
91 filter.show_action_types(keymap_ui_actions.iter());
92 filter.show_namespace(KEYMAP_EDITOR_NAMESPACE);
93 },
94 );
95 } else {
96 command_palette_hooks::CommandPaletteFilter::update_global(
97 cx,
98 |filter, _cx| {
99 filter.hide_action_types(&keymap_ui_actions);
100 filter.hide_namespace(KEYMAP_EDITOR_NAMESPACE);
101 },
102 );
103 }
104 },
105 )
106 .detach();
107 })
108 .detach();
109
110 register_serializable_item::<KeymapEditor>(cx);
111}
112
113pub struct KeymapEventChannel {}
114
115impl Global for KeymapEventChannel {}
116
117impl KeymapEventChannel {
118 fn new() -> Self {
119 Self {}
120 }
121
122 pub fn trigger_keymap_changed(cx: &mut App) {
123 let Some(_event_channel) = cx.try_global::<Self>() else {
124 // don't panic if no global defined. This usually happens in tests
125 return;
126 };
127 cx.update_global(|_event_channel: &mut Self, _| {
128 /* triggers observers in KeymapEditors */
129 });
130 }
131}
132
133struct KeymapEditor {
134 workspace: WeakEntity<Workspace>,
135 focus_handle: FocusHandle,
136 _keymap_subscription: Subscription,
137 keybindings: Vec<ProcessedKeybinding>,
138 // corresponds 1 to 1 with keybindings
139 string_match_candidates: Arc<Vec<StringMatchCandidate>>,
140 matches: Vec<StringMatch>,
141 table_interaction_state: Entity<TableInteractionState>,
142 filter_editor: Entity<Editor>,
143 selected_index: Option<usize>,
144}
145
146impl EventEmitter<()> for KeymapEditor {}
147
148impl Focusable for KeymapEditor {
149 fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
150 return self.filter_editor.focus_handle(cx);
151 }
152}
153
154impl KeymapEditor {
155 fn new(workspace: WeakEntity<Workspace>, window: &mut Window, cx: &mut Context<Self>) -> Self {
156 let focus_handle = cx.focus_handle();
157
158 let _keymap_subscription =
159 cx.observe_global::<KeymapEventChannel>(Self::update_keybindings);
160 let table_interaction_state = TableInteractionState::new(window, cx);
161
162 let filter_editor = cx.new(|cx| {
163 let mut editor = Editor::single_line(window, cx);
164 editor.set_placeholder_text("Filter action names…", cx);
165 editor
166 });
167
168 cx.subscribe(&filter_editor, |this, _, e: &EditorEvent, cx| {
169 if !matches!(e, EditorEvent::BufferEdited) {
170 return;
171 }
172
173 this.update_matches(cx);
174 })
175 .detach();
176
177 let mut this = Self {
178 workspace,
179 keybindings: vec![],
180 string_match_candidates: Arc::new(vec![]),
181 matches: vec![],
182 focus_handle: focus_handle.clone(),
183 _keymap_subscription,
184 table_interaction_state,
185 filter_editor,
186 selected_index: None,
187 };
188
189 this.update_keybindings(cx);
190
191 this
192 }
193
194 fn current_query(&self, cx: &mut Context<Self>) -> String {
195 self.filter_editor.read(cx).text(cx)
196 }
197
198 fn update_matches(&self, cx: &mut Context<Self>) {
199 let query = self.current_query(cx);
200
201 cx.spawn(async move |this, cx| Self::process_query(this, query, cx).await)
202 .detach();
203 }
204
205 async fn process_query(
206 this: WeakEntity<Self>,
207 query: String,
208 cx: &mut AsyncApp,
209 ) -> anyhow::Result<()> {
210 let query = command_palette::normalize_action_query(&query);
211 let (string_match_candidates, keybind_count) = this.read_with(cx, |this, _| {
212 (this.string_match_candidates.clone(), this.keybindings.len())
213 })?;
214 let executor = cx.background_executor().clone();
215 let mut matches = fuzzy::match_strings(
216 &string_match_candidates,
217 &query,
218 true,
219 true,
220 keybind_count,
221 &Default::default(),
222 executor,
223 )
224 .await;
225 this.update(cx, |this, cx| {
226 if query.is_empty() {
227 // apply default sort
228 // sorts by source precedence, and alphabetically by action name within each source
229 matches.sort_by_key(|match_item| {
230 let keybind = &this.keybindings[match_item.candidate_id];
231 let source = keybind.source.as_ref().map(|s| s.0);
232 use KeybindSource::*;
233 let source_precedence = match source {
234 Some(User) => 0,
235 Some(Vim) => 1,
236 Some(Base) => 2,
237 Some(Default) => 3,
238 None => 4,
239 };
240 return (source_precedence, keybind.action.as_ref());
241 });
242 }
243 this.selected_index.take();
244 this.scroll_to_item(0, ScrollStrategy::Top, cx);
245 this.matches = matches;
246 cx.notify();
247 })
248 }
249
250 fn process_bindings(
251 json_language: Arc<Language>,
252 cx: &mut App,
253 ) -> (Vec<ProcessedKeybinding>, Vec<StringMatchCandidate>) {
254 let key_bindings_ptr = cx.key_bindings();
255 let lock = key_bindings_ptr.borrow();
256 let key_bindings = lock.bindings();
257 let mut unmapped_action_names = HashSet::from_iter(cx.all_action_names());
258
259 let mut processed_bindings = Vec::new();
260 let mut string_match_candidates = Vec::new();
261
262 for key_binding in key_bindings {
263 let source = key_binding.meta().map(settings::KeybindSource::from_meta);
264
265 let keystroke_text = ui::text_for_keystrokes(key_binding.keystrokes(), cx);
266 let ui_key_binding = Some(
267 ui::KeyBinding::new_from_gpui(key_binding.clone(), cx)
268 .vim_mode(source == Some(settings::KeybindSource::Vim)),
269 );
270
271 let context = key_binding
272 .predicate()
273 .map(|predicate| KeybindContextString::Local(predicate.to_string().into()))
274 .unwrap_or(KeybindContextString::Global);
275
276 let source = source.map(|source| (source, source.name().into()));
277
278 let action_name = key_binding.action().name();
279 unmapped_action_names.remove(&action_name);
280 let action_input = key_binding
281 .action_input()
282 .map(|input| SyntaxHighlightedText::new(input, json_language.clone()));
283
284 let index = processed_bindings.len();
285 let string_match_candidate = StringMatchCandidate::new(index, &action_name);
286 processed_bindings.push(ProcessedKeybinding {
287 keystroke_text: keystroke_text.into(),
288 ui_key_binding,
289 action: action_name.into(),
290 action_input,
291 context: Some(context),
292 source,
293 });
294 string_match_candidates.push(string_match_candidate);
295 }
296
297 let empty = SharedString::new_static("");
298 for action_name in unmapped_action_names.into_iter() {
299 let index = processed_bindings.len();
300 let string_match_candidate = StringMatchCandidate::new(index, &action_name);
301 processed_bindings.push(ProcessedKeybinding {
302 keystroke_text: empty.clone(),
303 ui_key_binding: None,
304 action: (*action_name).into(),
305 action_input: None,
306 context: None,
307 source: None,
308 });
309 string_match_candidates.push(string_match_candidate);
310 }
311
312 (processed_bindings, string_match_candidates)
313 }
314
315 fn update_keybindings(&mut self, cx: &mut Context<KeymapEditor>) {
316 let workspace = self.workspace.clone();
317 cx.spawn(async move |this, cx| {
318 let json_language = Self::load_json_language(workspace, cx).await;
319
320 let query = this.update(cx, |this, cx| {
321 let (key_bindings, string_match_candidates) =
322 Self::process_bindings(json_language.clone(), cx);
323 this.keybindings = key_bindings;
324 this.string_match_candidates = Arc::new(string_match_candidates);
325 this.matches = this
326 .string_match_candidates
327 .iter()
328 .enumerate()
329 .map(|(ix, candidate)| StringMatch {
330 candidate_id: ix,
331 score: 0.0,
332 positions: vec![],
333 string: candidate.string.clone(),
334 })
335 .collect();
336 this.current_query(cx)
337 })?;
338 // calls cx.notify
339 Self::process_query(this, query, cx).await
340 })
341 .detach_and_log_err(cx);
342 }
343
344 async fn load_json_language(
345 workspace: WeakEntity<Workspace>,
346 cx: &mut AsyncApp,
347 ) -> Arc<Language> {
348 let json_language_task = workspace
349 .read_with(cx, |workspace, cx| {
350 workspace
351 .project()
352 .read(cx)
353 .languages()
354 .language_for_name("JSON")
355 })
356 .context("Failed to load JSON language")
357 .log_err();
358 let json_language = match json_language_task {
359 Some(task) => task.await.context("Failed to load JSON language").log_err(),
360 None => None,
361 };
362 return json_language.unwrap_or_else(|| {
363 Arc::new(Language::new(
364 LanguageConfig {
365 name: "JSON".into(),
366 ..Default::default()
367 },
368 Some(tree_sitter_json::LANGUAGE.into()),
369 ))
370 });
371 }
372
373 fn dispatch_context(&self, _window: &Window, _cx: &Context<Self>) -> KeyContext {
374 let mut dispatch_context = KeyContext::new_with_defaults();
375 dispatch_context.add("KeymapEditor");
376 dispatch_context.add("menu");
377
378 dispatch_context
379 }
380
381 fn scroll_to_item(&self, index: usize, strategy: ScrollStrategy, cx: &mut App) {
382 let index = usize::min(index, self.matches.len().saturating_sub(1));
383 self.table_interaction_state.update(cx, |this, _cx| {
384 this.scroll_handle.scroll_to_item(index, strategy);
385 });
386 }
387
388 fn focus_search(
389 &mut self,
390 _: &search::FocusSearch,
391 window: &mut Window,
392 cx: &mut Context<Self>,
393 ) {
394 if !self
395 .filter_editor
396 .focus_handle(cx)
397 .contains_focused(window, cx)
398 {
399 window.focus(&self.filter_editor.focus_handle(cx));
400 } else {
401 self.filter_editor.update(cx, |editor, cx| {
402 editor.select_all(&Default::default(), window, cx);
403 });
404 }
405 self.selected_index.take();
406 }
407
408 fn selected_binding(&self) -> Option<&ProcessedKeybinding> {
409 self.selected_index
410 .and_then(|match_index| self.matches.get(match_index))
411 .map(|r#match| r#match.candidate_id)
412 .and_then(|keybind_index| self.keybindings.get(keybind_index))
413 }
414
415 fn select_next(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) {
416 if let Some(selected) = self.selected_index {
417 let selected = selected + 1;
418 if selected >= self.matches.len() {
419 self.select_last(&Default::default(), window, cx);
420 } else {
421 self.selected_index = Some(selected);
422 self.scroll_to_item(selected, ScrollStrategy::Center, cx);
423 cx.notify();
424 }
425 } else {
426 self.select_first(&Default::default(), window, cx);
427 }
428 }
429
430 fn select_previous(
431 &mut self,
432 _: &menu::SelectPrevious,
433 window: &mut Window,
434 cx: &mut Context<Self>,
435 ) {
436 if let Some(selected) = self.selected_index {
437 if selected == 0 {
438 return;
439 }
440
441 let selected = selected - 1;
442
443 if selected >= self.matches.len() {
444 self.select_last(&Default::default(), window, cx);
445 } else {
446 self.selected_index = Some(selected);
447 self.scroll_to_item(selected, ScrollStrategy::Center, cx);
448 cx.notify();
449 }
450 } else {
451 self.select_last(&Default::default(), window, cx);
452 }
453 }
454
455 fn select_first(
456 &mut self,
457 _: &menu::SelectFirst,
458 _window: &mut Window,
459 cx: &mut Context<Self>,
460 ) {
461 if self.matches.get(0).is_some() {
462 self.selected_index = Some(0);
463 self.scroll_to_item(0, ScrollStrategy::Center, cx);
464 cx.notify();
465 }
466 }
467
468 fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
469 if self.matches.last().is_some() {
470 let index = self.matches.len() - 1;
471 self.selected_index = Some(index);
472 self.scroll_to_item(index, ScrollStrategy::Center, cx);
473 cx.notify();
474 }
475 }
476
477 fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
478 self.edit_selected_keybinding(window, cx);
479 }
480
481 fn edit_selected_keybinding(&mut self, window: &mut Window, cx: &mut Context<Self>) {
482 let Some(keybind) = self.selected_binding() else {
483 return;
484 };
485 self.workspace
486 .update(cx, |workspace, cx| {
487 let fs = workspace.app_state().fs.clone();
488 workspace.toggle_modal(window, cx, |window, cx| {
489 let modal = KeybindingEditorModal::new(keybind.clone(), fs, window, cx);
490 window.focus(&modal.focus_handle(cx));
491 modal
492 });
493 })
494 .log_err();
495 }
496
497 fn edit_binding(&mut self, _: &EditBinding, window: &mut Window, cx: &mut Context<Self>) {
498 self.edit_selected_keybinding(window, cx);
499 }
500
501 fn copy_context_to_clipboard(
502 &mut self,
503 _: &CopyContext,
504 _window: &mut Window,
505 cx: &mut Context<Self>,
506 ) {
507 let context = self
508 .selected_binding()
509 .and_then(|binding| binding.context.as_ref())
510 .and_then(KeybindContextString::local_str)
511 .map(|context| context.to_string());
512 let Some(context) = context else {
513 return;
514 };
515 cx.write_to_clipboard(gpui::ClipboardItem::new_string(context.clone()));
516 }
517
518 fn copy_action_to_clipboard(
519 &mut self,
520 _: &CopyAction,
521 _window: &mut Window,
522 cx: &mut Context<Self>,
523 ) {
524 let action = self
525 .selected_binding()
526 .map(|binding| binding.action.to_string());
527 let Some(action) = action else {
528 return;
529 };
530 cx.write_to_clipboard(gpui::ClipboardItem::new_string(action.clone()));
531 }
532}
533
534#[derive(Clone)]
535struct ProcessedKeybinding {
536 keystroke_text: SharedString,
537 ui_key_binding: Option<ui::KeyBinding>,
538 action: SharedString,
539 action_input: Option<SyntaxHighlightedText>,
540 context: Option<KeybindContextString>,
541 source: Option<(KeybindSource, SharedString)>,
542}
543
544#[derive(Clone, Debug, IntoElement)]
545enum KeybindContextString {
546 Global,
547 Local(SharedString),
548}
549
550impl KeybindContextString {
551 const GLOBAL: SharedString = SharedString::new_static("<global>");
552
553 pub fn local(&self) -> Option<&SharedString> {
554 match self {
555 KeybindContextString::Global => None,
556 KeybindContextString::Local(name) => Some(name),
557 }
558 }
559
560 pub fn local_str(&self) -> Option<&str> {
561 match self {
562 KeybindContextString::Global => None,
563 KeybindContextString::Local(name) => Some(name),
564 }
565 }
566}
567
568impl RenderOnce for KeybindContextString {
569 fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
570 match self {
571 KeybindContextString::Global => KeybindContextString::GLOBAL.clone(),
572 KeybindContextString::Local(name) => name,
573 }
574 }
575}
576
577impl Item for KeymapEditor {
578 type Event = ();
579
580 fn tab_content_text(&self, _detail: usize, _cx: &App) -> ui::SharedString {
581 "Keymap Editor".into()
582 }
583}
584
585impl Render for KeymapEditor {
586 fn render(&mut self, window: &mut Window, cx: &mut ui::Context<Self>) -> impl ui::IntoElement {
587 let row_count = self.matches.len();
588 let theme = cx.theme();
589
590 v_flex()
591 .id("keymap-editor")
592 .track_focus(&self.focus_handle)
593 .key_context(self.dispatch_context(window, cx))
594 .on_action(cx.listener(Self::select_next))
595 .on_action(cx.listener(Self::select_previous))
596 .on_action(cx.listener(Self::select_first))
597 .on_action(cx.listener(Self::select_last))
598 .on_action(cx.listener(Self::focus_search))
599 .on_action(cx.listener(Self::confirm))
600 .on_action(cx.listener(Self::edit_binding))
601 .on_action(cx.listener(Self::copy_action_to_clipboard))
602 .on_action(cx.listener(Self::copy_context_to_clipboard))
603 .size_full()
604 .p_2()
605 .gap_1()
606 .bg(theme.colors().editor_background)
607 .child(
608 h_flex()
609 .key_context({
610 let mut context = KeyContext::new_with_defaults();
611 context.add("BufferSearchBar");
612 context
613 })
614 .h_8()
615 .pl_2()
616 .pr_1()
617 .py_1()
618 .border_1()
619 .border_color(theme.colors().border)
620 .rounded_lg()
621 .child(self.filter_editor.clone()),
622 )
623 .child(
624 Table::new()
625 .interactable(&self.table_interaction_state)
626 .striped()
627 .column_widths([rems(16.), rems(16.), rems(16.), rems(32.), rems(8.)])
628 .header(["Action", "Arguments", "Keystrokes", "Context", "Source"])
629 .uniform_list(
630 "keymap-editor-table",
631 row_count,
632 cx.processor(move |this, range: Range<usize>, _window, _cx| {
633 range
634 .filter_map(|index| {
635 let candidate_id = this.matches.get(index)?.candidate_id;
636 let binding = &this.keybindings[candidate_id];
637
638 let action = binding.action.clone().into_any_element();
639 let keystrokes = binding.ui_key_binding.clone().map_or(
640 binding.keystroke_text.clone().into_any_element(),
641 IntoElement::into_any_element,
642 );
643 let action_input = binding
644 .action_input
645 .clone()
646 .map_or(gpui::Empty.into_any_element(), |input| {
647 input.into_any_element()
648 });
649 let context = binding
650 .context
651 .clone()
652 .map_or(gpui::Empty.into_any_element(), |context| {
653 context.into_any_element()
654 });
655 let source = binding
656 .source
657 .clone()
658 .map(|(_source, name)| name)
659 .unwrap_or_default()
660 .into_any_element();
661 Some([action, action_input, keystrokes, context, source])
662 })
663 .collect()
664 }),
665 )
666 .map_row(
667 cx.processor(|this, (row_index, row): (usize, Div), _window, cx| {
668 let is_selected = this.selected_index == Some(row_index);
669 let row = row
670 .id(("keymap-table-row", row_index))
671 .on_click(cx.listener(move |this, _event, _window, _cx| {
672 this.selected_index = Some(row_index);
673 }))
674 .border_2()
675 .border_color(transparent_black())
676 .when(is_selected, |row| {
677 row.border_color(cx.theme().colors().panel_focused_border)
678 });
679
680 right_click_menu(("keymap-table-row-menu", row_index))
681 .trigger({
682 let this = cx.weak_entity();
683 move |is_menu_open: bool, _window, cx| {
684 if is_menu_open {
685 this.update(cx, |this, cx| {
686 if this.selected_index != Some(row_index) {
687 this.selected_index = Some(row_index);
688 cx.notify();
689 }
690 })
691 .ok();
692 }
693 row
694 }
695 })
696 .menu({
697 let this = cx.weak_entity();
698 move |window, cx| build_keybind_context_menu(&this, window, cx)
699 })
700 .into_any_element()
701 }),
702 ),
703 )
704 }
705}
706
707#[derive(Debug, Clone, IntoElement)]
708struct SyntaxHighlightedText {
709 text: SharedString,
710 language: Arc<Language>,
711}
712
713impl SyntaxHighlightedText {
714 pub fn new(text: impl Into<SharedString>, language: Arc<Language>) -> Self {
715 Self {
716 text: text.into(),
717 language,
718 }
719 }
720}
721
722impl RenderOnce for SyntaxHighlightedText {
723 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
724 let text_style = window.text_style();
725 let syntax_theme = cx.theme().syntax();
726
727 let text = self.text.clone();
728
729 let highlights = self
730 .language
731 .highlight_text(&text.as_ref().into(), 0..text.len());
732 let mut runs = Vec::with_capacity(highlights.len());
733 let mut offset = 0;
734
735 for (highlight_range, highlight_id) in highlights {
736 // Add un-highlighted text before the current highlight
737 if highlight_range.start > offset {
738 runs.push(text_style.to_run(highlight_range.start - offset));
739 }
740
741 let mut run_style = text_style.clone();
742 if let Some(highlight_style) = highlight_id.style(syntax_theme) {
743 run_style = run_style.highlight(highlight_style);
744 }
745 // add the highlighted range
746 runs.push(run_style.to_run(highlight_range.len()));
747 offset = highlight_range.end;
748 }
749
750 // Add any remaining un-highlighted text
751 if offset < text.len() {
752 runs.push(text_style.to_run(text.len() - offset));
753 }
754
755 return StyledText::new(text).with_runs(runs);
756 }
757}
758
759struct KeybindingEditorModal {
760 editing_keybind: ProcessedKeybinding,
761 keybind_editor: Entity<KeystrokeInput>,
762 fs: Arc<dyn Fs>,
763 error: Option<String>,
764}
765
766impl ModalView for KeybindingEditorModal {}
767
768impl EventEmitter<DismissEvent> for KeybindingEditorModal {}
769
770impl Focusable for KeybindingEditorModal {
771 fn focus_handle(&self, cx: &App) -> FocusHandle {
772 self.keybind_editor.focus_handle(cx)
773 }
774}
775
776impl KeybindingEditorModal {
777 pub fn new(
778 editing_keybind: ProcessedKeybinding,
779 fs: Arc<dyn Fs>,
780 _window: &mut Window,
781 cx: &mut App,
782 ) -> Self {
783 let keybind_editor = cx.new(KeystrokeInput::new);
784 Self {
785 editing_keybind,
786 fs,
787 keybind_editor,
788 error: None,
789 }
790 }
791}
792
793impl Render for KeybindingEditorModal {
794 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
795 let theme = cx.theme().colors();
796
797 return v_flex()
798 .w(rems(34.))
799 .elevation_3(cx)
800 .child(
801 v_flex()
802 .p_3()
803 .gap_2()
804 .child(
805 v_flex().child(Label::new("Edit Keystroke")).child(
806 Label::new(
807 "Input the desired keystroke for the selected action and hit save.",
808 )
809 .color(Color::Muted),
810 ),
811 )
812 .child(self.keybind_editor.clone()),
813 )
814 .child(
815 h_flex()
816 .p_2()
817 .w_full()
818 .gap_1()
819 .justify_end()
820 .border_t_1()
821 .border_color(cx.theme().colors().border_variant)
822 .child(
823 Button::new("cancel", "Cancel")
824 .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
825 )
826 .child(Button::new("save-btn", "Save").on_click(cx.listener(
827 |this, _event, _window, cx| {
828 let existing_keybind = this.editing_keybind.clone();
829 let fs = this.fs.clone();
830 let new_keystrokes = this
831 .keybind_editor
832 .read_with(cx, |editor, _| editor.keystrokes.clone());
833 if new_keystrokes.is_empty() {
834 this.error = Some("Keystrokes cannot be empty".to_string());
835 cx.notify();
836 return;
837 }
838 let tab_size = cx.global::<settings::SettingsStore>().json_tab_size();
839 cx.spawn(async move |this, cx| {
840 if let Err(err) = save_keybinding_update(
841 existing_keybind,
842 &new_keystrokes,
843 &fs,
844 tab_size,
845 )
846 .await
847 {
848 this.update(cx, |this, cx| {
849 this.error = Some(err.to_string());
850 cx.notify();
851 })
852 .log_err();
853 }
854 })
855 .detach();
856 },
857 ))),
858 )
859 .when_some(self.error.clone(), |this, error| {
860 this.child(
861 div()
862 .bg(theme.background)
863 .border_color(theme.border)
864 .border_2()
865 .rounded_md()
866 .child(error),
867 )
868 });
869 }
870}
871
872async fn save_keybinding_update(
873 existing: ProcessedKeybinding,
874 new_keystrokes: &[Keystroke],
875 fs: &Arc<dyn Fs>,
876 tab_size: usize,
877) -> anyhow::Result<()> {
878 let keymap_contents = settings::KeymapFile::load_keymap_file(fs)
879 .await
880 .context("Failed to load keymap file")?;
881
882 let existing_keystrokes = existing
883 .ui_key_binding
884 .as_ref()
885 .map(|keybinding| keybinding.keystrokes.as_slice())
886 .unwrap_or_default();
887
888 let context = existing
889 .context
890 .as_ref()
891 .and_then(KeybindContextString::local_str);
892
893 let input = existing
894 .action_input
895 .as_ref()
896 .map(|input| input.text.as_ref());
897
898 let operation = if existing.ui_key_binding.is_some() {
899 settings::KeybindUpdateOperation::Replace {
900 target: settings::KeybindUpdateTarget {
901 context,
902 keystrokes: existing_keystrokes,
903 action_name: &existing.action,
904 use_key_equivalents: false,
905 input,
906 },
907 target_source: existing
908 .source
909 .map(|(source, _name)| source)
910 .unwrap_or(KeybindSource::User),
911 source: settings::KeybindUpdateTarget {
912 context,
913 keystrokes: new_keystrokes,
914 action_name: &existing.action,
915 use_key_equivalents: false,
916 input,
917 },
918 }
919 } else {
920 anyhow::bail!("Adding new bindings not implemented yet");
921 };
922 let updated_keymap_contents =
923 settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size)
924 .context("Failed to update keybinding")?;
925 fs.atomic_write(paths::keymap_file().clone(), updated_keymap_contents)
926 .await
927 .context("Failed to write keymap file")?;
928 Ok(())
929}
930
931struct KeystrokeInput {
932 keystrokes: Vec<Keystroke>,
933 focus_handle: FocusHandle,
934}
935
936impl KeystrokeInput {
937 fn new(cx: &mut Context<Self>) -> Self {
938 let focus_handle = cx.focus_handle();
939 Self {
940 keystrokes: Vec::new(),
941 focus_handle,
942 }
943 }
944
945 fn on_modifiers_changed(
946 &mut self,
947 event: &ModifiersChangedEvent,
948 _window: &mut Window,
949 cx: &mut Context<Self>,
950 ) {
951 if let Some(last) = self.keystrokes.last_mut()
952 && last.key.is_empty()
953 {
954 if !event.modifiers.modified() {
955 self.keystrokes.pop();
956 } else {
957 last.modifiers = event.modifiers;
958 }
959 } else {
960 self.keystrokes.push(Keystroke {
961 modifiers: event.modifiers,
962 key: "".to_string(),
963 key_char: None,
964 });
965 }
966 cx.stop_propagation();
967 cx.notify();
968 }
969
970 fn on_key_down(
971 &mut self,
972 event: &gpui::KeyDownEvent,
973 _window: &mut Window,
974 cx: &mut Context<Self>,
975 ) {
976 if event.is_held {
977 return;
978 }
979 if let Some(last) = self.keystrokes.last_mut()
980 && last.key.is_empty()
981 {
982 *last = event.keystroke.clone();
983 } else {
984 self.keystrokes.push(event.keystroke.clone());
985 }
986 cx.stop_propagation();
987 cx.notify();
988 }
989
990 fn on_key_up(
991 &mut self,
992 event: &gpui::KeyUpEvent,
993 _window: &mut Window,
994 cx: &mut Context<Self>,
995 ) {
996 if let Some(last) = self.keystrokes.last_mut()
997 && !last.key.is_empty()
998 && last.modifiers == event.keystroke.modifiers
999 {
1000 self.keystrokes.push(Keystroke {
1001 modifiers: event.keystroke.modifiers,
1002 key: "".to_string(),
1003 key_char: None,
1004 });
1005 }
1006 cx.stop_propagation();
1007 cx.notify();
1008 }
1009}
1010
1011impl Focusable for KeystrokeInput {
1012 fn focus_handle(&self, _cx: &App) -> FocusHandle {
1013 self.focus_handle.clone()
1014 }
1015}
1016
1017impl Render for KeystrokeInput {
1018 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1019 let colors = cx.theme().colors();
1020
1021 return h_flex()
1022 .id("keybinding_input")
1023 .track_focus(&self.focus_handle)
1024 .on_modifiers_changed(cx.listener(Self::on_modifiers_changed))
1025 .on_key_down(cx.listener(Self::on_key_down))
1026 .on_key_up(cx.listener(Self::on_key_up))
1027 .focus(|mut style| {
1028 style.border_color = Some(colors.border_focused);
1029 style
1030 })
1031 .py_2()
1032 .px_3()
1033 .gap_2()
1034 .min_h_8()
1035 .w_full()
1036 .justify_between()
1037 .bg(colors.editor_background)
1038 .border_1()
1039 .rounded_md()
1040 .flex_1()
1041 .overflow_hidden()
1042 .child(
1043 h_flex()
1044 .w_full()
1045 .min_w_0()
1046 .justify_center()
1047 .flex_wrap()
1048 .gap(ui::DynamicSpacing::Base04.rems(cx))
1049 .children(self.keystrokes.iter().map(|keystroke| {
1050 h_flex().children(ui::render_keystroke(
1051 keystroke,
1052 None,
1053 Some(rems(0.875).into()),
1054 ui::PlatformStyle::platform(),
1055 false,
1056 ))
1057 })),
1058 )
1059 .child(
1060 h_flex()
1061 .gap_0p5()
1062 .flex_none()
1063 .child(
1064 IconButton::new("backspace-btn", IconName::Delete)
1065 .tooltip(Tooltip::text("Delete Keystroke"))
1066 .on_click(cx.listener(|this, _event, _window, cx| {
1067 this.keystrokes.pop();
1068 cx.notify();
1069 })),
1070 )
1071 .child(
1072 IconButton::new("clear-btn", IconName::Eraser)
1073 .tooltip(Tooltip::text("Clear Keystrokes"))
1074 .on_click(cx.listener(|this, _event, _window, cx| {
1075 this.keystrokes.clear();
1076 cx.notify();
1077 })),
1078 ),
1079 );
1080 }
1081}
1082
1083fn build_keybind_context_menu(
1084 this: &WeakEntity<KeymapEditor>,
1085 window: &mut Window,
1086 cx: &mut App,
1087) -> Entity<ContextMenu> {
1088 ContextMenu::build(window, cx, |menu, _window, cx| {
1089 let Some(this) = this.upgrade() else {
1090 return menu;
1091 };
1092 let selected_binding = this.read_with(cx, |this, _cx| this.selected_binding().cloned());
1093 let Some(selected_binding) = selected_binding else {
1094 return menu;
1095 };
1096
1097 let selected_binding_has_context = selected_binding
1098 .context
1099 .as_ref()
1100 .and_then(KeybindContextString::local)
1101 .is_some();
1102
1103 menu.action("Edit Binding", Box::new(EditBinding))
1104 .action("Copy action", Box::new(CopyAction))
1105 .action_disabled_when(
1106 !selected_binding_has_context,
1107 "Copy Context",
1108 Box::new(CopyContext),
1109 )
1110 })
1111}
1112
1113impl SerializableItem for KeymapEditor {
1114 fn serialized_item_kind() -> &'static str {
1115 "KeymapEditor"
1116 }
1117
1118 fn cleanup(
1119 workspace_id: workspace::WorkspaceId,
1120 alive_items: Vec<workspace::ItemId>,
1121 _window: &mut Window,
1122 cx: &mut App,
1123 ) -> gpui::Task<gpui::Result<()>> {
1124 workspace::delete_unloaded_items(
1125 alive_items,
1126 workspace_id,
1127 "keybinding_editors",
1128 &KEYBINDING_EDITORS,
1129 cx,
1130 )
1131 }
1132
1133 fn deserialize(
1134 _project: Entity<project::Project>,
1135 workspace: WeakEntity<Workspace>,
1136 workspace_id: workspace::WorkspaceId,
1137 item_id: workspace::ItemId,
1138 window: &mut Window,
1139 cx: &mut App,
1140 ) -> gpui::Task<gpui::Result<Entity<Self>>> {
1141 window.spawn(cx, async move |cx| {
1142 if KEYBINDING_EDITORS
1143 .get_keybinding_editor(item_id, workspace_id)?
1144 .is_some()
1145 {
1146 cx.update(|window, cx| cx.new(|cx| KeymapEditor::new(workspace, window, cx)))
1147 } else {
1148 Err(anyhow!("No keybinding editor to deserialize"))
1149 }
1150 })
1151 }
1152
1153 fn serialize(
1154 &mut self,
1155 workspace: &mut Workspace,
1156 item_id: workspace::ItemId,
1157 _closing: bool,
1158 _window: &mut Window,
1159 cx: &mut ui::Context<Self>,
1160 ) -> Option<gpui::Task<gpui::Result<()>>> {
1161 let workspace_id = workspace.database_id()?;
1162 Some(cx.background_spawn(async move {
1163 KEYBINDING_EDITORS
1164 .save_keybinding_editor(item_id, workspace_id)
1165 .await
1166 }))
1167 }
1168
1169 fn should_serialize(&self, _event: &Self::Event) -> bool {
1170 false
1171 }
1172}
1173
1174mod persistence {
1175 use db::{define_connection, query, sqlez_macros::sql};
1176 use workspace::WorkspaceDb;
1177
1178 define_connection! {
1179 pub static ref KEYBINDING_EDITORS: KeybindingEditorDb<WorkspaceDb> =
1180 &[sql!(
1181 CREATE TABLE keybinding_editors (
1182 workspace_id INTEGER,
1183 item_id INTEGER UNIQUE,
1184
1185 PRIMARY KEY(workspace_id, item_id),
1186 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
1187 ON DELETE CASCADE
1188 ) STRICT;
1189 )];
1190 }
1191
1192 impl KeybindingEditorDb {
1193 query! {
1194 pub async fn save_keybinding_editor(
1195 item_id: workspace::ItemId,
1196 workspace_id: workspace::WorkspaceId
1197 ) -> Result<()> {
1198 INSERT OR REPLACE INTO keybinding_editors(item_id, workspace_id)
1199 VALUES (?, ?)
1200 }
1201 }
1202
1203 query! {
1204 pub fn get_keybinding_editor(
1205 item_id: workspace::ItemId,
1206 workspace_id: workspace::WorkspaceId
1207 ) -> Result<Option<workspace::ItemId>> {
1208 SELECT item_id
1209 FROM keybinding_editors
1210 WHERE item_id = ? AND workspace_id = ?
1211 }
1212 }
1213 }
1214}