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