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