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