1use std::{
2 cmp::{self},
3 ops::{Not as _, Range},
4 sync::Arc,
5 time::Duration,
6};
7
8use anyhow::{Context as _, anyhow};
9use collections::{HashMap, HashSet};
10use editor::{CompletionProvider, Editor, EditorEvent};
11use fs::Fs;
12use fuzzy::{StringMatch, StringMatchCandidate};
13use gpui::{
14 Action, Animation, AnimationExt, AppContext as _, AsyncApp, Axis, ClickEvent, Context,
15 DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, Global, IsZero,
16 KeyContext, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, Point, ScrollStrategy,
17 ScrollWheelEvent, Stateful, StyledText, Subscription, Task, TextStyleRefinement, WeakEntity,
18 actions, anchored, deferred, div,
19};
20use language::{Language, LanguageConfig, ToOffset as _};
21use notifications::status_toast::{StatusToast, ToastIcon};
22use project::Project;
23use settings::{BaseKeymap, KeybindSource, KeymapFile, Settings as _, SettingsAssets};
24use ui::{
25 ActiveTheme as _, App, Banner, BorrowAppContext, ContextMenu, IconButtonShape, Indicator,
26 Modal, ModalFooter, ModalHeader, ParentElement as _, Render, Section, SharedString,
27 Styled as _, Tooltip, Window, prelude::*,
28};
29use ui_input::SingleLineInput;
30use util::ResultExt;
31use workspace::{
32 Item, ModalView, SerializableItem, Workspace, notifications::NotifyTaskExt as _,
33 register_serializable_item,
34};
35
36use crate::{
37 keybindings::persistence::KEYBINDING_EDITORS,
38 ui_components::table::{ColumnWidths, ResizeBehavior, 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
51actions!(
52 keymap_editor,
53 [
54 /// Edits the selected key binding.
55 EditBinding,
56 /// Creates a new key binding for the selected action.
57 CreateBinding,
58 /// Deletes the selected key binding.
59 DeleteBinding,
60 /// Copies the action name to clipboard.
61 CopyAction,
62 /// Copies the context predicate to clipboard.
63 CopyContext,
64 /// Toggles Conflict Filtering
65 ToggleConflictFilter,
66 /// Toggle Keystroke search
67 ToggleKeystrokeSearch,
68 /// Toggles exact matching for keystroke search
69 ToggleExactKeystrokeMatching,
70 /// Shows matching keystrokes for the currently selected binding
71 ShowMatchingKeybinds
72 ]
73);
74
75actions!(
76 keystroke_input,
77 [
78 /// Starts recording keystrokes
79 StartRecording,
80 /// Stops recording keystrokes
81 StopRecording,
82 /// Clears the recorded keystrokes
83 ClearKeystrokes,
84 ]
85);
86
87pub fn init(cx: &mut App) {
88 let keymap_event_channel = KeymapEventChannel::new();
89 cx.set_global(keymap_event_channel);
90
91 cx.on_action(|_: &OpenKeymapEditor, cx| {
92 workspace::with_active_or_new_workspace(cx, move |workspace, window, cx| {
93 workspace
94 .with_local_workspace(window, cx, |workspace, window, cx| {
95 let existing = workspace
96 .active_pane()
97 .read(cx)
98 .items()
99 .find_map(|item| item.downcast::<KeymapEditor>());
100
101 if let Some(existing) = existing {
102 workspace.activate_item(&existing, true, true, window, cx);
103 } else {
104 let keymap_editor =
105 cx.new(|cx| KeymapEditor::new(workspace.weak_handle(), window, cx));
106 workspace.add_item_to_active_pane(
107 Box::new(keymap_editor),
108 None,
109 true,
110 window,
111 cx,
112 );
113 }
114 })
115 .detach();
116 })
117 });
118
119 register_serializable_item::<KeymapEditor>(cx);
120}
121
122pub struct KeymapEventChannel {}
123
124impl Global for KeymapEventChannel {}
125
126impl KeymapEventChannel {
127 fn new() -> Self {
128 Self {}
129 }
130
131 pub fn trigger_keymap_changed(cx: &mut App) {
132 let Some(_event_channel) = cx.try_global::<Self>() else {
133 // don't panic if no global defined. This usually happens in tests
134 return;
135 };
136 cx.update_global(|_event_channel: &mut Self, _| {
137 /* triggers observers in KeymapEditors */
138 });
139 }
140}
141
142#[derive(Default, PartialEq)]
143enum SearchMode {
144 #[default]
145 Normal,
146 KeyStroke {
147 exact_match: bool,
148 },
149}
150
151impl SearchMode {
152 fn invert(&self) -> Self {
153 match self {
154 SearchMode::Normal => SearchMode::KeyStroke { exact_match: false },
155 SearchMode::KeyStroke { .. } => SearchMode::Normal,
156 }
157 }
158
159 fn exact_match(&self) -> bool {
160 match self {
161 SearchMode::Normal => false,
162 SearchMode::KeyStroke { exact_match } => *exact_match,
163 }
164 }
165}
166
167#[derive(Default, PartialEq, Copy, Clone)]
168enum FilterState {
169 #[default]
170 All,
171 Conflicts,
172}
173
174impl FilterState {
175 fn invert(&self) -> Self {
176 match self {
177 FilterState::All => FilterState::Conflicts,
178 FilterState::Conflicts => FilterState::All,
179 }
180 }
181}
182
183#[derive(Debug, Default, PartialEq, Eq, Clone, Hash)]
184struct ActionMapping {
185 keystrokes: Vec<Keystroke>,
186 context: Option<SharedString>,
187}
188
189#[derive(Debug)]
190struct KeybindConflict {
191 first_conflict_index: usize,
192 remaining_conflict_amount: usize,
193}
194
195impl KeybindConflict {
196 fn from_iter<'a>(mut indices: impl Iterator<Item = &'a ConflictOrigin>) -> Option<Self> {
197 indices.next().map(|origin| Self {
198 first_conflict_index: origin.index,
199 remaining_conflict_amount: indices.count(),
200 })
201 }
202}
203
204#[derive(Clone, Copy, PartialEq)]
205struct ConflictOrigin {
206 override_source: KeybindSource,
207 overridden_source: Option<KeybindSource>,
208 index: usize,
209}
210
211impl ConflictOrigin {
212 fn new(source: KeybindSource, index: usize) -> Self {
213 Self {
214 override_source: source,
215 index,
216 overridden_source: None,
217 }
218 }
219
220 fn with_overridden_source(self, source: KeybindSource) -> Self {
221 Self {
222 overridden_source: Some(source),
223 ..self
224 }
225 }
226
227 fn get_conflict_with(&self, other: &Self) -> Option<Self> {
228 if self.override_source == KeybindSource::User
229 && other.override_source == KeybindSource::User
230 {
231 Some(
232 Self::new(KeybindSource::User, other.index)
233 .with_overridden_source(self.override_source),
234 )
235 } else if self.override_source > other.override_source {
236 Some(other.with_overridden_source(self.override_source))
237 } else {
238 None
239 }
240 }
241
242 fn is_user_keybind_conflict(&self) -> bool {
243 self.override_source == KeybindSource::User
244 && self.overridden_source == Some(KeybindSource::User)
245 }
246}
247
248#[derive(Default)]
249struct ConflictState {
250 conflicts: Vec<Option<ConflictOrigin>>,
251 keybind_mapping: HashMap<ActionMapping, Vec<ConflictOrigin>>,
252 has_user_conflicts: bool,
253}
254
255impl ConflictState {
256 fn new(key_bindings: &[ProcessedBinding]) -> Self {
257 let mut action_keybind_mapping: HashMap<_, Vec<ConflictOrigin>> = HashMap::default();
258
259 let mut largest_index = 0;
260 for (index, binding) in key_bindings
261 .iter()
262 .enumerate()
263 .flat_map(|(index, binding)| Some(index).zip(binding.keybind_information()))
264 {
265 action_keybind_mapping
266 .entry(binding.get_action_mapping())
267 .or_default()
268 .push(ConflictOrigin::new(binding.source, index));
269 largest_index = index;
270 }
271
272 let mut conflicts = vec![None; largest_index + 1];
273 let mut has_user_conflicts = false;
274
275 for indices in action_keybind_mapping.values_mut() {
276 indices.sort_unstable_by_key(|origin| origin.override_source);
277 let Some((fst, snd)) = indices.get(0).zip(indices.get(1)) else {
278 continue;
279 };
280
281 for origin in indices.iter() {
282 conflicts[origin.index] =
283 origin.get_conflict_with(if origin == fst { &snd } else { &fst })
284 }
285
286 has_user_conflicts |= fst.override_source == KeybindSource::User
287 && snd.override_source == KeybindSource::User;
288 }
289
290 Self {
291 conflicts,
292 keybind_mapping: action_keybind_mapping,
293 has_user_conflicts,
294 }
295 }
296
297 fn conflicting_indices_for_mapping(
298 &self,
299 action_mapping: &ActionMapping,
300 keybind_idx: Option<usize>,
301 ) -> Option<KeybindConflict> {
302 self.keybind_mapping
303 .get(action_mapping)
304 .and_then(|indices| {
305 KeybindConflict::from_iter(
306 indices
307 .iter()
308 .filter(|&conflict| Some(conflict.index) != keybind_idx),
309 )
310 })
311 }
312
313 fn conflict_for_idx(&self, idx: usize) -> Option<ConflictOrigin> {
314 self.conflicts.get(idx).copied().flatten()
315 }
316
317 fn has_user_conflict(&self, candidate_idx: usize) -> bool {
318 self.conflict_for_idx(candidate_idx)
319 .is_some_and(|conflict| conflict.is_user_keybind_conflict())
320 }
321
322 fn any_user_binding_conflicts(&self) -> bool {
323 self.has_user_conflicts
324 }
325}
326
327struct KeymapEditor {
328 workspace: WeakEntity<Workspace>,
329 focus_handle: FocusHandle,
330 _keymap_subscription: Subscription,
331 keybindings: Vec<ProcessedBinding>,
332 keybinding_conflict_state: ConflictState,
333 filter_state: FilterState,
334 search_mode: SearchMode,
335 search_query_debounce: Option<Task<()>>,
336 // corresponds 1 to 1 with keybindings
337 string_match_candidates: Arc<Vec<StringMatchCandidate>>,
338 matches: Vec<StringMatch>,
339 table_interaction_state: Entity<TableInteractionState>,
340 filter_editor: Entity<Editor>,
341 keystroke_editor: Entity<KeystrokeInput>,
342 selected_index: Option<usize>,
343 context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
344 previous_edit: Option<PreviousEdit>,
345 humanized_action_names: HumanizedActionNameCache,
346 current_widths: Entity<ColumnWidths<6>>,
347 show_hover_menus: bool,
348 /// In order for the JSON LSP to run in the actions arguments editor, we
349 /// require a backing file In order to avoid issues (primarily log spam)
350 /// with drop order between the buffer, file, worktree, etc, we create a
351 /// temporary directory for these backing files in the keymap editor struct
352 /// instead of here. This has the added benefit of only having to create a
353 /// worktree and directory once, although the perf improvement is negligible.
354 action_args_temp_dir_worktree: Option<Entity<project::Worktree>>,
355 action_args_temp_dir: Option<tempfile::TempDir>,
356}
357
358enum PreviousEdit {
359 /// When deleting, we want to maintain the same scroll position
360 ScrollBarOffset(Point<Pixels>),
361 /// When editing or creating, because the new keybinding could be in a different position in the sort order
362 /// we store metadata about the new binding (either the modified version or newly created one)
363 /// and upon reload, we search for this binding in the list of keybindings, and if we find the one that matches
364 /// this metadata, we set the selected index to it and scroll to it,
365 /// and if we don't find it, we scroll to 0 and don't set a selected index
366 Keybinding {
367 action_mapping: ActionMapping,
368 action_name: &'static str,
369 /// The scrollbar position to fallback to if we don't find the keybinding during a refresh
370 /// this can happen if there's a filter applied to the search and the keybinding modification
371 /// filters the binding from the search results
372 fallback: Point<Pixels>,
373 },
374}
375
376impl EventEmitter<()> for KeymapEditor {}
377
378impl Focusable for KeymapEditor {
379 fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
380 if self.selected_index.is_some() {
381 self.focus_handle.clone()
382 } else {
383 self.filter_editor.focus_handle(cx)
384 }
385 }
386}
387
388impl KeymapEditor {
389 fn new(workspace: WeakEntity<Workspace>, window: &mut Window, cx: &mut Context<Self>) -> Self {
390 let _keymap_subscription =
391 cx.observe_global_in::<KeymapEventChannel>(window, Self::on_keymap_changed);
392 let table_interaction_state = TableInteractionState::new(window, cx);
393
394 let keystroke_editor = cx.new(|cx| {
395 let mut keystroke_editor = KeystrokeInput::new(None, window, cx);
396 keystroke_editor.search = true;
397 keystroke_editor
398 });
399
400 let filter_editor = cx.new(|cx| {
401 let mut editor = Editor::single_line(window, cx);
402 editor.set_placeholder_text("Filter action names…", cx);
403 editor
404 });
405
406 cx.subscribe(&filter_editor, |this, _, e: &EditorEvent, cx| {
407 if !matches!(e, EditorEvent::BufferEdited) {
408 return;
409 }
410
411 this.on_query_changed(cx);
412 })
413 .detach();
414
415 cx.subscribe(&keystroke_editor, |this, _, _, cx| {
416 if matches!(this.search_mode, SearchMode::Normal) {
417 return;
418 }
419
420 this.on_query_changed(cx);
421 })
422 .detach();
423
424 cx.spawn({
425 let workspace = workspace.clone();
426 async move |this, cx| {
427 let temp_dir = tempfile::tempdir_in(paths::temp_dir())?;
428 let worktree = workspace
429 .update(cx, |ws, cx| {
430 ws.project()
431 .update(cx, |p, cx| p.create_worktree(temp_dir.path(), false, cx))
432 })?
433 .await?;
434 this.update(cx, |this, _| {
435 this.action_args_temp_dir = Some(temp_dir);
436 this.action_args_temp_dir_worktree = Some(worktree);
437 })
438 }
439 })
440 .detach();
441
442 let mut this = Self {
443 workspace,
444 keybindings: vec![],
445 keybinding_conflict_state: ConflictState::default(),
446 filter_state: FilterState::default(),
447 search_mode: SearchMode::default(),
448 string_match_candidates: Arc::new(vec![]),
449 matches: vec![],
450 focus_handle: cx.focus_handle(),
451 _keymap_subscription,
452 table_interaction_state,
453 filter_editor,
454 keystroke_editor,
455 selected_index: None,
456 context_menu: None,
457 previous_edit: None,
458 search_query_debounce: None,
459 humanized_action_names: HumanizedActionNameCache::new(cx),
460 show_hover_menus: true,
461 action_args_temp_dir: None,
462 action_args_temp_dir_worktree: None,
463 current_widths: cx.new(|cx| ColumnWidths::new(cx)),
464 };
465
466 this.on_keymap_changed(window, cx);
467
468 this
469 }
470
471 fn current_action_query(&self, cx: &App) -> String {
472 self.filter_editor.read(cx).text(cx)
473 }
474
475 fn current_keystroke_query(&self, cx: &App) -> Vec<Keystroke> {
476 match self.search_mode {
477 SearchMode::KeyStroke { .. } => self
478 .keystroke_editor
479 .read(cx)
480 .keystrokes()
481 .iter()
482 .cloned()
483 .collect(),
484 SearchMode::Normal => Default::default(),
485 }
486 }
487
488 fn on_query_changed(&mut self, cx: &mut Context<Self>) {
489 let action_query = self.current_action_query(cx);
490 let keystroke_query = self.current_keystroke_query(cx);
491 let exact_match = self.search_mode.exact_match();
492
493 let timer = cx.background_executor().timer(Duration::from_secs(1));
494 self.search_query_debounce = Some(cx.background_spawn({
495 let action_query = action_query.clone();
496 let keystroke_query = keystroke_query.clone();
497 async move {
498 timer.await;
499
500 let keystroke_query = keystroke_query
501 .into_iter()
502 .map(|keystroke| keystroke.unparse())
503 .collect::<Vec<String>>()
504 .join(" ");
505
506 telemetry::event!(
507 "Keystroke Search Completed",
508 action_query = action_query,
509 keystroke_query = keystroke_query,
510 keystroke_exact_match = exact_match
511 )
512 }
513 }));
514 cx.spawn(async move |this, cx| {
515 Self::update_matches(this.clone(), action_query, keystroke_query, cx).await?;
516 this.update(cx, |this, cx| {
517 this.scroll_to_item(0, ScrollStrategy::Top, cx)
518 })
519 })
520 .detach();
521 }
522
523 async fn update_matches(
524 this: WeakEntity<Self>,
525 action_query: String,
526 keystroke_query: Vec<Keystroke>,
527 cx: &mut AsyncApp,
528 ) -> anyhow::Result<()> {
529 let action_query = command_palette::normalize_action_query(&action_query);
530 let (string_match_candidates, keybind_count) = this.read_with(cx, |this, _| {
531 (this.string_match_candidates.clone(), this.keybindings.len())
532 })?;
533 let executor = cx.background_executor().clone();
534 let mut matches = fuzzy::match_strings(
535 &string_match_candidates,
536 &action_query,
537 true,
538 true,
539 keybind_count,
540 &Default::default(),
541 executor,
542 )
543 .await;
544 this.update(cx, |this, cx| {
545 match this.filter_state {
546 FilterState::Conflicts => {
547 matches.retain(|candidate| {
548 this.keybinding_conflict_state
549 .has_user_conflict(candidate.candidate_id)
550 });
551 }
552 FilterState::All => {}
553 }
554
555 match this.search_mode {
556 SearchMode::KeyStroke { exact_match } => {
557 matches.retain(|item| {
558 this.keybindings[item.candidate_id]
559 .keystrokes()
560 .is_some_and(|keystrokes| {
561 if exact_match {
562 keystroke_query.len() == keystrokes.len()
563 && keystroke_query.iter().zip(keystrokes).all(
564 |(query, keystroke)| {
565 query.key == keystroke.key
566 && query.modifiers == keystroke.modifiers
567 },
568 )
569 } else {
570 let key_press_query =
571 KeyPressIterator::new(keystroke_query.as_slice());
572 let mut last_match_idx = 0;
573
574 key_press_query.into_iter().all(|key| {
575 let key_presses = KeyPressIterator::new(keystrokes);
576 key_presses.into_iter().enumerate().any(
577 |(index, keystroke)| {
578 if last_match_idx > index || keystroke != key {
579 return false;
580 }
581
582 last_match_idx = index;
583 true
584 },
585 )
586 })
587 }
588 })
589 });
590 }
591 SearchMode::Normal => {}
592 }
593
594 if action_query.is_empty() {
595 matches.sort_by(|item1, item2| {
596 let binding1 = &this.keybindings[item1.candidate_id];
597 let binding2 = &this.keybindings[item2.candidate_id];
598
599 binding1.cmp(binding2)
600 });
601 }
602 this.selected_index.take();
603 this.matches = matches;
604
605 cx.notify();
606 })
607 }
608
609 fn get_conflict(&self, row_index: usize) -> Option<ConflictOrigin> {
610 self.matches.get(row_index).and_then(|candidate| {
611 self.keybinding_conflict_state
612 .conflict_for_idx(candidate.candidate_id)
613 })
614 }
615
616 fn process_bindings(
617 json_language: Arc<Language>,
618 zed_keybind_context_language: Arc<Language>,
619 humanized_action_names: &HumanizedActionNameCache,
620 cx: &mut App,
621 ) -> (Vec<ProcessedBinding>, Vec<StringMatchCandidate>) {
622 let key_bindings_ptr = cx.key_bindings();
623 let lock = key_bindings_ptr.borrow();
624 let key_bindings = lock.bindings();
625 let mut unmapped_action_names =
626 HashSet::from_iter(cx.all_action_names().into_iter().copied());
627 let action_documentation = cx.action_documentation();
628 let mut generator = KeymapFile::action_schema_generator();
629 let actions_with_schemas = HashSet::from_iter(
630 cx.action_schemas(&mut generator)
631 .into_iter()
632 .filter_map(|(name, schema)| schema.is_some().then_some(name)),
633 );
634
635 let mut processed_bindings = Vec::new();
636 let mut string_match_candidates = Vec::new();
637
638 for key_binding in key_bindings {
639 let source = key_binding
640 .meta()
641 .map(KeybindSource::from_meta)
642 .unwrap_or(KeybindSource::Unknown);
643
644 let keystroke_text = ui::text_for_keystrokes(key_binding.keystrokes(), cx);
645 let ui_key_binding = ui::KeyBinding::new_from_gpui(key_binding.clone(), cx)
646 .vim_mode(source == KeybindSource::Vim);
647
648 let context = key_binding
649 .predicate()
650 .map(|predicate| {
651 KeybindContextString::Local(
652 predicate.to_string().into(),
653 zed_keybind_context_language.clone(),
654 )
655 })
656 .unwrap_or(KeybindContextString::Global);
657
658 let action_name = key_binding.action().name();
659 unmapped_action_names.remove(&action_name);
660
661 let action_arguments = key_binding
662 .action_input()
663 .map(|arguments| SyntaxHighlightedText::new(arguments, json_language.clone()));
664 let action_information = ActionInformation::new(
665 action_name,
666 action_arguments,
667 &actions_with_schemas,
668 &action_documentation,
669 &humanized_action_names,
670 );
671
672 let index = processed_bindings.len();
673 let string_match_candidate =
674 StringMatchCandidate::new(index, &action_information.humanized_name);
675 processed_bindings.push(ProcessedBinding::new_mapped(
676 keystroke_text,
677 ui_key_binding,
678 context,
679 source,
680 action_information,
681 ));
682 string_match_candidates.push(string_match_candidate);
683 }
684
685 for action_name in unmapped_action_names.into_iter() {
686 let index = processed_bindings.len();
687 let action_information = ActionInformation::new(
688 action_name,
689 None,
690 &actions_with_schemas,
691 &action_documentation,
692 &humanized_action_names,
693 );
694 let string_match_candidate =
695 StringMatchCandidate::new(index, &action_information.humanized_name);
696
697 processed_bindings.push(ProcessedBinding::Unmapped(action_information));
698 string_match_candidates.push(string_match_candidate);
699 }
700
701 (processed_bindings, string_match_candidates)
702 }
703
704 fn on_keymap_changed(&mut self, window: &mut Window, cx: &mut Context<KeymapEditor>) {
705 let workspace = self.workspace.clone();
706 cx.spawn_in(window, async move |this, cx| {
707 let json_language = load_json_language(workspace.clone(), cx).await;
708 let zed_keybind_context_language =
709 load_keybind_context_language(workspace.clone(), cx).await;
710
711 let (action_query, keystroke_query) = this.update(cx, |this, cx| {
712 let (key_bindings, string_match_candidates) = Self::process_bindings(
713 json_language,
714 zed_keybind_context_language,
715 &this.humanized_action_names,
716 cx,
717 );
718
719 this.keybinding_conflict_state = ConflictState::new(&key_bindings);
720
721 this.keybindings = key_bindings;
722 this.string_match_candidates = Arc::new(string_match_candidates);
723 this.matches = this
724 .string_match_candidates
725 .iter()
726 .enumerate()
727 .map(|(ix, candidate)| StringMatch {
728 candidate_id: ix,
729 score: 0.0,
730 positions: vec![],
731 string: candidate.string.clone(),
732 })
733 .collect();
734 (
735 this.current_action_query(cx),
736 this.current_keystroke_query(cx),
737 )
738 })?;
739 // calls cx.notify
740 Self::update_matches(this.clone(), action_query, keystroke_query, cx).await?;
741 this.update_in(cx, |this, window, cx| {
742 if let Some(previous_edit) = this.previous_edit.take() {
743 match previous_edit {
744 // should remove scroll from process_query
745 PreviousEdit::ScrollBarOffset(offset) => {
746 this.table_interaction_state.update(cx, |table, _| {
747 table.set_scrollbar_offset(Axis::Vertical, offset)
748 })
749 // set selected index and scroll
750 }
751 PreviousEdit::Keybinding {
752 action_mapping,
753 action_name,
754 fallback,
755 } => {
756 let scroll_position =
757 this.matches.iter().enumerate().find_map(|(index, item)| {
758 let binding = &this.keybindings[item.candidate_id];
759 if binding.get_action_mapping().is_some_and(|binding_mapping| {
760 binding_mapping == action_mapping
761 }) && binding.action().name == action_name
762 {
763 Some(index)
764 } else {
765 None
766 }
767 });
768
769 if let Some(scroll_position) = scroll_position {
770 this.select_index(
771 scroll_position,
772 Some(ScrollStrategy::Top),
773 window,
774 cx,
775 );
776 } else {
777 this.table_interaction_state.update(cx, |table, _| {
778 table.set_scrollbar_offset(Axis::Vertical, fallback)
779 });
780 }
781 cx.notify();
782 }
783 }
784 }
785 })
786 })
787 .detach_and_log_err(cx);
788 }
789
790 fn key_context(&self) -> KeyContext {
791 let mut dispatch_context = KeyContext::new_with_defaults();
792 dispatch_context.add("KeymapEditor");
793 dispatch_context.add("menu");
794
795 dispatch_context
796 }
797
798 fn scroll_to_item(&self, index: usize, strategy: ScrollStrategy, cx: &mut App) {
799 let index = usize::min(index, self.matches.len().saturating_sub(1));
800 self.table_interaction_state.update(cx, |this, _cx| {
801 this.scroll_handle.scroll_to_item(index, strategy);
802 });
803 }
804
805 fn focus_search(
806 &mut self,
807 _: &search::FocusSearch,
808 window: &mut Window,
809 cx: &mut Context<Self>,
810 ) {
811 if !self
812 .filter_editor
813 .focus_handle(cx)
814 .contains_focused(window, cx)
815 {
816 window.focus(&self.filter_editor.focus_handle(cx));
817 } else {
818 self.filter_editor.update(cx, |editor, cx| {
819 editor.select_all(&Default::default(), window, cx);
820 });
821 }
822 self.selected_index.take();
823 }
824
825 fn selected_keybind_index(&self) -> Option<usize> {
826 self.selected_index
827 .and_then(|match_index| self.matches.get(match_index))
828 .map(|r#match| r#match.candidate_id)
829 }
830
831 fn selected_keybind_and_index(&self) -> Option<(&ProcessedBinding, usize)> {
832 self.selected_keybind_index()
833 .map(|keybind_index| (&self.keybindings[keybind_index], keybind_index))
834 }
835
836 fn selected_binding(&self) -> Option<&ProcessedBinding> {
837 self.selected_keybind_index()
838 .and_then(|keybind_index| self.keybindings.get(keybind_index))
839 }
840
841 fn select_index(
842 &mut self,
843 index: usize,
844 scroll: Option<ScrollStrategy>,
845 window: &mut Window,
846 cx: &mut Context<Self>,
847 ) {
848 if self.selected_index != Some(index) {
849 self.selected_index = Some(index);
850 if let Some(scroll_strategy) = scroll {
851 self.scroll_to_item(index, scroll_strategy, cx);
852 }
853 window.focus(&self.focus_handle);
854 cx.notify();
855 }
856 }
857
858 fn create_context_menu(
859 &mut self,
860 position: Point<Pixels>,
861 window: &mut Window,
862 cx: &mut Context<Self>,
863 ) {
864 self.context_menu = self.selected_binding().map(|selected_binding| {
865 let selected_binding_has_no_context = selected_binding
866 .context()
867 .and_then(KeybindContextString::local)
868 .is_none();
869
870 let selected_binding_is_unbound = selected_binding.is_unbound();
871
872 let context_menu = ContextMenu::build(window, cx, |menu, _window, _cx| {
873 menu.context(self.focus_handle.clone())
874 .action_disabled_when(
875 selected_binding_is_unbound,
876 "Edit",
877 Box::new(EditBinding),
878 )
879 .action("Create", Box::new(CreateBinding))
880 .action_disabled_when(
881 selected_binding_is_unbound,
882 "Delete",
883 Box::new(DeleteBinding),
884 )
885 .separator()
886 .action("Copy Action", Box::new(CopyAction))
887 .action_disabled_when(
888 selected_binding_has_no_context,
889 "Copy Context",
890 Box::new(CopyContext),
891 )
892 .separator()
893 .action_disabled_when(
894 selected_binding_has_no_context,
895 "Show Matching Keybindings",
896 Box::new(ShowMatchingKeybinds),
897 )
898 });
899
900 let context_menu_handle = context_menu.focus_handle(cx);
901 window.defer(cx, move |window, _cx| window.focus(&context_menu_handle));
902 let subscription = cx.subscribe_in(
903 &context_menu,
904 window,
905 |this, _, _: &DismissEvent, window, cx| {
906 this.dismiss_context_menu(window, cx);
907 },
908 );
909 (context_menu, position, subscription)
910 });
911
912 cx.notify();
913 }
914
915 fn dismiss_context_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) {
916 self.context_menu.take();
917 window.focus(&self.focus_handle);
918 cx.notify();
919 }
920
921 fn context_menu_deployed(&self) -> bool {
922 self.context_menu.is_some()
923 }
924
925 fn create_row_button(
926 &self,
927 index: usize,
928 conflict: Option<ConflictOrigin>,
929 cx: &mut Context<Self>,
930 ) -> IconButton {
931 if self.filter_state != FilterState::Conflicts
932 && let Some(conflict) = conflict
933 {
934 if conflict.is_user_keybind_conflict() {
935 base_button_style(index, IconName::Warning)
936 .icon_color(Color::Warning)
937 .tooltip(|window, cx| {
938 Tooltip::with_meta(
939 "View conflicts",
940 Some(&ToggleConflictFilter),
941 "Use alt+click to show all conflicts",
942 window,
943 cx,
944 )
945 })
946 .on_click(cx.listener(move |this, click: &ClickEvent, window, cx| {
947 if click.modifiers().alt {
948 this.set_filter_state(FilterState::Conflicts, cx);
949 } else {
950 this.select_index(index, None, window, cx);
951 this.open_edit_keybinding_modal(false, window, cx);
952 cx.stop_propagation();
953 }
954 }))
955 } else if self.search_mode.exact_match() {
956 base_button_style(index, IconName::Info)
957 .tooltip(|window, cx| {
958 Tooltip::with_meta(
959 "Edit this binding",
960 Some(&ShowMatchingKeybinds),
961 "This binding is overridden by other bindings.",
962 window,
963 cx,
964 )
965 })
966 .on_click(cx.listener(move |this, _: &ClickEvent, window, cx| {
967 this.select_index(index, None, window, cx);
968 this.open_edit_keybinding_modal(false, window, cx);
969 cx.stop_propagation();
970 }))
971 } else {
972 base_button_style(index, IconName::Info)
973 .tooltip(|window, cx| {
974 Tooltip::with_meta(
975 "Show matching keybinds",
976 Some(&ShowMatchingKeybinds),
977 "This binding is overridden by other bindings.\nUse alt+click to edit this binding",
978 window,
979 cx,
980 )
981 })
982 .on_click(cx.listener(move |this, click: &ClickEvent, window, cx| {
983 if click.modifiers().alt {
984 this.select_index(index, None, window, cx);
985 this.open_edit_keybinding_modal(false, window, cx);
986 cx.stop_propagation();
987 } else {
988 this.show_matching_keystrokes(&Default::default(), window, cx);
989 }
990 }))
991 }
992 } else {
993 base_button_style(index, IconName::Pencil)
994 .visible_on_hover(if self.selected_index == Some(index) {
995 "".into()
996 } else if self.show_hover_menus {
997 row_group_id(index)
998 } else {
999 "never-show".into()
1000 })
1001 .when(
1002 self.show_hover_menus && !self.context_menu_deployed(),
1003 |this| this.tooltip(Tooltip::for_action_title("Edit Keybinding", &EditBinding)),
1004 )
1005 .on_click(cx.listener(move |this, _, window, cx| {
1006 this.select_index(index, None, window, cx);
1007 this.open_edit_keybinding_modal(false, window, cx);
1008 cx.stop_propagation();
1009 }))
1010 }
1011 }
1012
1013 fn render_no_matches_hint(&self, _window: &mut Window, _cx: &App) -> AnyElement {
1014 let hint = match (self.filter_state, &self.search_mode) {
1015 (FilterState::Conflicts, _) => {
1016 if self.keybinding_conflict_state.any_user_binding_conflicts() {
1017 "No conflicting keybinds found that match the provided query"
1018 } else {
1019 "No conflicting keybinds found"
1020 }
1021 }
1022 (FilterState::All, SearchMode::KeyStroke { .. }) => {
1023 "No keybinds found matching the entered keystrokes"
1024 }
1025 (FilterState::All, SearchMode::Normal) => "No matches found for the provided query",
1026 };
1027
1028 Label::new(hint).color(Color::Muted).into_any_element()
1029 }
1030
1031 fn select_next(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) {
1032 self.show_hover_menus = false;
1033 if let Some(selected) = self.selected_index {
1034 let selected = selected + 1;
1035 if selected >= self.matches.len() {
1036 self.select_last(&Default::default(), window, cx);
1037 } else {
1038 self.select_index(selected, Some(ScrollStrategy::Center), window, cx);
1039 }
1040 } else {
1041 self.select_first(&Default::default(), window, cx);
1042 }
1043 }
1044
1045 fn select_previous(
1046 &mut self,
1047 _: &menu::SelectPrevious,
1048 window: &mut Window,
1049 cx: &mut Context<Self>,
1050 ) {
1051 self.show_hover_menus = false;
1052 if let Some(selected) = self.selected_index {
1053 if selected == 0 {
1054 return;
1055 }
1056
1057 let selected = selected - 1;
1058
1059 if selected >= self.matches.len() {
1060 self.select_last(&Default::default(), window, cx);
1061 } else {
1062 self.select_index(selected, Some(ScrollStrategy::Center), window, cx);
1063 }
1064 } else {
1065 self.select_last(&Default::default(), window, cx);
1066 }
1067 }
1068
1069 fn select_first(&mut self, _: &menu::SelectFirst, window: &mut Window, cx: &mut Context<Self>) {
1070 self.show_hover_menus = false;
1071 if self.matches.get(0).is_some() {
1072 self.select_index(0, Some(ScrollStrategy::Center), window, cx);
1073 }
1074 }
1075
1076 fn select_last(&mut self, _: &menu::SelectLast, window: &mut Window, cx: &mut Context<Self>) {
1077 self.show_hover_menus = false;
1078 if self.matches.last().is_some() {
1079 let index = self.matches.len() - 1;
1080 self.select_index(index, Some(ScrollStrategy::Center), window, cx);
1081 }
1082 }
1083
1084 fn open_edit_keybinding_modal(
1085 &mut self,
1086 create: bool,
1087 window: &mut Window,
1088 cx: &mut Context<Self>,
1089 ) {
1090 self.show_hover_menus = false;
1091 let Some((keybind, keybind_index)) = self.selected_keybind_and_index() else {
1092 return;
1093 };
1094 let keybind = keybind.clone();
1095 let keymap_editor = cx.entity();
1096
1097 let keystroke = keybind.keystroke_text().cloned().unwrap_or_default();
1098 let arguments = keybind
1099 .action()
1100 .arguments
1101 .as_ref()
1102 .map(|arguments| arguments.text.clone());
1103 let context = keybind
1104 .context()
1105 .map(|context| context.local_str().unwrap_or("global"));
1106 let action = keybind.action().name;
1107 let source = keybind.keybind_source().map(|source| source.name());
1108
1109 telemetry::event!(
1110 "Edit Keybinding Modal Opened",
1111 keystroke = keystroke,
1112 action = action,
1113 source = source,
1114 context = context,
1115 arguments = arguments,
1116 );
1117
1118 let temp_dir = self.action_args_temp_dir.as_ref().map(|dir| dir.path());
1119
1120 self.workspace
1121 .update(cx, |workspace, cx| {
1122 let fs = workspace.app_state().fs.clone();
1123 let workspace_weak = cx.weak_entity();
1124 workspace.toggle_modal(window, cx, |window, cx| {
1125 let modal = KeybindingEditorModal::new(
1126 create,
1127 keybind,
1128 keybind_index,
1129 keymap_editor,
1130 temp_dir,
1131 workspace_weak,
1132 fs,
1133 window,
1134 cx,
1135 );
1136 window.focus(&modal.focus_handle(cx));
1137 modal
1138 });
1139 })
1140 .log_err();
1141 }
1142
1143 fn edit_binding(&mut self, _: &EditBinding, window: &mut Window, cx: &mut Context<Self>) {
1144 self.open_edit_keybinding_modal(false, window, cx);
1145 }
1146
1147 fn create_binding(&mut self, _: &CreateBinding, window: &mut Window, cx: &mut Context<Self>) {
1148 self.open_edit_keybinding_modal(true, window, cx);
1149 }
1150
1151 fn delete_binding(&mut self, _: &DeleteBinding, window: &mut Window, cx: &mut Context<Self>) {
1152 let Some(to_remove) = self.selected_binding().cloned() else {
1153 return;
1154 };
1155
1156 let std::result::Result::Ok(fs) = self
1157 .workspace
1158 .read_with(cx, |workspace, _| workspace.app_state().fs.clone())
1159 else {
1160 return;
1161 };
1162 let tab_size = cx.global::<settings::SettingsStore>().json_tab_size();
1163 self.previous_edit = Some(PreviousEdit::ScrollBarOffset(
1164 self.table_interaction_state
1165 .read(cx)
1166 .get_scrollbar_offset(Axis::Vertical),
1167 ));
1168 cx.spawn(async move |_, _| remove_keybinding(to_remove, &fs, tab_size).await)
1169 .detach_and_notify_err(window, cx);
1170 }
1171
1172 fn copy_context_to_clipboard(
1173 &mut self,
1174 _: &CopyContext,
1175 _window: &mut Window,
1176 cx: &mut Context<Self>,
1177 ) {
1178 let context = self
1179 .selected_binding()
1180 .and_then(|binding| binding.context())
1181 .and_then(KeybindContextString::local_str)
1182 .map(|context| context.to_string());
1183 let Some(context) = context else {
1184 return;
1185 };
1186
1187 telemetry::event!("Keybinding Context Copied", context = context.clone());
1188 cx.write_to_clipboard(gpui::ClipboardItem::new_string(context.clone()));
1189 }
1190
1191 fn copy_action_to_clipboard(
1192 &mut self,
1193 _: &CopyAction,
1194 _window: &mut Window,
1195 cx: &mut Context<Self>,
1196 ) {
1197 let action = self
1198 .selected_binding()
1199 .map(|binding| binding.action().name.to_string());
1200 let Some(action) = action else {
1201 return;
1202 };
1203
1204 telemetry::event!("Keybinding Action Copied", action = action.clone());
1205 cx.write_to_clipboard(gpui::ClipboardItem::new_string(action.clone()));
1206 }
1207
1208 fn toggle_conflict_filter(
1209 &mut self,
1210 _: &ToggleConflictFilter,
1211 _: &mut Window,
1212 cx: &mut Context<Self>,
1213 ) {
1214 self.set_filter_state(self.filter_state.invert(), cx);
1215 }
1216
1217 fn set_filter_state(&mut self, filter_state: FilterState, cx: &mut Context<Self>) {
1218 if self.filter_state != filter_state {
1219 self.filter_state = filter_state;
1220 self.on_query_changed(cx);
1221 }
1222 }
1223
1224 fn toggle_keystroke_search(
1225 &mut self,
1226 _: &ToggleKeystrokeSearch,
1227 window: &mut Window,
1228 cx: &mut Context<Self>,
1229 ) {
1230 self.search_mode = self.search_mode.invert();
1231 self.on_query_changed(cx);
1232
1233 match self.search_mode {
1234 SearchMode::KeyStroke { .. } => {
1235 window.focus(&self.keystroke_editor.read(cx).recording_focus_handle(cx));
1236 }
1237 SearchMode::Normal => {
1238 self.keystroke_editor.update(cx, |editor, cx| {
1239 editor.clear_keystrokes(&ClearKeystrokes, window, cx)
1240 });
1241 window.focus(&self.filter_editor.focus_handle(cx));
1242 }
1243 }
1244 }
1245
1246 fn toggle_exact_keystroke_matching(
1247 &mut self,
1248 _: &ToggleExactKeystrokeMatching,
1249 _: &mut Window,
1250 cx: &mut Context<Self>,
1251 ) {
1252 let SearchMode::KeyStroke { exact_match } = &mut self.search_mode else {
1253 return;
1254 };
1255
1256 *exact_match = !(*exact_match);
1257 self.on_query_changed(cx);
1258 }
1259
1260 fn show_matching_keystrokes(
1261 &mut self,
1262 _: &ShowMatchingKeybinds,
1263 _: &mut Window,
1264 cx: &mut Context<Self>,
1265 ) {
1266 let Some(selected_binding) = self.selected_binding() else {
1267 return;
1268 };
1269
1270 let keystrokes = selected_binding
1271 .keystrokes()
1272 .map(Vec::from)
1273 .unwrap_or_default();
1274
1275 self.filter_state = FilterState::All;
1276 self.search_mode = SearchMode::KeyStroke { exact_match: true };
1277
1278 self.keystroke_editor.update(cx, |editor, cx| {
1279 editor.set_keystrokes(keystrokes, cx);
1280 });
1281 }
1282}
1283
1284struct HumanizedActionNameCache {
1285 cache: HashMap<&'static str, SharedString>,
1286}
1287
1288impl HumanizedActionNameCache {
1289 fn new(cx: &App) -> Self {
1290 let cache = HashMap::from_iter(cx.all_action_names().into_iter().map(|&action_name| {
1291 (
1292 action_name,
1293 command_palette::humanize_action_name(action_name).into(),
1294 )
1295 }));
1296 Self { cache }
1297 }
1298
1299 fn get(&self, action_name: &'static str) -> SharedString {
1300 match self.cache.get(action_name) {
1301 Some(name) => name.clone(),
1302 None => action_name.into(),
1303 }
1304 }
1305}
1306
1307#[derive(Clone)]
1308struct KeybindInformation {
1309 keystroke_text: SharedString,
1310 ui_binding: ui::KeyBinding,
1311 context: KeybindContextString,
1312 source: KeybindSource,
1313}
1314
1315impl KeybindInformation {
1316 fn get_action_mapping(&self) -> ActionMapping {
1317 ActionMapping {
1318 keystrokes: self.ui_binding.keystrokes.clone(),
1319 context: self.context.local().cloned(),
1320 }
1321 }
1322}
1323
1324#[derive(Clone)]
1325struct ActionInformation {
1326 name: &'static str,
1327 humanized_name: SharedString,
1328 arguments: Option<SyntaxHighlightedText>,
1329 documentation: Option<&'static str>,
1330 has_schema: bool,
1331}
1332
1333impl ActionInformation {
1334 fn new(
1335 action_name: &'static str,
1336 action_arguments: Option<SyntaxHighlightedText>,
1337 actions_with_schemas: &HashSet<&'static str>,
1338 action_documentation: &HashMap<&'static str, &'static str>,
1339 action_name_cache: &HumanizedActionNameCache,
1340 ) -> Self {
1341 Self {
1342 humanized_name: action_name_cache.get(action_name),
1343 has_schema: actions_with_schemas.contains(action_name),
1344 arguments: action_arguments,
1345 documentation: action_documentation.get(action_name).copied(),
1346 name: action_name,
1347 }
1348 }
1349}
1350
1351#[derive(Clone)]
1352enum ProcessedBinding {
1353 Mapped(KeybindInformation, ActionInformation),
1354 Unmapped(ActionInformation),
1355}
1356
1357impl ProcessedBinding {
1358 fn new_mapped(
1359 keystroke_text: impl Into<SharedString>,
1360 ui_key_binding: ui::KeyBinding,
1361 context: KeybindContextString,
1362 source: KeybindSource,
1363 action_information: ActionInformation,
1364 ) -> Self {
1365 Self::Mapped(
1366 KeybindInformation {
1367 keystroke_text: keystroke_text.into(),
1368 ui_binding: ui_key_binding,
1369 context,
1370 source,
1371 },
1372 action_information,
1373 )
1374 }
1375
1376 fn is_unbound(&self) -> bool {
1377 matches!(self, Self::Unmapped(_))
1378 }
1379
1380 fn get_action_mapping(&self) -> Option<ActionMapping> {
1381 self.keybind_information()
1382 .map(|keybind| keybind.get_action_mapping())
1383 }
1384
1385 fn keystrokes(&self) -> Option<&[Keystroke]> {
1386 self.ui_key_binding()
1387 .map(|binding| binding.keystrokes.as_slice())
1388 }
1389
1390 fn keybind_information(&self) -> Option<&KeybindInformation> {
1391 match self {
1392 Self::Mapped(keybind_information, _) => Some(keybind_information),
1393 Self::Unmapped(_) => None,
1394 }
1395 }
1396
1397 fn keybind_source(&self) -> Option<KeybindSource> {
1398 self.keybind_information().map(|keybind| keybind.source)
1399 }
1400
1401 fn context(&self) -> Option<&KeybindContextString> {
1402 self.keybind_information().map(|keybind| &keybind.context)
1403 }
1404
1405 fn ui_key_binding(&self) -> Option<&ui::KeyBinding> {
1406 self.keybind_information()
1407 .map(|keybind| &keybind.ui_binding)
1408 }
1409
1410 fn keystroke_text(&self) -> Option<&SharedString> {
1411 self.keybind_information()
1412 .map(|binding| &binding.keystroke_text)
1413 }
1414
1415 fn action(&self) -> &ActionInformation {
1416 match self {
1417 Self::Mapped(_, action) | Self::Unmapped(action) => action,
1418 }
1419 }
1420
1421 fn cmp(&self, other: &Self) -> cmp::Ordering {
1422 match (self, other) {
1423 (Self::Mapped(keybind1, action1), Self::Mapped(keybind2, action2)) => {
1424 match keybind1.source.cmp(&keybind2.source) {
1425 cmp::Ordering::Equal => action1.humanized_name.cmp(&action2.humanized_name),
1426 ordering => ordering,
1427 }
1428 }
1429 (Self::Mapped(_, _), Self::Unmapped(_)) => cmp::Ordering::Less,
1430 (Self::Unmapped(_), Self::Mapped(_, _)) => cmp::Ordering::Greater,
1431 (Self::Unmapped(action1), Self::Unmapped(action2)) => {
1432 action1.humanized_name.cmp(&action2.humanized_name)
1433 }
1434 }
1435 }
1436}
1437
1438#[derive(Clone, Debug, IntoElement, PartialEq, Eq, Hash)]
1439enum KeybindContextString {
1440 Global,
1441 Local(SharedString, Arc<Language>),
1442}
1443
1444impl KeybindContextString {
1445 const GLOBAL: SharedString = SharedString::new_static("<global>");
1446
1447 pub fn local(&self) -> Option<&SharedString> {
1448 match self {
1449 KeybindContextString::Global => None,
1450 KeybindContextString::Local(name, _) => Some(name),
1451 }
1452 }
1453
1454 pub fn local_str(&self) -> Option<&str> {
1455 match self {
1456 KeybindContextString::Global => None,
1457 KeybindContextString::Local(name, _) => Some(name),
1458 }
1459 }
1460}
1461
1462impl RenderOnce for KeybindContextString {
1463 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
1464 match self {
1465 KeybindContextString::Global => {
1466 muted_styled_text(KeybindContextString::GLOBAL.clone(), cx).into_any_element()
1467 }
1468 KeybindContextString::Local(name, language) => {
1469 SyntaxHighlightedText::new(name, language).into_any_element()
1470 }
1471 }
1472 }
1473}
1474
1475fn muted_styled_text(text: SharedString, cx: &App) -> StyledText {
1476 let len = text.len();
1477 StyledText::new(text).with_highlights([(
1478 0..len,
1479 gpui::HighlightStyle::color(cx.theme().colors().text_muted),
1480 )])
1481}
1482
1483impl Item for KeymapEditor {
1484 type Event = ();
1485
1486 fn tab_content_text(&self, _detail: usize, _cx: &App) -> ui::SharedString {
1487 "Keymap Editor".into()
1488 }
1489}
1490
1491impl Render for KeymapEditor {
1492 fn render(&mut self, _window: &mut Window, cx: &mut ui::Context<Self>) -> impl ui::IntoElement {
1493 let row_count = self.matches.len();
1494 let theme = cx.theme();
1495 let focus_handle = &self.focus_handle;
1496
1497 v_flex()
1498 .id("keymap-editor")
1499 .track_focus(focus_handle)
1500 .key_context(self.key_context())
1501 .on_action(cx.listener(Self::select_next))
1502 .on_action(cx.listener(Self::select_previous))
1503 .on_action(cx.listener(Self::select_first))
1504 .on_action(cx.listener(Self::select_last))
1505 .on_action(cx.listener(Self::focus_search))
1506 .on_action(cx.listener(Self::edit_binding))
1507 .on_action(cx.listener(Self::create_binding))
1508 .on_action(cx.listener(Self::delete_binding))
1509 .on_action(cx.listener(Self::copy_action_to_clipboard))
1510 .on_action(cx.listener(Self::copy_context_to_clipboard))
1511 .on_action(cx.listener(Self::toggle_conflict_filter))
1512 .on_action(cx.listener(Self::toggle_keystroke_search))
1513 .on_action(cx.listener(Self::toggle_exact_keystroke_matching))
1514 .on_action(cx.listener(Self::show_matching_keystrokes))
1515 .on_mouse_move(cx.listener(|this, _, _window, _cx| {
1516 this.show_hover_menus = true;
1517 }))
1518 .size_full()
1519 .p_2()
1520 .gap_1()
1521 .bg(theme.colors().editor_background)
1522 .child(
1523 v_flex()
1524 .gap_2()
1525 .child(
1526 h_flex()
1527 .gap_2()
1528 .child(
1529 div()
1530 .key_context({
1531 let mut context = KeyContext::new_with_defaults();
1532 context.add("BufferSearchBar");
1533 context
1534 })
1535 .size_full()
1536 .h_8()
1537 .pl_2()
1538 .pr_1()
1539 .py_1()
1540 .border_1()
1541 .border_color(theme.colors().border)
1542 .rounded_lg()
1543 .child(self.filter_editor.clone()),
1544 )
1545 .child(
1546 IconButton::new(
1547 "KeymapEditorToggleFiltersIcon",
1548 IconName::Keyboard,
1549 )
1550 .shape(ui::IconButtonShape::Square)
1551 .tooltip({
1552 let focus_handle = focus_handle.clone();
1553
1554 move |window, cx| {
1555 Tooltip::for_action_in(
1556 "Search by Keystroke",
1557 &ToggleKeystrokeSearch,
1558 &focus_handle.clone(),
1559 window,
1560 cx,
1561 )
1562 }
1563 })
1564 .toggle_state(matches!(
1565 self.search_mode,
1566 SearchMode::KeyStroke { .. }
1567 ))
1568 .on_click(|_, window, cx| {
1569 window.dispatch_action(ToggleKeystrokeSearch.boxed_clone(), cx);
1570 }),
1571 )
1572 .child(
1573 IconButton::new("KeymapEditorConflictIcon", IconName::Warning)
1574 .shape(ui::IconButtonShape::Square)
1575 .when(
1576 self.keybinding_conflict_state.any_user_binding_conflicts(),
1577 |this| {
1578 this.indicator(Indicator::dot().color(Color::Warning))
1579 },
1580 )
1581 .tooltip({
1582 let filter_state = self.filter_state;
1583 let focus_handle = focus_handle.clone();
1584
1585 move |window, cx| {
1586 Tooltip::for_action_in(
1587 match filter_state {
1588 FilterState::All => "Show Conflicts",
1589 FilterState::Conflicts => "Hide Conflicts",
1590 },
1591 &ToggleConflictFilter,
1592 &focus_handle.clone(),
1593 window,
1594 cx,
1595 )
1596 }
1597 })
1598 .selected_icon_color(Color::Warning)
1599 .toggle_state(matches!(
1600 self.filter_state,
1601 FilterState::Conflicts
1602 ))
1603 .on_click(|_, window, cx| {
1604 window.dispatch_action(
1605 ToggleConflictFilter.boxed_clone(),
1606 cx,
1607 );
1608 }),
1609 ),
1610 )
1611 .when_some(
1612 match self.search_mode {
1613 SearchMode::Normal => None,
1614 SearchMode::KeyStroke { exact_match } => Some(exact_match),
1615 },
1616 |this, exact_match| {
1617 this.child(
1618 h_flex()
1619 .map(|this| {
1620 if self
1621 .keybinding_conflict_state
1622 .any_user_binding_conflicts()
1623 {
1624 this.pr(rems_from_px(54.))
1625 } else {
1626 this.pr_7()
1627 }
1628 })
1629 .gap_2()
1630 .child(self.keystroke_editor.clone())
1631 .child(
1632 IconButton::new(
1633 "keystrokes-exact-match",
1634 IconName::CaseSensitive,
1635 )
1636 .tooltip({
1637 let keystroke_focus_handle =
1638 self.keystroke_editor.read(cx).focus_handle(cx);
1639
1640 move |window, cx| {
1641 Tooltip::for_action_in(
1642 "Toggle Exact Match Mode",
1643 &ToggleExactKeystrokeMatching,
1644 &keystroke_focus_handle,
1645 window,
1646 cx,
1647 )
1648 }
1649 })
1650 .shape(IconButtonShape::Square)
1651 .toggle_state(exact_match)
1652 .on_click(
1653 cx.listener(|_, _, window, cx| {
1654 window.dispatch_action(
1655 ToggleExactKeystrokeMatching.boxed_clone(),
1656 cx,
1657 );
1658 }),
1659 ),
1660 ),
1661 )
1662 },
1663 ),
1664 )
1665 .child(
1666 Table::new()
1667 .interactable(&self.table_interaction_state)
1668 .striped()
1669 .empty_table_callback({
1670 let this = cx.entity();
1671 move |window, cx| this.read(cx).render_no_matches_hint(window, cx)
1672 })
1673 .column_widths([
1674 DefiniteLength::Absolute(AbsoluteLength::Pixels(px(40.))),
1675 DefiniteLength::Fraction(0.25),
1676 DefiniteLength::Fraction(0.20),
1677 DefiniteLength::Fraction(0.14),
1678 DefiniteLength::Fraction(0.45),
1679 DefiniteLength::Fraction(0.08),
1680 ])
1681 .resizable_columns(
1682 [
1683 ResizeBehavior::None,
1684 ResizeBehavior::Resizable,
1685 ResizeBehavior::Resizable,
1686 ResizeBehavior::Resizable,
1687 ResizeBehavior::Resizable,
1688 ResizeBehavior::Resizable, // this column doesn't matter
1689 ],
1690 &self.current_widths,
1691 cx,
1692 )
1693 .header(["", "Action", "Arguments", "Keystrokes", "Context", "Source"])
1694 .uniform_list(
1695 "keymap-editor-table",
1696 row_count,
1697 cx.processor(move |this, range: Range<usize>, _window, cx| {
1698 let context_menu_deployed = this.context_menu_deployed();
1699 range
1700 .filter_map(|index| {
1701 let candidate_id = this.matches.get(index)?.candidate_id;
1702 let binding = &this.keybindings[candidate_id];
1703 let action_name = binding.action().name;
1704 let conflict = this.get_conflict(index);
1705 let is_overridden = conflict.is_some_and(|conflict| {
1706 !conflict.is_user_keybind_conflict()
1707 });
1708
1709 let icon = this.create_row_button(index, conflict, cx);
1710
1711 let action = div()
1712 .id(("keymap action", index))
1713 .child({
1714 if action_name != gpui::NoAction.name() {
1715 binding
1716 .action()
1717 .humanized_name
1718 .clone()
1719 .into_any_element()
1720 } else {
1721 const NULL: SharedString =
1722 SharedString::new_static("<null>");
1723 muted_styled_text(NULL.clone(), cx)
1724 .into_any_element()
1725 }
1726 })
1727 .when(
1728 !context_menu_deployed
1729 && this.show_hover_menus
1730 && !is_overridden,
1731 |this| {
1732 this.tooltip({
1733 let action_name = binding.action().name;
1734 let action_docs =
1735 binding.action().documentation;
1736 move |_, cx| {
1737 let action_tooltip =
1738 Tooltip::new(action_name);
1739 let action_tooltip = match action_docs {
1740 Some(docs) => action_tooltip.meta(docs),
1741 None => action_tooltip,
1742 };
1743 cx.new(|_| action_tooltip).into()
1744 }
1745 })
1746 },
1747 )
1748 .into_any_element();
1749 let keystrokes = binding.ui_key_binding().cloned().map_or(
1750 binding
1751 .keystroke_text()
1752 .cloned()
1753 .unwrap_or_default()
1754 .into_any_element(),
1755 IntoElement::into_any_element,
1756 );
1757 let action_arguments = match binding.action().arguments.clone()
1758 {
1759 Some(arguments) => arguments.into_any_element(),
1760 None => {
1761 if binding.action().has_schema {
1762 muted_styled_text(NO_ACTION_ARGUMENTS_TEXT, cx)
1763 .into_any_element()
1764 } else {
1765 gpui::Empty.into_any_element()
1766 }
1767 }
1768 };
1769 let context = binding.context().cloned().map_or(
1770 gpui::Empty.into_any_element(),
1771 |context| {
1772 let is_local = context.local().is_some();
1773
1774 div()
1775 .id(("keymap context", index))
1776 .child(context.clone())
1777 .when(
1778 is_local
1779 && !context_menu_deployed
1780 && !is_overridden
1781 && this.show_hover_menus,
1782 |this| {
1783 this.tooltip(Tooltip::element({
1784 move |_, _| {
1785 context.clone().into_any_element()
1786 }
1787 }))
1788 },
1789 )
1790 .into_any_element()
1791 },
1792 );
1793 let source = binding
1794 .keybind_source()
1795 .map(|source| source.name())
1796 .unwrap_or_default()
1797 .into_any_element();
1798 Some([
1799 icon.into_any_element(),
1800 action,
1801 action_arguments,
1802 keystrokes,
1803 context,
1804 source,
1805 ])
1806 })
1807 .collect()
1808 }),
1809 )
1810 .map_row(cx.processor(
1811 |this, (row_index, row): (usize, Stateful<Div>), _window, cx| {
1812 let conflict = this.get_conflict(row_index);
1813 let is_selected = this.selected_index == Some(row_index);
1814
1815 let row_id = row_group_id(row_index);
1816
1817 div()
1818 .id(("keymap-row-wrapper", row_index))
1819 .child(
1820 row.id(row_id.clone())
1821 .on_any_mouse_down(cx.listener(
1822 move |this,
1823 mouse_down_event: &gpui::MouseDownEvent,
1824 window,
1825 cx| {
1826 match mouse_down_event.button {
1827 MouseButton::Right => {
1828 this.select_index(
1829 row_index, None, window, cx,
1830 );
1831 this.create_context_menu(
1832 mouse_down_event.position,
1833 window,
1834 cx,
1835 );
1836 }
1837 _ => {}
1838 }
1839 },
1840 ))
1841 .on_click(cx.listener(
1842 move |this, event: &ClickEvent, window, cx| {
1843 this.select_index(row_index, None, window, cx);
1844 if event.up.click_count == 2 {
1845 this.open_edit_keybinding_modal(
1846 false, window, cx,
1847 );
1848 }
1849 },
1850 ))
1851 .group(row_id)
1852 .when(
1853 conflict.is_some_and(|conflict| {
1854 !conflict.is_user_keybind_conflict()
1855 }),
1856 |row| {
1857 const OVERRIDDEN_OPACITY: f32 = 0.5;
1858 row.opacity(OVERRIDDEN_OPACITY)
1859 },
1860 )
1861 .when_some(
1862 conflict.filter(|conflict| {
1863 !this.context_menu_deployed() &&
1864 !conflict.is_user_keybind_conflict()
1865 }),
1866 |row, conflict| {
1867 let overriding_binding = this.keybindings.get(conflict.index);
1868 let context = overriding_binding.and_then(|binding| {
1869 match conflict.override_source {
1870 KeybindSource::User => Some("your keymap"),
1871 KeybindSource::Vim => Some("the vim keymap"),
1872 KeybindSource::Base => Some("your base keymap"),
1873 _ => {
1874 log::error!("Unexpected override from the {} keymap", conflict.override_source.name());
1875 None
1876 }
1877 }.map(|source| format!("This keybinding is overridden by the '{}' binding from {}.", binding.action().humanized_name, source))
1878 }).unwrap_or_else(|| "This binding is overridden.".to_string());
1879
1880 row.tooltip(Tooltip::text(context))},
1881 ),
1882 )
1883 .border_2()
1884 .when(
1885 conflict.is_some_and(|conflict| {
1886 conflict.is_user_keybind_conflict()
1887 }),
1888 |row| row.bg(cx.theme().status().error_background),
1889 )
1890 .when(is_selected, |row| {
1891 row.border_color(cx.theme().colors().panel_focused_border)
1892 })
1893 .into_any_element()
1894 }),
1895 ),
1896 )
1897 .on_scroll_wheel(cx.listener(|this, event: &ScrollWheelEvent, _, cx| {
1898 // This ensures that the menu is not dismissed in cases where scroll events
1899 // with a delta of zero are emitted
1900 if !event.delta.pixel_delta(px(1.)).y.is_zero() {
1901 this.context_menu.take();
1902 cx.notify();
1903 }
1904 }))
1905 .children(self.context_menu.as_ref().map(|(menu, position, _)| {
1906 deferred(
1907 anchored()
1908 .position(*position)
1909 .anchor(gpui::Corner::TopLeft)
1910 .child(menu.clone()),
1911 )
1912 .with_priority(1)
1913 }))
1914 }
1915}
1916
1917fn row_group_id(row_index: usize) -> SharedString {
1918 SharedString::new(format!("keymap-table-row-{}", row_index))
1919}
1920
1921fn base_button_style(row_index: usize, icon: IconName) -> IconButton {
1922 IconButton::new(("keymap-icon", row_index), icon)
1923 .shape(IconButtonShape::Square)
1924 .size(ButtonSize::Compact)
1925}
1926
1927#[derive(Debug, Clone, IntoElement)]
1928struct SyntaxHighlightedText {
1929 text: SharedString,
1930 language: Arc<Language>,
1931}
1932
1933impl SyntaxHighlightedText {
1934 pub fn new(text: impl Into<SharedString>, language: Arc<Language>) -> Self {
1935 Self {
1936 text: text.into(),
1937 language,
1938 }
1939 }
1940}
1941
1942impl RenderOnce for SyntaxHighlightedText {
1943 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
1944 let text_style = window.text_style();
1945 let syntax_theme = cx.theme().syntax();
1946
1947 let text = self.text.clone();
1948
1949 let highlights = self
1950 .language
1951 .highlight_text(&text.as_ref().into(), 0..text.len());
1952 let mut runs = Vec::with_capacity(highlights.len());
1953 let mut offset = 0;
1954
1955 for (highlight_range, highlight_id) in highlights {
1956 // Add un-highlighted text before the current highlight
1957 if highlight_range.start > offset {
1958 runs.push(text_style.to_run(highlight_range.start - offset));
1959 }
1960
1961 let mut run_style = text_style.clone();
1962 if let Some(highlight_style) = highlight_id.style(syntax_theme) {
1963 run_style = run_style.highlight(highlight_style);
1964 }
1965 // add the highlighted range
1966 runs.push(run_style.to_run(highlight_range.len()));
1967 offset = highlight_range.end;
1968 }
1969
1970 // Add any remaining un-highlighted text
1971 if offset < text.len() {
1972 runs.push(text_style.to_run(text.len() - offset));
1973 }
1974
1975 StyledText::new(text).with_runs(runs)
1976 }
1977}
1978
1979#[derive(PartialEq)]
1980struct InputError {
1981 severity: ui::Severity,
1982 content: SharedString,
1983}
1984
1985impl InputError {
1986 fn warning(message: impl Into<SharedString>) -> Self {
1987 Self {
1988 severity: ui::Severity::Warning,
1989 content: message.into(),
1990 }
1991 }
1992
1993 fn error(message: anyhow::Error) -> Self {
1994 Self {
1995 severity: ui::Severity::Error,
1996 content: message.to_string().into(),
1997 }
1998 }
1999}
2000
2001struct KeybindingEditorModal {
2002 creating: bool,
2003 editing_keybind: ProcessedBinding,
2004 editing_keybind_idx: usize,
2005 keybind_editor: Entity<KeystrokeInput>,
2006 context_editor: Entity<SingleLineInput>,
2007 action_arguments_editor: Option<Entity<ActionArgumentsEditor>>,
2008 fs: Arc<dyn Fs>,
2009 error: Option<InputError>,
2010 keymap_editor: Entity<KeymapEditor>,
2011 workspace: WeakEntity<Workspace>,
2012 focus_state: KeybindingEditorModalFocusState,
2013}
2014
2015impl ModalView for KeybindingEditorModal {}
2016
2017impl EventEmitter<DismissEvent> for KeybindingEditorModal {}
2018
2019impl Focusable for KeybindingEditorModal {
2020 fn focus_handle(&self, cx: &App) -> FocusHandle {
2021 self.keybind_editor.focus_handle(cx)
2022 }
2023}
2024
2025impl KeybindingEditorModal {
2026 pub fn new(
2027 create: bool,
2028 editing_keybind: ProcessedBinding,
2029 editing_keybind_idx: usize,
2030 keymap_editor: Entity<KeymapEditor>,
2031 action_args_temp_dir: Option<&std::path::Path>,
2032 workspace: WeakEntity<Workspace>,
2033 fs: Arc<dyn Fs>,
2034 window: &mut Window,
2035 cx: &mut App,
2036 ) -> Self {
2037 let keybind_editor = cx
2038 .new(|cx| KeystrokeInput::new(editing_keybind.keystrokes().map(Vec::from), window, cx));
2039
2040 let context_editor: Entity<SingleLineInput> = cx.new(|cx| {
2041 let input = SingleLineInput::new(window, cx, "Keybinding Context")
2042 .label("Edit Context")
2043 .label_size(LabelSize::Default);
2044
2045 if let Some(context) = editing_keybind
2046 .context()
2047 .and_then(KeybindContextString::local)
2048 {
2049 input.editor().update(cx, |editor, cx| {
2050 editor.set_text(context.clone(), window, cx);
2051 });
2052 }
2053
2054 let editor_entity = input.editor().clone();
2055 let workspace = workspace.clone();
2056 cx.spawn(async move |_input_handle, cx| {
2057 let contexts = cx
2058 .background_spawn(async { collect_contexts_from_assets() })
2059 .await;
2060
2061 let language = load_keybind_context_language(workspace, cx).await;
2062 editor_entity
2063 .update(cx, |editor, cx| {
2064 if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
2065 buffer.update(cx, |buffer, cx| {
2066 buffer.set_language(Some(language), cx);
2067 });
2068 }
2069 editor.set_completion_provider(Some(std::rc::Rc::new(
2070 KeyContextCompletionProvider { contexts },
2071 )));
2072 })
2073 .context("Failed to load completions for keybinding context")
2074 })
2075 .detach_and_log_err(cx);
2076
2077 input
2078 });
2079
2080 let action_arguments_editor = editing_keybind.action().has_schema.then(|| {
2081 let arguments = editing_keybind
2082 .action()
2083 .arguments
2084 .as_ref()
2085 .map(|args| args.text.clone());
2086 cx.new(|cx| {
2087 ActionArgumentsEditor::new(
2088 editing_keybind.action().name,
2089 arguments,
2090 action_args_temp_dir,
2091 workspace.clone(),
2092 window,
2093 cx,
2094 )
2095 })
2096 });
2097
2098 let focus_state = KeybindingEditorModalFocusState::new(
2099 keybind_editor.focus_handle(cx),
2100 action_arguments_editor
2101 .as_ref()
2102 .map(|args_editor| args_editor.focus_handle(cx)),
2103 context_editor.focus_handle(cx),
2104 );
2105
2106 Self {
2107 creating: create,
2108 editing_keybind,
2109 editing_keybind_idx,
2110 fs,
2111 keybind_editor,
2112 context_editor,
2113 action_arguments_editor,
2114 error: None,
2115 keymap_editor,
2116 workspace,
2117 focus_state,
2118 }
2119 }
2120
2121 fn set_error(&mut self, error: InputError, cx: &mut Context<Self>) -> bool {
2122 if self.error.as_ref().is_some_and(|old_error| {
2123 old_error.severity == ui::Severity::Warning && *old_error == error
2124 }) {
2125 false
2126 } else {
2127 self.error = Some(error);
2128 cx.notify();
2129 true
2130 }
2131 }
2132
2133 fn validate_action_arguments(&self, cx: &App) -> anyhow::Result<Option<String>> {
2134 let action_arguments = self
2135 .action_arguments_editor
2136 .as_ref()
2137 .map(|editor| editor.read(cx).editor.read(cx).text(cx));
2138
2139 let value = action_arguments
2140 .as_ref()
2141 .map(|args| {
2142 serde_json::from_str(args).context("Failed to parse action arguments as JSON")
2143 })
2144 .transpose()?;
2145
2146 cx.build_action(&self.editing_keybind.action().name, value)
2147 .context("Failed to validate action arguments")?;
2148 Ok(action_arguments)
2149 }
2150
2151 fn validate_keystrokes(&self, cx: &App) -> anyhow::Result<Vec<Keystroke>> {
2152 let new_keystrokes = self
2153 .keybind_editor
2154 .read_with(cx, |editor, _| editor.keystrokes().to_vec());
2155 anyhow::ensure!(!new_keystrokes.is_empty(), "Keystrokes cannot be empty");
2156 Ok(new_keystrokes)
2157 }
2158
2159 fn validate_context(&self, cx: &App) -> anyhow::Result<Option<String>> {
2160 let new_context = self
2161 .context_editor
2162 .read_with(cx, |input, cx| input.editor().read(cx).text(cx));
2163 let Some(context) = new_context.is_empty().not().then_some(new_context) else {
2164 return Ok(None);
2165 };
2166 gpui::KeyBindingContextPredicate::parse(&context).context("Failed to parse key context")?;
2167
2168 Ok(Some(context))
2169 }
2170
2171 fn save_or_display_error(&mut self, cx: &mut Context<Self>) {
2172 self.save(cx).map_err(|err| self.set_error(err, cx)).ok();
2173 }
2174
2175 fn save(&mut self, cx: &mut Context<Self>) -> Result<(), InputError> {
2176 let existing_keybind = self.editing_keybind.clone();
2177 let fs = self.fs.clone();
2178 let tab_size = cx.global::<settings::SettingsStore>().json_tab_size();
2179
2180 let new_keystrokes = self
2181 .validate_keystrokes(cx)
2182 .map_err(InputError::error)?
2183 .into_iter()
2184 .map(remove_key_char)
2185 .collect::<Vec<_>>();
2186
2187 let new_context = self.validate_context(cx).map_err(InputError::error)?;
2188 let new_action_args = self
2189 .validate_action_arguments(cx)
2190 .map_err(InputError::error)?;
2191
2192 let action_mapping = ActionMapping {
2193 keystrokes: new_keystrokes,
2194 context: new_context.map(SharedString::from),
2195 };
2196
2197 let conflicting_indices = self
2198 .keymap_editor
2199 .read(cx)
2200 .keybinding_conflict_state
2201 .conflicting_indices_for_mapping(
2202 &action_mapping,
2203 self.creating.not().then_some(self.editing_keybind_idx),
2204 );
2205
2206 conflicting_indices.map(|KeybindConflict {
2207 first_conflict_index,
2208 remaining_conflict_amount,
2209 }|
2210 {
2211 let conflicting_action_name = self
2212 .keymap_editor
2213 .read(cx)
2214 .keybindings
2215 .get(first_conflict_index)
2216 .map(|keybind| keybind.action().name);
2217
2218 let warning_message = match conflicting_action_name {
2219 Some(name) => {
2220 if remaining_conflict_amount > 0 {
2221 format!(
2222 "Your keybind would conflict with the \"{}\" action and {} other bindings",
2223 name, remaining_conflict_amount
2224 )
2225 } else {
2226 format!("Your keybind would conflict with the \"{}\" action", name)
2227 }
2228 }
2229 None => {
2230 log::info!(
2231 "Could not find action in keybindings with index {}",
2232 first_conflict_index
2233 );
2234 "Your keybind would conflict with other actions".to_string()
2235 }
2236 };
2237
2238 let warning = InputError::warning(warning_message);
2239 if self.error.as_ref().is_some_and(|old_error| *old_error == warning) {
2240 Ok(())
2241 } else {
2242 Err(warning)
2243 }
2244 }).unwrap_or(Ok(()))?;
2245
2246 let create = self.creating;
2247
2248 let status_toast = StatusToast::new(
2249 format!(
2250 "Saved edits to the {} action.",
2251 &self.editing_keybind.action().humanized_name
2252 ),
2253 cx,
2254 move |this, _cx| {
2255 this.icon(ToastIcon::new(IconName::Check).color(Color::Success))
2256 .dismiss_button(true)
2257 // .action("Undo", f) todo: wire the undo functionality
2258 },
2259 );
2260
2261 self.workspace
2262 .update(cx, |workspace, cx| {
2263 workspace.toggle_status_toast(status_toast, cx);
2264 })
2265 .log_err();
2266
2267 cx.spawn(async move |this, cx| {
2268 let action_name = existing_keybind.action().name;
2269
2270 if let Err(err) = save_keybinding_update(
2271 create,
2272 existing_keybind,
2273 &action_mapping,
2274 new_action_args.as_deref(),
2275 &fs,
2276 tab_size,
2277 )
2278 .await
2279 {
2280 this.update(cx, |this, cx| {
2281 this.set_error(InputError::error(err), cx);
2282 })
2283 .log_err();
2284 } else {
2285 this.update(cx, |this, cx| {
2286 this.keymap_editor.update(cx, |keymap, cx| {
2287 keymap.previous_edit = Some(PreviousEdit::Keybinding {
2288 action_mapping,
2289 action_name,
2290 fallback: keymap
2291 .table_interaction_state
2292 .read(cx)
2293 .get_scrollbar_offset(Axis::Vertical),
2294 })
2295 });
2296 cx.emit(DismissEvent);
2297 })
2298 .ok();
2299 }
2300 })
2301 .detach();
2302
2303 Ok(())
2304 }
2305
2306 fn key_context(&self) -> KeyContext {
2307 let mut key_context = KeyContext::new_with_defaults();
2308 key_context.add("KeybindEditorModal");
2309 key_context
2310 }
2311
2312 fn focus_next(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) {
2313 self.focus_state.focus_next(window, cx);
2314 }
2315
2316 fn focus_prev(
2317 &mut self,
2318 _: &menu::SelectPrevious,
2319 window: &mut Window,
2320 cx: &mut Context<Self>,
2321 ) {
2322 self.focus_state.focus_previous(window, cx);
2323 }
2324
2325 fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
2326 self.save_or_display_error(cx);
2327 }
2328
2329 fn cancel(&mut self, _: &menu::Cancel, _window: &mut Window, cx: &mut Context<Self>) {
2330 cx.emit(DismissEvent)
2331 }
2332}
2333
2334fn remove_key_char(Keystroke { modifiers, key, .. }: Keystroke) -> Keystroke {
2335 Keystroke {
2336 modifiers,
2337 key,
2338 ..Default::default()
2339 }
2340}
2341
2342impl Render for KeybindingEditorModal {
2343 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2344 let theme = cx.theme().colors();
2345
2346 v_flex()
2347 .w(rems(34.))
2348 .elevation_3(cx)
2349 .key_context(self.key_context())
2350 .on_action(cx.listener(Self::focus_next))
2351 .on_action(cx.listener(Self::focus_prev))
2352 .on_action(cx.listener(Self::confirm))
2353 .on_action(cx.listener(Self::cancel))
2354 .child(
2355 Modal::new("keybinding_editor_modal", None)
2356 .header(
2357 ModalHeader::new().child(
2358 v_flex()
2359 .pb_1p5()
2360 .mb_1()
2361 .gap_0p5()
2362 .border_b_1()
2363 .border_color(theme.border_variant)
2364 .child(Label::new(
2365 self.editing_keybind.action().humanized_name.clone(),
2366 ))
2367 .when_some(
2368 self.editing_keybind.action().documentation,
2369 |this, docs| {
2370 this.child(
2371 Label::new(docs)
2372 .size(LabelSize::Small)
2373 .color(Color::Muted),
2374 )
2375 },
2376 ),
2377 ),
2378 )
2379 .section(
2380 Section::new().child(
2381 v_flex()
2382 .gap_2()
2383 .child(
2384 v_flex()
2385 .child(Label::new("Edit Keystroke"))
2386 .gap_1()
2387 .child(self.keybind_editor.clone()),
2388 )
2389 .when_some(self.action_arguments_editor.clone(), |this, editor| {
2390 this.child(
2391 v_flex()
2392 .mt_1p5()
2393 .gap_1()
2394 .child(Label::new("Edit Arguments"))
2395 .child(editor),
2396 )
2397 })
2398 .child(self.context_editor.clone())
2399 .when_some(self.error.as_ref(), |this, error| {
2400 this.child(
2401 Banner::new()
2402 .severity(error.severity)
2403 // For some reason, the div overflows its container to the
2404 //right. The padding accounts for that.
2405 .child(
2406 div()
2407 .size_full()
2408 .pr_2()
2409 .child(Label::new(error.content.clone())),
2410 ),
2411 )
2412 }),
2413 ),
2414 )
2415 .footer(
2416 ModalFooter::new().end_slot(
2417 h_flex()
2418 .gap_1()
2419 .child(
2420 Button::new("cancel", "Cancel")
2421 .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
2422 )
2423 .child(Button::new("save-btn", "Save").on_click(cx.listener(
2424 |this, _event, _window, cx| {
2425 this.save_or_display_error(cx);
2426 },
2427 ))),
2428 ),
2429 ),
2430 )
2431 }
2432}
2433
2434struct KeybindingEditorModalFocusState {
2435 handles: Vec<FocusHandle>,
2436}
2437
2438impl KeybindingEditorModalFocusState {
2439 fn new(
2440 keystrokes: FocusHandle,
2441 action_input: Option<FocusHandle>,
2442 context: FocusHandle,
2443 ) -> Self {
2444 Self {
2445 handles: Vec::from_iter(
2446 [Some(keystrokes), action_input, Some(context)]
2447 .into_iter()
2448 .flatten(),
2449 ),
2450 }
2451 }
2452
2453 fn focused_index(&self, window: &Window, cx: &App) -> Option<i32> {
2454 self.handles
2455 .iter()
2456 .position(|handle| handle.contains_focused(window, cx))
2457 .map(|i| i as i32)
2458 }
2459
2460 fn focus_index(&self, mut index: i32, window: &mut Window) {
2461 if index < 0 {
2462 index = self.handles.len() as i32 - 1;
2463 }
2464 if index >= self.handles.len() as i32 {
2465 index = 0;
2466 }
2467 window.focus(&self.handles[index as usize]);
2468 }
2469
2470 fn focus_next(&self, window: &mut Window, cx: &App) {
2471 let index_to_focus = if let Some(index) = self.focused_index(window, cx) {
2472 index + 1
2473 } else {
2474 0
2475 };
2476 self.focus_index(index_to_focus, window);
2477 }
2478
2479 fn focus_previous(&self, window: &mut Window, cx: &App) {
2480 let index_to_focus = if let Some(index) = self.focused_index(window, cx) {
2481 index - 1
2482 } else {
2483 self.handles.len() as i32 - 1
2484 };
2485 self.focus_index(index_to_focus, window);
2486 }
2487}
2488
2489struct ActionArgumentsEditor {
2490 editor: Entity<Editor>,
2491 focus_handle: FocusHandle,
2492 is_loading: bool,
2493 /// See documentation in `KeymapEditor` for why a temp dir is needed.
2494 /// This field exists because the keymap editor temp dir creation may fail,
2495 /// and rather than implement a complicated retry mechanism, we simply
2496 /// fallback to trying to create a temporary directory in this editor on
2497 /// demand. Of note is that the TempDir struct will remove the directory
2498 /// when dropped.
2499 backup_temp_dir: Option<tempfile::TempDir>,
2500}
2501
2502impl Focusable for ActionArgumentsEditor {
2503 fn focus_handle(&self, _cx: &App) -> FocusHandle {
2504 self.focus_handle.clone()
2505 }
2506}
2507
2508impl ActionArgumentsEditor {
2509 fn new(
2510 action_name: &'static str,
2511 arguments: Option<SharedString>,
2512 temp_dir: Option<&std::path::Path>,
2513 workspace: WeakEntity<Workspace>,
2514 window: &mut Window,
2515 cx: &mut Context<Self>,
2516 ) -> Self {
2517 let focus_handle = cx.focus_handle();
2518 cx.on_focus_in(&focus_handle, window, |this, window, cx| {
2519 this.editor.focus_handle(cx).focus(window);
2520 })
2521 .detach();
2522 let editor = cx.new(|cx| {
2523 let mut editor = Editor::auto_height_unbounded(1, window, cx);
2524 Self::set_editor_text(&mut editor, arguments.clone(), window, cx);
2525 editor.set_read_only(true);
2526 editor
2527 });
2528
2529 let temp_dir = temp_dir.map(|path| path.to_owned());
2530 cx.spawn_in(window, async move |this, cx| {
2531 let result = async {
2532 let (project, fs) = workspace.read_with(cx, |workspace, _cx| {
2533 (
2534 workspace.project().downgrade(),
2535 workspace.app_state().fs.clone(),
2536 )
2537 })?;
2538
2539 let file_name =
2540 project::lsp_store::json_language_server_ext::normalized_action_file_name(
2541 action_name,
2542 );
2543
2544 let (buffer, backup_temp_dir) =
2545 Self::create_temp_buffer(temp_dir, file_name.clone(), project.clone(), fs, cx)
2546 .await
2547 .context(concat!(
2548 "Failed to create temporary buffer for action arguments. ",
2549 "Auto-complete will not work"
2550 ))?;
2551
2552 let editor = cx.new_window_entity(|window, cx| {
2553 let multi_buffer = cx.new(|cx| editor::MultiBuffer::singleton(buffer, cx));
2554 let mut editor = Editor::new(
2555 editor::EditorMode::Full {
2556 scale_ui_elements_with_buffer_font_size: true,
2557 show_active_line_background: false,
2558 sized_by_content: true,
2559 },
2560 multi_buffer,
2561 project.upgrade(),
2562 window,
2563 cx,
2564 );
2565 editor.set_searchable(false);
2566 editor.disable_scrollbars_and_minimap(window, cx);
2567 editor.set_show_edit_predictions(Some(false), window, cx);
2568 editor.set_show_gutter(false, cx);
2569 Self::set_editor_text(&mut editor, arguments, window, cx);
2570 editor
2571 })?;
2572
2573 this.update_in(cx, |this, window, cx| {
2574 if this.editor.focus_handle(cx).is_focused(window) {
2575 editor.focus_handle(cx).focus(window);
2576 }
2577 this.editor = editor;
2578 this.backup_temp_dir = backup_temp_dir;
2579 this.is_loading = false;
2580 })?;
2581
2582 anyhow::Ok(())
2583 }
2584 .await;
2585 if result.is_err() {
2586 let json_language = load_json_language(workspace.clone(), cx).await;
2587 this.update(cx, |this, cx| {
2588 this.editor.update(cx, |editor, cx| {
2589 if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
2590 buffer.update(cx, |buffer, cx| {
2591 buffer.set_language(Some(json_language.clone()), cx)
2592 });
2593 }
2594 })
2595 // .context("Failed to load JSON language for editing keybinding action arguments input")
2596 })
2597 .ok();
2598 this.update(cx, |this, _cx| {
2599 this.is_loading = false;
2600 })
2601 .ok();
2602 }
2603 return result;
2604 })
2605 .detach_and_log_err(cx);
2606 Self {
2607 editor,
2608 focus_handle,
2609 is_loading: true,
2610 backup_temp_dir: None,
2611 }
2612 }
2613
2614 fn set_editor_text(
2615 editor: &mut Editor,
2616 arguments: Option<SharedString>,
2617 window: &mut Window,
2618 cx: &mut Context<Editor>,
2619 ) {
2620 if let Some(arguments) = arguments {
2621 editor.set_text(arguments, window, cx);
2622 } else {
2623 // TODO: default value from schema?
2624 editor.set_placeholder_text("Action Arguments", cx);
2625 }
2626 }
2627
2628 async fn create_temp_buffer(
2629 temp_dir: Option<std::path::PathBuf>,
2630 file_name: String,
2631 project: WeakEntity<Project>,
2632 fs: Arc<dyn Fs>,
2633 cx: &mut AsyncApp,
2634 ) -> anyhow::Result<(Entity<language::Buffer>, Option<tempfile::TempDir>)> {
2635 let (temp_file_path, temp_dir) = {
2636 let file_name = file_name.clone();
2637 async move {
2638 let temp_dir_backup = match temp_dir.as_ref() {
2639 Some(_) => None,
2640 None => {
2641 let temp_dir = paths::temp_dir();
2642 let sub_temp_dir = tempfile::Builder::new()
2643 .tempdir_in(temp_dir)
2644 .context("Failed to create temporary directory")?;
2645 Some(sub_temp_dir)
2646 }
2647 };
2648 let dir_path = temp_dir.as_deref().unwrap_or_else(|| {
2649 temp_dir_backup
2650 .as_ref()
2651 .expect("created backup tempdir")
2652 .path()
2653 });
2654 let path = dir_path.join(file_name);
2655 fs.create_file(
2656 &path,
2657 fs::CreateOptions {
2658 ignore_if_exists: true,
2659 overwrite: true,
2660 },
2661 )
2662 .await
2663 .context("Failed to create temporary file")?;
2664 anyhow::Ok((path, temp_dir_backup))
2665 }
2666 }
2667 .await
2668 .context("Failed to create backing file")?;
2669
2670 project
2671 .update(cx, |project, cx| {
2672 project.open_local_buffer(temp_file_path, cx)
2673 })?
2674 .await
2675 .context("Failed to create buffer")
2676 .map(|buffer| (buffer, temp_dir))
2677 }
2678}
2679
2680impl Render for ActionArgumentsEditor {
2681 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2682 let background_color;
2683 let border_color;
2684 let text_style = {
2685 let colors = cx.theme().colors();
2686 let settings = theme::ThemeSettings::get_global(cx);
2687 background_color = colors.editor_background;
2688 border_color = if self.is_loading {
2689 colors.border_disabled
2690 } else {
2691 colors.border_variant
2692 };
2693 TextStyleRefinement {
2694 font_size: Some(rems(0.875).into()),
2695 font_weight: Some(settings.buffer_font.weight),
2696 line_height: Some(relative(1.2)),
2697 font_style: Some(gpui::FontStyle::Normal),
2698 color: self.is_loading.then_some(colors.text_disabled),
2699 ..Default::default()
2700 }
2701 };
2702
2703 self.editor
2704 .update(cx, |editor, _| editor.set_text_style_refinement(text_style));
2705
2706 return v_flex().w_full().child(
2707 h_flex()
2708 .min_h_8()
2709 .min_w_48()
2710 .px_2()
2711 .py_1p5()
2712 .flex_grow()
2713 .rounded_lg()
2714 .bg(background_color)
2715 .border_1()
2716 .border_color(border_color)
2717 .track_focus(&self.focus_handle)
2718 .child(self.editor.clone()),
2719 );
2720 }
2721}
2722
2723struct KeyContextCompletionProvider {
2724 contexts: Vec<SharedString>,
2725}
2726
2727impl CompletionProvider for KeyContextCompletionProvider {
2728 fn completions(
2729 &self,
2730 _excerpt_id: editor::ExcerptId,
2731 buffer: &Entity<language::Buffer>,
2732 buffer_position: language::Anchor,
2733 _trigger: editor::CompletionContext,
2734 _window: &mut Window,
2735 cx: &mut Context<Editor>,
2736 ) -> gpui::Task<anyhow::Result<Vec<project::CompletionResponse>>> {
2737 let buffer = buffer.read(cx);
2738 let mut count_back = 0;
2739 for char in buffer.reversed_chars_at(buffer_position) {
2740 if char.is_ascii_alphanumeric() || char == '_' {
2741 count_back += 1;
2742 } else {
2743 break;
2744 }
2745 }
2746 let start_anchor = buffer.anchor_before(
2747 buffer_position
2748 .to_offset(&buffer)
2749 .saturating_sub(count_back),
2750 );
2751 let replace_range = start_anchor..buffer_position;
2752 gpui::Task::ready(Ok(vec![project::CompletionResponse {
2753 completions: self
2754 .contexts
2755 .iter()
2756 .map(|context| project::Completion {
2757 replace_range: replace_range.clone(),
2758 label: language::CodeLabel::plain(context.to_string(), None),
2759 new_text: context.to_string(),
2760 documentation: None,
2761 source: project::CompletionSource::Custom,
2762 icon_path: None,
2763 insert_text_mode: None,
2764 confirm: None,
2765 })
2766 .collect(),
2767 is_incomplete: false,
2768 }]))
2769 }
2770
2771 fn is_completion_trigger(
2772 &self,
2773 _buffer: &Entity<language::Buffer>,
2774 _position: language::Anchor,
2775 text: &str,
2776 _trigger_in_words: bool,
2777 _menu_is_open: bool,
2778 _cx: &mut Context<Editor>,
2779 ) -> bool {
2780 text.chars().last().map_or(false, |last_char| {
2781 last_char.is_ascii_alphanumeric() || last_char == '_'
2782 })
2783 }
2784}
2785
2786async fn load_json_language(workspace: WeakEntity<Workspace>, cx: &mut AsyncApp) -> Arc<Language> {
2787 let json_language_task = workspace
2788 .read_with(cx, |workspace, cx| {
2789 workspace
2790 .project()
2791 .read(cx)
2792 .languages()
2793 .language_for_name("JSON")
2794 })
2795 .context("Failed to load JSON language")
2796 .log_err();
2797 let json_language = match json_language_task {
2798 Some(task) => task.await.context("Failed to load JSON language").log_err(),
2799 None => None,
2800 };
2801 return json_language.unwrap_or_else(|| {
2802 Arc::new(Language::new(
2803 LanguageConfig {
2804 name: "JSON".into(),
2805 ..Default::default()
2806 },
2807 Some(tree_sitter_json::LANGUAGE.into()),
2808 ))
2809 });
2810}
2811
2812async fn load_keybind_context_language(
2813 workspace: WeakEntity<Workspace>,
2814 cx: &mut AsyncApp,
2815) -> Arc<Language> {
2816 let language_task = workspace
2817 .read_with(cx, |workspace, cx| {
2818 workspace
2819 .project()
2820 .read(cx)
2821 .languages()
2822 .language_for_name("Zed Keybind Context")
2823 })
2824 .context("Failed to load Zed Keybind Context language")
2825 .log_err();
2826 let language = match language_task {
2827 Some(task) => task
2828 .await
2829 .context("Failed to load Zed Keybind Context language")
2830 .log_err(),
2831 None => None,
2832 };
2833 return language.unwrap_or_else(|| {
2834 Arc::new(Language::new(
2835 LanguageConfig {
2836 name: "Zed Keybind Context".into(),
2837 ..Default::default()
2838 },
2839 Some(tree_sitter_rust::LANGUAGE.into()),
2840 ))
2841 });
2842}
2843
2844async fn save_keybinding_update(
2845 create: bool,
2846 existing: ProcessedBinding,
2847 action_mapping: &ActionMapping,
2848 new_args: Option<&str>,
2849 fs: &Arc<dyn Fs>,
2850 tab_size: usize,
2851) -> anyhow::Result<()> {
2852 let keymap_contents = settings::KeymapFile::load_keymap_file(fs)
2853 .await
2854 .context("Failed to load keymap file")?;
2855
2856 let existing_keystrokes = existing.keystrokes().unwrap_or_default();
2857 let existing_context = existing.context().and_then(KeybindContextString::local_str);
2858 let existing_args = existing
2859 .action()
2860 .arguments
2861 .as_ref()
2862 .map(|args| args.text.as_ref());
2863
2864 let target = settings::KeybindUpdateTarget {
2865 context: existing_context,
2866 keystrokes: existing_keystrokes,
2867 action_name: &existing.action().name,
2868 action_arguments: existing_args,
2869 };
2870
2871 let source = settings::KeybindUpdateTarget {
2872 context: action_mapping.context.as_ref().map(|a| &***a),
2873 keystrokes: &action_mapping.keystrokes,
2874 action_name: &existing.action().name,
2875 action_arguments: new_args,
2876 };
2877
2878 let operation = if !create {
2879 settings::KeybindUpdateOperation::Replace {
2880 target,
2881 target_keybind_source: existing.keybind_source().unwrap_or(KeybindSource::User),
2882 source,
2883 }
2884 } else {
2885 settings::KeybindUpdateOperation::Add {
2886 source,
2887 from: Some(target),
2888 }
2889 };
2890
2891 let (new_keybinding, removed_keybinding, source) = operation.generate_telemetry();
2892
2893 let updated_keymap_contents =
2894 settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size)
2895 .context("Failed to update keybinding")?;
2896 fs.write(
2897 paths::keymap_file().as_path(),
2898 updated_keymap_contents.as_bytes(),
2899 )
2900 .await
2901 .context("Failed to write keymap file")?;
2902
2903 telemetry::event!(
2904 "Keybinding Updated",
2905 new_keybinding = new_keybinding,
2906 removed_keybinding = removed_keybinding,
2907 source = source
2908 );
2909 Ok(())
2910}
2911
2912async fn remove_keybinding(
2913 existing: ProcessedBinding,
2914 fs: &Arc<dyn Fs>,
2915 tab_size: usize,
2916) -> anyhow::Result<()> {
2917 let Some(keystrokes) = existing.keystrokes() else {
2918 anyhow::bail!("Cannot remove a keybinding that does not exist");
2919 };
2920 let keymap_contents = settings::KeymapFile::load_keymap_file(fs)
2921 .await
2922 .context("Failed to load keymap file")?;
2923
2924 let operation = settings::KeybindUpdateOperation::Remove {
2925 target: settings::KeybindUpdateTarget {
2926 context: existing.context().and_then(KeybindContextString::local_str),
2927 keystrokes,
2928 action_name: &existing.action().name,
2929 action_arguments: existing
2930 .action()
2931 .arguments
2932 .as_ref()
2933 .map(|arguments| arguments.text.as_ref()),
2934 },
2935 target_keybind_source: existing.keybind_source().unwrap_or(KeybindSource::User),
2936 };
2937
2938 let (new_keybinding, removed_keybinding, source) = operation.generate_telemetry();
2939 let updated_keymap_contents =
2940 settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size)
2941 .context("Failed to update keybinding")?;
2942 fs.write(
2943 paths::keymap_file().as_path(),
2944 updated_keymap_contents.as_bytes(),
2945 )
2946 .await
2947 .context("Failed to write keymap file")?;
2948
2949 telemetry::event!(
2950 "Keybinding Removed",
2951 new_keybinding = new_keybinding,
2952 removed_keybinding = removed_keybinding,
2953 source = source
2954 );
2955 Ok(())
2956}
2957
2958#[derive(PartialEq, Eq, Debug, Copy, Clone)]
2959enum CloseKeystrokeResult {
2960 Partial,
2961 Close,
2962 None,
2963}
2964
2965#[derive(PartialEq, Eq, Debug, Clone)]
2966enum KeyPress<'a> {
2967 Alt,
2968 Control,
2969 Function,
2970 Shift,
2971 Platform,
2972 Key(&'a String),
2973}
2974
2975struct KeystrokeInput {
2976 keystrokes: Vec<Keystroke>,
2977 placeholder_keystrokes: Option<Vec<Keystroke>>,
2978 outer_focus_handle: FocusHandle,
2979 inner_focus_handle: FocusHandle,
2980 intercept_subscription: Option<Subscription>,
2981 _focus_subscriptions: [Subscription; 2],
2982 search: bool,
2983 /// Handles tripe escape to stop recording
2984 close_keystrokes: Option<Vec<Keystroke>>,
2985 close_keystrokes_start: Option<usize>,
2986}
2987
2988impl KeystrokeInput {
2989 const KEYSTROKE_COUNT_MAX: usize = 3;
2990
2991 fn new(
2992 placeholder_keystrokes: Option<Vec<Keystroke>>,
2993 window: &mut Window,
2994 cx: &mut Context<Self>,
2995 ) -> Self {
2996 let outer_focus_handle = cx.focus_handle();
2997 let inner_focus_handle = cx.focus_handle();
2998 let _focus_subscriptions = [
2999 cx.on_focus_in(&inner_focus_handle, window, Self::on_inner_focus_in),
3000 cx.on_focus_out(&inner_focus_handle, window, Self::on_inner_focus_out),
3001 ];
3002 Self {
3003 keystrokes: Vec::new(),
3004 placeholder_keystrokes,
3005 inner_focus_handle,
3006 outer_focus_handle,
3007 intercept_subscription: None,
3008 _focus_subscriptions,
3009 search: false,
3010 close_keystrokes: None,
3011 close_keystrokes_start: None,
3012 }
3013 }
3014
3015 fn set_keystrokes(&mut self, keystrokes: Vec<Keystroke>, cx: &mut Context<Self>) {
3016 self.keystrokes = keystrokes;
3017 self.keystrokes_changed(cx);
3018 }
3019
3020 fn dummy(modifiers: Modifiers) -> Keystroke {
3021 return Keystroke {
3022 modifiers,
3023 key: "".to_string(),
3024 key_char: None,
3025 };
3026 }
3027
3028 fn keystrokes_changed(&self, cx: &mut Context<Self>) {
3029 cx.emit(());
3030 cx.notify();
3031 }
3032
3033 fn key_context() -> KeyContext {
3034 let mut key_context = KeyContext::new_with_defaults();
3035 key_context.add("KeystrokeInput");
3036 key_context
3037 }
3038
3039 fn handle_possible_close_keystroke(
3040 &mut self,
3041 keystroke: &Keystroke,
3042 window: &mut Window,
3043 cx: &mut Context<Self>,
3044 ) -> CloseKeystrokeResult {
3045 let Some(keybind_for_close_action) = window
3046 .highest_precedence_binding_for_action_in_context(&StopRecording, Self::key_context())
3047 else {
3048 log::trace!("No keybinding to stop recording keystrokes in keystroke input");
3049 self.close_keystrokes.take();
3050 self.close_keystrokes_start.take();
3051 return CloseKeystrokeResult::None;
3052 };
3053 let action_keystrokes = keybind_for_close_action.keystrokes();
3054
3055 if let Some(mut close_keystrokes) = self.close_keystrokes.take() {
3056 let mut index = 0;
3057
3058 while index < action_keystrokes.len() && index < close_keystrokes.len() {
3059 if !close_keystrokes[index].should_match(&action_keystrokes[index]) {
3060 break;
3061 }
3062 index += 1;
3063 }
3064 if index == close_keystrokes.len() {
3065 if index >= action_keystrokes.len() {
3066 self.close_keystrokes_start.take();
3067 return CloseKeystrokeResult::None;
3068 }
3069 if keystroke.should_match(&action_keystrokes[index]) {
3070 if action_keystrokes.len() >= 1 && index == action_keystrokes.len() - 1 {
3071 self.stop_recording(&StopRecording, window, cx);
3072 return CloseKeystrokeResult::Close;
3073 } else {
3074 close_keystrokes.push(keystroke.clone());
3075 self.close_keystrokes = Some(close_keystrokes);
3076 return CloseKeystrokeResult::Partial;
3077 }
3078 } else {
3079 self.close_keystrokes_start.take();
3080 return CloseKeystrokeResult::None;
3081 }
3082 }
3083 } else if let Some(first_action_keystroke) = action_keystrokes.first()
3084 && keystroke.should_match(first_action_keystroke)
3085 {
3086 self.close_keystrokes = Some(vec![keystroke.clone()]);
3087 return CloseKeystrokeResult::Partial;
3088 }
3089 self.close_keystrokes_start.take();
3090 return CloseKeystrokeResult::None;
3091 }
3092
3093 fn on_modifiers_changed(
3094 &mut self,
3095 event: &ModifiersChangedEvent,
3096 _window: &mut Window,
3097 cx: &mut Context<Self>,
3098 ) {
3099 let keystrokes_len = self.keystrokes.len();
3100
3101 if let Some(last) = self.keystrokes.last_mut()
3102 && last.key.is_empty()
3103 && keystrokes_len <= Self::KEYSTROKE_COUNT_MAX
3104 {
3105 if self.search {
3106 last.modifiers = last.modifiers.xor(&event.modifiers);
3107 } else if !event.modifiers.modified() {
3108 self.keystrokes.pop();
3109 } else {
3110 last.modifiers = event.modifiers;
3111 }
3112
3113 self.keystrokes_changed(cx);
3114 } else if keystrokes_len < Self::KEYSTROKE_COUNT_MAX {
3115 self.keystrokes.push(Self::dummy(event.modifiers));
3116 self.keystrokes_changed(cx);
3117 }
3118 cx.stop_propagation();
3119 }
3120
3121 fn handle_keystroke(
3122 &mut self,
3123 keystroke: &Keystroke,
3124 window: &mut Window,
3125 cx: &mut Context<Self>,
3126 ) {
3127 let close_keystroke_result = self.handle_possible_close_keystroke(keystroke, window, cx);
3128 if close_keystroke_result != CloseKeystrokeResult::Close {
3129 let key_len = self.keystrokes.len();
3130 if let Some(last) = self.keystrokes.last_mut()
3131 && last.key.is_empty()
3132 && key_len <= Self::KEYSTROKE_COUNT_MAX
3133 {
3134 if self.search {
3135 last.key = keystroke.key.clone();
3136 if close_keystroke_result == CloseKeystrokeResult::Partial
3137 && self.close_keystrokes_start.is_none()
3138 {
3139 self.close_keystrokes_start = Some(self.keystrokes.len() - 1);
3140 }
3141 self.keystrokes_changed(cx);
3142 cx.stop_propagation();
3143 return;
3144 } else {
3145 self.keystrokes.pop();
3146 }
3147 }
3148 if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX {
3149 if close_keystroke_result == CloseKeystrokeResult::Partial
3150 && self.close_keystrokes_start.is_none()
3151 {
3152 self.close_keystrokes_start = Some(self.keystrokes.len());
3153 }
3154 self.keystrokes.push(keystroke.clone());
3155 if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX {
3156 self.keystrokes.push(Self::dummy(keystroke.modifiers));
3157 }
3158 } else if close_keystroke_result != CloseKeystrokeResult::Partial {
3159 self.clear_keystrokes(&ClearKeystrokes, window, cx);
3160 }
3161 }
3162 self.keystrokes_changed(cx);
3163 cx.stop_propagation();
3164 }
3165
3166 fn on_inner_focus_in(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
3167 if self.intercept_subscription.is_none() {
3168 let listener = cx.listener(|this, event: &gpui::KeystrokeEvent, window, cx| {
3169 this.handle_keystroke(&event.keystroke, window, cx);
3170 });
3171 self.intercept_subscription = Some(cx.intercept_keystrokes(listener))
3172 }
3173 }
3174
3175 fn on_inner_focus_out(
3176 &mut self,
3177 _event: gpui::FocusOutEvent,
3178 _window: &mut Window,
3179 cx: &mut Context<Self>,
3180 ) {
3181 self.intercept_subscription.take();
3182 cx.notify();
3183 }
3184
3185 fn keystrokes(&self) -> &[Keystroke] {
3186 if let Some(placeholders) = self.placeholder_keystrokes.as_ref()
3187 && self.keystrokes.is_empty()
3188 {
3189 return placeholders;
3190 }
3191 if !self.search
3192 && self
3193 .keystrokes
3194 .last()
3195 .map_or(false, |last| last.key.is_empty())
3196 {
3197 return &self.keystrokes[..self.keystrokes.len() - 1];
3198 }
3199 return &self.keystrokes;
3200 }
3201
3202 fn render_keystrokes(&self, is_recording: bool) -> impl Iterator<Item = Div> {
3203 let keystrokes = if let Some(placeholders) = self.placeholder_keystrokes.as_ref()
3204 && self.keystrokes.is_empty()
3205 {
3206 if is_recording {
3207 &[]
3208 } else {
3209 placeholders.as_slice()
3210 }
3211 } else {
3212 &self.keystrokes
3213 };
3214 keystrokes.iter().map(move |keystroke| {
3215 h_flex().children(ui::render_keystroke(
3216 keystroke,
3217 Some(Color::Default),
3218 Some(rems(0.875).into()),
3219 ui::PlatformStyle::platform(),
3220 false,
3221 ))
3222 })
3223 }
3224
3225 fn recording_focus_handle(&self, _cx: &App) -> FocusHandle {
3226 self.inner_focus_handle.clone()
3227 }
3228
3229 fn start_recording(&mut self, _: &StartRecording, window: &mut Window, cx: &mut Context<Self>) {
3230 if !self.outer_focus_handle.is_focused(window) {
3231 return;
3232 }
3233 self.clear_keystrokes(&ClearKeystrokes, window, cx);
3234 window.focus(&self.inner_focus_handle);
3235 cx.notify();
3236 }
3237
3238 fn stop_recording(&mut self, _: &StopRecording, window: &mut Window, cx: &mut Context<Self>) {
3239 if !self.inner_focus_handle.is_focused(window) {
3240 return;
3241 }
3242 window.focus(&self.outer_focus_handle);
3243 if let Some(close_keystrokes_start) = self.close_keystrokes_start.take()
3244 && close_keystrokes_start < self.keystrokes.len()
3245 {
3246 self.keystrokes.drain(close_keystrokes_start..);
3247 }
3248 self.close_keystrokes.take();
3249 cx.notify();
3250 }
3251
3252 fn clear_keystrokes(
3253 &mut self,
3254 _: &ClearKeystrokes,
3255 _window: &mut Window,
3256 cx: &mut Context<Self>,
3257 ) {
3258 self.keystrokes.clear();
3259 self.keystrokes_changed(cx);
3260 }
3261}
3262
3263impl EventEmitter<()> for KeystrokeInput {}
3264
3265impl Focusable for KeystrokeInput {
3266 fn focus_handle(&self, _cx: &App) -> FocusHandle {
3267 self.outer_focus_handle.clone()
3268 }
3269}
3270
3271impl Render for KeystrokeInput {
3272 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3273 let colors = cx.theme().colors();
3274 let is_focused = self.outer_focus_handle.contains_focused(window, cx);
3275 let is_recording = self.inner_focus_handle.is_focused(window);
3276
3277 let horizontal_padding = rems_from_px(64.);
3278
3279 let recording_bg_color = colors
3280 .editor_background
3281 .blend(colors.text_accent.opacity(0.1));
3282
3283 let recording_pulse = |color: Color| {
3284 Icon::new(IconName::Circle)
3285 .size(IconSize::Small)
3286 .color(Color::Error)
3287 .with_animation(
3288 "recording-pulse",
3289 Animation::new(std::time::Duration::from_secs(2))
3290 .repeat()
3291 .with_easing(gpui::pulsating_between(0.4, 0.8)),
3292 {
3293 let color = color.color(cx);
3294 move |this, delta| this.color(Color::Custom(color.opacity(delta)))
3295 },
3296 )
3297 };
3298
3299 let recording_indicator = h_flex()
3300 .h_4()
3301 .pr_1()
3302 .gap_0p5()
3303 .border_1()
3304 .border_color(colors.border)
3305 .bg(colors
3306 .editor_background
3307 .blend(colors.text_accent.opacity(0.1)))
3308 .rounded_sm()
3309 .child(recording_pulse(Color::Error))
3310 .child(
3311 Label::new("REC")
3312 .size(LabelSize::XSmall)
3313 .weight(FontWeight::SEMIBOLD)
3314 .color(Color::Error),
3315 );
3316
3317 let search_indicator = h_flex()
3318 .h_4()
3319 .pr_1()
3320 .gap_0p5()
3321 .border_1()
3322 .border_color(colors.border)
3323 .bg(colors
3324 .editor_background
3325 .blend(colors.text_accent.opacity(0.1)))
3326 .rounded_sm()
3327 .child(recording_pulse(Color::Accent))
3328 .child(
3329 Label::new("SEARCH")
3330 .size(LabelSize::XSmall)
3331 .weight(FontWeight::SEMIBOLD)
3332 .color(Color::Accent),
3333 );
3334
3335 let record_icon = if self.search {
3336 IconName::MagnifyingGlass
3337 } else {
3338 IconName::PlayFilled
3339 };
3340
3341 h_flex()
3342 .id("keystroke-input")
3343 .track_focus(&self.outer_focus_handle)
3344 .py_2()
3345 .px_3()
3346 .gap_2()
3347 .min_h_10()
3348 .w_full()
3349 .flex_1()
3350 .justify_between()
3351 .rounded_lg()
3352 .overflow_hidden()
3353 .map(|this| {
3354 if is_recording {
3355 this.bg(recording_bg_color)
3356 } else {
3357 this.bg(colors.editor_background)
3358 }
3359 })
3360 .border_1()
3361 .border_color(colors.border_variant)
3362 .when(is_focused, |parent| {
3363 parent.border_color(colors.border_focused)
3364 })
3365 .key_context(Self::key_context())
3366 .on_action(cx.listener(Self::start_recording))
3367 .on_action(cx.listener(Self::stop_recording))
3368 .child(
3369 h_flex()
3370 .w(horizontal_padding)
3371 .gap_0p5()
3372 .justify_start()
3373 .flex_none()
3374 .when(is_recording, |this| {
3375 this.map(|this| {
3376 if self.search {
3377 this.child(search_indicator)
3378 } else {
3379 this.child(recording_indicator)
3380 }
3381 })
3382 }),
3383 )
3384 .child(
3385 h_flex()
3386 .id("keystroke-input-inner")
3387 .track_focus(&self.inner_focus_handle)
3388 .on_modifiers_changed(cx.listener(Self::on_modifiers_changed))
3389 .size_full()
3390 .when(!self.search, |this| {
3391 this.focus(|mut style| {
3392 style.border_color = Some(colors.border_focused);
3393 style
3394 })
3395 })
3396 .w_full()
3397 .min_w_0()
3398 .justify_center()
3399 .flex_wrap()
3400 .gap(ui::DynamicSpacing::Base04.rems(cx))
3401 .children(self.render_keystrokes(is_recording)),
3402 )
3403 .child(
3404 h_flex()
3405 .w(horizontal_padding)
3406 .gap_0p5()
3407 .justify_end()
3408 .flex_none()
3409 .map(|this| {
3410 if is_recording {
3411 this.child(
3412 IconButton::new("stop-record-btn", IconName::StopFilled)
3413 .shape(ui::IconButtonShape::Square)
3414 .map(|this| {
3415 this.tooltip(Tooltip::for_action_title(
3416 if self.search {
3417 "Stop Searching"
3418 } else {
3419 "Stop Recording"
3420 },
3421 &StopRecording,
3422 ))
3423 })
3424 .icon_color(Color::Error)
3425 .on_click(cx.listener(|this, _event, window, cx| {
3426 this.stop_recording(&StopRecording, window, cx);
3427 })),
3428 )
3429 } else {
3430 this.child(
3431 IconButton::new("record-btn", record_icon)
3432 .shape(ui::IconButtonShape::Square)
3433 .map(|this| {
3434 this.tooltip(Tooltip::for_action_title(
3435 if self.search {
3436 "Start Searching"
3437 } else {
3438 "Start Recording"
3439 },
3440 &StartRecording,
3441 ))
3442 })
3443 .when(!is_focused, |this| this.icon_color(Color::Muted))
3444 .on_click(cx.listener(|this, _event, window, cx| {
3445 this.start_recording(&StartRecording, window, cx);
3446 })),
3447 )
3448 }
3449 })
3450 .child(
3451 IconButton::new("clear-btn", IconName::Delete)
3452 .shape(ui::IconButtonShape::Square)
3453 .tooltip(Tooltip::for_action_title(
3454 "Clear Keystrokes",
3455 &ClearKeystrokes,
3456 ))
3457 .when(!is_recording || !is_focused, |this| {
3458 this.icon_color(Color::Muted)
3459 })
3460 .on_click(cx.listener(|this, _event, window, cx| {
3461 this.clear_keystrokes(&ClearKeystrokes, window, cx);
3462 })),
3463 ),
3464 )
3465 }
3466}
3467
3468fn collect_contexts_from_assets() -> Vec<SharedString> {
3469 let mut keymap_assets = vec![
3470 util::asset_str::<SettingsAssets>(settings::DEFAULT_KEYMAP_PATH),
3471 util::asset_str::<SettingsAssets>(settings::VIM_KEYMAP_PATH),
3472 ];
3473 keymap_assets.extend(
3474 BaseKeymap::OPTIONS
3475 .iter()
3476 .filter_map(|(_, base_keymap)| base_keymap.asset_path())
3477 .map(util::asset_str::<SettingsAssets>),
3478 );
3479
3480 let mut contexts = HashSet::default();
3481
3482 for keymap_asset in keymap_assets {
3483 let Ok(keymap) = KeymapFile::parse(&keymap_asset) else {
3484 continue;
3485 };
3486
3487 for section in keymap.sections() {
3488 let context_expr = §ion.context;
3489 let mut queue = Vec::new();
3490 let Ok(root_context) = gpui::KeyBindingContextPredicate::parse(context_expr) else {
3491 continue;
3492 };
3493
3494 queue.push(root_context);
3495 while let Some(context) = queue.pop() {
3496 match context {
3497 gpui::KeyBindingContextPredicate::Identifier(ident) => {
3498 contexts.insert(ident);
3499 }
3500 gpui::KeyBindingContextPredicate::Equal(ident_a, ident_b) => {
3501 contexts.insert(ident_a);
3502 contexts.insert(ident_b);
3503 }
3504 gpui::KeyBindingContextPredicate::NotEqual(ident_a, ident_b) => {
3505 contexts.insert(ident_a);
3506 contexts.insert(ident_b);
3507 }
3508 gpui::KeyBindingContextPredicate::Descendant(ctx_a, ctx_b) => {
3509 queue.push(*ctx_a);
3510 queue.push(*ctx_b);
3511 }
3512 gpui::KeyBindingContextPredicate::Not(ctx) => {
3513 queue.push(*ctx);
3514 }
3515 gpui::KeyBindingContextPredicate::And(ctx_a, ctx_b) => {
3516 queue.push(*ctx_a);
3517 queue.push(*ctx_b);
3518 }
3519 gpui::KeyBindingContextPredicate::Or(ctx_a, ctx_b) => {
3520 queue.push(*ctx_a);
3521 queue.push(*ctx_b);
3522 }
3523 }
3524 }
3525 }
3526 }
3527
3528 let mut contexts = contexts.into_iter().collect::<Vec<_>>();
3529 contexts.sort();
3530
3531 return contexts;
3532}
3533
3534impl SerializableItem for KeymapEditor {
3535 fn serialized_item_kind() -> &'static str {
3536 "KeymapEditor"
3537 }
3538
3539 fn cleanup(
3540 workspace_id: workspace::WorkspaceId,
3541 alive_items: Vec<workspace::ItemId>,
3542 _window: &mut Window,
3543 cx: &mut App,
3544 ) -> gpui::Task<gpui::Result<()>> {
3545 workspace::delete_unloaded_items(
3546 alive_items,
3547 workspace_id,
3548 "keybinding_editors",
3549 &KEYBINDING_EDITORS,
3550 cx,
3551 )
3552 }
3553
3554 fn deserialize(
3555 _project: Entity<project::Project>,
3556 workspace: WeakEntity<Workspace>,
3557 workspace_id: workspace::WorkspaceId,
3558 item_id: workspace::ItemId,
3559 window: &mut Window,
3560 cx: &mut App,
3561 ) -> gpui::Task<gpui::Result<Entity<Self>>> {
3562 window.spawn(cx, async move |cx| {
3563 if KEYBINDING_EDITORS
3564 .get_keybinding_editor(item_id, workspace_id)?
3565 .is_some()
3566 {
3567 cx.update(|window, cx| cx.new(|cx| KeymapEditor::new(workspace, window, cx)))
3568 } else {
3569 Err(anyhow!("No keybinding editor to deserialize"))
3570 }
3571 })
3572 }
3573
3574 fn serialize(
3575 &mut self,
3576 workspace: &mut Workspace,
3577 item_id: workspace::ItemId,
3578 _closing: bool,
3579 _window: &mut Window,
3580 cx: &mut ui::Context<Self>,
3581 ) -> Option<gpui::Task<gpui::Result<()>>> {
3582 let workspace_id = workspace.database_id()?;
3583 Some(cx.background_spawn(async move {
3584 KEYBINDING_EDITORS
3585 .save_keybinding_editor(item_id, workspace_id)
3586 .await
3587 }))
3588 }
3589
3590 fn should_serialize(&self, _event: &Self::Event) -> bool {
3591 false
3592 }
3593}
3594
3595mod persistence {
3596 use db::{define_connection, query, sqlez_macros::sql};
3597 use workspace::WorkspaceDb;
3598
3599 define_connection! {
3600 pub static ref KEYBINDING_EDITORS: KeybindingEditorDb<WorkspaceDb> =
3601 &[sql!(
3602 CREATE TABLE keybinding_editors (
3603 workspace_id INTEGER,
3604 item_id INTEGER UNIQUE,
3605
3606 PRIMARY KEY(workspace_id, item_id),
3607 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
3608 ON DELETE CASCADE
3609 ) STRICT;
3610 )];
3611 }
3612
3613 impl KeybindingEditorDb {
3614 query! {
3615 pub async fn save_keybinding_editor(
3616 item_id: workspace::ItemId,
3617 workspace_id: workspace::WorkspaceId
3618 ) -> Result<()> {
3619 INSERT OR REPLACE INTO keybinding_editors(item_id, workspace_id)
3620 VALUES (?, ?)
3621 }
3622 }
3623
3624 query! {
3625 pub fn get_keybinding_editor(
3626 item_id: workspace::ItemId,
3627 workspace_id: workspace::WorkspaceId
3628 ) -> Result<Option<workspace::ItemId>> {
3629 SELECT item_id
3630 FROM keybinding_editors
3631 WHERE item_id = ? AND workspace_id = ?
3632 }
3633 }
3634 }
3635}
3636
3637/// Iterator that yields KeyPress values from a slice of Keystrokes
3638struct KeyPressIterator<'a> {
3639 keystrokes: &'a [Keystroke],
3640 current_keystroke_index: usize,
3641 current_key_press_index: usize,
3642}
3643
3644impl<'a> KeyPressIterator<'a> {
3645 fn new(keystrokes: &'a [Keystroke]) -> Self {
3646 Self {
3647 keystrokes,
3648 current_keystroke_index: 0,
3649 current_key_press_index: 0,
3650 }
3651 }
3652}
3653
3654impl<'a> Iterator for KeyPressIterator<'a> {
3655 type Item = KeyPress<'a>;
3656
3657 fn next(&mut self) -> Option<Self::Item> {
3658 loop {
3659 let keystroke = self.keystrokes.get(self.current_keystroke_index)?;
3660
3661 match self.current_key_press_index {
3662 0 => {
3663 self.current_key_press_index = 1;
3664 if keystroke.modifiers.platform {
3665 return Some(KeyPress::Platform);
3666 }
3667 }
3668 1 => {
3669 self.current_key_press_index = 2;
3670 if keystroke.modifiers.alt {
3671 return Some(KeyPress::Alt);
3672 }
3673 }
3674 2 => {
3675 self.current_key_press_index = 3;
3676 if keystroke.modifiers.control {
3677 return Some(KeyPress::Control);
3678 }
3679 }
3680 3 => {
3681 self.current_key_press_index = 4;
3682 if keystroke.modifiers.shift {
3683 return Some(KeyPress::Shift);
3684 }
3685 }
3686 4 => {
3687 self.current_key_press_index = 5;
3688 if keystroke.modifiers.function {
3689 return Some(KeyPress::Function);
3690 }
3691 }
3692 _ => {
3693 self.current_keystroke_index += 1;
3694 self.current_key_press_index = 0;
3695
3696 if keystroke.key.is_empty() {
3697 continue;
3698 }
3699 return Some(KeyPress::Key(&keystroke.key));
3700 }
3701 }
3702 }
3703 }
3704}