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