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