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(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("Copy action", Box::new(CopyAction))
636 .action_disabled_when(
637 selected_binding_has_no_context,
638 "Copy Context",
639 Box::new(CopyContext),
640 )
641 });
642
643 let context_menu_handle = context_menu.focus_handle(cx);
644 window.defer(cx, move |window, _cx| window.focus(&context_menu_handle));
645 let subscription = cx.subscribe_in(
646 &context_menu,
647 window,
648 |this, _, _: &DismissEvent, window, cx| {
649 this.dismiss_context_menu(window, cx);
650 },
651 );
652 (context_menu, position, subscription)
653 });
654
655 cx.notify();
656 }
657
658 fn dismiss_context_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) {
659 self.context_menu.take();
660 window.focus(&self.focus_handle);
661 cx.notify();
662 }
663
664 fn context_menu_deployed(&self) -> bool {
665 self.context_menu.is_some()
666 }
667
668 fn select_next(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) {
669 if let Some(selected) = self.selected_index {
670 let selected = selected + 1;
671 if selected >= self.matches.len() {
672 self.select_last(&Default::default(), window, cx);
673 } else {
674 self.selected_index = Some(selected);
675 self.scroll_to_item(selected, ScrollStrategy::Center, cx);
676 cx.notify();
677 }
678 } else {
679 self.select_first(&Default::default(), window, cx);
680 }
681 }
682
683 fn select_previous(
684 &mut self,
685 _: &menu::SelectPrevious,
686 window: &mut Window,
687 cx: &mut Context<Self>,
688 ) {
689 if let Some(selected) = self.selected_index {
690 if selected == 0 {
691 return;
692 }
693
694 let selected = selected - 1;
695
696 if selected >= self.matches.len() {
697 self.select_last(&Default::default(), window, cx);
698 } else {
699 self.selected_index = Some(selected);
700 self.scroll_to_item(selected, ScrollStrategy::Center, cx);
701 cx.notify();
702 }
703 } else {
704 self.select_last(&Default::default(), window, cx);
705 }
706 }
707
708 fn select_first(
709 &mut self,
710 _: &menu::SelectFirst,
711 _window: &mut Window,
712 cx: &mut Context<Self>,
713 ) {
714 if self.matches.get(0).is_some() {
715 self.selected_index = Some(0);
716 self.scroll_to_item(0, ScrollStrategy::Center, cx);
717 cx.notify();
718 }
719 }
720
721 fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
722 if self.matches.last().is_some() {
723 let index = self.matches.len() - 1;
724 self.selected_index = Some(index);
725 self.scroll_to_item(index, ScrollStrategy::Center, cx);
726 cx.notify();
727 }
728 }
729
730 fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
731 self.open_edit_keybinding_modal(false, window, cx);
732 }
733
734 fn open_edit_keybinding_modal(
735 &mut self,
736 create: bool,
737 window: &mut Window,
738 cx: &mut Context<Self>,
739 ) {
740 let Some((keybind_idx, keybind)) = self
741 .selected_keybind_idx()
742 .zip(self.selected_binding().cloned())
743 else {
744 return;
745 };
746 let keymap_editor = cx.entity();
747 self.workspace
748 .update(cx, |workspace, cx| {
749 let fs = workspace.app_state().fs.clone();
750 let workspace_weak = cx.weak_entity();
751 workspace.toggle_modal(window, cx, |window, cx| {
752 let modal = KeybindingEditorModal::new(
753 create,
754 keybind,
755 keybind_idx,
756 keymap_editor,
757 workspace_weak,
758 fs,
759 window,
760 cx,
761 );
762 window.focus(&modal.focus_handle(cx));
763 modal
764 });
765 })
766 .log_err();
767 }
768
769 fn edit_binding(&mut self, _: &EditBinding, window: &mut Window, cx: &mut Context<Self>) {
770 self.open_edit_keybinding_modal(false, window, cx);
771 }
772
773 fn create_binding(&mut self, _: &CreateBinding, window: &mut Window, cx: &mut Context<Self>) {
774 self.open_edit_keybinding_modal(true, window, cx);
775 }
776
777 fn delete_binding(&mut self, _: &DeleteBinding, window: &mut Window, cx: &mut Context<Self>) {
778 let Some(to_remove) = self.selected_binding().cloned() else {
779 return;
780 };
781 let Ok(fs) = self
782 .workspace
783 .read_with(cx, |workspace, _| workspace.app_state().fs.clone())
784 else {
785 return;
786 };
787 let tab_size = cx.global::<settings::SettingsStore>().json_tab_size();
788 cx.spawn(async move |_, _| remove_keybinding(to_remove, &fs, tab_size).await)
789 .detach_and_notify_err(window, cx);
790 }
791
792 fn copy_context_to_clipboard(
793 &mut self,
794 _: &CopyContext,
795 _window: &mut Window,
796 cx: &mut Context<Self>,
797 ) {
798 let context = self
799 .selected_binding()
800 .and_then(|binding| binding.context.as_ref())
801 .and_then(KeybindContextString::local_str)
802 .map(|context| context.to_string());
803 let Some(context) = context else {
804 return;
805 };
806 cx.write_to_clipboard(gpui::ClipboardItem::new_string(context.clone()));
807 }
808
809 fn copy_action_to_clipboard(
810 &mut self,
811 _: &CopyAction,
812 _window: &mut Window,
813 cx: &mut Context<Self>,
814 ) {
815 let action = self
816 .selected_binding()
817 .map(|binding| binding.action_name.to_string());
818 let Some(action) = action else {
819 return;
820 };
821 cx.write_to_clipboard(gpui::ClipboardItem::new_string(action.clone()));
822 }
823
824 fn toggle_conflict_filter(
825 &mut self,
826 _: &ToggleConflictFilter,
827 _: &mut Window,
828 cx: &mut Context<Self>,
829 ) {
830 self.filter_state = self.filter_state.invert();
831 self.update_matches(cx);
832 }
833
834 fn toggle_keystroke_search(
835 &mut self,
836 _: &ToggleKeystrokeSearch,
837 window: &mut Window,
838 cx: &mut Context<Self>,
839 ) {
840 self.search_mode = self.search_mode.invert();
841 self.update_matches(cx);
842
843 match self.search_mode {
844 SearchMode::KeyStroke => {
845 window.focus(&self.keystroke_editor.focus_handle(cx));
846 }
847 SearchMode::Normal => {}
848 }
849 }
850}
851
852#[derive(Clone)]
853struct ProcessedKeybinding {
854 keystroke_text: SharedString,
855 ui_key_binding: Option<ui::KeyBinding>,
856 action_name: SharedString,
857 action_input: Option<SyntaxHighlightedText>,
858 action_docs: Option<&'static str>,
859 action_schema: Option<schemars::Schema>,
860 context: Option<KeybindContextString>,
861 source: Option<(KeybindSource, SharedString)>,
862}
863
864impl ProcessedKeybinding {
865 fn get_action_mapping(&self) -> ActionMapping {
866 (
867 self.keystroke_text.clone(),
868 self.context
869 .as_ref()
870 .and_then(|context| context.local())
871 .cloned(),
872 )
873 }
874}
875
876#[derive(Clone, Debug, IntoElement, PartialEq, Eq, Hash)]
877enum KeybindContextString {
878 Global,
879 Local(SharedString, Arc<Language>),
880}
881
882impl KeybindContextString {
883 const GLOBAL: SharedString = SharedString::new_static("<global>");
884
885 pub fn local(&self) -> Option<&SharedString> {
886 match self {
887 KeybindContextString::Global => None,
888 KeybindContextString::Local(name, _) => Some(name),
889 }
890 }
891
892 pub fn local_str(&self) -> Option<&str> {
893 match self {
894 KeybindContextString::Global => None,
895 KeybindContextString::Local(name, _) => Some(name),
896 }
897 }
898}
899
900impl RenderOnce for KeybindContextString {
901 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
902 match self {
903 KeybindContextString::Global => {
904 muted_styled_text(KeybindContextString::GLOBAL.clone(), cx).into_any_element()
905 }
906 KeybindContextString::Local(name, language) => {
907 SyntaxHighlightedText::new(name, language).into_any_element()
908 }
909 }
910 }
911}
912
913fn muted_styled_text(text: SharedString, cx: &App) -> StyledText {
914 let len = text.len();
915 StyledText::new(text).with_highlights([(
916 0..len,
917 gpui::HighlightStyle::color(cx.theme().colors().text_muted),
918 )])
919}
920
921impl Item for KeymapEditor {
922 type Event = ();
923
924 fn tab_content_text(&self, _detail: usize, _cx: &App) -> ui::SharedString {
925 "Keymap Editor".into()
926 }
927}
928
929impl Render for KeymapEditor {
930 fn render(&mut self, window: &mut Window, cx: &mut ui::Context<Self>) -> impl ui::IntoElement {
931 let row_count = self.matches.len();
932 let theme = cx.theme();
933
934 v_flex()
935 .id("keymap-editor")
936 .track_focus(&self.focus_handle)
937 .key_context(self.dispatch_context(window, cx))
938 .on_action(cx.listener(Self::select_next))
939 .on_action(cx.listener(Self::select_previous))
940 .on_action(cx.listener(Self::select_first))
941 .on_action(cx.listener(Self::select_last))
942 .on_action(cx.listener(Self::focus_search))
943 .on_action(cx.listener(Self::confirm))
944 .on_action(cx.listener(Self::edit_binding))
945 .on_action(cx.listener(Self::create_binding))
946 .on_action(cx.listener(Self::delete_binding))
947 .on_action(cx.listener(Self::copy_action_to_clipboard))
948 .on_action(cx.listener(Self::copy_context_to_clipboard))
949 .on_action(cx.listener(Self::toggle_conflict_filter))
950 .on_action(cx.listener(Self::toggle_keystroke_search))
951 .size_full()
952 .p_2()
953 .gap_1()
954 .bg(theme.colors().editor_background)
955 .child(
956 h_flex()
957 .p_2()
958 .gap_1()
959 .key_context({
960 let mut context = KeyContext::new_with_defaults();
961 context.add("BufferSearchBar");
962 context
963 })
964 .child(
965 div()
966 .size_full()
967 .h_8()
968 .pl_2()
969 .pr_1()
970 .py_1()
971 .border_1()
972 .border_color(theme.colors().border)
973 .rounded_lg()
974 .child(self.filter_editor.clone()),
975 )
976 .child(
977 // TODO: Ask Mikyala if there's a way to get have items be aligned by horizontally
978 // without embedding a h_flex in another h_flex
979 h_flex()
980 .when(self.keybinding_conflict_state.any_conflicts(), |this| {
981 this.child(
982 IconButton::new("KeymapEditorConflictIcon", IconName::Warning)
983 .tooltip({
984 let filter_state = self.filter_state;
985
986 move |window, cx| {
987 Tooltip::for_action(
988 match filter_state {
989 FilterState::All => "Show conflicts",
990 FilterState::Conflicts => "Hide conflicts",
991 },
992 &ToggleConflictFilter,
993 window,
994 cx,
995 )
996 }
997 })
998 .selected_icon_color(Color::Error)
999 .toggle_state(matches!(
1000 self.filter_state,
1001 FilterState::Conflicts
1002 ))
1003 .on_click(|_, window, cx| {
1004 window.dispatch_action(
1005 ToggleConflictFilter.boxed_clone(),
1006 cx,
1007 );
1008 }),
1009 )
1010 })
1011 .child(
1012 IconButton::new("KeymapEditorToggleFiltersIcon", IconName::Filter)
1013 .tooltip(|window, cx| {
1014 Tooltip::for_action(
1015 "Toggle Keystroke Search",
1016 &ToggleKeystrokeSearch,
1017 window,
1018 cx,
1019 )
1020 })
1021 .toggle_state(matches!(self.search_mode, SearchMode::KeyStroke))
1022 .on_click(|_, window, cx| {
1023 window.dispatch_action(
1024 ToggleKeystrokeSearch.boxed_clone(),
1025 cx,
1026 );
1027 }),
1028 ),
1029 ),
1030 )
1031 .when(matches!(self.search_mode, SearchMode::KeyStroke), |this| {
1032 this.child(
1033 div()
1034 .child(self.keystroke_editor.clone())
1035 .border_1()
1036 .border_color(theme.colors().border)
1037 .rounded_lg(),
1038 )
1039 })
1040 .child(
1041 Table::new()
1042 .interactable(&self.table_interaction_state)
1043 .striped()
1044 .column_widths([rems(16.), rems(16.), rems(16.), rems(32.), rems(8.)])
1045 .header(["Action", "Arguments", "Keystrokes", "Context", "Source"])
1046 .uniform_list(
1047 "keymap-editor-table",
1048 row_count,
1049 cx.processor(move |this, range: Range<usize>, _window, cx| {
1050 let context_menu_deployed = this.context_menu_deployed();
1051 range
1052 .filter_map(|index| {
1053 let candidate_id = this.matches.get(index)?.candidate_id;
1054 let binding = &this.keybindings[candidate_id];
1055
1056 let action = div()
1057 .child(binding.action_name.clone())
1058 .id(("keymap action", index))
1059 .when(!context_menu_deployed, |this| {
1060 this.tooltip({
1061 let action_name = binding.action_name.clone();
1062 let action_docs = binding.action_docs;
1063 move |_, cx| {
1064 let action_tooltip = Tooltip::new(
1065 command_palette::humanize_action_name(
1066 &action_name,
1067 ),
1068 );
1069 let action_tooltip = match action_docs {
1070 Some(docs) => action_tooltip.meta(docs),
1071 None => action_tooltip,
1072 };
1073 cx.new(|_| action_tooltip).into()
1074 }
1075 })
1076 })
1077 .into_any_element();
1078 let keystrokes = binding.ui_key_binding.clone().map_or(
1079 binding.keystroke_text.clone().into_any_element(),
1080 IntoElement::into_any_element,
1081 );
1082 let action_input = match binding.action_input.clone() {
1083 Some(input) => input.into_any_element(),
1084 None => {
1085 if binding.action_schema.is_some() {
1086 muted_styled_text(NO_ACTION_ARGUMENTS_TEXT, cx)
1087 .into_any_element()
1088 } else {
1089 gpui::Empty.into_any_element()
1090 }
1091 }
1092 };
1093 let context = binding.context.clone().map_or(
1094 gpui::Empty.into_any_element(),
1095 |context| {
1096 let is_local = context.local().is_some();
1097
1098 div()
1099 .id(("keymap context", index))
1100 .child(context.clone())
1101 .when(is_local && !context_menu_deployed, |this| {
1102 this.tooltip(Tooltip::element({
1103 move |_, _| {
1104 context.clone().into_any_element()
1105 }
1106 }))
1107 })
1108 .into_any_element()
1109 },
1110 );
1111 let source = binding
1112 .source
1113 .clone()
1114 .map(|(_source, name)| name)
1115 .unwrap_or_default()
1116 .into_any_element();
1117 Some([action, action_input, keystrokes, context, source])
1118 })
1119 .collect()
1120 }),
1121 )
1122 .map_row(
1123 cx.processor(|this, (row_index, row): (usize, Div), _window, cx| {
1124 let is_conflict = this
1125 .matches
1126 .get(row_index)
1127 .map(|candidate| candidate.candidate_id)
1128 .is_some_and(|id| this.keybinding_conflict_state.has_conflict(&id));
1129 let is_selected = this.selected_index == Some(row_index);
1130
1131 let row = row
1132 .id(("keymap-table-row", row_index))
1133 .on_any_mouse_down(cx.listener(
1134 move |this,
1135 mouse_down_event: &gpui::MouseDownEvent,
1136 window,
1137 cx| {
1138 match mouse_down_event.button {
1139 MouseButton::Left => {
1140 this.select_index(row_index, cx);
1141 }
1142
1143 MouseButton::Right => {
1144 this.select_index(row_index, cx);
1145 this.create_context_menu(
1146 mouse_down_event.position,
1147 window,
1148 cx,
1149 );
1150 }
1151 _ => {}
1152 }
1153 },
1154 ))
1155 .on_click(cx.listener(
1156 move |this, event: &ClickEvent, window, cx| {
1157 if event.up.click_count == 2 {
1158 this.open_edit_keybinding_modal(false, window, cx);
1159 }
1160 },
1161 ))
1162 .border_2()
1163 .when(is_conflict, |row| {
1164 row.bg(cx.theme().status().error_background)
1165 })
1166 .when(is_selected, |row| {
1167 row.border_color(cx.theme().colors().panel_focused_border)
1168 });
1169
1170 row.into_any_element()
1171 }),
1172 ),
1173 )
1174 .on_scroll_wheel(cx.listener(|this, _, _, cx| {
1175 this.context_menu.take();
1176 cx.notify();
1177 }))
1178 .children(self.context_menu.as_ref().map(|(menu, position, _)| {
1179 deferred(
1180 anchored()
1181 .position(*position)
1182 .anchor(gpui::Corner::TopLeft)
1183 .child(menu.clone()),
1184 )
1185 .with_priority(1)
1186 }))
1187 }
1188}
1189
1190#[derive(Debug, Clone, IntoElement)]
1191struct SyntaxHighlightedText {
1192 text: SharedString,
1193 language: Arc<Language>,
1194}
1195
1196impl SyntaxHighlightedText {
1197 pub fn new(text: impl Into<SharedString>, language: Arc<Language>) -> Self {
1198 Self {
1199 text: text.into(),
1200 language,
1201 }
1202 }
1203}
1204
1205impl RenderOnce for SyntaxHighlightedText {
1206 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
1207 let text_style = window.text_style();
1208 let syntax_theme = cx.theme().syntax();
1209
1210 let text = self.text.clone();
1211
1212 let highlights = self
1213 .language
1214 .highlight_text(&text.as_ref().into(), 0..text.len());
1215 let mut runs = Vec::with_capacity(highlights.len());
1216 let mut offset = 0;
1217
1218 for (highlight_range, highlight_id) in highlights {
1219 // Add un-highlighted text before the current highlight
1220 if highlight_range.start > offset {
1221 runs.push(text_style.to_run(highlight_range.start - offset));
1222 }
1223
1224 let mut run_style = text_style.clone();
1225 if let Some(highlight_style) = highlight_id.style(syntax_theme) {
1226 run_style = run_style.highlight(highlight_style);
1227 }
1228 // add the highlighted range
1229 runs.push(run_style.to_run(highlight_range.len()));
1230 offset = highlight_range.end;
1231 }
1232
1233 // Add any remaining un-highlighted text
1234 if offset < text.len() {
1235 runs.push(text_style.to_run(text.len() - offset));
1236 }
1237
1238 return StyledText::new(text).with_runs(runs);
1239 }
1240}
1241
1242#[derive(PartialEq)]
1243enum InputError {
1244 Warning(SharedString),
1245 Error(SharedString),
1246}
1247
1248impl InputError {
1249 fn warning(message: impl Into<SharedString>) -> Self {
1250 Self::Warning(message.into())
1251 }
1252
1253 fn error(message: impl Into<SharedString>) -> Self {
1254 Self::Error(message.into())
1255 }
1256
1257 fn content(&self) -> &SharedString {
1258 match self {
1259 InputError::Warning(content) | InputError::Error(content) => content,
1260 }
1261 }
1262
1263 fn is_warning(&self) -> bool {
1264 matches!(self, InputError::Warning(_))
1265 }
1266}
1267
1268struct KeybindingEditorModal {
1269 creating: bool,
1270 editing_keybind: ProcessedKeybinding,
1271 editing_keybind_idx: usize,
1272 keybind_editor: Entity<KeystrokeInput>,
1273 context_editor: Entity<Editor>,
1274 input_editor: Option<Entity<Editor>>,
1275 fs: Arc<dyn Fs>,
1276 error: Option<InputError>,
1277 keymap_editor: Entity<KeymapEditor>,
1278}
1279
1280impl ModalView for KeybindingEditorModal {}
1281
1282impl EventEmitter<DismissEvent> for KeybindingEditorModal {}
1283
1284impl Focusable for KeybindingEditorModal {
1285 fn focus_handle(&self, cx: &App) -> FocusHandle {
1286 self.keybind_editor.focus_handle(cx)
1287 }
1288}
1289
1290impl KeybindingEditorModal {
1291 pub fn new(
1292 create: bool,
1293 editing_keybind: ProcessedKeybinding,
1294 editing_keybind_idx: usize,
1295 keymap_editor: Entity<KeymapEditor>,
1296 workspace: WeakEntity<Workspace>,
1297 fs: Arc<dyn Fs>,
1298 window: &mut Window,
1299 cx: &mut App,
1300 ) -> Self {
1301 let keybind_editor = cx.new(|cx| KeystrokeInput::new(window, cx));
1302
1303 let context_editor = cx.new(|cx| {
1304 let mut editor = Editor::single_line(window, cx);
1305
1306 if let Some(context) = editing_keybind
1307 .context
1308 .as_ref()
1309 .and_then(KeybindContextString::local)
1310 {
1311 editor.set_text(context.clone(), window, cx);
1312 } else {
1313 editor.set_placeholder_text("Keybinding context", cx);
1314 }
1315
1316 cx.spawn(async |editor, cx| {
1317 let contexts = cx
1318 .background_spawn(async { collect_contexts_from_assets() })
1319 .await;
1320
1321 editor
1322 .update(cx, |editor, _cx| {
1323 editor.set_completion_provider(Some(std::rc::Rc::new(
1324 KeyContextCompletionProvider { contexts },
1325 )));
1326 })
1327 .context("Failed to load completions for keybinding context")
1328 })
1329 .detach_and_log_err(cx);
1330
1331 editor
1332 });
1333
1334 let input_editor = editing_keybind.action_schema.clone().map(|_schema| {
1335 cx.new(|cx| {
1336 let mut editor = Editor::auto_height_unbounded(1, window, cx);
1337 if let Some(input) = editing_keybind.action_input.clone() {
1338 editor.set_text(input.text, window, cx);
1339 } else {
1340 // TODO: default value from schema?
1341 editor.set_placeholder_text("Action input", cx);
1342 }
1343 cx.spawn(async |editor, cx| {
1344 let json_language = load_json_language(workspace, cx).await;
1345 editor
1346 .update(cx, |editor, cx| {
1347 if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
1348 buffer.update(cx, |buffer, cx| {
1349 buffer.set_language(Some(json_language), cx)
1350 });
1351 }
1352 })
1353 .context("Failed to load JSON language for editing keybinding action input")
1354 })
1355 .detach_and_log_err(cx);
1356 editor
1357 })
1358 });
1359
1360 Self {
1361 creating: create,
1362 editing_keybind,
1363 editing_keybind_idx,
1364 fs,
1365 keybind_editor,
1366 context_editor,
1367 input_editor,
1368 error: None,
1369 keymap_editor,
1370 }
1371 }
1372
1373 fn set_error(&mut self, error: InputError, cx: &mut Context<Self>) -> bool {
1374 if self
1375 .error
1376 .as_ref()
1377 .is_some_and(|old_error| old_error.is_warning() && *old_error == error)
1378 {
1379 false
1380 } else {
1381 self.error = Some(error);
1382 cx.notify();
1383 true
1384 }
1385 }
1386
1387 fn save(&mut self, cx: &mut Context<Self>) {
1388 let existing_keybind = self.editing_keybind.clone();
1389 let fs = self.fs.clone();
1390 let new_keystrokes = self
1391 .keybind_editor
1392 .read_with(cx, |editor, _| editor.keystrokes().to_vec());
1393 if new_keystrokes.is_empty() {
1394 self.set_error(InputError::error("Keystrokes cannot be empty"), cx);
1395 return;
1396 }
1397 let tab_size = cx.global::<settings::SettingsStore>().json_tab_size();
1398 let new_context = self
1399 .context_editor
1400 .read_with(cx, |editor, cx| editor.text(cx));
1401 let new_context = new_context.is_empty().not().then_some(new_context);
1402 let new_context_err = new_context.as_deref().and_then(|context| {
1403 gpui::KeyBindingContextPredicate::parse(context)
1404 .context("Failed to parse key context")
1405 .err()
1406 });
1407 if let Some(err) = new_context_err {
1408 // TODO: store and display as separate error
1409 // TODO: also, should be validating on keystroke
1410 self.set_error(InputError::error(err.to_string()), cx);
1411 return;
1412 }
1413
1414 let action_mapping: ActionMapping = (
1415 ui::text_for_keystrokes(&new_keystrokes, cx).into(),
1416 new_context
1417 .as_ref()
1418 .map(Into::into)
1419 .or_else(|| existing_keybind.get_action_mapping().1),
1420 );
1421
1422 if let Some(conflicting_indices) = self
1423 .keymap_editor
1424 .read(cx)
1425 .keybinding_conflict_state
1426 .conflicting_indices_for_mapping(action_mapping, self.editing_keybind_idx)
1427 {
1428 let first_conflicting_index = conflicting_indices[0];
1429 let conflicting_action_name = self
1430 .keymap_editor
1431 .read(cx)
1432 .keybindings
1433 .get(first_conflicting_index)
1434 .map(|keybind| keybind.action_name.clone());
1435
1436 let warning_message = match conflicting_action_name {
1437 Some(name) => {
1438 let confliction_action_amount = conflicting_indices.len() - 1;
1439 if confliction_action_amount > 0 {
1440 format!(
1441 "Your keybind would conflict with the \"{}\" action and {} other bindings",
1442 name, confliction_action_amount
1443 )
1444 } else {
1445 format!("Your keybind would conflict with the \"{}\" action", name)
1446 }
1447 }
1448 None => {
1449 log::info!(
1450 "Could not find action in keybindings with index {}",
1451 first_conflicting_index
1452 );
1453 "Your keybind would conflict with other actions".to_string()
1454 }
1455 };
1456
1457 if self.set_error(InputError::warning(warning_message), cx) {
1458 return;
1459 }
1460 }
1461
1462 let create = self.creating;
1463
1464 cx.spawn(async move |this, cx| {
1465 if let Err(err) = save_keybinding_update(
1466 create,
1467 existing_keybind,
1468 &new_keystrokes,
1469 new_context.as_deref(),
1470 &fs,
1471 tab_size,
1472 )
1473 .await
1474 {
1475 this.update(cx, |this, cx| {
1476 this.set_error(InputError::error(err.to_string()), cx);
1477 })
1478 .log_err();
1479 } else {
1480 this.update(cx, |_this, cx| {
1481 cx.emit(DismissEvent);
1482 })
1483 .ok();
1484 }
1485 })
1486 .detach();
1487 }
1488}
1489
1490impl Render for KeybindingEditorModal {
1491 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1492 let theme = cx.theme().colors();
1493 let input_base = || {
1494 div()
1495 .w_full()
1496 .py_2()
1497 .px_3()
1498 .min_h_8()
1499 .rounded_md()
1500 .bg(theme.editor_background)
1501 .border_1()
1502 .border_color(theme.border_variant)
1503 };
1504
1505 v_flex()
1506 .w(rems(34.))
1507 .elevation_3(cx)
1508 .child(
1509 v_flex()
1510 .p_3()
1511 .child(Label::new("Edit Keystroke"))
1512 .child(
1513 Label::new("Input the desired keystroke for the selected action.")
1514 .color(Color::Muted)
1515 .mb_2(),
1516 )
1517 .child(self.keybind_editor.clone()),
1518 )
1519 .when_some(self.input_editor.clone(), |this, editor| {
1520 this.child(
1521 v_flex()
1522 .p_3()
1523 .pt_0()
1524 .child(Label::new("Edit Input"))
1525 .child(
1526 Label::new("Input the desired input to the binding.")
1527 .color(Color::Muted)
1528 .mb_2(),
1529 )
1530 .child(input_base().child(editor)),
1531 )
1532 })
1533 .child(
1534 v_flex()
1535 .p_3()
1536 .pt_0()
1537 .child(Label::new("Edit Context"))
1538 .child(
1539 Label::new("Input the desired context for the binding.")
1540 .color(Color::Muted)
1541 .mb_2(),
1542 )
1543 .child(input_base().child(self.context_editor.clone())),
1544 )
1545 .when_some(self.error.as_ref(), |this, error| {
1546 this.child(
1547 div().p_2().child(
1548 Banner::new()
1549 .map(|banner| match error {
1550 InputError::Error(_) => banner.severity(ui::Severity::Error),
1551 InputError::Warning(_) => banner.severity(ui::Severity::Warning),
1552 })
1553 // For some reason, the div overflows its container to the
1554 // right. The padding accounts for that.
1555 .child(div().size_full().pr_2().child(Label::new(error.content()))),
1556 ),
1557 )
1558 })
1559 .child(
1560 h_flex()
1561 .p_2()
1562 .w_full()
1563 .gap_1()
1564 .justify_end()
1565 .border_t_1()
1566 .border_color(theme.border_variant)
1567 .child(
1568 Button::new("cancel", "Cancel")
1569 .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
1570 )
1571 .child(
1572 Button::new("save-btn", "Save").on_click(
1573 cx.listener(|this, _event, _window, cx| Self::save(this, cx)),
1574 ),
1575 ),
1576 )
1577 }
1578}
1579
1580struct KeyContextCompletionProvider {
1581 contexts: Vec<SharedString>,
1582}
1583
1584impl CompletionProvider for KeyContextCompletionProvider {
1585 fn completions(
1586 &self,
1587 _excerpt_id: editor::ExcerptId,
1588 buffer: &Entity<language::Buffer>,
1589 buffer_position: language::Anchor,
1590 _trigger: editor::CompletionContext,
1591 _window: &mut Window,
1592 cx: &mut Context<Editor>,
1593 ) -> gpui::Task<anyhow::Result<Vec<project::CompletionResponse>>> {
1594 let buffer = buffer.read(cx);
1595 let mut count_back = 0;
1596 for char in buffer.reversed_chars_at(buffer_position) {
1597 if char.is_ascii_alphanumeric() || char == '_' {
1598 count_back += 1;
1599 } else {
1600 break;
1601 }
1602 }
1603 let start_anchor = buffer.anchor_before(
1604 buffer_position
1605 .to_offset(&buffer)
1606 .saturating_sub(count_back),
1607 );
1608 let replace_range = start_anchor..buffer_position;
1609 gpui::Task::ready(Ok(vec![project::CompletionResponse {
1610 completions: self
1611 .contexts
1612 .iter()
1613 .map(|context| project::Completion {
1614 replace_range: replace_range.clone(),
1615 label: language::CodeLabel::plain(context.to_string(), None),
1616 new_text: context.to_string(),
1617 documentation: None,
1618 source: project::CompletionSource::Custom,
1619 icon_path: None,
1620 insert_text_mode: None,
1621 confirm: None,
1622 })
1623 .collect(),
1624 is_incomplete: false,
1625 }]))
1626 }
1627
1628 fn is_completion_trigger(
1629 &self,
1630 _buffer: &Entity<language::Buffer>,
1631 _position: language::Anchor,
1632 text: &str,
1633 _trigger_in_words: bool,
1634 _menu_is_open: bool,
1635 _cx: &mut Context<Editor>,
1636 ) -> bool {
1637 text.chars().last().map_or(false, |last_char| {
1638 last_char.is_ascii_alphanumeric() || last_char == '_'
1639 })
1640 }
1641}
1642
1643async fn load_json_language(workspace: WeakEntity<Workspace>, cx: &mut AsyncApp) -> Arc<Language> {
1644 let json_language_task = workspace
1645 .read_with(cx, |workspace, cx| {
1646 workspace
1647 .project()
1648 .read(cx)
1649 .languages()
1650 .language_for_name("JSON")
1651 })
1652 .context("Failed to load JSON language")
1653 .log_err();
1654 let json_language = match json_language_task {
1655 Some(task) => task.await.context("Failed to load JSON language").log_err(),
1656 None => None,
1657 };
1658 return json_language.unwrap_or_else(|| {
1659 Arc::new(Language::new(
1660 LanguageConfig {
1661 name: "JSON".into(),
1662 ..Default::default()
1663 },
1664 Some(tree_sitter_json::LANGUAGE.into()),
1665 ))
1666 });
1667}
1668
1669async fn load_rust_language(workspace: WeakEntity<Workspace>, cx: &mut AsyncApp) -> Arc<Language> {
1670 let rust_language_task = workspace
1671 .read_with(cx, |workspace, cx| {
1672 workspace
1673 .project()
1674 .read(cx)
1675 .languages()
1676 .language_for_name("Rust")
1677 })
1678 .context("Failed to load Rust language")
1679 .log_err();
1680 let rust_language = match rust_language_task {
1681 Some(task) => task.await.context("Failed to load Rust language").log_err(),
1682 None => None,
1683 };
1684 return rust_language.unwrap_or_else(|| {
1685 Arc::new(Language::new(
1686 LanguageConfig {
1687 name: "Rust".into(),
1688 ..Default::default()
1689 },
1690 Some(tree_sitter_rust::LANGUAGE.into()),
1691 ))
1692 });
1693}
1694
1695async fn save_keybinding_update(
1696 create: bool,
1697 existing: ProcessedKeybinding,
1698 new_keystrokes: &[Keystroke],
1699 new_context: Option<&str>,
1700 fs: &Arc<dyn Fs>,
1701 tab_size: usize,
1702) -> anyhow::Result<()> {
1703 let keymap_contents = settings::KeymapFile::load_keymap_file(fs)
1704 .await
1705 .context("Failed to load keymap file")?;
1706
1707 let existing_keystrokes = existing
1708 .ui_key_binding
1709 .as_ref()
1710 .map(|keybinding| keybinding.keystrokes.as_slice())
1711 .unwrap_or_default();
1712
1713 let existing_context = existing
1714 .context
1715 .as_ref()
1716 .and_then(KeybindContextString::local_str);
1717
1718 let input = existing
1719 .action_input
1720 .as_ref()
1721 .map(|input| input.text.as_ref());
1722
1723 let operation = if !create {
1724 settings::KeybindUpdateOperation::Replace {
1725 target: settings::KeybindUpdateTarget {
1726 context: existing_context,
1727 keystrokes: existing_keystrokes,
1728 action_name: &existing.action_name,
1729 use_key_equivalents: false,
1730 input,
1731 },
1732 target_keybind_source: existing
1733 .source
1734 .map(|(source, _name)| source)
1735 .unwrap_or(KeybindSource::User),
1736 source: settings::KeybindUpdateTarget {
1737 context: new_context,
1738 keystrokes: new_keystrokes,
1739 action_name: &existing.action_name,
1740 use_key_equivalents: false,
1741 input,
1742 },
1743 }
1744 } else {
1745 settings::KeybindUpdateOperation::Add(settings::KeybindUpdateTarget {
1746 context: new_context,
1747 keystrokes: new_keystrokes,
1748 action_name: &existing.action_name,
1749 use_key_equivalents: false,
1750 input,
1751 })
1752 };
1753 let updated_keymap_contents =
1754 settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size)
1755 .context("Failed to update keybinding")?;
1756 fs.atomic_write(paths::keymap_file().clone(), updated_keymap_contents)
1757 .await
1758 .context("Failed to write keymap file")?;
1759 Ok(())
1760}
1761
1762async fn remove_keybinding(
1763 existing: ProcessedKeybinding,
1764 fs: &Arc<dyn Fs>,
1765 tab_size: usize,
1766) -> anyhow::Result<()> {
1767 let Some(ui_key_binding) = existing.ui_key_binding else {
1768 anyhow::bail!("Cannot remove a keybinding that does not exist");
1769 };
1770 let keymap_contents = settings::KeymapFile::load_keymap_file(fs)
1771 .await
1772 .context("Failed to load keymap file")?;
1773
1774 let operation = settings::KeybindUpdateOperation::Remove {
1775 target: settings::KeybindUpdateTarget {
1776 context: existing
1777 .context
1778 .as_ref()
1779 .and_then(KeybindContextString::local_str),
1780 keystrokes: &ui_key_binding.keystrokes,
1781 action_name: &existing.action_name,
1782 use_key_equivalents: false,
1783 input: existing
1784 .action_input
1785 .as_ref()
1786 .map(|input| input.text.as_ref()),
1787 },
1788 target_keybind_source: existing
1789 .source
1790 .map(|(source, _name)| source)
1791 .unwrap_or(KeybindSource::User),
1792 };
1793
1794 let updated_keymap_contents =
1795 settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size)
1796 .context("Failed to update keybinding")?;
1797 fs.atomic_write(paths::keymap_file().clone(), updated_keymap_contents)
1798 .await
1799 .context("Failed to write keymap file")?;
1800 Ok(())
1801}
1802
1803struct KeystrokeInput {
1804 keystrokes: Vec<Keystroke>,
1805 highlight_on_focus: bool,
1806 focus_handle: FocusHandle,
1807 intercept_subscription: Option<Subscription>,
1808 _focus_subscriptions: [Subscription; 2],
1809}
1810
1811impl KeystrokeInput {
1812 fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
1813 let focus_handle = cx.focus_handle();
1814 let _focus_subscriptions = [
1815 cx.on_focus_in(&focus_handle, window, Self::on_focus_in),
1816 cx.on_focus_out(&focus_handle, window, Self::on_focus_out),
1817 ];
1818 Self {
1819 keystrokes: Vec::new(),
1820 highlight_on_focus: true,
1821 focus_handle,
1822 intercept_subscription: None,
1823 _focus_subscriptions,
1824 }
1825 }
1826
1827 fn on_modifiers_changed(
1828 &mut self,
1829 event: &ModifiersChangedEvent,
1830 _window: &mut Window,
1831 cx: &mut Context<Self>,
1832 ) {
1833 if let Some(last) = self.keystrokes.last_mut()
1834 && last.key.is_empty()
1835 {
1836 if !event.modifiers.modified() {
1837 self.keystrokes.pop();
1838 cx.emit(());
1839 } else {
1840 last.modifiers = event.modifiers;
1841 }
1842 } else {
1843 self.keystrokes.push(Keystroke {
1844 modifiers: event.modifiers,
1845 key: "".to_string(),
1846 key_char: None,
1847 });
1848 cx.emit(());
1849 }
1850 cx.stop_propagation();
1851 cx.notify();
1852 }
1853
1854 fn handle_keystroke(&mut self, keystroke: &Keystroke, cx: &mut Context<Self>) {
1855 if let Some(last) = self.keystrokes.last_mut()
1856 && last.key.is_empty()
1857 {
1858 *last = keystroke.clone();
1859 } else if Some(keystroke) != self.keystrokes.last() {
1860 self.keystrokes.push(keystroke.clone());
1861 }
1862 cx.emit(());
1863 cx.stop_propagation();
1864 cx.notify();
1865 }
1866
1867 fn on_key_up(
1868 &mut self,
1869 event: &gpui::KeyUpEvent,
1870 _window: &mut Window,
1871 cx: &mut Context<Self>,
1872 ) {
1873 if let Some(last) = self.keystrokes.last_mut()
1874 && !last.key.is_empty()
1875 && last.modifiers == event.keystroke.modifiers
1876 {
1877 cx.emit(());
1878 self.keystrokes.push(Keystroke {
1879 modifiers: event.keystroke.modifiers,
1880 key: "".to_string(),
1881 key_char: None,
1882 });
1883 }
1884 cx.stop_propagation();
1885 cx.notify();
1886 }
1887
1888 fn on_focus_in(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
1889 if self.intercept_subscription.is_none() {
1890 let listener = cx.listener(|this, event: &gpui::KeystrokeEvent, _window, cx| {
1891 this.handle_keystroke(&event.keystroke, cx);
1892 });
1893 self.intercept_subscription = Some(cx.intercept_keystrokes(listener))
1894 }
1895 }
1896
1897 fn on_focus_out(
1898 &mut self,
1899 _event: gpui::FocusOutEvent,
1900 _window: &mut Window,
1901 _cx: &mut Context<Self>,
1902 ) {
1903 self.intercept_subscription.take();
1904 }
1905
1906 fn keystrokes(&self) -> &[Keystroke] {
1907 if self
1908 .keystrokes
1909 .last()
1910 .map_or(false, |last| last.key.is_empty())
1911 {
1912 return &self.keystrokes[..self.keystrokes.len() - 1];
1913 }
1914 return &self.keystrokes;
1915 }
1916}
1917
1918impl EventEmitter<()> for KeystrokeInput {}
1919
1920impl Focusable for KeystrokeInput {
1921 fn focus_handle(&self, _cx: &App) -> FocusHandle {
1922 self.focus_handle.clone()
1923 }
1924}
1925
1926impl Render for KeystrokeInput {
1927 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1928 let colors = cx.theme().colors();
1929 let is_focused = self.focus_handle.is_focused(window);
1930
1931 return h_flex()
1932 .id("keybinding_input")
1933 .track_focus(&self.focus_handle)
1934 .on_modifiers_changed(cx.listener(Self::on_modifiers_changed))
1935 .on_key_up(cx.listener(Self::on_key_up))
1936 .when(self.highlight_on_focus, |this| {
1937 this.focus(|mut style| {
1938 style.border_color = Some(colors.border_focused);
1939 style
1940 })
1941 })
1942 .py_2()
1943 .px_3()
1944 .gap_2()
1945 .min_h_8()
1946 .w_full()
1947 .flex_1()
1948 .justify_between()
1949 .rounded_md()
1950 .overflow_hidden()
1951 .bg(colors.editor_background)
1952 .border_1()
1953 .border_color(colors.border_variant)
1954 .child(
1955 h_flex()
1956 .w_full()
1957 .min_w_0()
1958 .justify_center()
1959 .flex_wrap()
1960 .gap(ui::DynamicSpacing::Base04.rems(cx))
1961 .children(self.keystrokes.iter().map(|keystroke| {
1962 h_flex().children(ui::render_keystroke(
1963 keystroke,
1964 None,
1965 Some(rems(0.875).into()),
1966 ui::PlatformStyle::platform(),
1967 false,
1968 ))
1969 })),
1970 )
1971 .child(
1972 h_flex()
1973 .gap_0p5()
1974 .flex_none()
1975 .child(
1976 IconButton::new("backspace-btn", IconName::Delete)
1977 .tooltip(Tooltip::text("Delete Keystroke"))
1978 .when(!is_focused, |this| this.icon_color(Color::Muted))
1979 .on_click(cx.listener(|this, _event, _window, cx| {
1980 this.keystrokes.pop();
1981 cx.emit(());
1982 cx.notify();
1983 })),
1984 )
1985 .child(
1986 IconButton::new("clear-btn", IconName::Eraser)
1987 .tooltip(Tooltip::text("Clear Keystrokes"))
1988 .when(!is_focused, |this| this.icon_color(Color::Muted))
1989 .on_click(cx.listener(|this, _event, _window, cx| {
1990 this.keystrokes.clear();
1991 cx.emit(());
1992 cx.notify();
1993 })),
1994 ),
1995 );
1996 }
1997}
1998
1999fn collect_contexts_from_assets() -> Vec<SharedString> {
2000 let mut keymap_assets = vec![
2001 util::asset_str::<SettingsAssets>(settings::DEFAULT_KEYMAP_PATH),
2002 util::asset_str::<SettingsAssets>(settings::VIM_KEYMAP_PATH),
2003 ];
2004 keymap_assets.extend(
2005 BaseKeymap::OPTIONS
2006 .iter()
2007 .filter_map(|(_, base_keymap)| base_keymap.asset_path())
2008 .map(util::asset_str::<SettingsAssets>),
2009 );
2010
2011 let mut contexts = HashSet::default();
2012
2013 for keymap_asset in keymap_assets {
2014 let Ok(keymap) = KeymapFile::parse(&keymap_asset) else {
2015 continue;
2016 };
2017
2018 for section in keymap.sections() {
2019 let context_expr = §ion.context;
2020 let mut queue = Vec::new();
2021 let Ok(root_context) = gpui::KeyBindingContextPredicate::parse(context_expr) else {
2022 continue;
2023 };
2024
2025 queue.push(root_context);
2026 while let Some(context) = queue.pop() {
2027 match context {
2028 gpui::KeyBindingContextPredicate::Identifier(ident) => {
2029 contexts.insert(ident);
2030 }
2031 gpui::KeyBindingContextPredicate::Equal(ident_a, ident_b) => {
2032 contexts.insert(ident_a);
2033 contexts.insert(ident_b);
2034 }
2035 gpui::KeyBindingContextPredicate::NotEqual(ident_a, ident_b) => {
2036 contexts.insert(ident_a);
2037 contexts.insert(ident_b);
2038 }
2039 gpui::KeyBindingContextPredicate::Child(ctx_a, ctx_b) => {
2040 queue.push(*ctx_a);
2041 queue.push(*ctx_b);
2042 }
2043 gpui::KeyBindingContextPredicate::Not(ctx) => {
2044 queue.push(*ctx);
2045 }
2046 gpui::KeyBindingContextPredicate::And(ctx_a, ctx_b) => {
2047 queue.push(*ctx_a);
2048 queue.push(*ctx_b);
2049 }
2050 gpui::KeyBindingContextPredicate::Or(ctx_a, ctx_b) => {
2051 queue.push(*ctx_a);
2052 queue.push(*ctx_b);
2053 }
2054 }
2055 }
2056 }
2057 }
2058
2059 let mut contexts = contexts.into_iter().collect::<Vec<_>>();
2060 contexts.sort();
2061
2062 return contexts;
2063}
2064
2065impl SerializableItem for KeymapEditor {
2066 fn serialized_item_kind() -> &'static str {
2067 "KeymapEditor"
2068 }
2069
2070 fn cleanup(
2071 workspace_id: workspace::WorkspaceId,
2072 alive_items: Vec<workspace::ItemId>,
2073 _window: &mut Window,
2074 cx: &mut App,
2075 ) -> gpui::Task<gpui::Result<()>> {
2076 workspace::delete_unloaded_items(
2077 alive_items,
2078 workspace_id,
2079 "keybinding_editors",
2080 &KEYBINDING_EDITORS,
2081 cx,
2082 )
2083 }
2084
2085 fn deserialize(
2086 _project: Entity<project::Project>,
2087 workspace: WeakEntity<Workspace>,
2088 workspace_id: workspace::WorkspaceId,
2089 item_id: workspace::ItemId,
2090 window: &mut Window,
2091 cx: &mut App,
2092 ) -> gpui::Task<gpui::Result<Entity<Self>>> {
2093 window.spawn(cx, async move |cx| {
2094 if KEYBINDING_EDITORS
2095 .get_keybinding_editor(item_id, workspace_id)?
2096 .is_some()
2097 {
2098 cx.update(|window, cx| cx.new(|cx| KeymapEditor::new(workspace, window, cx)))
2099 } else {
2100 Err(anyhow!("No keybinding editor to deserialize"))
2101 }
2102 })
2103 }
2104
2105 fn serialize(
2106 &mut self,
2107 workspace: &mut Workspace,
2108 item_id: workspace::ItemId,
2109 _closing: bool,
2110 _window: &mut Window,
2111 cx: &mut ui::Context<Self>,
2112 ) -> Option<gpui::Task<gpui::Result<()>>> {
2113 let workspace_id = workspace.database_id()?;
2114 Some(cx.background_spawn(async move {
2115 KEYBINDING_EDITORS
2116 .save_keybinding_editor(item_id, workspace_id)
2117 .await
2118 }))
2119 }
2120
2121 fn should_serialize(&self, _event: &Self::Event) -> bool {
2122 false
2123 }
2124}
2125
2126mod persistence {
2127 use db::{define_connection, query, sqlez_macros::sql};
2128 use workspace::WorkspaceDb;
2129
2130 define_connection! {
2131 pub static ref KEYBINDING_EDITORS: KeybindingEditorDb<WorkspaceDb> =
2132 &[sql!(
2133 CREATE TABLE keybinding_editors (
2134 workspace_id INTEGER,
2135 item_id INTEGER UNIQUE,
2136
2137 PRIMARY KEY(workspace_id, item_id),
2138 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
2139 ON DELETE CASCADE
2140 ) STRICT;
2141 )];
2142 }
2143
2144 impl KeybindingEditorDb {
2145 query! {
2146 pub async fn save_keybinding_editor(
2147 item_id: workspace::ItemId,
2148 workspace_id: workspace::WorkspaceId
2149 ) -> Result<()> {
2150 INSERT OR REPLACE INTO keybinding_editors(item_id, workspace_id)
2151 VALUES (?, ?)
2152 }
2153 }
2154
2155 query! {
2156 pub fn get_keybinding_editor(
2157 item_id: workspace::ItemId,
2158 workspace_id: workspace::WorkspaceId
2159 ) -> Result<Option<workspace::ItemId>> {
2160 SELECT item_id
2161 FROM keybinding_editors
2162 WHERE item_id = ? AND workspace_id = ?
2163 }
2164 }
2165 }
2166}