1use std::{
2 ops::{Not, Range},
3 sync::Arc,
4};
5
6use anyhow::{Context as _, anyhow};
7use collections::{HashMap, HashSet};
8use editor::{CompletionProvider, Editor, EditorEvent};
9use feature_flags::FeatureFlagViewExt;
10use fs::Fs;
11use fuzzy::{StringMatch, StringMatchCandidate};
12use gpui::{
13 Action, AppContext as _, AsyncApp, ClickEvent, Context, DismissEvent, Entity, EventEmitter,
14 FocusHandle, Focusable, Global, KeyContext, Keystroke, ModifiersChangedEvent, ScrollStrategy,
15 StyledText, Subscription, WeakEntity, actions, div,
16};
17use language::{Language, LanguageConfig, ToOffset as _};
18use settings::{BaseKeymap, KeybindSource, KeymapFile, SettingsAssets};
19
20use util::ResultExt;
21
22use ui::{
23 ActiveTheme as _, App, Banner, BorrowAppContext, ContextMenu, ParentElement as _, Render,
24 SharedString, Styled as _, Tooltip, Window, prelude::*, right_click_menu,
25};
26use workspace::{
27 Item, ModalView, SerializableItem, Workspace, notifications::NotifyTaskExt as _,
28 register_serializable_item,
29};
30
31use crate::{
32 SettingsUiFeatureFlag,
33 keybindings::persistence::KEYBINDING_EDITORS,
34 ui_components::table::{Table, TableInteractionState},
35};
36
37const NO_ACTION_ARGUMENTS_TEXT: SharedString = SharedString::new_static("<no arguments>");
38
39actions!(
40 zed,
41 [
42 /// Opens the keymap editor.
43 OpenKeymapEditor
44 ]
45);
46
47const KEYMAP_EDITOR_NAMESPACE: &'static str = "keymap_editor";
48actions!(
49 keymap_editor,
50 [
51 /// Edits the selected key binding.
52 EditBinding,
53 /// Creates a new key binding for the selected action.
54 CreateBinding,
55 /// Deletes the selected key binding.
56 DeleteBinding,
57 /// Copies the action name to clipboard.
58 CopyAction,
59 /// Copies the context predicate to clipboard.
60 CopyContext,
61 /// Toggles Conflict Filtering
62 ToggleConflictFilter,
63 /// Toggle Keystroke search
64 ToggleKeystrokeSearch,
65 ]
66);
67
68pub fn init(cx: &mut App) {
69 let keymap_event_channel = KeymapEventChannel::new();
70 cx.set_global(keymap_event_channel);
71
72 cx.on_action(|_: &OpenKeymapEditor, cx| {
73 workspace::with_active_or_new_workspace(cx, move |workspace, window, cx| {
74 let existing = workspace
75 .active_pane()
76 .read(cx)
77 .items()
78 .find_map(|item| item.downcast::<KeymapEditor>());
79
80 if let Some(existing) = existing {
81 workspace.activate_item(&existing, true, true, window, cx);
82 } else {
83 let keymap_editor =
84 cx.new(|cx| KeymapEditor::new(workspace.weak_handle(), window, cx));
85 workspace.add_item_to_active_pane(Box::new(keymap_editor), None, true, window, cx);
86 }
87 });
88 });
89
90 cx.observe_new(|_workspace: &mut Workspace, window, cx| {
91 let Some(window) = window else { return };
92
93 let keymap_ui_actions = [std::any::TypeId::of::<OpenKeymapEditor>()];
94
95 command_palette_hooks::CommandPaletteFilter::update_global(cx, |filter, _cx| {
96 filter.hide_action_types(&keymap_ui_actions);
97 filter.hide_namespace(KEYMAP_EDITOR_NAMESPACE);
98 });
99
100 cx.observe_flag::<SettingsUiFeatureFlag, _>(
101 window,
102 move |is_enabled, _workspace, _, cx| {
103 if is_enabled {
104 command_palette_hooks::CommandPaletteFilter::update_global(
105 cx,
106 |filter, _cx| {
107 filter.show_action_types(keymap_ui_actions.iter());
108 filter.show_namespace(KEYMAP_EDITOR_NAMESPACE);
109 },
110 );
111 } else {
112 command_palette_hooks::CommandPaletteFilter::update_global(
113 cx,
114 |filter, _cx| {
115 filter.hide_action_types(&keymap_ui_actions);
116 filter.hide_namespace(KEYMAP_EDITOR_NAMESPACE);
117 },
118 );
119 }
120 },
121 )
122 .detach();
123 })
124 .detach();
125
126 register_serializable_item::<KeymapEditor>(cx);
127}
128
129pub struct KeymapEventChannel {}
130
131impl Global for KeymapEventChannel {}
132
133impl KeymapEventChannel {
134 fn new() -> Self {
135 Self {}
136 }
137
138 pub fn trigger_keymap_changed(cx: &mut App) {
139 let Some(_event_channel) = cx.try_global::<Self>() else {
140 // don't panic if no global defined. This usually happens in tests
141 return;
142 };
143 cx.update_global(|_event_channel: &mut Self, _| {
144 /* triggers observers in KeymapEditors */
145 });
146 }
147}
148
149#[derive(Default, PartialEq)]
150enum SearchMode {
151 #[default]
152 Normal,
153 KeyStroke,
154}
155
156impl SearchMode {
157 fn invert(&self) -> Self {
158 match self {
159 SearchMode::Normal => SearchMode::KeyStroke,
160 SearchMode::KeyStroke => SearchMode::Normal,
161 }
162 }
163}
164
165#[derive(Default, PartialEq, Copy, Clone)]
166enum FilterState {
167 #[default]
168 All,
169 Conflicts,
170}
171
172impl FilterState {
173 fn invert(&self) -> Self {
174 match self {
175 FilterState::All => FilterState::Conflicts,
176 FilterState::Conflicts => FilterState::All,
177 }
178 }
179}
180
181type ActionMapping = (SharedString, Option<SharedString>);
182
183#[derive(Default)]
184struct ConflictState {
185 conflicts: Vec<usize>,
186 action_keybind_mapping: HashMap<ActionMapping, Vec<usize>>,
187}
188
189impl ConflictState {
190 fn new(key_bindings: &Vec<ProcessedKeybinding>) -> Self {
191 let mut action_keybind_mapping: HashMap<_, Vec<usize>> = HashMap::default();
192
193 key_bindings
194 .iter()
195 .enumerate()
196 .filter(|(_, binding)| {
197 !binding.keystroke_text.is_empty()
198 && binding
199 .source
200 .as_ref()
201 .is_some_and(|source| matches!(source.0, KeybindSource::User))
202 })
203 .for_each(|(index, binding)| {
204 action_keybind_mapping
205 .entry(binding.get_action_mapping())
206 .or_default()
207 .push(index);
208 });
209
210 Self {
211 conflicts: action_keybind_mapping
212 .values()
213 .filter(|indices| indices.len() > 1)
214 .flatten()
215 .copied()
216 .collect(),
217 action_keybind_mapping,
218 }
219 }
220
221 fn conflicting_indices_for_mapping(
222 &self,
223 action_mapping: ActionMapping,
224 keybind_idx: usize,
225 ) -> Option<Vec<usize>> {
226 self.action_keybind_mapping
227 .get(&action_mapping)
228 .and_then(|indices| {
229 let mut indices = indices.iter().filter(|&idx| *idx != keybind_idx).peekable();
230 indices.peek().is_some().then(|| indices.copied().collect())
231 })
232 }
233
234 fn has_conflict(&self, candidate_idx: &usize) -> bool {
235 self.conflicts.contains(candidate_idx)
236 }
237
238 fn any_conflicts(&self) -> bool {
239 !self.conflicts.is_empty()
240 }
241}
242
243struct KeymapEditor {
244 workspace: WeakEntity<Workspace>,
245 focus_handle: FocusHandle,
246 _keymap_subscription: Subscription,
247 keybindings: Vec<ProcessedKeybinding>,
248 keybinding_conflict_state: ConflictState,
249 filter_state: FilterState,
250 search_mode: SearchMode,
251 // corresponds 1 to 1 with keybindings
252 string_match_candidates: Arc<Vec<StringMatchCandidate>>,
253 matches: Vec<StringMatch>,
254 table_interaction_state: Entity<TableInteractionState>,
255 filter_editor: Entity<Editor>,
256 keystroke_editor: Entity<KeystrokeInput>,
257 selected_index: Option<usize>,
258}
259
260impl EventEmitter<()> for KeymapEditor {}
261
262impl Focusable for KeymapEditor {
263 fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
264 return self.filter_editor.focus_handle(cx);
265 }
266}
267
268impl KeymapEditor {
269 fn new(workspace: WeakEntity<Workspace>, window: &mut Window, cx: &mut Context<Self>) -> Self {
270 let focus_handle = cx.focus_handle();
271
272 let _keymap_subscription =
273 cx.observe_global::<KeymapEventChannel>(Self::update_keybindings);
274 let table_interaction_state = TableInteractionState::new(window, cx);
275
276 let keystroke_editor = cx.new(|cx| {
277 let mut keystroke_editor = KeystrokeInput::new(window, cx);
278 keystroke_editor.highlight_on_focus = false;
279 keystroke_editor
280 });
281
282 let filter_editor = cx.new(|cx| {
283 let mut editor = Editor::single_line(window, cx);
284 editor.set_placeholder_text("Filter action names…", cx);
285 editor
286 });
287
288 cx.subscribe(&filter_editor, |this, _, e: &EditorEvent, cx| {
289 if !matches!(e, EditorEvent::BufferEdited) {
290 return;
291 }
292
293 this.update_matches(cx);
294 })
295 .detach();
296
297 cx.subscribe(&keystroke_editor, |this, _, _, cx| {
298 if matches!(this.search_mode, SearchMode::Normal) {
299 return;
300 }
301
302 this.update_matches(cx);
303 })
304 .detach();
305
306 let mut this = Self {
307 workspace,
308 keybindings: vec![],
309 keybinding_conflict_state: ConflictState::default(),
310 filter_state: FilterState::default(),
311 search_mode: SearchMode::default(),
312 string_match_candidates: Arc::new(vec![]),
313 matches: vec![],
314 focus_handle: focus_handle.clone(),
315 _keymap_subscription,
316 table_interaction_state,
317 filter_editor,
318 keystroke_editor,
319 selected_index: None,
320 };
321
322 this.update_keybindings(cx);
323
324 this
325 }
326
327 fn current_action_query(&self, cx: &App) -> String {
328 self.filter_editor.read(cx).text(cx)
329 }
330
331 fn current_keystroke_query(&self, cx: &App) -> Vec<Keystroke> {
332 match self.search_mode {
333 SearchMode::KeyStroke => self
334 .keystroke_editor
335 .read(cx)
336 .keystrokes()
337 .iter()
338 .cloned()
339 .collect(),
340 SearchMode::Normal => Default::default(),
341 }
342 }
343
344 fn update_matches(&self, cx: &mut Context<Self>) {
345 let action_query = self.current_action_query(cx);
346 let keystroke_query = self.current_keystroke_query(cx);
347
348 cx.spawn(async move |this, cx| {
349 Self::process_query(this, action_query, keystroke_query, cx).await
350 })
351 .detach();
352 }
353
354 async fn process_query(
355 this: WeakEntity<Self>,
356 action_query: String,
357 keystroke_query: Vec<Keystroke>,
358 cx: &mut AsyncApp,
359 ) -> anyhow::Result<()> {
360 let action_query = command_palette::normalize_action_query(&action_query);
361 let (string_match_candidates, keybind_count) = this.read_with(cx, |this, _| {
362 (this.string_match_candidates.clone(), this.keybindings.len())
363 })?;
364 let executor = cx.background_executor().clone();
365 let mut matches = fuzzy::match_strings(
366 &string_match_candidates,
367 &action_query,
368 true,
369 true,
370 keybind_count,
371 &Default::default(),
372 executor,
373 )
374 .await;
375 this.update(cx, |this, cx| {
376 match this.filter_state {
377 FilterState::Conflicts => {
378 matches.retain(|candidate| {
379 this.keybinding_conflict_state
380 .has_conflict(&candidate.candidate_id)
381 });
382 }
383 FilterState::All => {}
384 }
385
386 match this.search_mode {
387 SearchMode::KeyStroke => {
388 matches.retain(|item| {
389 this.keybindings[item.candidate_id]
390 .ui_key_binding
391 .as_ref()
392 .is_some_and(|binding| {
393 keystroke_query.iter().all(|key| {
394 binding.keystrokes.iter().any(|keystroke| {
395 keystroke.key == key.key
396 && keystroke.modifiers == key.modifiers
397 })
398 })
399 })
400 });
401 }
402 SearchMode::Normal => {}
403 }
404
405 if action_query.is_empty() {
406 // apply default sort
407 // sorts by source precedence, and alphabetically by action name within each source
408 matches.sort_by_key(|match_item| {
409 let keybind = &this.keybindings[match_item.candidate_id];
410 let source = keybind.source.as_ref().map(|s| s.0);
411 use KeybindSource::*;
412 let source_precedence = match source {
413 Some(User) => 0,
414 Some(Vim) => 1,
415 Some(Base) => 2,
416 Some(Default) => 3,
417 None => 4,
418 };
419 return (source_precedence, keybind.action_name.as_ref());
420 });
421 }
422 this.selected_index.take();
423 this.scroll_to_item(0, ScrollStrategy::Top, cx);
424 this.matches = matches;
425 cx.notify();
426 })
427 }
428
429 fn process_bindings(
430 json_language: Arc<Language>,
431 rust_language: Arc<Language>,
432 cx: &mut App,
433 ) -> (Vec<ProcessedKeybinding>, Vec<StringMatchCandidate>) {
434 let key_bindings_ptr = cx.key_bindings();
435 let lock = key_bindings_ptr.borrow();
436 let key_bindings = lock.bindings();
437 let mut unmapped_action_names =
438 HashSet::from_iter(cx.all_action_names().into_iter().copied());
439 let action_documentation = cx.action_documentation();
440 let mut generator = KeymapFile::action_schema_generator();
441 let action_schema = HashMap::from_iter(
442 cx.action_schemas(&mut generator)
443 .into_iter()
444 .filter_map(|(name, schema)| schema.map(|schema| (name, schema))),
445 );
446
447 let mut processed_bindings = Vec::new();
448 let mut string_match_candidates = Vec::new();
449
450 for key_binding in key_bindings {
451 let source = key_binding.meta().map(settings::KeybindSource::from_meta);
452
453 let keystroke_text = ui::text_for_keystrokes(key_binding.keystrokes(), cx);
454 let ui_key_binding = Some(
455 ui::KeyBinding::new_from_gpui(key_binding.clone(), cx)
456 .vim_mode(source == Some(settings::KeybindSource::Vim)),
457 );
458
459 let context = key_binding
460 .predicate()
461 .map(|predicate| {
462 KeybindContextString::Local(predicate.to_string().into(), rust_language.clone())
463 })
464 .unwrap_or(KeybindContextString::Global);
465
466 let source = source.map(|source| (source, source.name().into()));
467
468 let action_name = key_binding.action().name();
469 unmapped_action_names.remove(&action_name);
470 let action_input = key_binding
471 .action_input()
472 .map(|input| SyntaxHighlightedText::new(input, json_language.clone()));
473 let action_docs = action_documentation.get(action_name).copied();
474
475 let index = processed_bindings.len();
476 let string_match_candidate = StringMatchCandidate::new(index, &action_name);
477 processed_bindings.push(ProcessedKeybinding {
478 keystroke_text: keystroke_text.into(),
479 ui_key_binding,
480 action_name: action_name.into(),
481 action_input,
482 action_docs,
483 action_schema: action_schema.get(action_name).cloned(),
484 context: Some(context),
485 source,
486 });
487 string_match_candidates.push(string_match_candidate);
488 }
489
490 let empty = SharedString::new_static("");
491 for action_name in unmapped_action_names.into_iter() {
492 let index = processed_bindings.len();
493 let string_match_candidate = StringMatchCandidate::new(index, &action_name);
494 processed_bindings.push(ProcessedKeybinding {
495 keystroke_text: empty.clone(),
496 ui_key_binding: None,
497 action_name: action_name.into(),
498 action_input: None,
499 action_docs: action_documentation.get(action_name).copied(),
500 action_schema: action_schema.get(action_name).cloned(),
501 context: None,
502 source: None,
503 });
504 string_match_candidates.push(string_match_candidate);
505 }
506
507 (processed_bindings, string_match_candidates)
508 }
509
510 fn update_keybindings(&mut self, cx: &mut Context<KeymapEditor>) {
511 let workspace = self.workspace.clone();
512 cx.spawn(async move |this, cx| {
513 let json_language = load_json_language(workspace.clone(), cx).await;
514 let rust_language = load_rust_language(workspace.clone(), cx).await;
515
516 let (action_query, keystroke_query) = this.update(cx, |this, cx| {
517 let (key_bindings, string_match_candidates) =
518 Self::process_bindings(json_language, rust_language, cx);
519
520 this.keybinding_conflict_state = ConflictState::new(&key_bindings);
521
522 if !this.keybinding_conflict_state.any_conflicts() {
523 this.filter_state = FilterState::All;
524 }
525
526 this.keybindings = key_bindings;
527 this.string_match_candidates = Arc::new(string_match_candidates);
528 this.matches = this
529 .string_match_candidates
530 .iter()
531 .enumerate()
532 .map(|(ix, candidate)| StringMatch {
533 candidate_id: ix,
534 score: 0.0,
535 positions: vec![],
536 string: candidate.string.clone(),
537 })
538 .collect();
539 (
540 this.current_action_query(cx),
541 this.current_keystroke_query(cx),
542 )
543 })?;
544 // calls cx.notify
545 Self::process_query(this, action_query, keystroke_query, cx).await
546 })
547 .detach_and_log_err(cx);
548 }
549
550 fn dispatch_context(&self, _window: &Window, _cx: &Context<Self>) -> KeyContext {
551 let mut dispatch_context = KeyContext::new_with_defaults();
552 dispatch_context.add("KeymapEditor");
553 dispatch_context.add("menu");
554
555 dispatch_context
556 }
557
558 fn scroll_to_item(&self, index: usize, strategy: ScrollStrategy, cx: &mut App) {
559 let index = usize::min(index, self.matches.len().saturating_sub(1));
560 self.table_interaction_state.update(cx, |this, _cx| {
561 this.scroll_handle.scroll_to_item(index, strategy);
562 });
563 }
564
565 fn focus_search(
566 &mut self,
567 _: &search::FocusSearch,
568 window: &mut Window,
569 cx: &mut Context<Self>,
570 ) {
571 if !self
572 .filter_editor
573 .focus_handle(cx)
574 .contains_focused(window, cx)
575 {
576 window.focus(&self.filter_editor.focus_handle(cx));
577 } else {
578 self.filter_editor.update(cx, |editor, cx| {
579 editor.select_all(&Default::default(), window, cx);
580 });
581 }
582 self.selected_index.take();
583 }
584
585 fn selected_keybind_idx(&self) -> Option<usize> {
586 self.selected_index
587 .and_then(|match_index| self.matches.get(match_index))
588 .map(|r#match| r#match.candidate_id)
589 }
590
591 fn selected_binding(&self) -> Option<&ProcessedKeybinding> {
592 self.selected_keybind_idx()
593 .and_then(|keybind_index| self.keybindings.get(keybind_index))
594 }
595
596 fn select_next(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) {
597 if let Some(selected) = self.selected_index {
598 let selected = selected + 1;
599 if selected >= self.matches.len() {
600 self.select_last(&Default::default(), window, cx);
601 } else {
602 self.selected_index = Some(selected);
603 self.scroll_to_item(selected, ScrollStrategy::Center, cx);
604 cx.notify();
605 }
606 } else {
607 self.select_first(&Default::default(), window, cx);
608 }
609 }
610
611 fn select_previous(
612 &mut self,
613 _: &menu::SelectPrevious,
614 window: &mut Window,
615 cx: &mut Context<Self>,
616 ) {
617 if let Some(selected) = self.selected_index {
618 if selected == 0 {
619 return;
620 }
621
622 let selected = selected - 1;
623
624 if selected >= self.matches.len() {
625 self.select_last(&Default::default(), window, cx);
626 } else {
627 self.selected_index = Some(selected);
628 self.scroll_to_item(selected, ScrollStrategy::Center, cx);
629 cx.notify();
630 }
631 } else {
632 self.select_last(&Default::default(), window, cx);
633 }
634 }
635
636 fn select_first(
637 &mut self,
638 _: &menu::SelectFirst,
639 _window: &mut Window,
640 cx: &mut Context<Self>,
641 ) {
642 if self.matches.get(0).is_some() {
643 self.selected_index = Some(0);
644 self.scroll_to_item(0, ScrollStrategy::Center, cx);
645 cx.notify();
646 }
647 }
648
649 fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
650 if self.matches.last().is_some() {
651 let index = self.matches.len() - 1;
652 self.selected_index = Some(index);
653 self.scroll_to_item(index, ScrollStrategy::Center, cx);
654 cx.notify();
655 }
656 }
657
658 fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
659 self.open_edit_keybinding_modal(false, window, cx);
660 }
661
662 fn open_edit_keybinding_modal(
663 &mut self,
664 create: bool,
665 window: &mut Window,
666 cx: &mut Context<Self>,
667 ) {
668 let Some((keybind_idx, keybind)) = self
669 .selected_keybind_idx()
670 .zip(self.selected_binding().cloned())
671 else {
672 return;
673 };
674 let keymap_editor = cx.entity();
675 self.workspace
676 .update(cx, |workspace, cx| {
677 let fs = workspace.app_state().fs.clone();
678 let workspace_weak = cx.weak_entity();
679 workspace.toggle_modal(window, cx, |window, cx| {
680 let modal = KeybindingEditorModal::new(
681 create,
682 keybind,
683 keybind_idx,
684 keymap_editor,
685 workspace_weak,
686 fs,
687 window,
688 cx,
689 );
690 window.focus(&modal.focus_handle(cx));
691 modal
692 });
693 })
694 .log_err();
695 }
696
697 fn edit_binding(&mut self, _: &EditBinding, window: &mut Window, cx: &mut Context<Self>) {
698 self.open_edit_keybinding_modal(false, window, cx);
699 }
700
701 fn create_binding(&mut self, _: &CreateBinding, window: &mut Window, cx: &mut Context<Self>) {
702 self.open_edit_keybinding_modal(true, window, cx);
703 }
704
705 fn delete_binding(&mut self, _: &DeleteBinding, window: &mut Window, cx: &mut Context<Self>) {
706 let Some(to_remove) = self.selected_binding().cloned() else {
707 return;
708 };
709 let Ok(fs) = self
710 .workspace
711 .read_with(cx, |workspace, _| workspace.app_state().fs.clone())
712 else {
713 return;
714 };
715 let tab_size = cx.global::<settings::SettingsStore>().json_tab_size();
716 cx.spawn(async move |_, _| remove_keybinding(to_remove, &fs, tab_size).await)
717 .detach_and_notify_err(window, cx);
718 }
719
720 fn copy_context_to_clipboard(
721 &mut self,
722 _: &CopyContext,
723 _window: &mut Window,
724 cx: &mut Context<Self>,
725 ) {
726 let context = self
727 .selected_binding()
728 .and_then(|binding| binding.context.as_ref())
729 .and_then(KeybindContextString::local_str)
730 .map(|context| context.to_string());
731 let Some(context) = context else {
732 return;
733 };
734 cx.write_to_clipboard(gpui::ClipboardItem::new_string(context.clone()));
735 }
736
737 fn copy_action_to_clipboard(
738 &mut self,
739 _: &CopyAction,
740 _window: &mut Window,
741 cx: &mut Context<Self>,
742 ) {
743 let action = self
744 .selected_binding()
745 .map(|binding| binding.action_name.to_string());
746 let Some(action) = action else {
747 return;
748 };
749 cx.write_to_clipboard(gpui::ClipboardItem::new_string(action.clone()));
750 }
751
752 fn toggle_conflict_filter(
753 &mut self,
754 _: &ToggleConflictFilter,
755 _: &mut Window,
756 cx: &mut Context<Self>,
757 ) {
758 self.filter_state = self.filter_state.invert();
759 self.update_matches(cx);
760 }
761
762 fn toggle_keystroke_search(
763 &mut self,
764 _: &ToggleKeystrokeSearch,
765 window: &mut Window,
766 cx: &mut Context<Self>,
767 ) {
768 self.search_mode = self.search_mode.invert();
769 self.update_matches(cx);
770
771 match self.search_mode {
772 SearchMode::KeyStroke => {
773 window.focus(&self.keystroke_editor.focus_handle(cx));
774 }
775 SearchMode::Normal => {}
776 }
777 }
778}
779
780#[derive(Clone)]
781struct ProcessedKeybinding {
782 keystroke_text: SharedString,
783 ui_key_binding: Option<ui::KeyBinding>,
784 action_name: SharedString,
785 action_input: Option<SyntaxHighlightedText>,
786 action_docs: Option<&'static str>,
787 action_schema: Option<schemars::Schema>,
788 context: Option<KeybindContextString>,
789 source: Option<(KeybindSource, SharedString)>,
790}
791
792impl ProcessedKeybinding {
793 fn get_action_mapping(&self) -> ActionMapping {
794 (
795 self.keystroke_text.clone(),
796 self.context
797 .as_ref()
798 .and_then(|context| context.local())
799 .cloned(),
800 )
801 }
802}
803
804#[derive(Clone, Debug, IntoElement, PartialEq, Eq, Hash)]
805enum KeybindContextString {
806 Global,
807 Local(SharedString, Arc<Language>),
808}
809
810impl KeybindContextString {
811 const GLOBAL: SharedString = SharedString::new_static("<global>");
812
813 pub fn local(&self) -> Option<&SharedString> {
814 match self {
815 KeybindContextString::Global => None,
816 KeybindContextString::Local(name, _) => Some(name),
817 }
818 }
819
820 pub fn local_str(&self) -> Option<&str> {
821 match self {
822 KeybindContextString::Global => None,
823 KeybindContextString::Local(name, _) => Some(name),
824 }
825 }
826}
827
828impl RenderOnce for KeybindContextString {
829 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
830 match self {
831 KeybindContextString::Global => {
832 muted_styled_text(KeybindContextString::GLOBAL.clone(), cx).into_any_element()
833 }
834 KeybindContextString::Local(name, language) => {
835 SyntaxHighlightedText::new(name, language).into_any_element()
836 }
837 }
838 }
839}
840
841fn muted_styled_text(text: SharedString, cx: &App) -> StyledText {
842 let len = text.len();
843 StyledText::new(text).with_highlights([(
844 0..len,
845 gpui::HighlightStyle::color(cx.theme().colors().text_muted),
846 )])
847}
848
849impl Item for KeymapEditor {
850 type Event = ();
851
852 fn tab_content_text(&self, _detail: usize, _cx: &App) -> ui::SharedString {
853 "Keymap Editor".into()
854 }
855}
856
857impl Render for KeymapEditor {
858 fn render(&mut self, window: &mut Window, cx: &mut ui::Context<Self>) -> impl ui::IntoElement {
859 let row_count = self.matches.len();
860 let theme = cx.theme();
861
862 v_flex()
863 .id("keymap-editor")
864 .track_focus(&self.focus_handle)
865 .key_context(self.dispatch_context(window, cx))
866 .on_action(cx.listener(Self::select_next))
867 .on_action(cx.listener(Self::select_previous))
868 .on_action(cx.listener(Self::select_first))
869 .on_action(cx.listener(Self::select_last))
870 .on_action(cx.listener(Self::focus_search))
871 .on_action(cx.listener(Self::confirm))
872 .on_action(cx.listener(Self::edit_binding))
873 .on_action(cx.listener(Self::create_binding))
874 .on_action(cx.listener(Self::delete_binding))
875 .on_action(cx.listener(Self::copy_action_to_clipboard))
876 .on_action(cx.listener(Self::copy_context_to_clipboard))
877 .on_action(cx.listener(Self::toggle_conflict_filter))
878 .on_action(cx.listener(Self::toggle_keystroke_search))
879 .size_full()
880 .p_2()
881 .gap_1()
882 .bg(theme.colors().editor_background)
883 .child(
884 h_flex()
885 .p_2()
886 .gap_1()
887 .key_context({
888 let mut context = KeyContext::new_with_defaults();
889 context.add("BufferSearchBar");
890 context
891 })
892 .child(
893 div()
894 .size_full()
895 .h_8()
896 .pl_2()
897 .pr_1()
898 .py_1()
899 .border_1()
900 .border_color(theme.colors().border)
901 .rounded_lg()
902 .child(self.filter_editor.clone()),
903 )
904 .child(
905 // TODO: Ask Mikyala if there's a way to get have items be aligned by horizontally
906 // without embedding a h_flex in another h_flex
907 h_flex()
908 .when(self.keybinding_conflict_state.any_conflicts(), |this| {
909 this.child(
910 IconButton::new("KeymapEditorConflictIcon", IconName::Warning)
911 .tooltip({
912 let filter_state = self.filter_state;
913
914 move |window, cx| {
915 Tooltip::for_action(
916 match filter_state {
917 FilterState::All => "Show conflicts",
918 FilterState::Conflicts => "Hide conflicts",
919 },
920 &ToggleConflictFilter,
921 window,
922 cx,
923 )
924 }
925 })
926 .selected_icon_color(Color::Error)
927 .toggle_state(matches!(
928 self.filter_state,
929 FilterState::Conflicts
930 ))
931 .on_click(|_, window, cx| {
932 window.dispatch_action(
933 ToggleConflictFilter.boxed_clone(),
934 cx,
935 );
936 }),
937 )
938 })
939 .child(
940 IconButton::new("KeymapEditorToggleFiltersIcon", IconName::Filter)
941 .tooltip(|window, cx| {
942 Tooltip::for_action(
943 "Toggle Keystroke Search",
944 &ToggleKeystrokeSearch,
945 window,
946 cx,
947 )
948 })
949 .toggle_state(matches!(self.search_mode, SearchMode::KeyStroke))
950 .on_click(|_, window, cx| {
951 window.dispatch_action(
952 ToggleKeystrokeSearch.boxed_clone(),
953 cx,
954 );
955 }),
956 ),
957 ),
958 )
959 .when(matches!(self.search_mode, SearchMode::KeyStroke), |this| {
960 this.child(
961 div()
962 .child(self.keystroke_editor.clone())
963 .border_1()
964 .border_color(theme.colors().border)
965 .rounded_lg(),
966 )
967 })
968 .child(
969 Table::new()
970 .interactable(&self.table_interaction_state)
971 .striped()
972 .column_widths([rems(16.), rems(16.), rems(16.), rems(32.), rems(8.)])
973 .header(["Action", "Arguments", "Keystrokes", "Context", "Source"])
974 .uniform_list(
975 "keymap-editor-table",
976 row_count,
977 cx.processor(move |this, range: Range<usize>, _window, cx| {
978 range
979 .filter_map(|index| {
980 let candidate_id = this.matches.get(index)?.candidate_id;
981 let binding = &this.keybindings[candidate_id];
982
983 let action = div()
984 .child(binding.action_name.clone())
985 .id(("keymap action", index))
986 .tooltip({
987 let action_name = binding.action_name.clone();
988 let action_docs = binding.action_docs;
989 move |_, cx| {
990 let action_tooltip = Tooltip::new(
991 command_palette::humanize_action_name(
992 &action_name,
993 ),
994 );
995 let action_tooltip = match action_docs {
996 Some(docs) => action_tooltip.meta(docs),
997 None => action_tooltip,
998 };
999 cx.new(|_| action_tooltip).into()
1000 }
1001 })
1002 .into_any_element();
1003 let keystrokes = binding.ui_key_binding.clone().map_or(
1004 binding.keystroke_text.clone().into_any_element(),
1005 IntoElement::into_any_element,
1006 );
1007 let action_input = match binding.action_input.clone() {
1008 Some(input) => input.into_any_element(),
1009 None => {
1010 if binding.action_schema.is_some() {
1011 muted_styled_text(NO_ACTION_ARGUMENTS_TEXT, cx)
1012 .into_any_element()
1013 } else {
1014 gpui::Empty.into_any_element()
1015 }
1016 }
1017 };
1018 let context = binding.context.clone().map_or(
1019 gpui::Empty.into_any_element(),
1020 |context| {
1021 let is_local = context.local().is_some();
1022
1023 div()
1024 .id(("keymap context", index))
1025 .child(context.clone())
1026 .when(is_local, |this| {
1027 this.tooltip(Tooltip::element({
1028 move |_, _| {
1029 context.clone().into_any_element()
1030 }
1031 }))
1032 })
1033 .into_any_element()
1034 },
1035 );
1036 let source = binding
1037 .source
1038 .clone()
1039 .map(|(_source, name)| name)
1040 .unwrap_or_default()
1041 .into_any_element();
1042 Some([action, action_input, keystrokes, context, source])
1043 })
1044 .collect()
1045 }),
1046 )
1047 .map_row(
1048 cx.processor(|this, (row_index, row): (usize, Div), _window, cx| {
1049 let is_conflict = this
1050 .matches
1051 .get(row_index)
1052 .map(|candidate| candidate.candidate_id)
1053 .is_some_and(|id| this.keybinding_conflict_state.has_conflict(&id));
1054 let is_selected = this.selected_index == Some(row_index);
1055
1056 let row = row
1057 .id(("keymap-table-row", row_index))
1058 .on_click(cx.listener(
1059 move |this, event: &ClickEvent, window, cx| {
1060 this.selected_index = Some(row_index);
1061 if event.up.click_count == 2 {
1062 this.open_edit_keybinding_modal(false, window, cx);
1063 }
1064 },
1065 ))
1066 .border_2()
1067 .when(is_conflict, |row| {
1068 row.bg(cx.theme().status().error_background)
1069 })
1070 .when(is_selected, |row| {
1071 row.border_color(cx.theme().colors().panel_focused_border)
1072 });
1073
1074 right_click_menu(("keymap-table-row-menu", row_index))
1075 .trigger(move |_, _, _| row)
1076 .menu({
1077 let this = cx.weak_entity();
1078 move |window, cx| {
1079 build_keybind_context_menu(&this, row_index, window, cx)
1080 }
1081 })
1082 .into_any_element()
1083 }),
1084 ),
1085 )
1086 }
1087}
1088
1089#[derive(Debug, Clone, IntoElement)]
1090struct SyntaxHighlightedText {
1091 text: SharedString,
1092 language: Arc<Language>,
1093}
1094
1095impl SyntaxHighlightedText {
1096 pub fn new(text: impl Into<SharedString>, language: Arc<Language>) -> Self {
1097 Self {
1098 text: text.into(),
1099 language,
1100 }
1101 }
1102}
1103
1104impl RenderOnce for SyntaxHighlightedText {
1105 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
1106 let text_style = window.text_style();
1107 let syntax_theme = cx.theme().syntax();
1108
1109 let text = self.text.clone();
1110
1111 let highlights = self
1112 .language
1113 .highlight_text(&text.as_ref().into(), 0..text.len());
1114 let mut runs = Vec::with_capacity(highlights.len());
1115 let mut offset = 0;
1116
1117 for (highlight_range, highlight_id) in highlights {
1118 // Add un-highlighted text before the current highlight
1119 if highlight_range.start > offset {
1120 runs.push(text_style.to_run(highlight_range.start - offset));
1121 }
1122
1123 let mut run_style = text_style.clone();
1124 if let Some(highlight_style) = highlight_id.style(syntax_theme) {
1125 run_style = run_style.highlight(highlight_style);
1126 }
1127 // add the highlighted range
1128 runs.push(run_style.to_run(highlight_range.len()));
1129 offset = highlight_range.end;
1130 }
1131
1132 // Add any remaining un-highlighted text
1133 if offset < text.len() {
1134 runs.push(text_style.to_run(text.len() - offset));
1135 }
1136
1137 return StyledText::new(text).with_runs(runs);
1138 }
1139}
1140
1141#[derive(PartialEq)]
1142enum InputError {
1143 Warning(SharedString),
1144 Error(SharedString),
1145}
1146
1147impl InputError {
1148 fn warning(message: impl Into<SharedString>) -> Self {
1149 Self::Warning(message.into())
1150 }
1151
1152 fn error(message: impl Into<SharedString>) -> Self {
1153 Self::Error(message.into())
1154 }
1155
1156 fn content(&self) -> &SharedString {
1157 match self {
1158 InputError::Warning(content) | InputError::Error(content) => content,
1159 }
1160 }
1161
1162 fn is_warning(&self) -> bool {
1163 matches!(self, InputError::Warning(_))
1164 }
1165}
1166
1167struct KeybindingEditorModal {
1168 creating: bool,
1169 editing_keybind: ProcessedKeybinding,
1170 editing_keybind_idx: usize,
1171 keybind_editor: Entity<KeystrokeInput>,
1172 context_editor: Entity<Editor>,
1173 input_editor: Option<Entity<Editor>>,
1174 fs: Arc<dyn Fs>,
1175 error: Option<InputError>,
1176 keymap_editor: Entity<KeymapEditor>,
1177}
1178
1179impl ModalView for KeybindingEditorModal {}
1180
1181impl EventEmitter<DismissEvent> for KeybindingEditorModal {}
1182
1183impl Focusable for KeybindingEditorModal {
1184 fn focus_handle(&self, cx: &App) -> FocusHandle {
1185 self.keybind_editor.focus_handle(cx)
1186 }
1187}
1188
1189impl KeybindingEditorModal {
1190 pub fn new(
1191 create: bool,
1192 editing_keybind: ProcessedKeybinding,
1193 editing_keybind_idx: usize,
1194 keymap_editor: Entity<KeymapEditor>,
1195 workspace: WeakEntity<Workspace>,
1196 fs: Arc<dyn Fs>,
1197 window: &mut Window,
1198 cx: &mut App,
1199 ) -> Self {
1200 let keybind_editor = cx.new(|cx| KeystrokeInput::new(window, cx));
1201
1202 let context_editor = cx.new(|cx| {
1203 let mut editor = Editor::single_line(window, cx);
1204
1205 if let Some(context) = editing_keybind
1206 .context
1207 .as_ref()
1208 .and_then(KeybindContextString::local)
1209 {
1210 editor.set_text(context.clone(), window, cx);
1211 } else {
1212 editor.set_placeholder_text("Keybinding context", cx);
1213 }
1214
1215 cx.spawn(async |editor, cx| {
1216 let contexts = cx
1217 .background_spawn(async { collect_contexts_from_assets() })
1218 .await;
1219
1220 editor
1221 .update(cx, |editor, _cx| {
1222 editor.set_completion_provider(Some(std::rc::Rc::new(
1223 KeyContextCompletionProvider { contexts },
1224 )));
1225 })
1226 .context("Failed to load completions for keybinding context")
1227 })
1228 .detach_and_log_err(cx);
1229
1230 editor
1231 });
1232
1233 let input_editor = editing_keybind.action_schema.clone().map(|_schema| {
1234 cx.new(|cx| {
1235 let mut editor = Editor::auto_height_unbounded(1, window, cx);
1236 if let Some(input) = editing_keybind.action_input.clone() {
1237 editor.set_text(input.text, window, cx);
1238 } else {
1239 // TODO: default value from schema?
1240 editor.set_placeholder_text("Action input", cx);
1241 }
1242 cx.spawn(async |editor, cx| {
1243 let json_language = load_json_language(workspace, cx).await;
1244 editor
1245 .update(cx, |editor, cx| {
1246 if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
1247 buffer.update(cx, |buffer, cx| {
1248 buffer.set_language(Some(json_language), cx)
1249 });
1250 }
1251 })
1252 .context("Failed to load JSON language for editing keybinding action input")
1253 })
1254 .detach_and_log_err(cx);
1255 editor
1256 })
1257 });
1258
1259 Self {
1260 creating: create,
1261 editing_keybind,
1262 editing_keybind_idx,
1263 fs,
1264 keybind_editor,
1265 context_editor,
1266 input_editor,
1267 error: None,
1268 keymap_editor,
1269 }
1270 }
1271
1272 fn set_error(&mut self, error: InputError, cx: &mut Context<Self>) -> bool {
1273 if self
1274 .error
1275 .as_ref()
1276 .is_some_and(|old_error| old_error.is_warning() && *old_error == error)
1277 {
1278 false
1279 } else {
1280 self.error = Some(error);
1281 cx.notify();
1282 true
1283 }
1284 }
1285
1286 fn save(&mut self, cx: &mut Context<Self>) {
1287 let existing_keybind = self.editing_keybind.clone();
1288 let fs = self.fs.clone();
1289 let new_keystrokes = self
1290 .keybind_editor
1291 .read_with(cx, |editor, _| editor.keystrokes().to_vec());
1292 if new_keystrokes.is_empty() {
1293 self.set_error(InputError::error("Keystrokes cannot be empty"), cx);
1294 return;
1295 }
1296 let tab_size = cx.global::<settings::SettingsStore>().json_tab_size();
1297 let new_context = self
1298 .context_editor
1299 .read_with(cx, |editor, cx| editor.text(cx));
1300 let new_context = new_context.is_empty().not().then_some(new_context);
1301 let new_context_err = new_context.as_deref().and_then(|context| {
1302 gpui::KeyBindingContextPredicate::parse(context)
1303 .context("Failed to parse key context")
1304 .err()
1305 });
1306 if let Some(err) = new_context_err {
1307 // TODO: store and display as separate error
1308 // TODO: also, should be validating on keystroke
1309 self.set_error(InputError::error(err.to_string()), cx);
1310 return;
1311 }
1312
1313 let action_mapping: ActionMapping = (
1314 ui::text_for_keystrokes(&new_keystrokes, cx).into(),
1315 new_context
1316 .as_ref()
1317 .map(Into::into)
1318 .or_else(|| existing_keybind.get_action_mapping().1),
1319 );
1320
1321 if let Some(conflicting_indices) = self
1322 .keymap_editor
1323 .read(cx)
1324 .keybinding_conflict_state
1325 .conflicting_indices_for_mapping(action_mapping, self.editing_keybind_idx)
1326 {
1327 let first_conflicting_index = conflicting_indices[0];
1328 let conflicting_action_name = self
1329 .keymap_editor
1330 .read(cx)
1331 .keybindings
1332 .get(first_conflicting_index)
1333 .map(|keybind| keybind.action_name.clone());
1334
1335 let warning_message = match conflicting_action_name {
1336 Some(name) => {
1337 let confliction_action_amount = conflicting_indices.len() - 1;
1338 if confliction_action_amount > 0 {
1339 format!(
1340 "Your keybind would conflict with the \"{}\" action and {} other bindings",
1341 name, confliction_action_amount
1342 )
1343 } else {
1344 format!("Your keybind would conflict with the \"{}\" action", name)
1345 }
1346 }
1347 None => {
1348 log::info!(
1349 "Could not find action in keybindings with index {}",
1350 first_conflicting_index
1351 );
1352 "Your keybind would conflict with other actions".to_string()
1353 }
1354 };
1355
1356 if self.set_error(InputError::warning(warning_message), cx) {
1357 return;
1358 }
1359 }
1360
1361 let create = self.creating;
1362
1363 cx.spawn(async move |this, cx| {
1364 if let Err(err) = save_keybinding_update(
1365 create,
1366 existing_keybind,
1367 &new_keystrokes,
1368 new_context.as_deref(),
1369 &fs,
1370 tab_size,
1371 )
1372 .await
1373 {
1374 this.update(cx, |this, cx| {
1375 this.set_error(InputError::error(err.to_string()), cx);
1376 })
1377 .log_err();
1378 } else {
1379 this.update(cx, |_this, cx| {
1380 cx.emit(DismissEvent);
1381 })
1382 .ok();
1383 }
1384 })
1385 .detach();
1386 }
1387}
1388
1389impl Render for KeybindingEditorModal {
1390 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1391 let theme = cx.theme().colors();
1392 let input_base = || {
1393 div()
1394 .w_full()
1395 .py_2()
1396 .px_3()
1397 .min_h_8()
1398 .rounded_md()
1399 .bg(theme.editor_background)
1400 .border_1()
1401 .border_color(theme.border_variant)
1402 };
1403
1404 v_flex()
1405 .w(rems(34.))
1406 .elevation_3(cx)
1407 .child(
1408 v_flex()
1409 .p_3()
1410 .child(Label::new("Edit Keystroke"))
1411 .child(
1412 Label::new("Input the desired keystroke for the selected action.")
1413 .color(Color::Muted)
1414 .mb_2(),
1415 )
1416 .child(self.keybind_editor.clone()),
1417 )
1418 .when_some(self.input_editor.clone(), |this, editor| {
1419 this.child(
1420 v_flex()
1421 .p_3()
1422 .pt_0()
1423 .child(Label::new("Edit Input"))
1424 .child(
1425 Label::new("Input the desired input to the binding.")
1426 .color(Color::Muted)
1427 .mb_2(),
1428 )
1429 .child(input_base().child(editor)),
1430 )
1431 })
1432 .child(
1433 v_flex()
1434 .p_3()
1435 .pt_0()
1436 .child(Label::new("Edit Context"))
1437 .child(
1438 Label::new("Input the desired context for the binding.")
1439 .color(Color::Muted)
1440 .mb_2(),
1441 )
1442 .child(input_base().child(self.context_editor.clone())),
1443 )
1444 .when_some(self.error.as_ref(), |this, error| {
1445 this.child(
1446 div().p_2().child(
1447 Banner::new()
1448 .map(|banner| match error {
1449 InputError::Error(_) => banner.severity(ui::Severity::Error),
1450 InputError::Warning(_) => banner.severity(ui::Severity::Warning),
1451 })
1452 // For some reason, the div overflows its container to the
1453 // right. The padding accounts for that.
1454 .child(div().size_full().pr_2().child(Label::new(error.content()))),
1455 ),
1456 )
1457 })
1458 .child(
1459 h_flex()
1460 .p_2()
1461 .w_full()
1462 .gap_1()
1463 .justify_end()
1464 .border_t_1()
1465 .border_color(theme.border_variant)
1466 .child(
1467 Button::new("cancel", "Cancel")
1468 .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
1469 )
1470 .child(
1471 Button::new("save-btn", "Save").on_click(
1472 cx.listener(|this, _event, _window, cx| Self::save(this, cx)),
1473 ),
1474 ),
1475 )
1476 }
1477}
1478
1479struct KeyContextCompletionProvider {
1480 contexts: Vec<SharedString>,
1481}
1482
1483impl CompletionProvider for KeyContextCompletionProvider {
1484 fn completions(
1485 &self,
1486 _excerpt_id: editor::ExcerptId,
1487 buffer: &Entity<language::Buffer>,
1488 buffer_position: language::Anchor,
1489 _trigger: editor::CompletionContext,
1490 _window: &mut Window,
1491 cx: &mut Context<Editor>,
1492 ) -> gpui::Task<anyhow::Result<Vec<project::CompletionResponse>>> {
1493 let buffer = buffer.read(cx);
1494 let mut count_back = 0;
1495 for char in buffer.reversed_chars_at(buffer_position) {
1496 if char.is_ascii_alphanumeric() || char == '_' {
1497 count_back += 1;
1498 } else {
1499 break;
1500 }
1501 }
1502 let start_anchor = buffer.anchor_before(
1503 buffer_position
1504 .to_offset(&buffer)
1505 .saturating_sub(count_back),
1506 );
1507 let replace_range = start_anchor..buffer_position;
1508 gpui::Task::ready(Ok(vec![project::CompletionResponse {
1509 completions: self
1510 .contexts
1511 .iter()
1512 .map(|context| project::Completion {
1513 replace_range: replace_range.clone(),
1514 label: language::CodeLabel::plain(context.to_string(), None),
1515 new_text: context.to_string(),
1516 documentation: None,
1517 source: project::CompletionSource::Custom,
1518 icon_path: None,
1519 insert_text_mode: None,
1520 confirm: None,
1521 })
1522 .collect(),
1523 is_incomplete: false,
1524 }]))
1525 }
1526
1527 fn is_completion_trigger(
1528 &self,
1529 _buffer: &Entity<language::Buffer>,
1530 _position: language::Anchor,
1531 text: &str,
1532 _trigger_in_words: bool,
1533 _menu_is_open: bool,
1534 _cx: &mut Context<Editor>,
1535 ) -> bool {
1536 text.chars().last().map_or(false, |last_char| {
1537 last_char.is_ascii_alphanumeric() || last_char == '_'
1538 })
1539 }
1540}
1541
1542async fn load_json_language(workspace: WeakEntity<Workspace>, cx: &mut AsyncApp) -> Arc<Language> {
1543 let json_language_task = workspace
1544 .read_with(cx, |workspace, cx| {
1545 workspace
1546 .project()
1547 .read(cx)
1548 .languages()
1549 .language_for_name("JSON")
1550 })
1551 .context("Failed to load JSON language")
1552 .log_err();
1553 let json_language = match json_language_task {
1554 Some(task) => task.await.context("Failed to load JSON language").log_err(),
1555 None => None,
1556 };
1557 return json_language.unwrap_or_else(|| {
1558 Arc::new(Language::new(
1559 LanguageConfig {
1560 name: "JSON".into(),
1561 ..Default::default()
1562 },
1563 Some(tree_sitter_json::LANGUAGE.into()),
1564 ))
1565 });
1566}
1567
1568async fn load_rust_language(workspace: WeakEntity<Workspace>, cx: &mut AsyncApp) -> Arc<Language> {
1569 let rust_language_task = workspace
1570 .read_with(cx, |workspace, cx| {
1571 workspace
1572 .project()
1573 .read(cx)
1574 .languages()
1575 .language_for_name("Rust")
1576 })
1577 .context("Failed to load Rust language")
1578 .log_err();
1579 let rust_language = match rust_language_task {
1580 Some(task) => task.await.context("Failed to load Rust language").log_err(),
1581 None => None,
1582 };
1583 return rust_language.unwrap_or_else(|| {
1584 Arc::new(Language::new(
1585 LanguageConfig {
1586 name: "Rust".into(),
1587 ..Default::default()
1588 },
1589 Some(tree_sitter_rust::LANGUAGE.into()),
1590 ))
1591 });
1592}
1593
1594async fn save_keybinding_update(
1595 create: bool,
1596 existing: ProcessedKeybinding,
1597 new_keystrokes: &[Keystroke],
1598 new_context: Option<&str>,
1599 fs: &Arc<dyn Fs>,
1600 tab_size: usize,
1601) -> anyhow::Result<()> {
1602 let keymap_contents = settings::KeymapFile::load_keymap_file(fs)
1603 .await
1604 .context("Failed to load keymap file")?;
1605
1606 let existing_keystrokes = existing
1607 .ui_key_binding
1608 .as_ref()
1609 .map(|keybinding| keybinding.keystrokes.as_slice())
1610 .unwrap_or_default();
1611
1612 let existing_context = existing
1613 .context
1614 .as_ref()
1615 .and_then(KeybindContextString::local_str);
1616
1617 let input = existing
1618 .action_input
1619 .as_ref()
1620 .map(|input| input.text.as_ref());
1621
1622 let operation = if !create {
1623 settings::KeybindUpdateOperation::Replace {
1624 target: settings::KeybindUpdateTarget {
1625 context: existing_context,
1626 keystrokes: existing_keystrokes,
1627 action_name: &existing.action_name,
1628 use_key_equivalents: false,
1629 input,
1630 },
1631 target_keybind_source: existing
1632 .source
1633 .map(|(source, _name)| source)
1634 .unwrap_or(KeybindSource::User),
1635 source: settings::KeybindUpdateTarget {
1636 context: new_context,
1637 keystrokes: new_keystrokes,
1638 action_name: &existing.action_name,
1639 use_key_equivalents: false,
1640 input,
1641 },
1642 }
1643 } else {
1644 settings::KeybindUpdateOperation::Add(settings::KeybindUpdateTarget {
1645 context: new_context,
1646 keystrokes: new_keystrokes,
1647 action_name: &existing.action_name,
1648 use_key_equivalents: false,
1649 input,
1650 })
1651 };
1652 let updated_keymap_contents =
1653 settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size)
1654 .context("Failed to update keybinding")?;
1655 fs.atomic_write(paths::keymap_file().clone(), updated_keymap_contents)
1656 .await
1657 .context("Failed to write keymap file")?;
1658 Ok(())
1659}
1660
1661async fn remove_keybinding(
1662 existing: ProcessedKeybinding,
1663 fs: &Arc<dyn Fs>,
1664 tab_size: usize,
1665) -> anyhow::Result<()> {
1666 let Some(ui_key_binding) = existing.ui_key_binding else {
1667 anyhow::bail!("Cannot remove a keybinding that does not exist");
1668 };
1669 let keymap_contents = settings::KeymapFile::load_keymap_file(fs)
1670 .await
1671 .context("Failed to load keymap file")?;
1672
1673 let operation = settings::KeybindUpdateOperation::Remove {
1674 target: settings::KeybindUpdateTarget {
1675 context: existing
1676 .context
1677 .as_ref()
1678 .and_then(KeybindContextString::local_str),
1679 keystrokes: &ui_key_binding.keystrokes,
1680 action_name: &existing.action_name,
1681 use_key_equivalents: false,
1682 input: existing
1683 .action_input
1684 .as_ref()
1685 .map(|input| input.text.as_ref()),
1686 },
1687 target_keybind_source: existing
1688 .source
1689 .map(|(source, _name)| source)
1690 .unwrap_or(KeybindSource::User),
1691 };
1692
1693 let updated_keymap_contents =
1694 settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size)
1695 .context("Failed to update keybinding")?;
1696 fs.atomic_write(paths::keymap_file().clone(), updated_keymap_contents)
1697 .await
1698 .context("Failed to write keymap file")?;
1699 Ok(())
1700}
1701
1702struct KeystrokeInput {
1703 keystrokes: Vec<Keystroke>,
1704 highlight_on_focus: bool,
1705 focus_handle: FocusHandle,
1706 intercept_subscription: Option<Subscription>,
1707 _focus_subscriptions: [Subscription; 2],
1708}
1709
1710impl KeystrokeInput {
1711 fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
1712 let focus_handle = cx.focus_handle();
1713 let _focus_subscriptions = [
1714 cx.on_focus_in(&focus_handle, window, Self::on_focus_in),
1715 cx.on_focus_out(&focus_handle, window, Self::on_focus_out),
1716 ];
1717 Self {
1718 keystrokes: Vec::new(),
1719 highlight_on_focus: true,
1720 focus_handle,
1721 intercept_subscription: None,
1722 _focus_subscriptions,
1723 }
1724 }
1725
1726 fn on_modifiers_changed(
1727 &mut self,
1728 event: &ModifiersChangedEvent,
1729 _window: &mut Window,
1730 cx: &mut Context<Self>,
1731 ) {
1732 if let Some(last) = self.keystrokes.last_mut()
1733 && last.key.is_empty()
1734 {
1735 if !event.modifiers.modified() {
1736 self.keystrokes.pop();
1737 cx.emit(());
1738 } else {
1739 last.modifiers = event.modifiers;
1740 }
1741 } else {
1742 self.keystrokes.push(Keystroke {
1743 modifiers: event.modifiers,
1744 key: "".to_string(),
1745 key_char: None,
1746 });
1747 cx.emit(());
1748 }
1749 cx.stop_propagation();
1750 cx.notify();
1751 }
1752
1753 fn handle_keystroke(&mut self, keystroke: &Keystroke, cx: &mut Context<Self>) {
1754 if let Some(last) = self.keystrokes.last_mut()
1755 && last.key.is_empty()
1756 {
1757 *last = keystroke.clone();
1758 } else if Some(keystroke) != self.keystrokes.last() {
1759 self.keystrokes.push(keystroke.clone());
1760 }
1761 cx.emit(());
1762 cx.stop_propagation();
1763 cx.notify();
1764 }
1765
1766 fn on_key_up(
1767 &mut self,
1768 event: &gpui::KeyUpEvent,
1769 _window: &mut Window,
1770 cx: &mut Context<Self>,
1771 ) {
1772 if let Some(last) = self.keystrokes.last_mut()
1773 && !last.key.is_empty()
1774 && last.modifiers == event.keystroke.modifiers
1775 {
1776 cx.emit(());
1777 self.keystrokes.push(Keystroke {
1778 modifiers: event.keystroke.modifiers,
1779 key: "".to_string(),
1780 key_char: None,
1781 });
1782 }
1783 cx.stop_propagation();
1784 cx.notify();
1785 }
1786
1787 fn on_focus_in(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
1788 if self.intercept_subscription.is_none() {
1789 let listener = cx.listener(|this, event: &gpui::KeystrokeEvent, _window, cx| {
1790 this.handle_keystroke(&event.keystroke, cx);
1791 });
1792 self.intercept_subscription = Some(cx.intercept_keystrokes(listener))
1793 }
1794 }
1795
1796 fn on_focus_out(
1797 &mut self,
1798 _event: gpui::FocusOutEvent,
1799 _window: &mut Window,
1800 _cx: &mut Context<Self>,
1801 ) {
1802 self.intercept_subscription.take();
1803 }
1804
1805 fn keystrokes(&self) -> &[Keystroke] {
1806 if self
1807 .keystrokes
1808 .last()
1809 .map_or(false, |last| last.key.is_empty())
1810 {
1811 return &self.keystrokes[..self.keystrokes.len() - 1];
1812 }
1813 return &self.keystrokes;
1814 }
1815}
1816
1817impl EventEmitter<()> for KeystrokeInput {}
1818
1819impl Focusable for KeystrokeInput {
1820 fn focus_handle(&self, _cx: &App) -> FocusHandle {
1821 self.focus_handle.clone()
1822 }
1823}
1824
1825impl Render for KeystrokeInput {
1826 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1827 let colors = cx.theme().colors();
1828 let is_focused = self.focus_handle.is_focused(window);
1829
1830 return h_flex()
1831 .id("keybinding_input")
1832 .track_focus(&self.focus_handle)
1833 .on_modifiers_changed(cx.listener(Self::on_modifiers_changed))
1834 .on_key_up(cx.listener(Self::on_key_up))
1835 .when(self.highlight_on_focus, |this| {
1836 this.focus(|mut style| {
1837 style.border_color = Some(colors.border_focused);
1838 style
1839 })
1840 })
1841 .py_2()
1842 .px_3()
1843 .gap_2()
1844 .min_h_8()
1845 .w_full()
1846 .flex_1()
1847 .justify_between()
1848 .rounded_md()
1849 .overflow_hidden()
1850 .bg(colors.editor_background)
1851 .border_1()
1852 .border_color(colors.border_variant)
1853 .child(
1854 h_flex()
1855 .w_full()
1856 .min_w_0()
1857 .justify_center()
1858 .flex_wrap()
1859 .gap(ui::DynamicSpacing::Base04.rems(cx))
1860 .children(self.keystrokes.iter().map(|keystroke| {
1861 h_flex().children(ui::render_keystroke(
1862 keystroke,
1863 None,
1864 Some(rems(0.875).into()),
1865 ui::PlatformStyle::platform(),
1866 false,
1867 ))
1868 })),
1869 )
1870 .child(
1871 h_flex()
1872 .gap_0p5()
1873 .flex_none()
1874 .child(
1875 IconButton::new("backspace-btn", IconName::Delete)
1876 .tooltip(Tooltip::text("Delete Keystroke"))
1877 .when(!is_focused, |this| this.icon_color(Color::Muted))
1878 .on_click(cx.listener(|this, _event, _window, cx| {
1879 this.keystrokes.pop();
1880 cx.emit(());
1881 cx.notify();
1882 })),
1883 )
1884 .child(
1885 IconButton::new("clear-btn", IconName::Eraser)
1886 .tooltip(Tooltip::text("Clear Keystrokes"))
1887 .when(!is_focused, |this| this.icon_color(Color::Muted))
1888 .on_click(cx.listener(|this, _event, _window, cx| {
1889 this.keystrokes.clear();
1890 cx.emit(());
1891 cx.notify();
1892 })),
1893 ),
1894 );
1895 }
1896}
1897
1898fn build_keybind_context_menu(
1899 this: &WeakEntity<KeymapEditor>,
1900 item_idx: usize,
1901 window: &mut Window,
1902 cx: &mut App,
1903) -> Entity<ContextMenu> {
1904 ContextMenu::build(window, cx, |menu, _window, cx| {
1905 let selected_binding = this
1906 .update(cx, |this, _cx| {
1907 this.selected_index = Some(item_idx);
1908 this.selected_binding().cloned()
1909 })
1910 .ok()
1911 .flatten();
1912
1913 let Some(selected_binding) = selected_binding else {
1914 return menu;
1915 };
1916
1917 let selected_binding_has_no_context = selected_binding
1918 .context
1919 .as_ref()
1920 .and_then(KeybindContextString::local)
1921 .is_none();
1922
1923 let selected_binding_is_unbound_action = selected_binding.ui_key_binding.is_none();
1924
1925 menu.action_disabled_when(
1926 selected_binding_is_unbound_action,
1927 "Edit",
1928 Box::new(EditBinding),
1929 )
1930 .action("Create", Box::new(CreateBinding))
1931 .action_disabled_when(
1932 selected_binding_is_unbound_action,
1933 "Delete",
1934 Box::new(DeleteBinding),
1935 )
1936 .action("Copy action", Box::new(CopyAction))
1937 .action_disabled_when(
1938 selected_binding_has_no_context,
1939 "Copy Context",
1940 Box::new(CopyContext),
1941 )
1942 })
1943}
1944
1945fn collect_contexts_from_assets() -> Vec<SharedString> {
1946 let mut keymap_assets = vec![
1947 util::asset_str::<SettingsAssets>(settings::DEFAULT_KEYMAP_PATH),
1948 util::asset_str::<SettingsAssets>(settings::VIM_KEYMAP_PATH),
1949 ];
1950 keymap_assets.extend(
1951 BaseKeymap::OPTIONS
1952 .iter()
1953 .filter_map(|(_, base_keymap)| base_keymap.asset_path())
1954 .map(util::asset_str::<SettingsAssets>),
1955 );
1956
1957 let mut contexts = HashSet::default();
1958
1959 for keymap_asset in keymap_assets {
1960 let Ok(keymap) = KeymapFile::parse(&keymap_asset) else {
1961 continue;
1962 };
1963
1964 for section in keymap.sections() {
1965 let context_expr = §ion.context;
1966 let mut queue = Vec::new();
1967 let Ok(root_context) = gpui::KeyBindingContextPredicate::parse(context_expr) else {
1968 continue;
1969 };
1970
1971 queue.push(root_context);
1972 while let Some(context) = queue.pop() {
1973 match context {
1974 gpui::KeyBindingContextPredicate::Identifier(ident) => {
1975 contexts.insert(ident);
1976 }
1977 gpui::KeyBindingContextPredicate::Equal(ident_a, ident_b) => {
1978 contexts.insert(ident_a);
1979 contexts.insert(ident_b);
1980 }
1981 gpui::KeyBindingContextPredicate::NotEqual(ident_a, ident_b) => {
1982 contexts.insert(ident_a);
1983 contexts.insert(ident_b);
1984 }
1985 gpui::KeyBindingContextPredicate::Child(ctx_a, ctx_b) => {
1986 queue.push(*ctx_a);
1987 queue.push(*ctx_b);
1988 }
1989 gpui::KeyBindingContextPredicate::Not(ctx) => {
1990 queue.push(*ctx);
1991 }
1992 gpui::KeyBindingContextPredicate::And(ctx_a, ctx_b) => {
1993 queue.push(*ctx_a);
1994 queue.push(*ctx_b);
1995 }
1996 gpui::KeyBindingContextPredicate::Or(ctx_a, ctx_b) => {
1997 queue.push(*ctx_a);
1998 queue.push(*ctx_b);
1999 }
2000 }
2001 }
2002 }
2003 }
2004
2005 let mut contexts = contexts.into_iter().collect::<Vec<_>>();
2006 contexts.sort();
2007
2008 return contexts;
2009}
2010
2011impl SerializableItem for KeymapEditor {
2012 fn serialized_item_kind() -> &'static str {
2013 "KeymapEditor"
2014 }
2015
2016 fn cleanup(
2017 workspace_id: workspace::WorkspaceId,
2018 alive_items: Vec<workspace::ItemId>,
2019 _window: &mut Window,
2020 cx: &mut App,
2021 ) -> gpui::Task<gpui::Result<()>> {
2022 workspace::delete_unloaded_items(
2023 alive_items,
2024 workspace_id,
2025 "keybinding_editors",
2026 &KEYBINDING_EDITORS,
2027 cx,
2028 )
2029 }
2030
2031 fn deserialize(
2032 _project: Entity<project::Project>,
2033 workspace: WeakEntity<Workspace>,
2034 workspace_id: workspace::WorkspaceId,
2035 item_id: workspace::ItemId,
2036 window: &mut Window,
2037 cx: &mut App,
2038 ) -> gpui::Task<gpui::Result<Entity<Self>>> {
2039 window.spawn(cx, async move |cx| {
2040 if KEYBINDING_EDITORS
2041 .get_keybinding_editor(item_id, workspace_id)?
2042 .is_some()
2043 {
2044 cx.update(|window, cx| cx.new(|cx| KeymapEditor::new(workspace, window, cx)))
2045 } else {
2046 Err(anyhow!("No keybinding editor to deserialize"))
2047 }
2048 })
2049 }
2050
2051 fn serialize(
2052 &mut self,
2053 workspace: &mut Workspace,
2054 item_id: workspace::ItemId,
2055 _closing: bool,
2056 _window: &mut Window,
2057 cx: &mut ui::Context<Self>,
2058 ) -> Option<gpui::Task<gpui::Result<()>>> {
2059 let workspace_id = workspace.database_id()?;
2060 Some(cx.background_spawn(async move {
2061 KEYBINDING_EDITORS
2062 .save_keybinding_editor(item_id, workspace_id)
2063 .await
2064 }))
2065 }
2066
2067 fn should_serialize(&self, _event: &Self::Event) -> bool {
2068 false
2069 }
2070}
2071
2072mod persistence {
2073 use db::{define_connection, query, sqlez_macros::sql};
2074 use workspace::WorkspaceDb;
2075
2076 define_connection! {
2077 pub static ref KEYBINDING_EDITORS: KeybindingEditorDb<WorkspaceDb> =
2078 &[sql!(
2079 CREATE TABLE keybinding_editors (
2080 workspace_id INTEGER,
2081 item_id INTEGER UNIQUE,
2082
2083 PRIMARY KEY(workspace_id, item_id),
2084 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
2085 ON DELETE CASCADE
2086 ) STRICT;
2087 )];
2088 }
2089
2090 impl KeybindingEditorDb {
2091 query! {
2092 pub async fn save_keybinding_editor(
2093 item_id: workspace::ItemId,
2094 workspace_id: workspace::WorkspaceId
2095 ) -> Result<()> {
2096 INSERT OR REPLACE INTO keybinding_editors(item_id, workspace_id)
2097 VALUES (?, ?)
2098 }
2099 }
2100
2101 query! {
2102 pub fn get_keybinding_editor(
2103 item_id: workspace::ItemId,
2104 workspace_id: workspace::WorkspaceId
2105 ) -> Result<Option<workspace::ItemId>> {
2106 SELECT item_id
2107 FROM keybinding_editors
2108 WHERE item_id = ? AND workspace_id = ?
2109 }
2110 }
2111 }
2112}