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