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, KeyContext, KeyDownEvent, Keystroke,
15 ModifiersChangedEvent, MouseButton, Point, ScrollStrategy, StyledText, Subscription,
16 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, _, _, cx| {
1186 this.context_menu.take();
1187 cx.notify();
1188 }))
1189 .children(self.context_menu.as_ref().map(|(menu, position, _)| {
1190 deferred(
1191 anchored()
1192 .position(*position)
1193 .anchor(gpui::Corner::TopLeft)
1194 .child(menu.clone()),
1195 )
1196 .with_priority(1)
1197 }))
1198 }
1199}
1200
1201#[derive(Debug, Clone, IntoElement)]
1202struct SyntaxHighlightedText {
1203 text: SharedString,
1204 language: Arc<Language>,
1205}
1206
1207impl SyntaxHighlightedText {
1208 pub fn new(text: impl Into<SharedString>, language: Arc<Language>) -> Self {
1209 Self {
1210 text: text.into(),
1211 language,
1212 }
1213 }
1214}
1215
1216impl RenderOnce for SyntaxHighlightedText {
1217 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
1218 let text_style = window.text_style();
1219 let syntax_theme = cx.theme().syntax();
1220
1221 let text = self.text.clone();
1222
1223 let highlights = self
1224 .language
1225 .highlight_text(&text.as_ref().into(), 0..text.len());
1226 let mut runs = Vec::with_capacity(highlights.len());
1227 let mut offset = 0;
1228
1229 for (highlight_range, highlight_id) in highlights {
1230 // Add un-highlighted text before the current highlight
1231 if highlight_range.start > offset {
1232 runs.push(text_style.to_run(highlight_range.start - offset));
1233 }
1234
1235 let mut run_style = text_style.clone();
1236 if let Some(highlight_style) = highlight_id.style(syntax_theme) {
1237 run_style = run_style.highlight(highlight_style);
1238 }
1239 // add the highlighted range
1240 runs.push(run_style.to_run(highlight_range.len()));
1241 offset = highlight_range.end;
1242 }
1243
1244 // Add any remaining un-highlighted text
1245 if offset < text.len() {
1246 runs.push(text_style.to_run(text.len() - offset));
1247 }
1248
1249 return StyledText::new(text).with_runs(runs);
1250 }
1251}
1252
1253#[derive(PartialEq)]
1254enum InputError {
1255 Warning(SharedString),
1256 Error(SharedString),
1257}
1258
1259impl InputError {
1260 fn warning(message: impl Into<SharedString>) -> Self {
1261 Self::Warning(message.into())
1262 }
1263
1264 fn error(message: impl Into<SharedString>) -> Self {
1265 Self::Error(message.into())
1266 }
1267
1268 fn content(&self) -> &SharedString {
1269 match self {
1270 InputError::Warning(content) | InputError::Error(content) => content,
1271 }
1272 }
1273
1274 fn is_warning(&self) -> bool {
1275 matches!(self, InputError::Warning(_))
1276 }
1277}
1278
1279struct KeybindingEditorModal {
1280 creating: bool,
1281 editing_keybind: ProcessedKeybinding,
1282 editing_keybind_idx: usize,
1283 keybind_editor: Entity<KeystrokeInput>,
1284 context_editor: Entity<Editor>,
1285 input_editor: Option<Entity<Editor>>,
1286 fs: Arc<dyn Fs>,
1287 error: Option<InputError>,
1288 keymap_editor: Entity<KeymapEditor>,
1289}
1290
1291impl ModalView for KeybindingEditorModal {}
1292
1293impl EventEmitter<DismissEvent> for KeybindingEditorModal {}
1294
1295impl Focusable for KeybindingEditorModal {
1296 fn focus_handle(&self, cx: &App) -> FocusHandle {
1297 self.keybind_editor.focus_handle(cx)
1298 }
1299}
1300
1301impl KeybindingEditorModal {
1302 pub fn new(
1303 create: bool,
1304 editing_keybind: ProcessedKeybinding,
1305 editing_keybind_idx: usize,
1306 keymap_editor: Entity<KeymapEditor>,
1307 workspace: WeakEntity<Workspace>,
1308 fs: Arc<dyn Fs>,
1309 window: &mut Window,
1310 cx: &mut App,
1311 ) -> Self {
1312 let keybind_editor = cx
1313 .new(|cx| KeystrokeInput::new(editing_keybind.keystrokes().map(Vec::from), window, cx));
1314
1315 let context_editor = cx.new(|cx| {
1316 let mut editor = Editor::single_line(window, cx);
1317
1318 if let Some(context) = editing_keybind
1319 .context
1320 .as_ref()
1321 .and_then(KeybindContextString::local)
1322 {
1323 editor.set_text(context.clone(), window, cx);
1324 } else {
1325 editor.set_placeholder_text("Keybinding context", cx);
1326 }
1327
1328 cx.spawn(async |editor, cx| {
1329 let contexts = cx
1330 .background_spawn(async { collect_contexts_from_assets() })
1331 .await;
1332
1333 editor
1334 .update(cx, |editor, _cx| {
1335 editor.set_completion_provider(Some(std::rc::Rc::new(
1336 KeyContextCompletionProvider { contexts },
1337 )));
1338 })
1339 .context("Failed to load completions for keybinding context")
1340 })
1341 .detach_and_log_err(cx);
1342
1343 editor
1344 });
1345
1346 let input_editor = editing_keybind.action_schema.clone().map(|_schema| {
1347 cx.new(|cx| {
1348 let mut editor = Editor::auto_height_unbounded(1, window, cx);
1349 if let Some(input) = editing_keybind.action_input.clone() {
1350 editor.set_text(input.text, window, cx);
1351 } else {
1352 // TODO: default value from schema?
1353 editor.set_placeholder_text("Action input", cx);
1354 }
1355 cx.spawn(async |editor, cx| {
1356 let json_language = load_json_language(workspace, cx).await;
1357 editor
1358 .update(cx, |editor, cx| {
1359 if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
1360 buffer.update(cx, |buffer, cx| {
1361 buffer.set_language(Some(json_language), cx)
1362 });
1363 }
1364 })
1365 .context("Failed to load JSON language for editing keybinding action input")
1366 })
1367 .detach_and_log_err(cx);
1368 editor
1369 })
1370 });
1371
1372 Self {
1373 creating: create,
1374 editing_keybind,
1375 editing_keybind_idx,
1376 fs,
1377 keybind_editor,
1378 context_editor,
1379 input_editor,
1380 error: None,
1381 keymap_editor,
1382 }
1383 }
1384
1385 fn set_error(&mut self, error: InputError, cx: &mut Context<Self>) -> bool {
1386 if self
1387 .error
1388 .as_ref()
1389 .is_some_and(|old_error| old_error.is_warning() && *old_error == error)
1390 {
1391 false
1392 } else {
1393 self.error = Some(error);
1394 cx.notify();
1395 true
1396 }
1397 }
1398
1399 fn validate_action_input(&self, cx: &App) -> anyhow::Result<Option<String>> {
1400 let input = self
1401 .input_editor
1402 .as_ref()
1403 .map(|editor| editor.read(cx).text(cx));
1404
1405 let value = input
1406 .as_ref()
1407 .map(|input| {
1408 serde_json::from_str(input).context("Failed to parse action input as JSON")
1409 })
1410 .transpose()?;
1411
1412 cx.build_action(&self.editing_keybind.action_name, value)
1413 .context("Failed to validate action input")?;
1414 Ok(input)
1415 }
1416
1417 fn save(&mut self, cx: &mut Context<Self>) {
1418 let existing_keybind = self.editing_keybind.clone();
1419 let fs = self.fs.clone();
1420 let new_keystrokes = self
1421 .keybind_editor
1422 .read_with(cx, |editor, _| editor.keystrokes().to_vec());
1423 if new_keystrokes.is_empty() {
1424 self.set_error(InputError::error("Keystrokes cannot be empty"), cx);
1425 return;
1426 }
1427 let tab_size = cx.global::<settings::SettingsStore>().json_tab_size();
1428 let new_context = self
1429 .context_editor
1430 .read_with(cx, |editor, cx| editor.text(cx));
1431 let new_context = new_context.is_empty().not().then_some(new_context);
1432 let new_context_err = new_context.as_deref().and_then(|context| {
1433 gpui::KeyBindingContextPredicate::parse(context)
1434 .context("Failed to parse key context")
1435 .err()
1436 });
1437 if let Some(err) = new_context_err {
1438 // TODO: store and display as separate error
1439 // TODO: also, should be validating on keystroke
1440 self.set_error(InputError::error(err.to_string()), cx);
1441 return;
1442 }
1443
1444 let new_input = match self.validate_action_input(cx) {
1445 Err(input_err) => {
1446 self.set_error(InputError::error(input_err.to_string()), cx);
1447 return;
1448 }
1449 Ok(input) => input,
1450 };
1451
1452 let action_mapping: ActionMapping = (
1453 ui::text_for_keystrokes(&new_keystrokes, cx).into(),
1454 new_context
1455 .as_ref()
1456 .map(Into::into)
1457 .or_else(|| existing_keybind.get_action_mapping().1),
1458 );
1459
1460 if let Some(conflicting_indices) = self
1461 .keymap_editor
1462 .read(cx)
1463 .keybinding_conflict_state
1464 .conflicting_indices_for_mapping(action_mapping, self.editing_keybind_idx)
1465 {
1466 let first_conflicting_index = conflicting_indices[0];
1467 let conflicting_action_name = self
1468 .keymap_editor
1469 .read(cx)
1470 .keybindings
1471 .get(first_conflicting_index)
1472 .map(|keybind| keybind.action_name.clone());
1473
1474 let warning_message = match conflicting_action_name {
1475 Some(name) => {
1476 let confliction_action_amount = conflicting_indices.len() - 1;
1477 if confliction_action_amount > 0 {
1478 format!(
1479 "Your keybind would conflict with the \"{}\" action and {} other bindings",
1480 name, confliction_action_amount
1481 )
1482 } else {
1483 format!("Your keybind would conflict with the \"{}\" action", name)
1484 }
1485 }
1486 None => {
1487 log::info!(
1488 "Could not find action in keybindings with index {}",
1489 first_conflicting_index
1490 );
1491 "Your keybind would conflict with other actions".to_string()
1492 }
1493 };
1494
1495 if self.set_error(InputError::warning(warning_message), cx) {
1496 return;
1497 }
1498 }
1499
1500 let create = self.creating;
1501
1502 cx.spawn(async move |this, cx| {
1503 if let Err(err) = save_keybinding_update(
1504 create,
1505 existing_keybind,
1506 &new_keystrokes,
1507 new_context.as_deref(),
1508 new_input.as_deref(),
1509 &fs,
1510 tab_size,
1511 )
1512 .await
1513 {
1514 this.update(cx, |this, cx| {
1515 this.set_error(InputError::error(err.to_string()), cx);
1516 })
1517 .log_err();
1518 } else {
1519 this.update(cx, |_this, cx| {
1520 cx.emit(DismissEvent);
1521 })
1522 .ok();
1523 }
1524 })
1525 .detach();
1526 }
1527}
1528
1529impl Render for KeybindingEditorModal {
1530 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1531 let theme = cx.theme().colors();
1532 let input_base = || {
1533 div()
1534 .w_full()
1535 .py_2()
1536 .px_3()
1537 .min_h_8()
1538 .rounded_md()
1539 .bg(theme.editor_background)
1540 .border_1()
1541 .border_color(theme.border_variant)
1542 };
1543
1544 v_flex()
1545 .w(rems(34.))
1546 .elevation_3(cx)
1547 .child(
1548 v_flex()
1549 .p_3()
1550 .child(Label::new("Edit Keystroke"))
1551 .child(
1552 Label::new("Input the desired keystroke for the selected action.")
1553 .color(Color::Muted)
1554 .mb_2(),
1555 )
1556 .child(self.keybind_editor.clone()),
1557 )
1558 .when_some(self.input_editor.clone(), |this, editor| {
1559 this.child(
1560 v_flex()
1561 .p_3()
1562 .pt_0()
1563 .child(Label::new("Edit Input"))
1564 .child(
1565 Label::new("Input the desired input to the binding.")
1566 .color(Color::Muted)
1567 .mb_2(),
1568 )
1569 .child(input_base().child(editor)),
1570 )
1571 })
1572 .child(
1573 v_flex()
1574 .p_3()
1575 .pt_0()
1576 .child(Label::new("Edit Context"))
1577 .child(
1578 Label::new("Input the desired context for the binding.")
1579 .color(Color::Muted)
1580 .mb_2(),
1581 )
1582 .child(input_base().child(self.context_editor.clone())),
1583 )
1584 .when_some(self.error.as_ref(), |this, error| {
1585 this.child(
1586 div().p_2().child(
1587 Banner::new()
1588 .map(|banner| match error {
1589 InputError::Error(_) => banner.severity(ui::Severity::Error),
1590 InputError::Warning(_) => banner.severity(ui::Severity::Warning),
1591 })
1592 // For some reason, the div overflows its container to the
1593 // right. The padding accounts for that.
1594 .child(div().size_full().pr_2().child(Label::new(error.content()))),
1595 ),
1596 )
1597 })
1598 .child(
1599 h_flex()
1600 .p_2()
1601 .w_full()
1602 .gap_1()
1603 .justify_end()
1604 .border_t_1()
1605 .border_color(theme.border_variant)
1606 .child(
1607 Button::new("cancel", "Cancel")
1608 .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
1609 )
1610 .child(
1611 Button::new("save-btn", "Save").on_click(
1612 cx.listener(|this, _event, _window, cx| Self::save(this, cx)),
1613 ),
1614 ),
1615 )
1616 }
1617}
1618
1619struct KeyContextCompletionProvider {
1620 contexts: Vec<SharedString>,
1621}
1622
1623impl CompletionProvider for KeyContextCompletionProvider {
1624 fn completions(
1625 &self,
1626 _excerpt_id: editor::ExcerptId,
1627 buffer: &Entity<language::Buffer>,
1628 buffer_position: language::Anchor,
1629 _trigger: editor::CompletionContext,
1630 _window: &mut Window,
1631 cx: &mut Context<Editor>,
1632 ) -> gpui::Task<anyhow::Result<Vec<project::CompletionResponse>>> {
1633 let buffer = buffer.read(cx);
1634 let mut count_back = 0;
1635 for char in buffer.reversed_chars_at(buffer_position) {
1636 if char.is_ascii_alphanumeric() || char == '_' {
1637 count_back += 1;
1638 } else {
1639 break;
1640 }
1641 }
1642 let start_anchor = buffer.anchor_before(
1643 buffer_position
1644 .to_offset(&buffer)
1645 .saturating_sub(count_back),
1646 );
1647 let replace_range = start_anchor..buffer_position;
1648 gpui::Task::ready(Ok(vec![project::CompletionResponse {
1649 completions: self
1650 .contexts
1651 .iter()
1652 .map(|context| project::Completion {
1653 replace_range: replace_range.clone(),
1654 label: language::CodeLabel::plain(context.to_string(), None),
1655 new_text: context.to_string(),
1656 documentation: None,
1657 source: project::CompletionSource::Custom,
1658 icon_path: None,
1659 insert_text_mode: None,
1660 confirm: None,
1661 })
1662 .collect(),
1663 is_incomplete: false,
1664 }]))
1665 }
1666
1667 fn is_completion_trigger(
1668 &self,
1669 _buffer: &Entity<language::Buffer>,
1670 _position: language::Anchor,
1671 text: &str,
1672 _trigger_in_words: bool,
1673 _menu_is_open: bool,
1674 _cx: &mut Context<Editor>,
1675 ) -> bool {
1676 text.chars().last().map_or(false, |last_char| {
1677 last_char.is_ascii_alphanumeric() || last_char == '_'
1678 })
1679 }
1680}
1681
1682async fn load_json_language(workspace: WeakEntity<Workspace>, cx: &mut AsyncApp) -> Arc<Language> {
1683 let json_language_task = workspace
1684 .read_with(cx, |workspace, cx| {
1685 workspace
1686 .project()
1687 .read(cx)
1688 .languages()
1689 .language_for_name("JSON")
1690 })
1691 .context("Failed to load JSON language")
1692 .log_err();
1693 let json_language = match json_language_task {
1694 Some(task) => task.await.context("Failed to load JSON language").log_err(),
1695 None => None,
1696 };
1697 return json_language.unwrap_or_else(|| {
1698 Arc::new(Language::new(
1699 LanguageConfig {
1700 name: "JSON".into(),
1701 ..Default::default()
1702 },
1703 Some(tree_sitter_json::LANGUAGE.into()),
1704 ))
1705 });
1706}
1707
1708async fn load_rust_language(workspace: WeakEntity<Workspace>, cx: &mut AsyncApp) -> Arc<Language> {
1709 let rust_language_task = workspace
1710 .read_with(cx, |workspace, cx| {
1711 workspace
1712 .project()
1713 .read(cx)
1714 .languages()
1715 .language_for_name("Rust")
1716 })
1717 .context("Failed to load Rust language")
1718 .log_err();
1719 let rust_language = match rust_language_task {
1720 Some(task) => task.await.context("Failed to load Rust language").log_err(),
1721 None => None,
1722 };
1723 return rust_language.unwrap_or_else(|| {
1724 Arc::new(Language::new(
1725 LanguageConfig {
1726 name: "Rust".into(),
1727 ..Default::default()
1728 },
1729 Some(tree_sitter_rust::LANGUAGE.into()),
1730 ))
1731 });
1732}
1733
1734async fn save_keybinding_update(
1735 create: bool,
1736 existing: ProcessedKeybinding,
1737 new_keystrokes: &[Keystroke],
1738 new_context: Option<&str>,
1739 new_input: Option<&str>,
1740 fs: &Arc<dyn Fs>,
1741 tab_size: usize,
1742) -> anyhow::Result<()> {
1743 let keymap_contents = settings::KeymapFile::load_keymap_file(fs)
1744 .await
1745 .context("Failed to load keymap file")?;
1746
1747 let operation = if !create {
1748 let existing_keystrokes = existing.keystrokes().unwrap_or_default();
1749 let existing_context = existing
1750 .context
1751 .as_ref()
1752 .and_then(KeybindContextString::local_str);
1753 let existing_input = existing
1754 .action_input
1755 .as_ref()
1756 .map(|input| input.text.as_ref());
1757
1758 settings::KeybindUpdateOperation::Replace {
1759 target: settings::KeybindUpdateTarget {
1760 context: existing_context,
1761 keystrokes: existing_keystrokes,
1762 action_name: &existing.action_name,
1763 use_key_equivalents: false,
1764 input: existing_input,
1765 },
1766 target_keybind_source: existing
1767 .source
1768 .as_ref()
1769 .map(|(source, _name)| *source)
1770 .unwrap_or(KeybindSource::User),
1771 source: settings::KeybindUpdateTarget {
1772 context: new_context,
1773 keystrokes: new_keystrokes,
1774 action_name: &existing.action_name,
1775 use_key_equivalents: false,
1776 input: new_input,
1777 },
1778 }
1779 } else {
1780 settings::KeybindUpdateOperation::Add(settings::KeybindUpdateTarget {
1781 context: new_context,
1782 keystrokes: new_keystrokes,
1783 action_name: &existing.action_name,
1784 use_key_equivalents: false,
1785 input: new_input,
1786 })
1787 };
1788 let updated_keymap_contents =
1789 settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size)
1790 .context("Failed to update keybinding")?;
1791 fs.atomic_write(paths::keymap_file().clone(), updated_keymap_contents)
1792 .await
1793 .context("Failed to write keymap file")?;
1794 Ok(())
1795}
1796
1797async fn remove_keybinding(
1798 existing: ProcessedKeybinding,
1799 fs: &Arc<dyn Fs>,
1800 tab_size: usize,
1801) -> anyhow::Result<()> {
1802 let Some(keystrokes) = existing.keystrokes() else {
1803 anyhow::bail!("Cannot remove a keybinding that does not exist");
1804 };
1805 let keymap_contents = settings::KeymapFile::load_keymap_file(fs)
1806 .await
1807 .context("Failed to load keymap file")?;
1808
1809 let operation = settings::KeybindUpdateOperation::Remove {
1810 target: settings::KeybindUpdateTarget {
1811 context: existing
1812 .context
1813 .as_ref()
1814 .and_then(KeybindContextString::local_str),
1815 keystrokes,
1816 action_name: &existing.action_name,
1817 use_key_equivalents: false,
1818 input: existing
1819 .action_input
1820 .as_ref()
1821 .map(|input| input.text.as_ref()),
1822 },
1823 target_keybind_source: existing
1824 .source
1825 .as_ref()
1826 .map(|(source, _name)| *source)
1827 .unwrap_or(KeybindSource::User),
1828 };
1829
1830 let updated_keymap_contents =
1831 settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size)
1832 .context("Failed to update keybinding")?;
1833 fs.atomic_write(paths::keymap_file().clone(), updated_keymap_contents)
1834 .await
1835 .context("Failed to write keymap file")?;
1836 Ok(())
1837}
1838
1839struct KeystrokeInput {
1840 keystrokes: Vec<Keystroke>,
1841 placeholder_keystrokes: Option<Vec<Keystroke>>,
1842 highlight_on_focus: bool,
1843 outer_focus_handle: FocusHandle,
1844 inner_focus_handle: FocusHandle,
1845 intercept_subscription: Option<Subscription>,
1846 _focus_subscriptions: [Subscription; 2],
1847}
1848
1849impl KeystrokeInput {
1850 fn new(
1851 placeholder_keystrokes: Option<Vec<Keystroke>>,
1852 window: &mut Window,
1853 cx: &mut Context<Self>,
1854 ) -> Self {
1855 let outer_focus_handle = cx.focus_handle();
1856 let inner_focus_handle = cx.focus_handle();
1857 let _focus_subscriptions = [
1858 cx.on_focus_in(&inner_focus_handle, window, Self::on_inner_focus_in),
1859 cx.on_focus_out(&inner_focus_handle, window, Self::on_inner_focus_out),
1860 ];
1861 Self {
1862 keystrokes: Vec::new(),
1863 placeholder_keystrokes,
1864 highlight_on_focus: true,
1865 inner_focus_handle,
1866 outer_focus_handle,
1867 intercept_subscription: None,
1868 _focus_subscriptions,
1869 }
1870 }
1871
1872 fn on_modifiers_changed(
1873 &mut self,
1874 event: &ModifiersChangedEvent,
1875 _window: &mut Window,
1876 cx: &mut Context<Self>,
1877 ) {
1878 if let Some(last) = self.keystrokes.last_mut()
1879 && last.key.is_empty()
1880 {
1881 if !event.modifiers.modified() {
1882 self.keystrokes.pop();
1883 cx.emit(());
1884 } else {
1885 last.modifiers = event.modifiers;
1886 }
1887 } else {
1888 self.keystrokes.push(Keystroke {
1889 modifiers: event.modifiers,
1890 key: "".to_string(),
1891 key_char: None,
1892 });
1893 cx.emit(());
1894 }
1895 cx.stop_propagation();
1896 cx.notify();
1897 }
1898
1899 fn handle_keystroke(&mut self, keystroke: &Keystroke, cx: &mut Context<Self>) {
1900 if let Some(last) = self.keystrokes.last_mut()
1901 && last.key.is_empty()
1902 {
1903 *last = keystroke.clone();
1904 } else if Some(keystroke) != self.keystrokes.last() {
1905 self.keystrokes.push(keystroke.clone());
1906 }
1907 cx.emit(());
1908 cx.stop_propagation();
1909 cx.notify();
1910 }
1911
1912 fn on_key_up(
1913 &mut self,
1914 event: &gpui::KeyUpEvent,
1915 _window: &mut Window,
1916 cx: &mut Context<Self>,
1917 ) {
1918 if let Some(last) = self.keystrokes.last_mut()
1919 && !last.key.is_empty()
1920 && last.modifiers == event.keystroke.modifiers
1921 {
1922 cx.emit(());
1923 self.keystrokes.push(Keystroke {
1924 modifiers: event.keystroke.modifiers,
1925 key: "".to_string(),
1926 key_char: None,
1927 });
1928 }
1929 cx.stop_propagation();
1930 cx.notify();
1931 }
1932
1933 fn on_inner_focus_in(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
1934 if self.intercept_subscription.is_none() {
1935 let listener = cx.listener(|this, event: &gpui::KeystrokeEvent, _window, cx| {
1936 this.handle_keystroke(&event.keystroke, cx);
1937 });
1938 self.intercept_subscription = Some(cx.intercept_keystrokes(listener))
1939 }
1940 }
1941
1942 fn on_inner_focus_out(
1943 &mut self,
1944 _event: gpui::FocusOutEvent,
1945 _window: &mut Window,
1946 cx: &mut Context<Self>,
1947 ) {
1948 self.intercept_subscription.take();
1949 cx.notify();
1950 }
1951
1952 fn keystrokes(&self) -> &[Keystroke] {
1953 if let Some(placeholders) = self.placeholder_keystrokes.as_ref()
1954 && self.keystrokes.is_empty()
1955 {
1956 return placeholders;
1957 }
1958 if self
1959 .keystrokes
1960 .last()
1961 .map_or(false, |last| last.key.is_empty())
1962 {
1963 return &self.keystrokes[..self.keystrokes.len() - 1];
1964 }
1965 return &self.keystrokes;
1966 }
1967
1968 fn render_keystrokes(&self) -> impl Iterator<Item = Div> {
1969 let (keystrokes, color) = if let Some(placeholders) = self.placeholder_keystrokes.as_ref()
1970 && self.keystrokes.is_empty()
1971 {
1972 (placeholders, Color::Placeholder)
1973 } else {
1974 (&self.keystrokes, Color::Default)
1975 };
1976 keystrokes.iter().map(move |keystroke| {
1977 h_flex().children(ui::render_keystroke(
1978 keystroke,
1979 Some(color),
1980 Some(rems(0.875).into()),
1981 ui::PlatformStyle::platform(),
1982 false,
1983 ))
1984 })
1985 }
1986}
1987
1988impl EventEmitter<()> for KeystrokeInput {}
1989
1990impl Focusable for KeystrokeInput {
1991 fn focus_handle(&self, _cx: &App) -> FocusHandle {
1992 self.outer_focus_handle.clone()
1993 }
1994}
1995
1996impl Render for KeystrokeInput {
1997 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1998 let colors = cx.theme().colors();
1999 let is_inner_focused = self.inner_focus_handle.is_focused(window);
2000
2001 return h_flex()
2002 .id("keystroke-input")
2003 .track_focus(&self.outer_focus_handle)
2004 .py_2()
2005 .px_3()
2006 .gap_2()
2007 .min_h_8()
2008 .w_full()
2009 .flex_1()
2010 .justify_between()
2011 .rounded_md()
2012 .overflow_hidden()
2013 .bg(colors.editor_background)
2014 .border_2()
2015 .border_color(colors.border_variant)
2016 .focus(|mut s| {
2017 s.border_color = Some(colors.border_focused);
2018 s
2019 })
2020 .on_key_down(cx.listener(|this, event: &KeyDownEvent, window, cx| {
2021 // TODO: replace with action
2022 if !event.keystroke.modifiers.modified() && event.keystroke.key == "enter" {
2023 window.focus(&this.inner_focus_handle);
2024 cx.notify();
2025 }
2026 }))
2027 .child(
2028 h_flex()
2029 .id("keystroke-input-inner")
2030 .track_focus(&self.inner_focus_handle)
2031 .on_modifiers_changed(cx.listener(Self::on_modifiers_changed))
2032 .on_key_up(cx.listener(Self::on_key_up))
2033 .when(self.highlight_on_focus, |this| {
2034 this.focus(|mut style| {
2035 style.border_color = Some(colors.border_focused);
2036 style
2037 })
2038 })
2039 .w_full()
2040 .min_w_0()
2041 .justify_center()
2042 .flex_wrap()
2043 .gap(ui::DynamicSpacing::Base04.rems(cx))
2044 .children(self.render_keystrokes()),
2045 )
2046 .child(
2047 h_flex()
2048 .gap_0p5()
2049 .flex_none()
2050 .when(is_inner_focused, |this| {
2051 this.child(
2052 Icon::new(IconName::Circle)
2053 .color(Color::Error)
2054 .with_animation(
2055 "recording-pulse",
2056 gpui::Animation::new(std::time::Duration::from_secs(1))
2057 .repeat()
2058 .with_easing(gpui::pulsating_between(0.8, 1.0)),
2059 {
2060 let color = Color::Error.color(cx);
2061 move |this, delta| {
2062 this.color(Color::Custom(color.opacity(delta)))
2063 }
2064 },
2065 ),
2066 )
2067 })
2068 .child(
2069 IconButton::new("backspace-btn", IconName::Delete)
2070 .tooltip(Tooltip::text("Delete Keystroke"))
2071 .when(!is_inner_focused, |this| this.icon_color(Color::Muted))
2072 .on_click(cx.listener(|this, _event, _window, cx| {
2073 this.keystrokes.pop();
2074 cx.emit(());
2075 cx.notify();
2076 })),
2077 )
2078 .child(
2079 IconButton::new("clear-btn", IconName::Eraser)
2080 .tooltip(Tooltip::text("Clear Keystrokes"))
2081 .when(!is_inner_focused, |this| this.icon_color(Color::Muted))
2082 .on_click(cx.listener(|this, _event, _window, cx| {
2083 this.keystrokes.clear();
2084 cx.emit(());
2085 cx.notify();
2086 })),
2087 ),
2088 );
2089 }
2090}
2091
2092fn collect_contexts_from_assets() -> Vec<SharedString> {
2093 let mut keymap_assets = vec![
2094 util::asset_str::<SettingsAssets>(settings::DEFAULT_KEYMAP_PATH),
2095 util::asset_str::<SettingsAssets>(settings::VIM_KEYMAP_PATH),
2096 ];
2097 keymap_assets.extend(
2098 BaseKeymap::OPTIONS
2099 .iter()
2100 .filter_map(|(_, base_keymap)| base_keymap.asset_path())
2101 .map(util::asset_str::<SettingsAssets>),
2102 );
2103
2104 let mut contexts = HashSet::default();
2105
2106 for keymap_asset in keymap_assets {
2107 let Ok(keymap) = KeymapFile::parse(&keymap_asset) else {
2108 continue;
2109 };
2110
2111 for section in keymap.sections() {
2112 let context_expr = §ion.context;
2113 let mut queue = Vec::new();
2114 let Ok(root_context) = gpui::KeyBindingContextPredicate::parse(context_expr) else {
2115 continue;
2116 };
2117
2118 queue.push(root_context);
2119 while let Some(context) = queue.pop() {
2120 match context {
2121 gpui::KeyBindingContextPredicate::Identifier(ident) => {
2122 contexts.insert(ident);
2123 }
2124 gpui::KeyBindingContextPredicate::Equal(ident_a, ident_b) => {
2125 contexts.insert(ident_a);
2126 contexts.insert(ident_b);
2127 }
2128 gpui::KeyBindingContextPredicate::NotEqual(ident_a, ident_b) => {
2129 contexts.insert(ident_a);
2130 contexts.insert(ident_b);
2131 }
2132 gpui::KeyBindingContextPredicate::Child(ctx_a, ctx_b) => {
2133 queue.push(*ctx_a);
2134 queue.push(*ctx_b);
2135 }
2136 gpui::KeyBindingContextPredicate::Not(ctx) => {
2137 queue.push(*ctx);
2138 }
2139 gpui::KeyBindingContextPredicate::And(ctx_a, ctx_b) => {
2140 queue.push(*ctx_a);
2141 queue.push(*ctx_b);
2142 }
2143 gpui::KeyBindingContextPredicate::Or(ctx_a, ctx_b) => {
2144 queue.push(*ctx_a);
2145 queue.push(*ctx_b);
2146 }
2147 }
2148 }
2149 }
2150 }
2151
2152 let mut contexts = contexts.into_iter().collect::<Vec<_>>();
2153 contexts.sort();
2154
2155 return contexts;
2156}
2157
2158impl SerializableItem for KeymapEditor {
2159 fn serialized_item_kind() -> &'static str {
2160 "KeymapEditor"
2161 }
2162
2163 fn cleanup(
2164 workspace_id: workspace::WorkspaceId,
2165 alive_items: Vec<workspace::ItemId>,
2166 _window: &mut Window,
2167 cx: &mut App,
2168 ) -> gpui::Task<gpui::Result<()>> {
2169 workspace::delete_unloaded_items(
2170 alive_items,
2171 workspace_id,
2172 "keybinding_editors",
2173 &KEYBINDING_EDITORS,
2174 cx,
2175 )
2176 }
2177
2178 fn deserialize(
2179 _project: Entity<project::Project>,
2180 workspace: WeakEntity<Workspace>,
2181 workspace_id: workspace::WorkspaceId,
2182 item_id: workspace::ItemId,
2183 window: &mut Window,
2184 cx: &mut App,
2185 ) -> gpui::Task<gpui::Result<Entity<Self>>> {
2186 window.spawn(cx, async move |cx| {
2187 if KEYBINDING_EDITORS
2188 .get_keybinding_editor(item_id, workspace_id)?
2189 .is_some()
2190 {
2191 cx.update(|window, cx| cx.new(|cx| KeymapEditor::new(workspace, window, cx)))
2192 } else {
2193 Err(anyhow!("No keybinding editor to deserialize"))
2194 }
2195 })
2196 }
2197
2198 fn serialize(
2199 &mut self,
2200 workspace: &mut Workspace,
2201 item_id: workspace::ItemId,
2202 _closing: bool,
2203 _window: &mut Window,
2204 cx: &mut ui::Context<Self>,
2205 ) -> Option<gpui::Task<gpui::Result<()>>> {
2206 let workspace_id = workspace.database_id()?;
2207 Some(cx.background_spawn(async move {
2208 KEYBINDING_EDITORS
2209 .save_keybinding_editor(item_id, workspace_id)
2210 .await
2211 }))
2212 }
2213
2214 fn should_serialize(&self, _event: &Self::Event) -> bool {
2215 false
2216 }
2217}
2218
2219mod persistence {
2220 use db::{define_connection, query, sqlez_macros::sql};
2221 use workspace::WorkspaceDb;
2222
2223 define_connection! {
2224 pub static ref KEYBINDING_EDITORS: KeybindingEditorDb<WorkspaceDb> =
2225 &[sql!(
2226 CREATE TABLE keybinding_editors (
2227 workspace_id INTEGER,
2228 item_id INTEGER UNIQUE,
2229
2230 PRIMARY KEY(workspace_id, item_id),
2231 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
2232 ON DELETE CASCADE
2233 ) STRICT;
2234 )];
2235 }
2236
2237 impl KeybindingEditorDb {
2238 query! {
2239 pub async fn save_keybinding_editor(
2240 item_id: workspace::ItemId,
2241 workspace_id: workspace::WorkspaceId
2242 ) -> Result<()> {
2243 INSERT OR REPLACE INTO keybinding_editors(item_id, workspace_id)
2244 VALUES (?, ?)
2245 }
2246 }
2247
2248 query! {
2249 pub fn get_keybinding_editor(
2250 item_id: workspace::ItemId,
2251 workspace_id: workspace::WorkspaceId
2252 ) -> Result<Option<workspace::ItemId>> {
2253 SELECT item_id
2254 FROM keybinding_editors
2255 WHERE item_id = ? AND workspace_id = ?
2256 }
2257 }
2258 }
2259}