1use std::{
2 ops::{Not as _, 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, Animation, AnimationExt, AppContext as _, AsyncApp, Axis, ClickEvent, Context,
14 DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, Global, IsZero,
15 KeyContext, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, Point, ScrollStrategy,
16 ScrollWheelEvent, StyledText, Subscription, WeakEntity, actions, anchored, deferred, div,
17};
18use language::{Language, LanguageConfig, ToOffset as _};
19use notifications::status_toast::{StatusToast, ToastIcon};
20use settings::{BaseKeymap, KeybindSource, KeymapFile, SettingsAssets};
21
22use util::ResultExt;
23
24use ui::{
25 ActiveTheme as _, App, Banner, BorrowAppContext, ContextMenu, IconButtonShape, Modal,
26 ModalFooter, ModalHeader, ParentElement as _, Render, Section, SharedString, Styled as _,
27 Tooltip, Window, prelude::*,
28};
29use ui_input::SingleLineInput;
30use workspace::{
31 Item, ModalView, SerializableItem, Workspace, notifications::NotifyTaskExt as _,
32 register_serializable_item,
33};
34
35use crate::{
36 SettingsUiFeatureFlag,
37 keybindings::persistence::KEYBINDING_EDITORS,
38 ui_components::table::{Table, TableInteractionState},
39};
40
41const NO_ACTION_ARGUMENTS_TEXT: SharedString = SharedString::new_static("<no arguments>");
42
43actions!(
44 zed,
45 [
46 /// Opens the keymap editor.
47 OpenKeymapEditor
48 ]
49);
50
51const KEYMAP_EDITOR_NAMESPACE: &'static str = "keymap_editor";
52actions!(
53 keymap_editor,
54 [
55 /// Edits the selected key binding.
56 EditBinding,
57 /// Creates a new key binding for the selected action.
58 CreateBinding,
59 /// Deletes the selected key binding.
60 DeleteBinding,
61 /// Copies the action name to clipboard.
62 CopyAction,
63 /// Copies the context predicate to clipboard.
64 CopyContext,
65 /// Toggles Conflict Filtering
66 ToggleConflictFilter,
67 /// Toggle Keystroke search
68 ToggleKeystrokeSearch,
69 /// Toggles exact matching for keystroke search
70 ToggleExactKeystrokeMatching,
71 ]
72);
73
74actions!(
75 keystroke_input,
76 [
77 /// Starts recording keystrokes
78 StartRecording,
79 /// Stops recording keystrokes
80 StopRecording,
81 /// Clears the recorded keystrokes
82 ClearKeystrokes,
83 ]
84);
85
86pub fn init(cx: &mut App) {
87 let keymap_event_channel = KeymapEventChannel::new();
88 cx.set_global(keymap_event_channel);
89
90 cx.on_action(|_: &OpenKeymapEditor, cx| {
91 workspace::with_active_or_new_workspace(cx, move |workspace, window, cx| {
92 workspace
93 .with_local_workspace(window, cx, |workspace, window, cx| {
94 let existing = workspace
95 .active_pane()
96 .read(cx)
97 .items()
98 .find_map(|item| item.downcast::<KeymapEditor>());
99
100 if let Some(existing) = existing {
101 workspace.activate_item(&existing, true, true, window, cx);
102 } else {
103 let keymap_editor =
104 cx.new(|cx| KeymapEditor::new(workspace.weak_handle(), window, cx));
105 workspace.add_item_to_active_pane(
106 Box::new(keymap_editor),
107 None,
108 true,
109 window,
110 cx,
111 );
112 }
113 })
114 .detach();
115 })
116 });
117
118 cx.observe_new(|_workspace: &mut Workspace, window, cx| {
119 let Some(window) = window else { return };
120
121 let keymap_ui_actions = [std::any::TypeId::of::<OpenKeymapEditor>()];
122
123 command_palette_hooks::CommandPaletteFilter::update_global(cx, |filter, _cx| {
124 filter.hide_action_types(&keymap_ui_actions);
125 filter.hide_namespace(KEYMAP_EDITOR_NAMESPACE);
126 });
127
128 cx.observe_flag::<SettingsUiFeatureFlag, _>(
129 window,
130 move |is_enabled, _workspace, _, cx| {
131 if is_enabled {
132 command_palette_hooks::CommandPaletteFilter::update_global(
133 cx,
134 |filter, _cx| {
135 filter.show_action_types(keymap_ui_actions.iter());
136 filter.show_namespace(KEYMAP_EDITOR_NAMESPACE);
137 },
138 );
139 } else {
140 command_palette_hooks::CommandPaletteFilter::update_global(
141 cx,
142 |filter, _cx| {
143 filter.hide_action_types(&keymap_ui_actions);
144 filter.hide_namespace(KEYMAP_EDITOR_NAMESPACE);
145 },
146 );
147 }
148 },
149 )
150 .detach();
151 })
152 .detach();
153
154 register_serializable_item::<KeymapEditor>(cx);
155}
156
157pub struct KeymapEventChannel {}
158
159impl Global for KeymapEventChannel {}
160
161impl KeymapEventChannel {
162 fn new() -> Self {
163 Self {}
164 }
165
166 pub fn trigger_keymap_changed(cx: &mut App) {
167 let Some(_event_channel) = cx.try_global::<Self>() else {
168 // don't panic if no global defined. This usually happens in tests
169 return;
170 };
171 cx.update_global(|_event_channel: &mut Self, _| {
172 /* triggers observers in KeymapEditors */
173 });
174 }
175}
176
177#[derive(Default, PartialEq)]
178enum SearchMode {
179 #[default]
180 Normal,
181 KeyStroke {
182 exact_match: bool,
183 },
184}
185
186impl SearchMode {
187 fn invert(&self) -> Self {
188 match self {
189 SearchMode::Normal => SearchMode::KeyStroke { exact_match: false },
190 SearchMode::KeyStroke { .. } => SearchMode::Normal,
191 }
192 }
193}
194
195#[derive(Default, PartialEq, Copy, Clone)]
196enum FilterState {
197 #[default]
198 All,
199 Conflicts,
200}
201
202impl FilterState {
203 fn invert(&self) -> Self {
204 match self {
205 FilterState::All => FilterState::Conflicts,
206 FilterState::Conflicts => FilterState::All,
207 }
208 }
209}
210
211#[derive(Debug, Default, PartialEq, Eq, Clone, Hash)]
212struct ActionMapping {
213 keystroke_text: SharedString,
214 context: Option<SharedString>,
215}
216
217#[derive(Default)]
218struct ConflictState {
219 conflicts: Vec<usize>,
220 action_keybind_mapping: HashMap<ActionMapping, Vec<usize>>,
221}
222
223impl ConflictState {
224 fn new(key_bindings: &[ProcessedKeybinding]) -> Self {
225 let mut action_keybind_mapping: HashMap<_, Vec<usize>> = HashMap::default();
226
227 key_bindings
228 .iter()
229 .enumerate()
230 .filter(|(_, binding)| {
231 !binding.keystroke_text.is_empty()
232 && binding
233 .source
234 .as_ref()
235 .is_some_and(|source| matches!(source.0, KeybindSource::User))
236 })
237 .for_each(|(index, binding)| {
238 action_keybind_mapping
239 .entry(binding.get_action_mapping())
240 .or_default()
241 .push(index);
242 });
243
244 Self {
245 conflicts: action_keybind_mapping
246 .values()
247 .filter(|indices| indices.len() > 1)
248 .flatten()
249 .copied()
250 .collect(),
251 action_keybind_mapping,
252 }
253 }
254
255 fn conflicting_indices_for_mapping(
256 &self,
257 action_mapping: ActionMapping,
258 keybind_idx: usize,
259 ) -> Option<Vec<usize>> {
260 self.action_keybind_mapping
261 .get(&action_mapping)
262 .and_then(|indices| {
263 let mut indices = indices.iter().filter(|&idx| *idx != keybind_idx).peekable();
264 indices.peek().is_some().then(|| indices.copied().collect())
265 })
266 }
267
268 fn will_conflict(&self, action_mapping: ActionMapping) -> Option<Vec<usize>> {
269 self.action_keybind_mapping
270 .get(&action_mapping)
271 .and_then(|indices| indices.is_empty().not().then_some(indices.clone()))
272 }
273
274 fn has_conflict(&self, candidate_idx: &usize) -> bool {
275 self.conflicts.contains(candidate_idx)
276 }
277
278 fn any_conflicts(&self) -> bool {
279 !self.conflicts.is_empty()
280 }
281}
282
283struct KeymapEditor {
284 workspace: WeakEntity<Workspace>,
285 focus_handle: FocusHandle,
286 _keymap_subscription: Subscription,
287 keybindings: Vec<ProcessedKeybinding>,
288 keybinding_conflict_state: ConflictState,
289 filter_state: FilterState,
290 search_mode: SearchMode,
291 // corresponds 1 to 1 with keybindings
292 string_match_candidates: Arc<Vec<StringMatchCandidate>>,
293 matches: Vec<StringMatch>,
294 table_interaction_state: Entity<TableInteractionState>,
295 filter_editor: Entity<Editor>,
296 keystroke_editor: Entity<KeystrokeInput>,
297 selected_index: Option<usize>,
298 context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
299 previous_edit: Option<PreviousEdit>,
300}
301
302enum PreviousEdit {
303 /// When deleting, we want to maintain the same scroll position
304 ScrollBarOffset(Point<Pixels>),
305 /// When editing or creating, because the new keybinding could be in a different position in the sort order
306 /// we store metadata about the new binding (either the modified version or newly created one)
307 /// and upon reload, we search for this binding in the list of keybindings, and if we find the one that matches
308 /// this metadata, we set the selected index to it and scroll to it,
309 /// and if we don't find it, we scroll to 0 and don't set a selected index
310 Keybinding {
311 action_mapping: ActionMapping,
312 action_name: SharedString,
313 /// The scrollbar position to fallback to if we don't find the keybinding during a refresh
314 /// this can happen if there's a filter applied to the search and the keybinding modification
315 /// filters the binding from the search results
316 fallback: Point<Pixels>,
317 },
318}
319
320impl EventEmitter<()> for KeymapEditor {}
321
322impl Focusable for KeymapEditor {
323 fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
324 return self.filter_editor.focus_handle(cx);
325 }
326}
327
328impl KeymapEditor {
329 fn new(workspace: WeakEntity<Workspace>, window: &mut Window, cx: &mut Context<Self>) -> Self {
330 let _keymap_subscription = cx.observe_global::<KeymapEventChannel>(Self::on_keymap_changed);
331 let table_interaction_state = TableInteractionState::new(window, cx);
332
333 let keystroke_editor = cx.new(|cx| {
334 let mut keystroke_editor = KeystrokeInput::new(None, window, cx);
335 keystroke_editor.highlight_on_focus = false;
336 keystroke_editor
337 });
338
339 let filter_editor = cx.new(|cx| {
340 let mut editor = Editor::single_line(window, cx);
341 editor.set_placeholder_text("Filter action names…", cx);
342 editor
343 });
344
345 cx.subscribe(&filter_editor, |this, _, e: &EditorEvent, cx| {
346 if !matches!(e, EditorEvent::BufferEdited) {
347 return;
348 }
349
350 this.on_query_changed(cx);
351 })
352 .detach();
353
354 cx.subscribe(&keystroke_editor, |this, _, _, cx| {
355 if matches!(this.search_mode, SearchMode::Normal) {
356 return;
357 }
358
359 this.on_query_changed(cx);
360 })
361 .detach();
362
363 let mut this = Self {
364 workspace,
365 keybindings: vec![],
366 keybinding_conflict_state: ConflictState::default(),
367 filter_state: FilterState::default(),
368 search_mode: SearchMode::default(),
369 string_match_candidates: Arc::new(vec![]),
370 matches: vec![],
371 focus_handle: cx.focus_handle(),
372 _keymap_subscription,
373 table_interaction_state,
374 filter_editor,
375 keystroke_editor,
376 selected_index: None,
377 context_menu: None,
378 previous_edit: None,
379 };
380
381 this.on_keymap_changed(cx);
382
383 this
384 }
385
386 fn current_action_query(&self, cx: &App) -> String {
387 self.filter_editor.read(cx).text(cx)
388 }
389
390 fn current_keystroke_query(&self, cx: &App) -> Vec<Keystroke> {
391 match self.search_mode {
392 SearchMode::KeyStroke { .. } => self
393 .keystroke_editor
394 .read(cx)
395 .keystrokes()
396 .iter()
397 .cloned()
398 .collect(),
399 SearchMode::Normal => Default::default(),
400 }
401 }
402
403 fn on_query_changed(&self, cx: &mut Context<Self>) {
404 let action_query = self.current_action_query(cx);
405 let keystroke_query = self.current_keystroke_query(cx);
406
407 cx.spawn(async move |this, cx| {
408 Self::update_matches(this.clone(), action_query, keystroke_query, cx).await?;
409 this.update(cx, |this, cx| {
410 this.scroll_to_item(0, ScrollStrategy::Top, cx)
411 })
412 })
413 .detach();
414 }
415
416 async fn update_matches(
417 this: WeakEntity<Self>,
418 action_query: String,
419 keystroke_query: Vec<Keystroke>,
420 cx: &mut AsyncApp,
421 ) -> anyhow::Result<()> {
422 let action_query = command_palette::normalize_action_query(&action_query);
423 let (string_match_candidates, keybind_count) = this.read_with(cx, |this, _| {
424 (this.string_match_candidates.clone(), this.keybindings.len())
425 })?;
426 let executor = cx.background_executor().clone();
427 let mut matches = fuzzy::match_strings(
428 &string_match_candidates,
429 &action_query,
430 true,
431 true,
432 keybind_count,
433 &Default::default(),
434 executor,
435 )
436 .await;
437 this.update(cx, |this, cx| {
438 match this.filter_state {
439 FilterState::Conflicts => {
440 matches.retain(|candidate| {
441 this.keybinding_conflict_state
442 .has_conflict(&candidate.candidate_id)
443 });
444 }
445 FilterState::All => {}
446 }
447
448 match this.search_mode {
449 SearchMode::KeyStroke { exact_match } => {
450 matches.retain(|item| {
451 this.keybindings[item.candidate_id]
452 .keystrokes()
453 .is_some_and(|keystrokes| {
454 if exact_match {
455 keystroke_query.len() == keystrokes.len()
456 && keystroke_query.iter().zip(keystrokes).all(
457 |(query, keystroke)| {
458 query.key == keystroke.key
459 && query.modifiers == keystroke.modifiers
460 },
461 )
462 } else {
463 keystroke_query.iter().all(|key| {
464 keystrokes.iter().any(|keystroke| {
465 keystroke.key == key.key
466 && keystroke.modifiers == key.modifiers
467 })
468 })
469 }
470 })
471 });
472 }
473 SearchMode::Normal => {}
474 }
475
476 if action_query.is_empty() {
477 // apply default sort
478 // sorts by source precedence, and alphabetically by action name within each source
479 matches.sort_by_key(|match_item| {
480 let keybind = &this.keybindings[match_item.candidate_id];
481 let source = keybind.source.as_ref().map(|s| s.0);
482 use KeybindSource::*;
483 let source_precedence = match source {
484 Some(User) => 0,
485 Some(Vim) => 1,
486 Some(Base) => 2,
487 Some(Default) => 3,
488 None => 4,
489 };
490 return (source_precedence, keybind.action_name.as_ref());
491 });
492 }
493 this.selected_index.take();
494 this.matches = matches;
495 cx.notify();
496 })
497 }
498
499 fn has_conflict(&self, row_index: usize) -> bool {
500 self.matches
501 .get(row_index)
502 .map(|candidate| candidate.candidate_id)
503 .is_some_and(|id| self.keybinding_conflict_state.has_conflict(&id))
504 }
505
506 fn process_bindings(
507 json_language: Arc<Language>,
508 zed_keybind_context_language: Arc<Language>,
509 cx: &mut App,
510 ) -> (Vec<ProcessedKeybinding>, Vec<StringMatchCandidate>) {
511 let key_bindings_ptr = cx.key_bindings();
512 let lock = key_bindings_ptr.borrow();
513 let key_bindings = lock.bindings();
514 let mut unmapped_action_names =
515 HashSet::from_iter(cx.all_action_names().into_iter().copied());
516 let action_documentation = cx.action_documentation();
517 let mut generator = KeymapFile::action_schema_generator();
518 let action_schema = HashMap::from_iter(
519 cx.action_schemas(&mut generator)
520 .into_iter()
521 .filter_map(|(name, schema)| schema.map(|schema| (name, schema))),
522 );
523
524 let mut processed_bindings = Vec::new();
525 let mut string_match_candidates = Vec::new();
526
527 for key_binding in key_bindings {
528 let source = key_binding.meta().map(settings::KeybindSource::from_meta);
529
530 let keystroke_text = ui::text_for_keystrokes(key_binding.keystrokes(), cx);
531 let ui_key_binding = Some(
532 ui::KeyBinding::new_from_gpui(key_binding.clone(), cx)
533 .vim_mode(source == Some(settings::KeybindSource::Vim)),
534 );
535
536 let context = key_binding
537 .predicate()
538 .map(|predicate| {
539 KeybindContextString::Local(
540 predicate.to_string().into(),
541 zed_keybind_context_language.clone(),
542 )
543 })
544 .unwrap_or(KeybindContextString::Global);
545
546 let source = source.map(|source| (source, source.name().into()));
547
548 let action_name = key_binding.action().name();
549 unmapped_action_names.remove(&action_name);
550 let action_arguments = key_binding
551 .action_input()
552 .map(|arguments| SyntaxHighlightedText::new(arguments, json_language.clone()));
553 let action_docs = action_documentation.get(action_name).copied();
554
555 let index = processed_bindings.len();
556 let string_match_candidate = StringMatchCandidate::new(index, &action_name);
557 processed_bindings.push(ProcessedKeybinding {
558 keystroke_text: keystroke_text.into(),
559 ui_key_binding,
560 action_name: action_name.into(),
561 action_arguments,
562 action_docs,
563 action_schema: action_schema.get(action_name).cloned(),
564 context: Some(context),
565 source,
566 });
567 string_match_candidates.push(string_match_candidate);
568 }
569
570 let empty = SharedString::new_static("");
571 for action_name in unmapped_action_names.into_iter() {
572 let index = processed_bindings.len();
573 let string_match_candidate = StringMatchCandidate::new(index, &action_name);
574 processed_bindings.push(ProcessedKeybinding {
575 keystroke_text: empty.clone(),
576 ui_key_binding: None,
577 action_name: action_name.into(),
578 action_arguments: None,
579 action_docs: action_documentation.get(action_name).copied(),
580 action_schema: action_schema.get(action_name).cloned(),
581 context: None,
582 source: None,
583 });
584 string_match_candidates.push(string_match_candidate);
585 }
586
587 (processed_bindings, string_match_candidates)
588 }
589
590 fn on_keymap_changed(&mut self, cx: &mut Context<KeymapEditor>) {
591 let workspace = self.workspace.clone();
592 cx.spawn(async move |this, cx| {
593 let json_language = load_json_language(workspace.clone(), cx).await;
594 let zed_keybind_context_language =
595 load_keybind_context_language(workspace.clone(), cx).await;
596
597 let (action_query, keystroke_query) = this.update(cx, |this, cx| {
598 let (key_bindings, string_match_candidates) =
599 Self::process_bindings(json_language, zed_keybind_context_language, cx);
600
601 this.keybinding_conflict_state = ConflictState::new(&key_bindings);
602
603 if !this.keybinding_conflict_state.any_conflicts() {
604 this.filter_state = FilterState::All;
605 }
606
607 this.keybindings = key_bindings;
608 this.string_match_candidates = Arc::new(string_match_candidates);
609 this.matches = this
610 .string_match_candidates
611 .iter()
612 .enumerate()
613 .map(|(ix, candidate)| StringMatch {
614 candidate_id: ix,
615 score: 0.0,
616 positions: vec![],
617 string: candidate.string.clone(),
618 })
619 .collect();
620 (
621 this.current_action_query(cx),
622 this.current_keystroke_query(cx),
623 )
624 })?;
625 // calls cx.notify
626 Self::update_matches(this.clone(), action_query, keystroke_query, cx).await?;
627 this.update(cx, |this, cx| {
628 if let Some(previous_edit) = this.previous_edit.take() {
629 match previous_edit {
630 // should remove scroll from process_query
631 PreviousEdit::ScrollBarOffset(offset) => {
632 this.table_interaction_state.update(cx, |table, _| {
633 table.set_scrollbar_offset(Axis::Vertical, offset)
634 })
635 // set selected index and scroll
636 }
637 PreviousEdit::Keybinding {
638 action_mapping,
639 action_name,
640 fallback,
641 } => {
642 let scroll_position =
643 this.matches.iter().enumerate().find_map(|(index, item)| {
644 let binding = &this.keybindings[item.candidate_id];
645 if binding.get_action_mapping() == action_mapping
646 && binding.action_name == action_name
647 {
648 Some(index)
649 } else {
650 None
651 }
652 });
653
654 if let Some(scroll_position) = scroll_position {
655 this.scroll_to_item(scroll_position, ScrollStrategy::Top, cx);
656 this.selected_index = Some(scroll_position);
657 } else {
658 this.table_interaction_state.update(cx, |table, _| {
659 table.set_scrollbar_offset(Axis::Vertical, fallback)
660 });
661 }
662 cx.notify();
663 }
664 }
665 }
666 })
667 })
668 .detach_and_log_err(cx);
669 }
670
671 fn dispatch_context(&self, _window: &Window, _cx: &Context<Self>) -> KeyContext {
672 let mut dispatch_context = KeyContext::new_with_defaults();
673 dispatch_context.add("KeymapEditor");
674 dispatch_context.add("menu");
675
676 dispatch_context
677 }
678
679 fn scroll_to_item(&self, index: usize, strategy: ScrollStrategy, cx: &mut App) {
680 let index = usize::min(index, self.matches.len().saturating_sub(1));
681 self.table_interaction_state.update(cx, |this, _cx| {
682 this.scroll_handle.scroll_to_item(index, strategy);
683 });
684 }
685
686 fn focus_search(
687 &mut self,
688 _: &search::FocusSearch,
689 window: &mut Window,
690 cx: &mut Context<Self>,
691 ) {
692 if !self
693 .filter_editor
694 .focus_handle(cx)
695 .contains_focused(window, cx)
696 {
697 window.focus(&self.filter_editor.focus_handle(cx));
698 } else {
699 self.filter_editor.update(cx, |editor, cx| {
700 editor.select_all(&Default::default(), window, cx);
701 });
702 }
703 self.selected_index.take();
704 }
705
706 fn selected_keybind_idx(&self) -> Option<usize> {
707 self.selected_index
708 .and_then(|match_index| self.matches.get(match_index))
709 .map(|r#match| r#match.candidate_id)
710 }
711
712 fn selected_binding(&self) -> Option<&ProcessedKeybinding> {
713 self.selected_keybind_idx()
714 .and_then(|keybind_index| self.keybindings.get(keybind_index))
715 }
716
717 fn select_index(&mut self, index: usize, cx: &mut Context<Self>) {
718 if self.selected_index != Some(index) {
719 self.selected_index = Some(index);
720 cx.notify();
721 }
722 }
723
724 fn create_context_menu(
725 &mut self,
726 position: Point<Pixels>,
727 window: &mut Window,
728 cx: &mut Context<Self>,
729 ) {
730 let weak = cx.weak_entity();
731 self.context_menu = self.selected_binding().map(|selected_binding| {
732 let key_strokes = selected_binding
733 .keystrokes()
734 .map(Vec::from)
735 .unwrap_or_default();
736 let selected_binding_has_no_context = selected_binding
737 .context
738 .as_ref()
739 .and_then(KeybindContextString::local)
740 .is_none();
741
742 let selected_binding_is_unbound = selected_binding.keystrokes().is_none();
743
744 let context_menu = ContextMenu::build(window, cx, |menu, _window, _cx| {
745 menu.action_disabled_when(
746 selected_binding_is_unbound,
747 "Edit",
748 Box::new(EditBinding),
749 )
750 .action("Create", Box::new(CreateBinding))
751 .action_disabled_when(
752 selected_binding_is_unbound,
753 "Delete",
754 Box::new(DeleteBinding),
755 )
756 .separator()
757 .action("Copy Action", Box::new(CopyAction))
758 .action_disabled_when(
759 selected_binding_has_no_context,
760 "Copy Context",
761 Box::new(CopyContext),
762 )
763 .entry("Show matching keybindings", None, {
764 let weak = weak.clone();
765 let key_strokes = key_strokes.clone();
766
767 move |_, cx| {
768 weak.update(cx, |this, cx| {
769 this.filter_state = FilterState::All;
770 this.search_mode = SearchMode::KeyStroke { exact_match: true };
771
772 this.keystroke_editor.update(cx, |editor, cx| {
773 editor.set_keystrokes(key_strokes.clone(), cx);
774 });
775 })
776 .ok();
777 }
778 })
779 });
780
781 let context_menu_handle = context_menu.focus_handle(cx);
782 window.defer(cx, move |window, _cx| window.focus(&context_menu_handle));
783 let subscription = cx.subscribe_in(
784 &context_menu,
785 window,
786 |this, _, _: &DismissEvent, window, cx| {
787 this.dismiss_context_menu(window, cx);
788 },
789 );
790 (context_menu, position, subscription)
791 });
792
793 cx.notify();
794 }
795
796 fn dismiss_context_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) {
797 self.context_menu.take();
798 window.focus(&self.focus_handle);
799 cx.notify();
800 }
801
802 fn context_menu_deployed(&self) -> bool {
803 self.context_menu.is_some()
804 }
805
806 fn select_next(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) {
807 if let Some(selected) = self.selected_index {
808 let selected = selected + 1;
809 if selected >= self.matches.len() {
810 self.select_last(&Default::default(), window, cx);
811 } else {
812 self.selected_index = Some(selected);
813 self.scroll_to_item(selected, ScrollStrategy::Center, cx);
814 cx.notify();
815 }
816 } else {
817 self.select_first(&Default::default(), window, cx);
818 }
819 }
820
821 fn select_previous(
822 &mut self,
823 _: &menu::SelectPrevious,
824 window: &mut Window,
825 cx: &mut Context<Self>,
826 ) {
827 if let Some(selected) = self.selected_index {
828 if selected == 0 {
829 return;
830 }
831
832 let selected = selected - 1;
833
834 if selected >= self.matches.len() {
835 self.select_last(&Default::default(), window, cx);
836 } else {
837 self.selected_index = Some(selected);
838 self.scroll_to_item(selected, ScrollStrategy::Center, cx);
839 cx.notify();
840 }
841 } else {
842 self.select_last(&Default::default(), window, cx);
843 }
844 }
845
846 fn select_first(
847 &mut self,
848 _: &menu::SelectFirst,
849 _window: &mut Window,
850 cx: &mut Context<Self>,
851 ) {
852 if self.matches.get(0).is_some() {
853 self.selected_index = Some(0);
854 self.scroll_to_item(0, ScrollStrategy::Center, cx);
855 cx.notify();
856 }
857 }
858
859 fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
860 if self.matches.last().is_some() {
861 let index = self.matches.len() - 1;
862 self.selected_index = Some(index);
863 self.scroll_to_item(index, ScrollStrategy::Center, cx);
864 cx.notify();
865 }
866 }
867
868 fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
869 self.open_edit_keybinding_modal(false, window, cx);
870 }
871
872 fn open_edit_keybinding_modal(
873 &mut self,
874 create: bool,
875 window: &mut Window,
876 cx: &mut Context<Self>,
877 ) {
878 let Some((keybind_idx, keybind)) = self
879 .selected_keybind_idx()
880 .zip(self.selected_binding().cloned())
881 else {
882 return;
883 };
884 let keymap_editor = cx.entity();
885 self.workspace
886 .update(cx, |workspace, cx| {
887 let fs = workspace.app_state().fs.clone();
888 let workspace_weak = cx.weak_entity();
889 workspace.toggle_modal(window, cx, |window, cx| {
890 let modal = KeybindingEditorModal::new(
891 create,
892 keybind,
893 keybind_idx,
894 keymap_editor,
895 workspace_weak,
896 fs,
897 window,
898 cx,
899 );
900 window.focus(&modal.focus_handle(cx));
901 modal
902 });
903 })
904 .log_err();
905 }
906
907 fn edit_binding(&mut self, _: &EditBinding, window: &mut Window, cx: &mut Context<Self>) {
908 self.open_edit_keybinding_modal(false, window, cx);
909 }
910
911 fn create_binding(&mut self, _: &CreateBinding, window: &mut Window, cx: &mut Context<Self>) {
912 self.open_edit_keybinding_modal(true, window, cx);
913 }
914
915 fn delete_binding(&mut self, _: &DeleteBinding, window: &mut Window, cx: &mut Context<Self>) {
916 let Some(to_remove) = self.selected_binding().cloned() else {
917 return;
918 };
919
920 let Ok(fs) = self
921 .workspace
922 .read_with(cx, |workspace, _| workspace.app_state().fs.clone())
923 else {
924 return;
925 };
926 let tab_size = cx.global::<settings::SettingsStore>().json_tab_size();
927 self.previous_edit = Some(PreviousEdit::ScrollBarOffset(
928 self.table_interaction_state
929 .read(cx)
930 .get_scrollbar_offset(Axis::Vertical),
931 ));
932 cx.spawn(async move |_, _| remove_keybinding(to_remove, &fs, tab_size).await)
933 .detach_and_notify_err(window, cx);
934 }
935
936 fn copy_context_to_clipboard(
937 &mut self,
938 _: &CopyContext,
939 _window: &mut Window,
940 cx: &mut Context<Self>,
941 ) {
942 let context = self
943 .selected_binding()
944 .and_then(|binding| binding.context.as_ref())
945 .and_then(KeybindContextString::local_str)
946 .map(|context| context.to_string());
947 let Some(context) = context else {
948 return;
949 };
950 cx.write_to_clipboard(gpui::ClipboardItem::new_string(context.clone()));
951 }
952
953 fn copy_action_to_clipboard(
954 &mut self,
955 _: &CopyAction,
956 _window: &mut Window,
957 cx: &mut Context<Self>,
958 ) {
959 let action = self
960 .selected_binding()
961 .map(|binding| binding.action_name.to_string());
962 let Some(action) = action else {
963 return;
964 };
965 cx.write_to_clipboard(gpui::ClipboardItem::new_string(action.clone()));
966 }
967
968 fn toggle_conflict_filter(
969 &mut self,
970 _: &ToggleConflictFilter,
971 _: &mut Window,
972 cx: &mut Context<Self>,
973 ) {
974 self.set_filter_state(self.filter_state.invert(), cx);
975 }
976
977 fn set_filter_state(&mut self, filter_state: FilterState, cx: &mut Context<Self>) {
978 if self.filter_state != filter_state {
979 self.filter_state = filter_state;
980 self.on_query_changed(cx);
981 }
982 }
983
984 fn toggle_keystroke_search(
985 &mut self,
986 _: &ToggleKeystrokeSearch,
987 window: &mut Window,
988 cx: &mut Context<Self>,
989 ) {
990 self.search_mode = self.search_mode.invert();
991 self.on_query_changed(cx);
992
993 // Update the keystroke editor to turn the `search` bool on
994 self.keystroke_editor.update(cx, |keystroke_editor, cx| {
995 keystroke_editor
996 .set_search_mode(matches!(self.search_mode, SearchMode::KeyStroke { .. }));
997 cx.notify();
998 });
999
1000 match self.search_mode {
1001 SearchMode::KeyStroke { .. } => {
1002 window.focus(&self.keystroke_editor.read(cx).recording_focus_handle(cx));
1003 }
1004 SearchMode::Normal => {}
1005 }
1006 }
1007
1008 fn toggle_exact_keystroke_matching(
1009 &mut self,
1010 _: &ToggleExactKeystrokeMatching,
1011 _: &mut Window,
1012 cx: &mut Context<Self>,
1013 ) {
1014 let SearchMode::KeyStroke { exact_match } = &mut self.search_mode else {
1015 return;
1016 };
1017
1018 *exact_match = !(*exact_match);
1019 self.on_query_changed(cx);
1020 }
1021}
1022
1023#[derive(Clone)]
1024struct ProcessedKeybinding {
1025 keystroke_text: SharedString,
1026 ui_key_binding: Option<ui::KeyBinding>,
1027 action_name: SharedString,
1028 action_arguments: Option<SyntaxHighlightedText>,
1029 action_docs: Option<&'static str>,
1030 action_schema: Option<schemars::Schema>,
1031 context: Option<KeybindContextString>,
1032 source: Option<(KeybindSource, SharedString)>,
1033}
1034
1035impl ProcessedKeybinding {
1036 fn get_action_mapping(&self) -> ActionMapping {
1037 ActionMapping {
1038 keystroke_text: self.keystroke_text.clone(),
1039 context: self
1040 .context
1041 .as_ref()
1042 .and_then(|context| context.local())
1043 .cloned(),
1044 }
1045 }
1046
1047 fn keystrokes(&self) -> Option<&[Keystroke]> {
1048 self.ui_key_binding
1049 .as_ref()
1050 .map(|binding| binding.keystrokes.as_slice())
1051 }
1052}
1053
1054#[derive(Clone, Debug, IntoElement, PartialEq, Eq, Hash)]
1055enum KeybindContextString {
1056 Global,
1057 Local(SharedString, Arc<Language>),
1058}
1059
1060impl KeybindContextString {
1061 const GLOBAL: SharedString = SharedString::new_static("<global>");
1062
1063 pub fn local(&self) -> Option<&SharedString> {
1064 match self {
1065 KeybindContextString::Global => None,
1066 KeybindContextString::Local(name, _) => Some(name),
1067 }
1068 }
1069
1070 pub fn local_str(&self) -> Option<&str> {
1071 match self {
1072 KeybindContextString::Global => None,
1073 KeybindContextString::Local(name, _) => Some(name),
1074 }
1075 }
1076}
1077
1078impl RenderOnce for KeybindContextString {
1079 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
1080 match self {
1081 KeybindContextString::Global => {
1082 muted_styled_text(KeybindContextString::GLOBAL.clone(), cx).into_any_element()
1083 }
1084 KeybindContextString::Local(name, language) => {
1085 SyntaxHighlightedText::new(name, language).into_any_element()
1086 }
1087 }
1088 }
1089}
1090
1091fn muted_styled_text(text: SharedString, cx: &App) -> StyledText {
1092 let len = text.len();
1093 StyledText::new(text).with_highlights([(
1094 0..len,
1095 gpui::HighlightStyle::color(cx.theme().colors().text_muted),
1096 )])
1097}
1098
1099impl Item for KeymapEditor {
1100 type Event = ();
1101
1102 fn tab_content_text(&self, _detail: usize, _cx: &App) -> ui::SharedString {
1103 "Keymap Editor".into()
1104 }
1105}
1106
1107impl Render for KeymapEditor {
1108 fn render(&mut self, window: &mut Window, cx: &mut ui::Context<Self>) -> impl ui::IntoElement {
1109 let row_count = self.matches.len();
1110 let theme = cx.theme();
1111
1112 v_flex()
1113 .id("keymap-editor")
1114 .track_focus(&self.focus_handle)
1115 .key_context(self.dispatch_context(window, cx))
1116 .on_action(cx.listener(Self::select_next))
1117 .on_action(cx.listener(Self::select_previous))
1118 .on_action(cx.listener(Self::select_first))
1119 .on_action(cx.listener(Self::select_last))
1120 .on_action(cx.listener(Self::focus_search))
1121 .on_action(cx.listener(Self::confirm))
1122 .on_action(cx.listener(Self::edit_binding))
1123 .on_action(cx.listener(Self::create_binding))
1124 .on_action(cx.listener(Self::delete_binding))
1125 .on_action(cx.listener(Self::copy_action_to_clipboard))
1126 .on_action(cx.listener(Self::copy_context_to_clipboard))
1127 .on_action(cx.listener(Self::toggle_conflict_filter))
1128 .on_action(cx.listener(Self::toggle_keystroke_search))
1129 .on_action(cx.listener(Self::toggle_exact_keystroke_matching))
1130 .size_full()
1131 .p_2()
1132 .gap_1()
1133 .bg(theme.colors().editor_background)
1134 .child(
1135 v_flex()
1136 .p_2()
1137 .gap_2()
1138 .child(
1139 h_flex()
1140 .gap_2()
1141 .child(
1142 div()
1143 .key_context({
1144 let mut context = KeyContext::new_with_defaults();
1145 context.add("BufferSearchBar");
1146 context
1147 })
1148 .size_full()
1149 .h_8()
1150 .pl_2()
1151 .pr_1()
1152 .py_1()
1153 .border_1()
1154 .border_color(theme.colors().border)
1155 .rounded_lg()
1156 .child(self.filter_editor.clone()),
1157 )
1158 .child(
1159 IconButton::new(
1160 "KeymapEditorToggleFiltersIcon",
1161 IconName::Keyboard,
1162 )
1163 .shape(ui::IconButtonShape::Square)
1164 .tooltip(|window, cx| {
1165 Tooltip::for_action(
1166 "Search by Keystroke",
1167 &ToggleKeystrokeSearch,
1168 window,
1169 cx,
1170 )
1171 })
1172 .toggle_state(matches!(
1173 self.search_mode,
1174 SearchMode::KeyStroke { .. }
1175 ))
1176 .on_click(|_, window, cx| {
1177 window.dispatch_action(ToggleKeystrokeSearch.boxed_clone(), cx);
1178 }),
1179 )
1180 .when(self.keybinding_conflict_state.any_conflicts(), |this| {
1181 this.child(
1182 IconButton::new("KeymapEditorConflictIcon", IconName::Warning)
1183 .shape(ui::IconButtonShape::Square)
1184 .tooltip({
1185 let filter_state = self.filter_state;
1186
1187 move |window, cx| {
1188 Tooltip::for_action(
1189 match filter_state {
1190 FilterState::All => "Show Conflicts",
1191 FilterState::Conflicts => "Hide Conflicts",
1192 },
1193 &ToggleConflictFilter,
1194 window,
1195 cx,
1196 )
1197 }
1198 })
1199 .selected_icon_color(Color::Warning)
1200 .toggle_state(matches!(
1201 self.filter_state,
1202 FilterState::Conflicts
1203 ))
1204 .on_click(|_, window, cx| {
1205 window.dispatch_action(
1206 ToggleConflictFilter.boxed_clone(),
1207 cx,
1208 );
1209 }),
1210 )
1211 }),
1212 )
1213 .when_some(
1214 match self.search_mode {
1215 SearchMode::Normal => None,
1216 SearchMode::KeyStroke { exact_match } => Some(exact_match),
1217 },
1218 |this, exact_match| {
1219 this.child(
1220 h_flex()
1221 .map(|this| {
1222 if self.keybinding_conflict_state.any_conflicts() {
1223 this.pr(rems_from_px(54.))
1224 } else {
1225 this.pr_7()
1226 }
1227 })
1228 .child(self.keystroke_editor.clone())
1229 .child(
1230 div().p_1().child(
1231 IconButton::new(
1232 "keystrokes-exact-match",
1233 IconName::Equal,
1234 )
1235 .shape(IconButtonShape::Square)
1236 .toggle_state(exact_match)
1237 .on_click(
1238 cx.listener(|_, _, window, cx| {
1239 window.dispatch_action(
1240 ToggleExactKeystrokeMatching.boxed_clone(),
1241 cx,
1242 );
1243 }),
1244 ),
1245 ),
1246 ),
1247 )
1248 },
1249 ),
1250 )
1251 .child(
1252 Table::new()
1253 .interactable(&self.table_interaction_state)
1254 .striped()
1255 .column_widths([
1256 rems(2.5),
1257 rems(16.),
1258 rems(16.),
1259 rems(16.),
1260 rems(32.),
1261 rems(8.),
1262 ])
1263 .header(["", "Action", "Arguments", "Keystrokes", "Context", "Source"])
1264 .uniform_list(
1265 "keymap-editor-table",
1266 row_count,
1267 cx.processor(move |this, range: Range<usize>, _window, cx| {
1268 let context_menu_deployed = this.context_menu_deployed();
1269 range
1270 .filter_map(|index| {
1271 let candidate_id = this.matches.get(index)?.candidate_id;
1272 let binding = &this.keybindings[candidate_id];
1273 let action_name = binding.action_name.clone();
1274
1275 let icon = (this.filter_state != FilterState::Conflicts
1276 && this.has_conflict(index))
1277 .then(|| {
1278 base_button_style(index, IconName::Warning)
1279 .icon_color(Color::Warning)
1280 .tooltip(|window, cx| {
1281 Tooltip::with_meta(
1282 "Edit Keybinding",
1283 None,
1284 "Use alt+click to show conflicts",
1285 window,
1286 cx,
1287 )
1288 })
1289 .on_click(cx.listener(
1290 move |this, click: &ClickEvent, window, cx| {
1291 if click.modifiers().alt {
1292 this.set_filter_state(
1293 FilterState::Conflicts,
1294 cx,
1295 );
1296 } else {
1297 this.select_index(index, cx);
1298 this.open_edit_keybinding_modal(
1299 false, window, cx,
1300 );
1301 cx.stop_propagation();
1302 }
1303 },
1304 ))
1305 })
1306 .unwrap_or_else(|| {
1307 base_button_style(index, IconName::Pencil)
1308 .visible_on_hover(row_group_id(index))
1309 .tooltip(Tooltip::text("Edit Keybinding"))
1310 .on_click(cx.listener(move |this, _, window, cx| {
1311 this.select_index(index, cx);
1312 this.open_edit_keybinding_modal(false, window, cx);
1313 cx.stop_propagation();
1314 }))
1315 })
1316 .into_any_element();
1317
1318 let action = div()
1319 .id(("keymap action", index))
1320 .child(command_palette::humanize_action_name(&action_name))
1321 .when(!context_menu_deployed, |this| {
1322 this.tooltip({
1323 let action_name = binding.action_name.clone();
1324 let action_docs = binding.action_docs;
1325 move |_, cx| {
1326 let action_tooltip = Tooltip::new(&action_name);
1327 let action_tooltip = match action_docs {
1328 Some(docs) => action_tooltip.meta(docs),
1329 None => action_tooltip,
1330 };
1331 cx.new(|_| action_tooltip).into()
1332 }
1333 })
1334 })
1335 .into_any_element();
1336 let keystrokes = binding.ui_key_binding.clone().map_or(
1337 binding.keystroke_text.clone().into_any_element(),
1338 IntoElement::into_any_element,
1339 );
1340 let action_arguments = match binding.action_arguments.clone() {
1341 Some(arguments) => arguments.into_any_element(),
1342 None => {
1343 if binding.action_schema.is_some() {
1344 muted_styled_text(NO_ACTION_ARGUMENTS_TEXT, cx)
1345 .into_any_element()
1346 } else {
1347 gpui::Empty.into_any_element()
1348 }
1349 }
1350 };
1351 let context = binding.context.clone().map_or(
1352 gpui::Empty.into_any_element(),
1353 |context| {
1354 let is_local = context.local().is_some();
1355
1356 div()
1357 .id(("keymap context", index))
1358 .child(context.clone())
1359 .when(is_local && !context_menu_deployed, |this| {
1360 this.tooltip(Tooltip::element({
1361 move |_, _| {
1362 context.clone().into_any_element()
1363 }
1364 }))
1365 })
1366 .into_any_element()
1367 },
1368 );
1369 let source = binding
1370 .source
1371 .clone()
1372 .map(|(_source, name)| name)
1373 .unwrap_or_default()
1374 .into_any_element();
1375 Some([
1376 icon,
1377 action,
1378 action_arguments,
1379 keystrokes,
1380 context,
1381 source,
1382 ])
1383 })
1384 .collect()
1385 }),
1386 )
1387 .map_row(
1388 cx.processor(|this, (row_index, row): (usize, Div), _window, cx| {
1389 let is_conflict = this.has_conflict(row_index);
1390 let is_selected = this.selected_index == Some(row_index);
1391
1392 let row_id = row_group_id(row_index);
1393
1394 let row = row
1395 .id(row_id.clone())
1396 .on_any_mouse_down(cx.listener(
1397 move |this,
1398 mouse_down_event: &gpui::MouseDownEvent,
1399 window,
1400 cx| {
1401 match mouse_down_event.button {
1402 MouseButton::Right => {
1403 this.select_index(row_index, cx);
1404 this.create_context_menu(
1405 mouse_down_event.position,
1406 window,
1407 cx,
1408 );
1409 }
1410 _ => {}
1411 }
1412 },
1413 ))
1414 .on_click(cx.listener(
1415 move |this, event: &ClickEvent, window, cx| {
1416 this.select_index(row_index, cx);
1417 if event.up.click_count == 2 {
1418 this.open_edit_keybinding_modal(false, window, cx);
1419 }
1420 },
1421 ))
1422 .group(row_id)
1423 .border_2()
1424 .when(is_conflict, |row| {
1425 row.bg(cx.theme().status().error_background)
1426 })
1427 .when(is_selected, |row| {
1428 row.border_color(cx.theme().colors().panel_focused_border)
1429 });
1430
1431 row.into_any_element()
1432 }),
1433 ),
1434 )
1435 .on_scroll_wheel(cx.listener(|this, event: &ScrollWheelEvent, _, cx| {
1436 // This ensures that the menu is not dismissed in cases where scroll events
1437 // with a delta of zero are emitted
1438 if !event.delta.pixel_delta(px(1.)).y.is_zero() {
1439 this.context_menu.take();
1440 cx.notify();
1441 }
1442 }))
1443 .children(self.context_menu.as_ref().map(|(menu, position, _)| {
1444 deferred(
1445 anchored()
1446 .position(*position)
1447 .anchor(gpui::Corner::TopLeft)
1448 .child(menu.clone()),
1449 )
1450 .with_priority(1)
1451 }))
1452 }
1453}
1454
1455fn row_group_id(row_index: usize) -> SharedString {
1456 SharedString::new(format!("keymap-table-row-{}", row_index))
1457}
1458
1459fn base_button_style(row_index: usize, icon: IconName) -> IconButton {
1460 IconButton::new(("keymap-icon", row_index), icon)
1461 .shape(IconButtonShape::Square)
1462 .size(ButtonSize::Compact)
1463}
1464
1465#[derive(Debug, Clone, IntoElement)]
1466struct SyntaxHighlightedText {
1467 text: SharedString,
1468 language: Arc<Language>,
1469}
1470
1471impl SyntaxHighlightedText {
1472 pub fn new(text: impl Into<SharedString>, language: Arc<Language>) -> Self {
1473 Self {
1474 text: text.into(),
1475 language,
1476 }
1477 }
1478}
1479
1480impl RenderOnce for SyntaxHighlightedText {
1481 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
1482 let text_style = window.text_style();
1483 let syntax_theme = cx.theme().syntax();
1484
1485 let text = self.text.clone();
1486
1487 let highlights = self
1488 .language
1489 .highlight_text(&text.as_ref().into(), 0..text.len());
1490 let mut runs = Vec::with_capacity(highlights.len());
1491 let mut offset = 0;
1492
1493 for (highlight_range, highlight_id) in highlights {
1494 // Add un-highlighted text before the current highlight
1495 if highlight_range.start > offset {
1496 runs.push(text_style.to_run(highlight_range.start - offset));
1497 }
1498
1499 let mut run_style = text_style.clone();
1500 if let Some(highlight_style) = highlight_id.style(syntax_theme) {
1501 run_style = run_style.highlight(highlight_style);
1502 }
1503 // add the highlighted range
1504 runs.push(run_style.to_run(highlight_range.len()));
1505 offset = highlight_range.end;
1506 }
1507
1508 // Add any remaining un-highlighted text
1509 if offset < text.len() {
1510 runs.push(text_style.to_run(text.len() - offset));
1511 }
1512
1513 return StyledText::new(text).with_runs(runs);
1514 }
1515}
1516
1517#[derive(PartialEq)]
1518enum InputError {
1519 Warning(SharedString),
1520 Error(SharedString),
1521}
1522
1523impl InputError {
1524 fn warning(message: impl Into<SharedString>) -> Self {
1525 Self::Warning(message.into())
1526 }
1527
1528 fn error(message: impl Into<SharedString>) -> Self {
1529 Self::Error(message.into())
1530 }
1531
1532 fn content(&self) -> &SharedString {
1533 match self {
1534 InputError::Warning(content) | InputError::Error(content) => content,
1535 }
1536 }
1537
1538 fn is_warning(&self) -> bool {
1539 matches!(self, InputError::Warning(_))
1540 }
1541}
1542
1543struct KeybindingEditorModal {
1544 creating: bool,
1545 editing_keybind: ProcessedKeybinding,
1546 editing_keybind_idx: usize,
1547 keybind_editor: Entity<KeystrokeInput>,
1548 context_editor: Entity<SingleLineInput>,
1549 action_arguments_editor: Option<Entity<Editor>>,
1550 fs: Arc<dyn Fs>,
1551 error: Option<InputError>,
1552 keymap_editor: Entity<KeymapEditor>,
1553 workspace: WeakEntity<Workspace>,
1554 focus_state: KeybindingEditorModalFocusState,
1555}
1556
1557impl ModalView for KeybindingEditorModal {}
1558
1559impl EventEmitter<DismissEvent> for KeybindingEditorModal {}
1560
1561impl Focusable for KeybindingEditorModal {
1562 fn focus_handle(&self, cx: &App) -> FocusHandle {
1563 self.keybind_editor.focus_handle(cx)
1564 }
1565}
1566
1567impl KeybindingEditorModal {
1568 pub fn new(
1569 create: bool,
1570 editing_keybind: ProcessedKeybinding,
1571 editing_keybind_idx: usize,
1572 keymap_editor: Entity<KeymapEditor>,
1573 workspace: WeakEntity<Workspace>,
1574 fs: Arc<dyn Fs>,
1575 window: &mut Window,
1576 cx: &mut App,
1577 ) -> Self {
1578 let keybind_editor = cx
1579 .new(|cx| KeystrokeInput::new(editing_keybind.keystrokes().map(Vec::from), window, cx));
1580
1581 let context_editor: Entity<SingleLineInput> = cx.new(|cx| {
1582 let input = SingleLineInput::new(window, cx, "Keybinding Context")
1583 .label("Edit Context")
1584 .label_size(LabelSize::Default);
1585
1586 if let Some(context) = editing_keybind
1587 .context
1588 .as_ref()
1589 .and_then(KeybindContextString::local)
1590 {
1591 input.editor().update(cx, |editor, cx| {
1592 editor.set_text(context.clone(), window, cx);
1593 });
1594 }
1595
1596 let editor_entity = input.editor().clone();
1597 let workspace = workspace.clone();
1598 cx.spawn(async move |_input_handle, cx| {
1599 let contexts = cx
1600 .background_spawn(async { collect_contexts_from_assets() })
1601 .await;
1602
1603 let language = load_keybind_context_language(workspace, cx).await;
1604 editor_entity
1605 .update(cx, |editor, cx| {
1606 if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
1607 buffer.update(cx, |buffer, cx| {
1608 buffer.set_language(Some(language), cx);
1609 });
1610 }
1611 editor.set_completion_provider(Some(std::rc::Rc::new(
1612 KeyContextCompletionProvider { contexts },
1613 )));
1614 })
1615 .context("Failed to load completions for keybinding context")
1616 })
1617 .detach_and_log_err(cx);
1618
1619 input
1620 });
1621
1622 let action_arguments_editor = editing_keybind.action_schema.clone().map(|_schema| {
1623 cx.new(|cx| {
1624 let mut editor = Editor::auto_height_unbounded(1, window, cx);
1625 let workspace = workspace.clone();
1626
1627 if let Some(arguments) = editing_keybind.action_arguments.clone() {
1628 editor.set_text(arguments.text, window, cx);
1629 } else {
1630 // TODO: default value from schema?
1631 editor.set_placeholder_text("Action Arguments", cx);
1632 }
1633 cx.spawn(async |editor, cx| {
1634 let json_language = load_json_language(workspace, cx).await;
1635 editor
1636 .update(cx, |editor, cx| {
1637 if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
1638 buffer.update(cx, |buffer, cx| {
1639 buffer.set_language(Some(json_language), cx)
1640 });
1641 }
1642 })
1643 .context("Failed to load JSON language for editing keybinding action arguments input")
1644 })
1645 .detach_and_log_err(cx);
1646 editor
1647 })
1648 });
1649
1650 let focus_state = KeybindingEditorModalFocusState::new(
1651 keybind_editor.read_with(cx, |keybind_editor, cx| keybind_editor.focus_handle(cx)),
1652 action_arguments_editor.as_ref().map(|args_editor| {
1653 args_editor.read_with(cx, |args_editor, cx| args_editor.focus_handle(cx))
1654 }),
1655 context_editor.read_with(cx, |context_editor, cx| context_editor.focus_handle(cx)),
1656 );
1657
1658 Self {
1659 creating: create,
1660 editing_keybind,
1661 editing_keybind_idx,
1662 fs,
1663 keybind_editor,
1664 context_editor,
1665 action_arguments_editor,
1666 error: None,
1667 keymap_editor,
1668 workspace,
1669 focus_state,
1670 }
1671 }
1672
1673 fn set_error(&mut self, error: InputError, cx: &mut Context<Self>) -> bool {
1674 if self
1675 .error
1676 .as_ref()
1677 .is_some_and(|old_error| old_error.is_warning() && *old_error == error)
1678 {
1679 false
1680 } else {
1681 self.error = Some(error);
1682 cx.notify();
1683 true
1684 }
1685 }
1686
1687 fn validate_action_arguments(&self, cx: &App) -> anyhow::Result<Option<String>> {
1688 let action_arguments = self
1689 .action_arguments_editor
1690 .as_ref()
1691 .map(|editor| editor.read(cx).text(cx));
1692
1693 let value = action_arguments
1694 .as_ref()
1695 .map(|args| {
1696 serde_json::from_str(args).context("Failed to parse action arguments as JSON")
1697 })
1698 .transpose()?;
1699
1700 cx.build_action(&self.editing_keybind.action_name, value)
1701 .context("Failed to validate action arguments")?;
1702 Ok(action_arguments)
1703 }
1704
1705 fn validate_keystrokes(&self, cx: &App) -> anyhow::Result<Vec<Keystroke>> {
1706 let new_keystrokes = self
1707 .keybind_editor
1708 .read_with(cx, |editor, _| editor.keystrokes().to_vec());
1709 anyhow::ensure!(!new_keystrokes.is_empty(), "Keystrokes cannot be empty");
1710 Ok(new_keystrokes)
1711 }
1712
1713 fn validate_context(&self, cx: &App) -> anyhow::Result<Option<String>> {
1714 let new_context = self
1715 .context_editor
1716 .read_with(cx, |input, cx| input.editor().read(cx).text(cx));
1717 let Some(context) = new_context.is_empty().not().then_some(new_context) else {
1718 return Ok(None);
1719 };
1720 gpui::KeyBindingContextPredicate::parse(&context).context("Failed to parse key context")?;
1721
1722 Ok(Some(context))
1723 }
1724
1725 fn save(&mut self, cx: &mut Context<Self>) {
1726 let existing_keybind = self.editing_keybind.clone();
1727 let fs = self.fs.clone();
1728 let tab_size = cx.global::<settings::SettingsStore>().json_tab_size();
1729 let new_keystrokes = match self.validate_keystrokes(cx) {
1730 Err(err) => {
1731 self.set_error(InputError::error(err.to_string()), cx);
1732 return;
1733 }
1734 Ok(keystrokes) => keystrokes,
1735 };
1736
1737 let new_context = match self.validate_context(cx) {
1738 Err(err) => {
1739 self.set_error(InputError::error(err.to_string()), cx);
1740 return;
1741 }
1742 Ok(context) => context,
1743 };
1744
1745 let new_action_args = match self.validate_action_arguments(cx) {
1746 Err(input_err) => {
1747 self.set_error(InputError::error(input_err.to_string()), cx);
1748 return;
1749 }
1750 Ok(input) => input,
1751 };
1752
1753 let action_mapping = ActionMapping {
1754 keystroke_text: ui::text_for_keystrokes(&new_keystrokes, cx).into(),
1755 context: new_context.as_ref().map(Into::into),
1756 };
1757
1758 let conflicting_indices = if self.creating {
1759 self.keymap_editor
1760 .read(cx)
1761 .keybinding_conflict_state
1762 .will_conflict(action_mapping)
1763 } else {
1764 self.keymap_editor
1765 .read(cx)
1766 .keybinding_conflict_state
1767 .conflicting_indices_for_mapping(action_mapping, self.editing_keybind_idx)
1768 };
1769 if let Some(conflicting_indices) = conflicting_indices {
1770 let first_conflicting_index = conflicting_indices[0];
1771 let conflicting_action_name = self
1772 .keymap_editor
1773 .read(cx)
1774 .keybindings
1775 .get(first_conflicting_index)
1776 .map(|keybind| keybind.action_name.clone());
1777
1778 let warning_message = match conflicting_action_name {
1779 Some(name) => {
1780 let confliction_action_amount = conflicting_indices.len() - 1;
1781 if confliction_action_amount > 0 {
1782 format!(
1783 "Your keybind would conflict with the \"{}\" action and {} other bindings",
1784 name, confliction_action_amount
1785 )
1786 } else {
1787 format!("Your keybind would conflict with the \"{}\" action", name)
1788 }
1789 }
1790 None => {
1791 log::info!(
1792 "Could not find action in keybindings with index {}",
1793 first_conflicting_index
1794 );
1795 "Your keybind would conflict with other actions".to_string()
1796 }
1797 };
1798
1799 if self.set_error(InputError::warning(warning_message), cx) {
1800 return;
1801 }
1802 }
1803
1804 let create = self.creating;
1805
1806 let status_toast = StatusToast::new(
1807 format!(
1808 "Saved edits to the {} action.",
1809 command_palette::humanize_action_name(&self.editing_keybind.action_name)
1810 ),
1811 cx,
1812 move |this, _cx| {
1813 this.icon(ToastIcon::new(IconName::Check).color(Color::Success))
1814 .dismiss_button(true)
1815 // .action("Undo", f) todo: wire the undo functionality
1816 },
1817 );
1818
1819 self.workspace
1820 .update(cx, |workspace, cx| {
1821 workspace.toggle_status_toast(status_toast, cx);
1822 })
1823 .log_err();
1824
1825 cx.spawn(async move |this, cx| {
1826 let action_name = existing_keybind.action_name.clone();
1827
1828 if let Err(err) = save_keybinding_update(
1829 create,
1830 existing_keybind,
1831 &new_keystrokes,
1832 new_context.as_deref(),
1833 new_action_args.as_deref(),
1834 &fs,
1835 tab_size,
1836 )
1837 .await
1838 {
1839 this.update(cx, |this, cx| {
1840 this.set_error(InputError::error(err.to_string()), cx);
1841 })
1842 .log_err();
1843 } else {
1844 this.update(cx, |this, cx| {
1845 let action_mapping = ActionMapping {
1846 keystroke_text: ui::text_for_keystrokes(new_keystrokes.as_slice(), cx)
1847 .into(),
1848 context: new_context.map(SharedString::from),
1849 };
1850
1851 this.keymap_editor.update(cx, |keymap, cx| {
1852 keymap.previous_edit = Some(PreviousEdit::Keybinding {
1853 action_mapping,
1854 action_name,
1855 fallback: keymap
1856 .table_interaction_state
1857 .read(cx)
1858 .get_scrollbar_offset(Axis::Vertical),
1859 })
1860 });
1861 cx.emit(DismissEvent);
1862 })
1863 .ok();
1864 }
1865 })
1866 .detach();
1867 }
1868
1869 fn key_context(&self) -> KeyContext {
1870 let mut key_context = KeyContext::new_with_defaults();
1871 key_context.add("KeybindEditorModal");
1872 key_context
1873 }
1874
1875 fn focus_next(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) {
1876 self.focus_state.focus_next(window, cx);
1877 }
1878
1879 fn focus_prev(
1880 &mut self,
1881 _: &menu::SelectPrevious,
1882 window: &mut Window,
1883 cx: &mut Context<Self>,
1884 ) {
1885 self.focus_state.focus_previous(window, cx);
1886 }
1887
1888 fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
1889 self.save(cx);
1890 }
1891
1892 fn cancel(&mut self, _: &menu::Cancel, _window: &mut Window, cx: &mut Context<Self>) {
1893 cx.emit(DismissEvent)
1894 }
1895}
1896
1897impl Render for KeybindingEditorModal {
1898 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1899 let theme = cx.theme().colors();
1900 let action_name =
1901 command_palette::humanize_action_name(&self.editing_keybind.action_name).to_string();
1902
1903 v_flex()
1904 .w(rems(34.))
1905 .elevation_3(cx)
1906 .key_context(self.key_context())
1907 .on_action(cx.listener(Self::focus_next))
1908 .on_action(cx.listener(Self::focus_prev))
1909 .on_action(cx.listener(Self::confirm))
1910 .on_action(cx.listener(Self::cancel))
1911 .child(
1912 Modal::new("keybinding_editor_modal", None)
1913 .header(
1914 ModalHeader::new().child(
1915 v_flex()
1916 .pb_1p5()
1917 .mb_1()
1918 .gap_0p5()
1919 .border_b_1()
1920 .border_color(theme.border_variant)
1921 .child(Label::new(action_name))
1922 .when_some(self.editing_keybind.action_docs, |this, docs| {
1923 this.child(
1924 Label::new(docs).size(LabelSize::Small).color(Color::Muted),
1925 )
1926 }),
1927 ),
1928 )
1929 .section(
1930 Section::new().child(
1931 v_flex()
1932 .gap_2()
1933 .child(
1934 v_flex()
1935 .child(Label::new("Edit Keystroke"))
1936 .gap_1()
1937 .child(self.keybind_editor.clone()),
1938 )
1939 .when_some(self.action_arguments_editor.clone(), |this, editor| {
1940 this.child(
1941 v_flex()
1942 .mt_1p5()
1943 .gap_1()
1944 .child(Label::new("Edit Arguments"))
1945 .child(
1946 div()
1947 .w_full()
1948 .py_1()
1949 .px_1p5()
1950 .rounded_lg()
1951 .bg(theme.editor_background)
1952 .border_1()
1953 .border_color(theme.border_variant)
1954 .child(editor),
1955 ),
1956 )
1957 })
1958 .child(self.context_editor.clone())
1959 .when_some(self.error.as_ref(), |this, error| {
1960 this.child(
1961 Banner::new()
1962 .map(|banner| match error {
1963 InputError::Error(_) => {
1964 banner.severity(ui::Severity::Error)
1965 }
1966 InputError::Warning(_) => {
1967 banner.severity(ui::Severity::Warning)
1968 }
1969 })
1970 // For some reason, the div overflows its container to the
1971 //right. The padding accounts for that.
1972 .child(
1973 div()
1974 .size_full()
1975 .pr_2()
1976 .child(Label::new(error.content())),
1977 ),
1978 )
1979 }),
1980 ),
1981 )
1982 .footer(
1983 ModalFooter::new().end_slot(
1984 h_flex()
1985 .gap_1()
1986 .child(
1987 Button::new("cancel", "Cancel")
1988 .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
1989 )
1990 .child(Button::new("save-btn", "Save").on_click(cx.listener(
1991 |this, _event, _window, cx| {
1992 this.save(cx);
1993 },
1994 ))),
1995 ),
1996 ),
1997 )
1998 }
1999}
2000
2001struct KeybindingEditorModalFocusState {
2002 handles: Vec<FocusHandle>,
2003}
2004
2005impl KeybindingEditorModalFocusState {
2006 fn new(
2007 keystrokes: FocusHandle,
2008 action_input: Option<FocusHandle>,
2009 context: FocusHandle,
2010 ) -> Self {
2011 Self {
2012 handles: Vec::from_iter(
2013 [Some(keystrokes), action_input, Some(context)]
2014 .into_iter()
2015 .flatten(),
2016 ),
2017 }
2018 }
2019
2020 fn focused_index(&self, window: &Window, cx: &App) -> Option<i32> {
2021 self.handles
2022 .iter()
2023 .position(|handle| handle.contains_focused(window, cx))
2024 .map(|i| i as i32)
2025 }
2026
2027 fn focus_index(&self, mut index: i32, window: &mut Window) {
2028 if index < 0 {
2029 index = self.handles.len() as i32 - 1;
2030 }
2031 if index >= self.handles.len() as i32 {
2032 index = 0;
2033 }
2034 window.focus(&self.handles[index as usize]);
2035 }
2036
2037 fn focus_next(&self, window: &mut Window, cx: &App) {
2038 let index_to_focus = if let Some(index) = self.focused_index(window, cx) {
2039 index + 1
2040 } else {
2041 0
2042 };
2043 self.focus_index(index_to_focus, window);
2044 }
2045
2046 fn focus_previous(&self, window: &mut Window, cx: &App) {
2047 let index_to_focus = if let Some(index) = self.focused_index(window, cx) {
2048 index - 1
2049 } else {
2050 self.handles.len() as i32 - 1
2051 };
2052 self.focus_index(index_to_focus, window);
2053 }
2054}
2055
2056struct KeyContextCompletionProvider {
2057 contexts: Vec<SharedString>,
2058}
2059
2060impl CompletionProvider for KeyContextCompletionProvider {
2061 fn completions(
2062 &self,
2063 _excerpt_id: editor::ExcerptId,
2064 buffer: &Entity<language::Buffer>,
2065 buffer_position: language::Anchor,
2066 _trigger: editor::CompletionContext,
2067 _window: &mut Window,
2068 cx: &mut Context<Editor>,
2069 ) -> gpui::Task<anyhow::Result<Vec<project::CompletionResponse>>> {
2070 let buffer = buffer.read(cx);
2071 let mut count_back = 0;
2072 for char in buffer.reversed_chars_at(buffer_position) {
2073 if char.is_ascii_alphanumeric() || char == '_' {
2074 count_back += 1;
2075 } else {
2076 break;
2077 }
2078 }
2079 let start_anchor = buffer.anchor_before(
2080 buffer_position
2081 .to_offset(&buffer)
2082 .saturating_sub(count_back),
2083 );
2084 let replace_range = start_anchor..buffer_position;
2085 gpui::Task::ready(Ok(vec![project::CompletionResponse {
2086 completions: self
2087 .contexts
2088 .iter()
2089 .map(|context| project::Completion {
2090 replace_range: replace_range.clone(),
2091 label: language::CodeLabel::plain(context.to_string(), None),
2092 new_text: context.to_string(),
2093 documentation: None,
2094 source: project::CompletionSource::Custom,
2095 icon_path: None,
2096 insert_text_mode: None,
2097 confirm: None,
2098 })
2099 .collect(),
2100 is_incomplete: false,
2101 }]))
2102 }
2103
2104 fn is_completion_trigger(
2105 &self,
2106 _buffer: &Entity<language::Buffer>,
2107 _position: language::Anchor,
2108 text: &str,
2109 _trigger_in_words: bool,
2110 _menu_is_open: bool,
2111 _cx: &mut Context<Editor>,
2112 ) -> bool {
2113 text.chars().last().map_or(false, |last_char| {
2114 last_char.is_ascii_alphanumeric() || last_char == '_'
2115 })
2116 }
2117}
2118
2119async fn load_json_language(workspace: WeakEntity<Workspace>, cx: &mut AsyncApp) -> Arc<Language> {
2120 let json_language_task = workspace
2121 .read_with(cx, |workspace, cx| {
2122 workspace
2123 .project()
2124 .read(cx)
2125 .languages()
2126 .language_for_name("JSON")
2127 })
2128 .context("Failed to load JSON language")
2129 .log_err();
2130 let json_language = match json_language_task {
2131 Some(task) => task.await.context("Failed to load JSON language").log_err(),
2132 None => None,
2133 };
2134 return json_language.unwrap_or_else(|| {
2135 Arc::new(Language::new(
2136 LanguageConfig {
2137 name: "JSON".into(),
2138 ..Default::default()
2139 },
2140 Some(tree_sitter_json::LANGUAGE.into()),
2141 ))
2142 });
2143}
2144
2145async fn load_keybind_context_language(
2146 workspace: WeakEntity<Workspace>,
2147 cx: &mut AsyncApp,
2148) -> Arc<Language> {
2149 let language_task = workspace
2150 .read_with(cx, |workspace, cx| {
2151 workspace
2152 .project()
2153 .read(cx)
2154 .languages()
2155 .language_for_name("Zed Keybind Context")
2156 })
2157 .context("Failed to load Zed Keybind Context language")
2158 .log_err();
2159 let language = match language_task {
2160 Some(task) => task
2161 .await
2162 .context("Failed to load Zed Keybind Context language")
2163 .log_err(),
2164 None => None,
2165 };
2166 return language.unwrap_or_else(|| {
2167 Arc::new(Language::new(
2168 LanguageConfig {
2169 name: "Zed Keybind Context".into(),
2170 ..Default::default()
2171 },
2172 Some(tree_sitter_rust::LANGUAGE.into()),
2173 ))
2174 });
2175}
2176
2177async fn save_keybinding_update(
2178 create: bool,
2179 existing: ProcessedKeybinding,
2180 new_keystrokes: &[Keystroke],
2181 new_context: Option<&str>,
2182 new_args: Option<&str>,
2183 fs: &Arc<dyn Fs>,
2184 tab_size: usize,
2185) -> anyhow::Result<()> {
2186 let keymap_contents = settings::KeymapFile::load_keymap_file(fs)
2187 .await
2188 .context("Failed to load keymap file")?;
2189
2190 let existing_keystrokes = existing.keystrokes().unwrap_or_default();
2191 let existing_context = existing
2192 .context
2193 .as_ref()
2194 .and_then(KeybindContextString::local_str);
2195 let existing_args = existing
2196 .action_arguments
2197 .as_ref()
2198 .map(|args| args.text.as_ref());
2199
2200 let target = settings::KeybindUpdateTarget {
2201 context: existing_context,
2202 keystrokes: existing_keystrokes,
2203 action_name: &existing.action_name,
2204 action_arguments: existing_args,
2205 };
2206
2207 let source = settings::KeybindUpdateTarget {
2208 context: new_context,
2209 keystrokes: new_keystrokes,
2210 action_name: &existing.action_name,
2211 action_arguments: new_args,
2212 };
2213
2214 let operation = if !create {
2215 settings::KeybindUpdateOperation::Replace {
2216 target,
2217 target_keybind_source: existing
2218 .source
2219 .as_ref()
2220 .map(|(source, _name)| *source)
2221 .unwrap_or(KeybindSource::User),
2222 source,
2223 }
2224 } else {
2225 settings::KeybindUpdateOperation::Add {
2226 source,
2227 from: Some(target),
2228 }
2229 };
2230 let updated_keymap_contents =
2231 settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size)
2232 .context("Failed to update keybinding")?;
2233 fs.write(
2234 paths::keymap_file().as_path(),
2235 updated_keymap_contents.as_bytes(),
2236 )
2237 .await
2238 .context("Failed to write keymap file")?;
2239 Ok(())
2240}
2241
2242async fn remove_keybinding(
2243 existing: ProcessedKeybinding,
2244 fs: &Arc<dyn Fs>,
2245 tab_size: usize,
2246) -> anyhow::Result<()> {
2247 let Some(keystrokes) = existing.keystrokes() else {
2248 anyhow::bail!("Cannot remove a keybinding that does not exist");
2249 };
2250 let keymap_contents = settings::KeymapFile::load_keymap_file(fs)
2251 .await
2252 .context("Failed to load keymap file")?;
2253
2254 let operation = settings::KeybindUpdateOperation::Remove {
2255 target: settings::KeybindUpdateTarget {
2256 context: existing
2257 .context
2258 .as_ref()
2259 .and_then(KeybindContextString::local_str),
2260 keystrokes,
2261 action_name: &existing.action_name,
2262 action_arguments: existing
2263 .action_arguments
2264 .as_ref()
2265 .map(|arguments| arguments.text.as_ref()),
2266 },
2267 target_keybind_source: existing
2268 .source
2269 .as_ref()
2270 .map(|(source, _name)| *source)
2271 .unwrap_or(KeybindSource::User),
2272 };
2273
2274 let updated_keymap_contents =
2275 settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size)
2276 .context("Failed to update keybinding")?;
2277 fs.write(
2278 paths::keymap_file().as_path(),
2279 updated_keymap_contents.as_bytes(),
2280 )
2281 .await
2282 .context("Failed to write keymap file")?;
2283 Ok(())
2284}
2285
2286#[derive(PartialEq, Eq, Debug, Copy, Clone)]
2287enum CloseKeystrokeResult {
2288 Partial,
2289 Close,
2290 None,
2291}
2292
2293struct KeystrokeInput {
2294 keystrokes: Vec<Keystroke>,
2295 placeholder_keystrokes: Option<Vec<Keystroke>>,
2296 highlight_on_focus: bool,
2297 outer_focus_handle: FocusHandle,
2298 inner_focus_handle: FocusHandle,
2299 intercept_subscription: Option<Subscription>,
2300 _focus_subscriptions: [Subscription; 2],
2301 search: bool,
2302 close_keystrokes: Option<Vec<Keystroke>>,
2303 close_keystrokes_start: Option<usize>,
2304}
2305
2306impl KeystrokeInput {
2307 const KEYSTROKE_COUNT_MAX: usize = 3;
2308
2309 fn new(
2310 placeholder_keystrokes: Option<Vec<Keystroke>>,
2311 window: &mut Window,
2312 cx: &mut Context<Self>,
2313 ) -> Self {
2314 let outer_focus_handle = cx.focus_handle();
2315 let inner_focus_handle = cx.focus_handle();
2316 let _focus_subscriptions = [
2317 cx.on_focus_in(&inner_focus_handle, window, Self::on_inner_focus_in),
2318 cx.on_focus_out(&inner_focus_handle, window, Self::on_inner_focus_out),
2319 ];
2320 Self {
2321 keystrokes: Vec::new(),
2322 placeholder_keystrokes,
2323 highlight_on_focus: true,
2324 inner_focus_handle,
2325 outer_focus_handle,
2326 intercept_subscription: None,
2327 _focus_subscriptions,
2328 search: false,
2329 close_keystrokes: None,
2330 close_keystrokes_start: None,
2331 }
2332 }
2333
2334 fn set_keystrokes(&mut self, keystrokes: Vec<Keystroke>, cx: &mut Context<Self>) {
2335 self.keystrokes = keystrokes;
2336 self.keystrokes_changed(cx);
2337 }
2338
2339 fn dummy(modifiers: Modifiers) -> Keystroke {
2340 return Keystroke {
2341 modifiers,
2342 key: "".to_string(),
2343 key_char: None,
2344 };
2345 }
2346
2347 fn keystrokes_changed(&self, cx: &mut Context<Self>) {
2348 cx.emit(());
2349 cx.notify();
2350 }
2351
2352 fn key_context() -> KeyContext {
2353 let mut key_context = KeyContext::new_with_defaults();
2354 key_context.add("KeystrokeInput");
2355 key_context
2356 }
2357
2358 fn handle_possible_close_keystroke(
2359 &mut self,
2360 keystroke: &Keystroke,
2361 window: &mut Window,
2362 cx: &mut Context<Self>,
2363 ) -> CloseKeystrokeResult {
2364 let Some(keybind_for_close_action) = window
2365 .highest_precedence_binding_for_action_in_context(&StopRecording, Self::key_context())
2366 else {
2367 log::trace!("No keybinding to stop recording keystrokes in keystroke input");
2368 self.close_keystrokes.take();
2369 return CloseKeystrokeResult::None;
2370 };
2371 let action_keystrokes = keybind_for_close_action.keystrokes();
2372
2373 if let Some(mut close_keystrokes) = self.close_keystrokes.take() {
2374 let mut index = 0;
2375
2376 while index < action_keystrokes.len() && index < close_keystrokes.len() {
2377 if !close_keystrokes[index].should_match(&action_keystrokes[index]) {
2378 break;
2379 }
2380 index += 1;
2381 }
2382 if index == close_keystrokes.len() {
2383 if index >= action_keystrokes.len() {
2384 self.close_keystrokes_start.take();
2385 return CloseKeystrokeResult::None;
2386 }
2387 if keystroke.should_match(&action_keystrokes[index]) {
2388 if action_keystrokes.len() >= 1 && index == action_keystrokes.len() - 1 {
2389 self.stop_recording(&StopRecording, window, cx);
2390 return CloseKeystrokeResult::Close;
2391 } else {
2392 close_keystrokes.push(keystroke.clone());
2393 self.close_keystrokes = Some(close_keystrokes);
2394 return CloseKeystrokeResult::Partial;
2395 }
2396 } else {
2397 self.close_keystrokes_start.take();
2398 return CloseKeystrokeResult::None;
2399 }
2400 }
2401 } else if let Some(first_action_keystroke) = action_keystrokes.first()
2402 && keystroke.should_match(first_action_keystroke)
2403 {
2404 self.close_keystrokes = Some(vec![keystroke.clone()]);
2405 return CloseKeystrokeResult::Partial;
2406 }
2407 self.close_keystrokes_start.take();
2408 return CloseKeystrokeResult::None;
2409 }
2410
2411 fn on_modifiers_changed(
2412 &mut self,
2413 event: &ModifiersChangedEvent,
2414 _window: &mut Window,
2415 cx: &mut Context<Self>,
2416 ) {
2417 let keystrokes_len = self.keystrokes.len();
2418
2419 if let Some(last) = self.keystrokes.last_mut()
2420 && last.key.is_empty()
2421 && keystrokes_len <= Self::KEYSTROKE_COUNT_MAX
2422 {
2423 if !event.modifiers.modified() {
2424 self.keystrokes.pop();
2425 } else {
2426 last.modifiers = event.modifiers;
2427 }
2428 self.keystrokes_changed(cx);
2429 } else if keystrokes_len < Self::KEYSTROKE_COUNT_MAX {
2430 self.keystrokes.push(Self::dummy(event.modifiers));
2431 self.keystrokes_changed(cx);
2432 }
2433 cx.stop_propagation();
2434 }
2435
2436 fn handle_keystroke(
2437 &mut self,
2438 keystroke: &Keystroke,
2439 window: &mut Window,
2440 cx: &mut Context<Self>,
2441 ) {
2442 let close_keystroke_result = self.handle_possible_close_keystroke(keystroke, window, cx);
2443 if close_keystroke_result != CloseKeystrokeResult::Close {
2444 if let Some(last) = self.keystrokes.last()
2445 && last.key.is_empty()
2446 && self.keystrokes.len() <= Self::KEYSTROKE_COUNT_MAX
2447 {
2448 self.keystrokes.pop();
2449 }
2450 if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX {
2451 if close_keystroke_result == CloseKeystrokeResult::Partial
2452 && self.close_keystrokes_start.is_none()
2453 {
2454 self.close_keystrokes_start = Some(self.keystrokes.len());
2455 }
2456 self.keystrokes.push(keystroke.clone());
2457 if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX {
2458 self.keystrokes.push(Self::dummy(keystroke.modifiers));
2459 }
2460 }
2461 }
2462 self.keystrokes_changed(cx);
2463 cx.stop_propagation();
2464 }
2465
2466 fn on_inner_focus_in(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
2467 if self.intercept_subscription.is_none() {
2468 let listener = cx.listener(|this, event: &gpui::KeystrokeEvent, window, cx| {
2469 this.handle_keystroke(&event.keystroke, window, cx);
2470 });
2471 self.intercept_subscription = Some(cx.intercept_keystrokes(listener))
2472 }
2473 }
2474
2475 fn on_inner_focus_out(
2476 &mut self,
2477 _event: gpui::FocusOutEvent,
2478 _window: &mut Window,
2479 cx: &mut Context<Self>,
2480 ) {
2481 self.intercept_subscription.take();
2482 cx.notify();
2483 }
2484
2485 fn keystrokes(&self) -> &[Keystroke] {
2486 if let Some(placeholders) = self.placeholder_keystrokes.as_ref()
2487 && self.keystrokes.is_empty()
2488 {
2489 return placeholders;
2490 }
2491 if self
2492 .keystrokes
2493 .last()
2494 .map_or(false, |last| last.key.is_empty())
2495 {
2496 return &self.keystrokes[..self.keystrokes.len() - 1];
2497 }
2498 return &self.keystrokes;
2499 }
2500
2501 fn render_keystrokes(&self, is_recording: bool) -> impl Iterator<Item = Div> {
2502 let keystrokes = if let Some(placeholders) = self.placeholder_keystrokes.as_ref()
2503 && self.keystrokes.is_empty()
2504 {
2505 if is_recording {
2506 &[]
2507 } else {
2508 placeholders.as_slice()
2509 }
2510 } else {
2511 &self.keystrokes
2512 };
2513 keystrokes.iter().map(move |keystroke| {
2514 h_flex().children(ui::render_keystroke(
2515 keystroke,
2516 Some(Color::Default),
2517 Some(rems(0.875).into()),
2518 ui::PlatformStyle::platform(),
2519 false,
2520 ))
2521 })
2522 }
2523
2524 fn recording_focus_handle(&self, _cx: &App) -> FocusHandle {
2525 self.inner_focus_handle.clone()
2526 }
2527
2528 fn set_search_mode(&mut self, search: bool) {
2529 self.search = search;
2530 }
2531
2532 fn start_recording(&mut self, _: &StartRecording, window: &mut Window, cx: &mut Context<Self>) {
2533 if !self.outer_focus_handle.is_focused(window) {
2534 return;
2535 }
2536 self.clear_keystrokes(&ClearKeystrokes, window, cx);
2537 window.focus(&self.inner_focus_handle);
2538 cx.notify();
2539 }
2540
2541 fn stop_recording(&mut self, _: &StopRecording, window: &mut Window, cx: &mut Context<Self>) {
2542 if !self.inner_focus_handle.is_focused(window) {
2543 return;
2544 }
2545 window.focus(&self.outer_focus_handle);
2546 if let Some(close_keystrokes_start) = self.close_keystrokes_start.take() {
2547 self.keystrokes.drain(close_keystrokes_start..);
2548 }
2549 self.close_keystrokes.take();
2550 cx.notify();
2551 }
2552
2553 fn clear_keystrokes(
2554 &mut self,
2555 _: &ClearKeystrokes,
2556 _window: &mut Window,
2557 cx: &mut Context<Self>,
2558 ) {
2559 self.keystrokes.clear();
2560 self.keystrokes_changed(cx);
2561 }
2562}
2563
2564impl EventEmitter<()> for KeystrokeInput {}
2565
2566impl Focusable for KeystrokeInput {
2567 fn focus_handle(&self, _cx: &App) -> FocusHandle {
2568 self.outer_focus_handle.clone()
2569 }
2570}
2571
2572impl Render for KeystrokeInput {
2573 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2574 let colors = cx.theme().colors();
2575 let is_focused = self.outer_focus_handle.contains_focused(window, cx);
2576 let is_recording = self.inner_focus_handle.is_focused(window);
2577
2578 let horizontal_padding = rems_from_px(64.);
2579
2580 let recording_bg_color = colors
2581 .editor_background
2582 .blend(colors.text_accent.opacity(0.1));
2583
2584 let recording_pulse = || {
2585 Icon::new(IconName::Circle)
2586 .size(IconSize::Small)
2587 .color(Color::Error)
2588 .with_animation(
2589 "recording-pulse",
2590 Animation::new(std::time::Duration::from_secs(2))
2591 .repeat()
2592 .with_easing(gpui::pulsating_between(0.4, 0.8)),
2593 {
2594 let color = Color::Error.color(cx);
2595 move |this, delta| this.color(Color::Custom(color.opacity(delta)))
2596 },
2597 )
2598 };
2599
2600 let recording_indicator = h_flex()
2601 .h_4()
2602 .pr_1()
2603 .gap_0p5()
2604 .border_1()
2605 .border_color(colors.border)
2606 .bg(colors
2607 .editor_background
2608 .blend(colors.text_accent.opacity(0.1)))
2609 .rounded_sm()
2610 .child(recording_pulse())
2611 .child(
2612 Label::new("REC")
2613 .size(LabelSize::XSmall)
2614 .weight(FontWeight::SEMIBOLD)
2615 .color(Color::Error),
2616 );
2617
2618 let search_indicator = h_flex()
2619 .h_4()
2620 .pr_1()
2621 .gap_0p5()
2622 .border_1()
2623 .border_color(colors.border)
2624 .bg(colors
2625 .editor_background
2626 .blend(colors.text_accent.opacity(0.1)))
2627 .rounded_sm()
2628 .child(recording_pulse())
2629 .child(
2630 Label::new("SEARCH")
2631 .size(LabelSize::XSmall)
2632 .weight(FontWeight::SEMIBOLD)
2633 .color(Color::Accent),
2634 );
2635
2636 let record_icon = if self.search {
2637 IconName::MagnifyingGlass
2638 } else {
2639 IconName::PlayFilled
2640 };
2641
2642 return h_flex()
2643 .id("keystroke-input")
2644 .track_focus(&self.outer_focus_handle)
2645 .py_2()
2646 .px_3()
2647 .gap_2()
2648 .min_h_10()
2649 .w_full()
2650 .flex_1()
2651 .justify_between()
2652 .rounded_lg()
2653 .overflow_hidden()
2654 .map(|this| {
2655 if is_recording {
2656 this.bg(recording_bg_color)
2657 } else {
2658 this.bg(colors.editor_background)
2659 }
2660 })
2661 .border_1()
2662 .border_color(colors.border_variant)
2663 .when(is_focused, |parent| {
2664 parent.border_color(colors.border_focused)
2665 })
2666 .key_context(Self::key_context())
2667 .on_action(cx.listener(Self::start_recording))
2668 .on_action(cx.listener(Self::stop_recording))
2669 .child(
2670 h_flex()
2671 .w(horizontal_padding)
2672 .gap_0p5()
2673 .justify_start()
2674 .flex_none()
2675 .when(is_recording, |this| {
2676 this.map(|this| {
2677 if self.search {
2678 this.child(search_indicator)
2679 } else {
2680 this.child(recording_indicator)
2681 }
2682 })
2683 }),
2684 )
2685 .child(
2686 h_flex()
2687 .id("keystroke-input-inner")
2688 .track_focus(&self.inner_focus_handle)
2689 .on_modifiers_changed(cx.listener(Self::on_modifiers_changed))
2690 .size_full()
2691 .when(self.highlight_on_focus, |this| {
2692 this.focus(|mut style| {
2693 style.border_color = Some(colors.border_focused);
2694 style
2695 })
2696 })
2697 .w_full()
2698 .min_w_0()
2699 .justify_center()
2700 .flex_wrap()
2701 .gap(ui::DynamicSpacing::Base04.rems(cx))
2702 .children(self.render_keystrokes(is_recording)),
2703 )
2704 .child(
2705 h_flex()
2706 .w(horizontal_padding)
2707 .gap_0p5()
2708 .justify_end()
2709 .flex_none()
2710 .map(|this| {
2711 if is_recording {
2712 this.child(
2713 IconButton::new("stop-record-btn", IconName::StopFilled)
2714 .shape(ui::IconButtonShape::Square)
2715 .map(|this| {
2716 this.tooltip(Tooltip::for_action_title(
2717 if self.search {
2718 "Stop Searching"
2719 } else {
2720 "Stop Recording"
2721 },
2722 &StopRecording,
2723 ))
2724 })
2725 .icon_color(Color::Error)
2726 .on_click(cx.listener(|this, _event, window, cx| {
2727 this.stop_recording(&StopRecording, window, cx);
2728 })),
2729 )
2730 } else {
2731 this.child(
2732 IconButton::new("record-btn", record_icon)
2733 .shape(ui::IconButtonShape::Square)
2734 .map(|this| {
2735 this.tooltip(Tooltip::for_action_title(
2736 if self.search {
2737 "Start Searching"
2738 } else {
2739 "Start Recording"
2740 },
2741 &StartRecording,
2742 ))
2743 })
2744 .when(!is_focused, |this| this.icon_color(Color::Muted))
2745 .on_click(cx.listener(|this, _event, window, cx| {
2746 this.start_recording(&StartRecording, window, cx);
2747 })),
2748 )
2749 }
2750 })
2751 .child(
2752 IconButton::new("clear-btn", IconName::Delete)
2753 .shape(ui::IconButtonShape::Square)
2754 .tooltip(Tooltip::for_action_title(
2755 "Clear Keystrokes",
2756 &ClearKeystrokes,
2757 ))
2758 .when(!is_recording || !is_focused, |this| {
2759 this.icon_color(Color::Muted)
2760 })
2761 .on_click(cx.listener(|this, _event, window, cx| {
2762 this.clear_keystrokes(&ClearKeystrokes, window, cx);
2763 })),
2764 ),
2765 );
2766 }
2767}
2768
2769fn collect_contexts_from_assets() -> Vec<SharedString> {
2770 let mut keymap_assets = vec![
2771 util::asset_str::<SettingsAssets>(settings::DEFAULT_KEYMAP_PATH),
2772 util::asset_str::<SettingsAssets>(settings::VIM_KEYMAP_PATH),
2773 ];
2774 keymap_assets.extend(
2775 BaseKeymap::OPTIONS
2776 .iter()
2777 .filter_map(|(_, base_keymap)| base_keymap.asset_path())
2778 .map(util::asset_str::<SettingsAssets>),
2779 );
2780
2781 let mut contexts = HashSet::default();
2782
2783 for keymap_asset in keymap_assets {
2784 let Ok(keymap) = KeymapFile::parse(&keymap_asset) else {
2785 continue;
2786 };
2787
2788 for section in keymap.sections() {
2789 let context_expr = §ion.context;
2790 let mut queue = Vec::new();
2791 let Ok(root_context) = gpui::KeyBindingContextPredicate::parse(context_expr) else {
2792 continue;
2793 };
2794
2795 queue.push(root_context);
2796 while let Some(context) = queue.pop() {
2797 match context {
2798 gpui::KeyBindingContextPredicate::Identifier(ident) => {
2799 contexts.insert(ident);
2800 }
2801 gpui::KeyBindingContextPredicate::Equal(ident_a, ident_b) => {
2802 contexts.insert(ident_a);
2803 contexts.insert(ident_b);
2804 }
2805 gpui::KeyBindingContextPredicate::NotEqual(ident_a, ident_b) => {
2806 contexts.insert(ident_a);
2807 contexts.insert(ident_b);
2808 }
2809 gpui::KeyBindingContextPredicate::Child(ctx_a, ctx_b) => {
2810 queue.push(*ctx_a);
2811 queue.push(*ctx_b);
2812 }
2813 gpui::KeyBindingContextPredicate::Not(ctx) => {
2814 queue.push(*ctx);
2815 }
2816 gpui::KeyBindingContextPredicate::And(ctx_a, ctx_b) => {
2817 queue.push(*ctx_a);
2818 queue.push(*ctx_b);
2819 }
2820 gpui::KeyBindingContextPredicate::Or(ctx_a, ctx_b) => {
2821 queue.push(*ctx_a);
2822 queue.push(*ctx_b);
2823 }
2824 }
2825 }
2826 }
2827 }
2828
2829 let mut contexts = contexts.into_iter().collect::<Vec<_>>();
2830 contexts.sort();
2831
2832 return contexts;
2833}
2834
2835impl SerializableItem for KeymapEditor {
2836 fn serialized_item_kind() -> &'static str {
2837 "KeymapEditor"
2838 }
2839
2840 fn cleanup(
2841 workspace_id: workspace::WorkspaceId,
2842 alive_items: Vec<workspace::ItemId>,
2843 _window: &mut Window,
2844 cx: &mut App,
2845 ) -> gpui::Task<gpui::Result<()>> {
2846 workspace::delete_unloaded_items(
2847 alive_items,
2848 workspace_id,
2849 "keybinding_editors",
2850 &KEYBINDING_EDITORS,
2851 cx,
2852 )
2853 }
2854
2855 fn deserialize(
2856 _project: Entity<project::Project>,
2857 workspace: WeakEntity<Workspace>,
2858 workspace_id: workspace::WorkspaceId,
2859 item_id: workspace::ItemId,
2860 window: &mut Window,
2861 cx: &mut App,
2862 ) -> gpui::Task<gpui::Result<Entity<Self>>> {
2863 window.spawn(cx, async move |cx| {
2864 if KEYBINDING_EDITORS
2865 .get_keybinding_editor(item_id, workspace_id)?
2866 .is_some()
2867 {
2868 cx.update(|window, cx| cx.new(|cx| KeymapEditor::new(workspace, window, cx)))
2869 } else {
2870 Err(anyhow!("No keybinding editor to deserialize"))
2871 }
2872 })
2873 }
2874
2875 fn serialize(
2876 &mut self,
2877 workspace: &mut Workspace,
2878 item_id: workspace::ItemId,
2879 _closing: bool,
2880 _window: &mut Window,
2881 cx: &mut ui::Context<Self>,
2882 ) -> Option<gpui::Task<gpui::Result<()>>> {
2883 let workspace_id = workspace.database_id()?;
2884 Some(cx.background_spawn(async move {
2885 KEYBINDING_EDITORS
2886 .save_keybinding_editor(item_id, workspace_id)
2887 .await
2888 }))
2889 }
2890
2891 fn should_serialize(&self, _event: &Self::Event) -> bool {
2892 false
2893 }
2894}
2895
2896mod persistence {
2897 use db::{define_connection, query, sqlez_macros::sql};
2898 use workspace::WorkspaceDb;
2899
2900 define_connection! {
2901 pub static ref KEYBINDING_EDITORS: KeybindingEditorDb<WorkspaceDb> =
2902 &[sql!(
2903 CREATE TABLE keybinding_editors (
2904 workspace_id INTEGER,
2905 item_id INTEGER UNIQUE,
2906
2907 PRIMARY KEY(workspace_id, item_id),
2908 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
2909 ON DELETE CASCADE
2910 ) STRICT;
2911 )];
2912 }
2913
2914 impl KeybindingEditorDb {
2915 query! {
2916 pub async fn save_keybinding_editor(
2917 item_id: workspace::ItemId,
2918 workspace_id: workspace::WorkspaceId
2919 ) -> Result<()> {
2920 INSERT OR REPLACE INTO keybinding_editors(item_id, workspace_id)
2921 VALUES (?, ?)
2922 }
2923 }
2924
2925 query! {
2926 pub fn get_keybinding_editor(
2927 item_id: workspace::ItemId,
2928 workspace_id: workspace::WorkspaceId
2929 ) -> Result<Option<workspace::ItemId>> {
2930 SELECT item_id
2931 FROM keybinding_editors
2932 WHERE item_id = ? AND workspace_id = ?
2933 }
2934 }
2935 }
2936}