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