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