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