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