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