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