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(40.))),
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 let keystrokes = binding.ui_key_binding().cloned().map_or(
1769 binding
1770 .keystroke_text()
1771 .cloned()
1772 .unwrap_or_default()
1773 .into_any_element(),
1774 IntoElement::into_any_element,
1775 );
1776 let action_arguments = match binding.action().arguments.clone()
1777 {
1778 Some(arguments) => arguments.into_any_element(),
1779 None => {
1780 if binding.action().has_schema {
1781 muted_styled_text(NO_ACTION_ARGUMENTS_TEXT, cx)
1782 .into_any_element()
1783 } else {
1784 gpui::Empty.into_any_element()
1785 }
1786 }
1787 };
1788 let context = binding.context().cloned().map_or(
1789 gpui::Empty.into_any_element(),
1790 |context| {
1791 let is_local = context.local().is_some();
1792
1793 div()
1794 .id(("keymap context", index))
1795 .child(context.clone())
1796 .when(
1797 is_local
1798 && !context_menu_deployed
1799 && !is_overridden
1800 && this.show_hover_menus,
1801 |this| {
1802 this.tooltip(Tooltip::element({
1803 move |_, _| {
1804 context.clone().into_any_element()
1805 }
1806 }))
1807 },
1808 )
1809 .into_any_element()
1810 },
1811 );
1812 let source = binding
1813 .keybind_source()
1814 .map(|source| source.name())
1815 .unwrap_or_default()
1816 .into_any_element();
1817 Some([
1818 icon.into_any_element(),
1819 action,
1820 action_arguments,
1821 keystrokes,
1822 context,
1823 source,
1824 ])
1825 })
1826 .collect()
1827 }),
1828 )
1829 .map_row(cx.processor(
1830 |this, (row_index, row): (usize, Stateful<Div>), _window, cx| {
1831 let conflict = this.get_conflict(row_index);
1832 let is_selected = this.selected_index == Some(row_index);
1833
1834 let row_id = row_group_id(row_index);
1835
1836 div()
1837 .id(("keymap-row-wrapper", row_index))
1838 .child(
1839 row.id(row_id.clone())
1840 .on_any_mouse_down(cx.listener(
1841 move |this,
1842 mouse_down_event: &gpui::MouseDownEvent,
1843 window,
1844 cx| {
1845 match mouse_down_event.button {
1846 MouseButton::Right => {
1847 this.select_index(
1848 row_index, None, window, cx,
1849 );
1850 this.create_context_menu(
1851 mouse_down_event.position,
1852 window,
1853 cx,
1854 );
1855 }
1856 _ => {}
1857 }
1858 },
1859 ))
1860 .on_click(cx.listener(
1861 move |this, event: &ClickEvent, window, cx| {
1862 this.select_index(row_index, None, window, cx);
1863 if event.up.click_count == 2 {
1864 this.open_edit_keybinding_modal(
1865 false, window, cx,
1866 );
1867 }
1868 },
1869 ))
1870 .group(row_id)
1871 .when(
1872 conflict.is_some_and(|conflict| {
1873 !conflict.is_user_keybind_conflict()
1874 }),
1875 |row| {
1876 const OVERRIDDEN_OPACITY: f32 = 0.5;
1877 row.opacity(OVERRIDDEN_OPACITY)
1878 },
1879 )
1880 .when_some(
1881 conflict.filter(|conflict| {
1882 !this.context_menu_deployed() &&
1883 !conflict.is_user_keybind_conflict()
1884 }),
1885 |row, conflict| {
1886 let overriding_binding = this.keybindings.get(conflict.index);
1887 let context = overriding_binding.and_then(|binding| {
1888 match conflict.override_source {
1889 KeybindSource::User => Some("your keymap"),
1890 KeybindSource::Vim => Some("the vim keymap"),
1891 KeybindSource::Base => Some("your base keymap"),
1892 _ => {
1893 log::error!("Unexpected override from the {} keymap", conflict.override_source.name());
1894 None
1895 }
1896 }.map(|source| format!("This keybinding is overridden by the '{}' binding from {}.", binding.action().humanized_name, source))
1897 }).unwrap_or_else(|| "This binding is overridden.".to_string());
1898
1899 row.tooltip(Tooltip::text(context))},
1900 ),
1901 )
1902 .border_2()
1903 .when(
1904 conflict.is_some_and(|conflict| {
1905 conflict.is_user_keybind_conflict()
1906 }),
1907 |row| row.bg(cx.theme().status().error_background),
1908 )
1909 .when(is_selected, |row| {
1910 row.border_color(cx.theme().colors().panel_focused_border)
1911 })
1912 .into_any_element()
1913 }),
1914 ),
1915 )
1916 .on_scroll_wheel(cx.listener(|this, event: &ScrollWheelEvent, _, cx| {
1917 // This ensures that the menu is not dismissed in cases where scroll events
1918 // with a delta of zero are emitted
1919 if !event.delta.pixel_delta(px(1.)).y.is_zero() {
1920 this.context_menu.take();
1921 cx.notify();
1922 }
1923 }))
1924 .children(self.context_menu.as_ref().map(|(menu, position, _)| {
1925 deferred(
1926 anchored()
1927 .position(*position)
1928 .anchor(gpui::Corner::TopLeft)
1929 .child(menu.clone()),
1930 )
1931 .with_priority(1)
1932 }))
1933 }
1934}
1935
1936fn row_group_id(row_index: usize) -> SharedString {
1937 SharedString::new(format!("keymap-table-row-{}", row_index))
1938}
1939
1940fn base_button_style(row_index: usize, icon: IconName) -> IconButton {
1941 IconButton::new(("keymap-icon", row_index), icon)
1942 .shape(IconButtonShape::Square)
1943 .size(ButtonSize::Compact)
1944}
1945
1946#[derive(Debug, Clone, IntoElement)]
1947struct SyntaxHighlightedText {
1948 text: SharedString,
1949 language: Arc<Language>,
1950}
1951
1952impl SyntaxHighlightedText {
1953 pub fn new(text: impl Into<SharedString>, language: Arc<Language>) -> Self {
1954 Self {
1955 text: text.into(),
1956 language,
1957 }
1958 }
1959}
1960
1961impl RenderOnce for SyntaxHighlightedText {
1962 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
1963 let text_style = window.text_style();
1964 let syntax_theme = cx.theme().syntax();
1965
1966 let text = self.text.clone();
1967
1968 let highlights = self
1969 .language
1970 .highlight_text(&text.as_ref().into(), 0..text.len());
1971 let mut runs = Vec::with_capacity(highlights.len());
1972 let mut offset = 0;
1973
1974 for (highlight_range, highlight_id) in highlights {
1975 // Add un-highlighted text before the current highlight
1976 if highlight_range.start > offset {
1977 runs.push(text_style.to_run(highlight_range.start - offset));
1978 }
1979
1980 let mut run_style = text_style.clone();
1981 if let Some(highlight_style) = highlight_id.style(syntax_theme) {
1982 run_style = run_style.highlight(highlight_style);
1983 }
1984 // add the highlighted range
1985 runs.push(run_style.to_run(highlight_range.len()));
1986 offset = highlight_range.end;
1987 }
1988
1989 // Add any remaining un-highlighted text
1990 if offset < text.len() {
1991 runs.push(text_style.to_run(text.len() - offset));
1992 }
1993
1994 StyledText::new(text).with_runs(runs)
1995 }
1996}
1997
1998#[derive(PartialEq)]
1999struct InputError {
2000 severity: ui::Severity,
2001 content: SharedString,
2002}
2003
2004impl InputError {
2005 fn warning(message: impl Into<SharedString>) -> Self {
2006 Self {
2007 severity: ui::Severity::Warning,
2008 content: message.into(),
2009 }
2010 }
2011
2012 fn error(message: anyhow::Error) -> Self {
2013 Self {
2014 severity: ui::Severity::Error,
2015 content: message.to_string().into(),
2016 }
2017 }
2018}
2019
2020struct KeybindingEditorModal {
2021 creating: bool,
2022 editing_keybind: ProcessedBinding,
2023 editing_keybind_idx: usize,
2024 keybind_editor: Entity<KeystrokeInput>,
2025 context_editor: Entity<SingleLineInput>,
2026 action_arguments_editor: Option<Entity<ActionArgumentsEditor>>,
2027 fs: Arc<dyn Fs>,
2028 error: Option<InputError>,
2029 keymap_editor: Entity<KeymapEditor>,
2030 workspace: WeakEntity<Workspace>,
2031 focus_state: KeybindingEditorModalFocusState,
2032}
2033
2034impl ModalView for KeybindingEditorModal {}
2035
2036impl EventEmitter<DismissEvent> for KeybindingEditorModal {}
2037
2038impl Focusable for KeybindingEditorModal {
2039 fn focus_handle(&self, cx: &App) -> FocusHandle {
2040 self.keybind_editor.focus_handle(cx)
2041 }
2042}
2043
2044impl KeybindingEditorModal {
2045 pub fn new(
2046 create: bool,
2047 editing_keybind: ProcessedBinding,
2048 editing_keybind_idx: usize,
2049 keymap_editor: Entity<KeymapEditor>,
2050 action_args_temp_dir: Option<&std::path::Path>,
2051 workspace: WeakEntity<Workspace>,
2052 fs: Arc<dyn Fs>,
2053 window: &mut Window,
2054 cx: &mut App,
2055 ) -> Self {
2056 let keybind_editor = cx
2057 .new(|cx| KeystrokeInput::new(editing_keybind.keystrokes().map(Vec::from), window, cx));
2058
2059 let context_editor: Entity<SingleLineInput> = cx.new(|cx| {
2060 let input = SingleLineInput::new(window, cx, "Keybinding Context")
2061 .label("Edit Context")
2062 .label_size(LabelSize::Default);
2063
2064 if let Some(context) = editing_keybind
2065 .context()
2066 .and_then(KeybindContextString::local)
2067 {
2068 input.editor().update(cx, |editor, cx| {
2069 editor.set_text(context.clone(), window, cx);
2070 });
2071 }
2072
2073 let editor_entity = input.editor().clone();
2074 let workspace = workspace.clone();
2075 cx.spawn(async move |_input_handle, cx| {
2076 let contexts = cx
2077 .background_spawn(async { collect_contexts_from_assets() })
2078 .await;
2079
2080 let language = load_keybind_context_language(workspace, cx).await;
2081 editor_entity
2082 .update(cx, |editor, cx| {
2083 if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
2084 buffer.update(cx, |buffer, cx| {
2085 buffer.set_language(Some(language), cx);
2086 });
2087 }
2088 editor.set_completion_provider(Some(std::rc::Rc::new(
2089 KeyContextCompletionProvider { contexts },
2090 )));
2091 })
2092 .context("Failed to load completions for keybinding context")
2093 })
2094 .detach_and_log_err(cx);
2095
2096 input
2097 });
2098
2099 let action_arguments_editor = editing_keybind.action().has_schema.then(|| {
2100 let arguments = editing_keybind
2101 .action()
2102 .arguments
2103 .as_ref()
2104 .map(|args| args.text.clone());
2105 cx.new(|cx| {
2106 ActionArgumentsEditor::new(
2107 editing_keybind.action().name,
2108 arguments,
2109 action_args_temp_dir,
2110 workspace.clone(),
2111 window,
2112 cx,
2113 )
2114 })
2115 });
2116
2117 let focus_state = KeybindingEditorModalFocusState::new(
2118 keybind_editor.focus_handle(cx),
2119 action_arguments_editor
2120 .as_ref()
2121 .map(|args_editor| args_editor.focus_handle(cx)),
2122 context_editor.focus_handle(cx),
2123 );
2124
2125 Self {
2126 creating: create,
2127 editing_keybind,
2128 editing_keybind_idx,
2129 fs,
2130 keybind_editor,
2131 context_editor,
2132 action_arguments_editor,
2133 error: None,
2134 keymap_editor,
2135 workspace,
2136 focus_state,
2137 }
2138 }
2139
2140 fn set_error(&mut self, error: InputError, cx: &mut Context<Self>) -> bool {
2141 if self.error.as_ref().is_some_and(|old_error| {
2142 old_error.severity == ui::Severity::Warning && *old_error == error
2143 }) {
2144 false
2145 } else {
2146 self.error = Some(error);
2147 cx.notify();
2148 true
2149 }
2150 }
2151
2152 fn validate_action_arguments(&self, cx: &App) -> anyhow::Result<Option<String>> {
2153 let action_arguments = self
2154 .action_arguments_editor
2155 .as_ref()
2156 .map(|editor| editor.read(cx).editor.read(cx).text(cx));
2157
2158 let value = action_arguments
2159 .as_ref()
2160 .map(|args| {
2161 serde_json::from_str(args).context("Failed to parse action arguments as JSON")
2162 })
2163 .transpose()?;
2164
2165 cx.build_action(&self.editing_keybind.action().name, value)
2166 .context("Failed to validate action arguments")?;
2167 Ok(action_arguments)
2168 }
2169
2170 fn validate_keystrokes(&self, cx: &App) -> anyhow::Result<Vec<Keystroke>> {
2171 let new_keystrokes = self
2172 .keybind_editor
2173 .read_with(cx, |editor, _| editor.keystrokes().to_vec());
2174 anyhow::ensure!(!new_keystrokes.is_empty(), "Keystrokes cannot be empty");
2175 Ok(new_keystrokes)
2176 }
2177
2178 fn validate_context(&self, cx: &App) -> anyhow::Result<Option<String>> {
2179 let new_context = self
2180 .context_editor
2181 .read_with(cx, |input, cx| input.editor().read(cx).text(cx));
2182 let Some(context) = new_context.is_empty().not().then_some(new_context) else {
2183 return Ok(None);
2184 };
2185 gpui::KeyBindingContextPredicate::parse(&context).context("Failed to parse key context")?;
2186
2187 Ok(Some(context))
2188 }
2189
2190 fn save_or_display_error(&mut self, cx: &mut Context<Self>) {
2191 self.save(cx).map_err(|err| self.set_error(err, cx)).ok();
2192 }
2193
2194 fn save(&mut self, cx: &mut Context<Self>) -> Result<(), InputError> {
2195 let existing_keybind = self.editing_keybind.clone();
2196 let fs = self.fs.clone();
2197 let tab_size = cx.global::<settings::SettingsStore>().json_tab_size();
2198
2199 let new_keystrokes = self
2200 .validate_keystrokes(cx)
2201 .map_err(InputError::error)?
2202 .into_iter()
2203 .map(remove_key_char)
2204 .collect::<Vec<_>>();
2205
2206 let new_context = self.validate_context(cx).map_err(InputError::error)?;
2207 let new_action_args = self
2208 .validate_action_arguments(cx)
2209 .map_err(InputError::error)?;
2210
2211 let action_mapping = ActionMapping {
2212 keystrokes: new_keystrokes,
2213 context: new_context.map(SharedString::from),
2214 };
2215
2216 let conflicting_indices = self
2217 .keymap_editor
2218 .read(cx)
2219 .keybinding_conflict_state
2220 .conflicting_indices_for_mapping(
2221 &action_mapping,
2222 self.creating.not().then_some(self.editing_keybind_idx),
2223 );
2224
2225 conflicting_indices.map(|KeybindConflict {
2226 first_conflict_index,
2227 remaining_conflict_amount,
2228 }|
2229 {
2230 let conflicting_action_name = self
2231 .keymap_editor
2232 .read(cx)
2233 .keybindings
2234 .get(first_conflict_index)
2235 .map(|keybind| keybind.action().name);
2236
2237 let warning_message = match conflicting_action_name {
2238 Some(name) => {
2239 if remaining_conflict_amount > 0 {
2240 format!(
2241 "Your keybind would conflict with the \"{}\" action and {} other bindings",
2242 name, remaining_conflict_amount
2243 )
2244 } else {
2245 format!("Your keybind would conflict with the \"{}\" action", name)
2246 }
2247 }
2248 None => {
2249 log::info!(
2250 "Could not find action in keybindings with index {}",
2251 first_conflict_index
2252 );
2253 "Your keybind would conflict with other actions".to_string()
2254 }
2255 };
2256
2257 let warning = InputError::warning(warning_message);
2258 if self.error.as_ref().is_some_and(|old_error| *old_error == warning) {
2259 Ok(())
2260 } else {
2261 Err(warning)
2262 }
2263 }).unwrap_or(Ok(()))?;
2264
2265 let create = self.creating;
2266
2267 let status_toast = StatusToast::new(
2268 format!(
2269 "Saved edits to the {} action.",
2270 &self.editing_keybind.action().humanized_name
2271 ),
2272 cx,
2273 move |this, _cx| {
2274 this.icon(ToastIcon::new(IconName::Check).color(Color::Success))
2275 .dismiss_button(true)
2276 // .action("Undo", f) todo: wire the undo functionality
2277 },
2278 );
2279
2280 self.workspace
2281 .update(cx, |workspace, cx| {
2282 workspace.toggle_status_toast(status_toast, cx);
2283 })
2284 .log_err();
2285
2286 cx.spawn(async move |this, cx| {
2287 let action_name = existing_keybind.action().name;
2288
2289 if let Err(err) = save_keybinding_update(
2290 create,
2291 existing_keybind,
2292 &action_mapping,
2293 new_action_args.as_deref(),
2294 &fs,
2295 tab_size,
2296 )
2297 .await
2298 {
2299 this.update(cx, |this, cx| {
2300 this.set_error(InputError::error(err), cx);
2301 })
2302 .log_err();
2303 } else {
2304 this.update(cx, |this, cx| {
2305 this.keymap_editor.update(cx, |keymap, cx| {
2306 keymap.previous_edit = Some(PreviousEdit::Keybinding {
2307 action_mapping,
2308 action_name,
2309 fallback: keymap
2310 .table_interaction_state
2311 .read(cx)
2312 .get_scrollbar_offset(Axis::Vertical),
2313 })
2314 });
2315 cx.emit(DismissEvent);
2316 })
2317 .ok();
2318 }
2319 })
2320 .detach();
2321
2322 Ok(())
2323 }
2324
2325 fn key_context(&self) -> KeyContext {
2326 let mut key_context = KeyContext::new_with_defaults();
2327 key_context.add("KeybindEditorModal");
2328 key_context
2329 }
2330
2331 fn focus_next(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) {
2332 self.focus_state.focus_next(window, cx);
2333 }
2334
2335 fn focus_prev(
2336 &mut self,
2337 _: &menu::SelectPrevious,
2338 window: &mut Window,
2339 cx: &mut Context<Self>,
2340 ) {
2341 self.focus_state.focus_previous(window, cx);
2342 }
2343
2344 fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
2345 self.save_or_display_error(cx);
2346 }
2347
2348 fn cancel(&mut self, _: &menu::Cancel, _window: &mut Window, cx: &mut Context<Self>) {
2349 cx.emit(DismissEvent)
2350 }
2351}
2352
2353fn remove_key_char(Keystroke { modifiers, key, .. }: Keystroke) -> Keystroke {
2354 Keystroke {
2355 modifiers,
2356 key,
2357 ..Default::default()
2358 }
2359}
2360
2361impl Render for KeybindingEditorModal {
2362 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2363 let theme = cx.theme().colors();
2364
2365 v_flex()
2366 .w(rems(34.))
2367 .elevation_3(cx)
2368 .key_context(self.key_context())
2369 .on_action(cx.listener(Self::focus_next))
2370 .on_action(cx.listener(Self::focus_prev))
2371 .on_action(cx.listener(Self::confirm))
2372 .on_action(cx.listener(Self::cancel))
2373 .child(
2374 Modal::new("keybinding_editor_modal", None)
2375 .header(
2376 ModalHeader::new().child(
2377 v_flex()
2378 .pb_1p5()
2379 .mb_1()
2380 .gap_0p5()
2381 .border_b_1()
2382 .border_color(theme.border_variant)
2383 .child(Label::new(
2384 self.editing_keybind.action().humanized_name.clone(),
2385 ))
2386 .when_some(
2387 self.editing_keybind.action().documentation,
2388 |this, docs| {
2389 this.child(
2390 Label::new(docs)
2391 .size(LabelSize::Small)
2392 .color(Color::Muted),
2393 )
2394 },
2395 ),
2396 ),
2397 )
2398 .section(
2399 Section::new().child(
2400 v_flex()
2401 .gap_2()
2402 .child(
2403 v_flex()
2404 .child(Label::new("Edit Keystroke"))
2405 .gap_1()
2406 .child(self.keybind_editor.clone()),
2407 )
2408 .when_some(self.action_arguments_editor.clone(), |this, editor| {
2409 this.child(
2410 v_flex()
2411 .mt_1p5()
2412 .gap_1()
2413 .child(Label::new("Edit Arguments"))
2414 .child(editor),
2415 )
2416 })
2417 .child(self.context_editor.clone())
2418 .when_some(self.error.as_ref(), |this, error| {
2419 this.child(
2420 Banner::new()
2421 .severity(error.severity)
2422 // For some reason, the div overflows its container to the
2423 //right. The padding accounts for that.
2424 .child(
2425 div()
2426 .size_full()
2427 .pr_2()
2428 .child(Label::new(error.content.clone())),
2429 ),
2430 )
2431 }),
2432 ),
2433 )
2434 .footer(
2435 ModalFooter::new().end_slot(
2436 h_flex()
2437 .gap_1()
2438 .child(
2439 Button::new("cancel", "Cancel")
2440 .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
2441 )
2442 .child(Button::new("save-btn", "Save").on_click(cx.listener(
2443 |this, _event, _window, cx| {
2444 this.save_or_display_error(cx);
2445 },
2446 ))),
2447 ),
2448 ),
2449 )
2450 }
2451}
2452
2453struct KeybindingEditorModalFocusState {
2454 handles: Vec<FocusHandle>,
2455}
2456
2457impl KeybindingEditorModalFocusState {
2458 fn new(
2459 keystrokes: FocusHandle,
2460 action_input: Option<FocusHandle>,
2461 context: FocusHandle,
2462 ) -> Self {
2463 Self {
2464 handles: Vec::from_iter(
2465 [Some(keystrokes), action_input, Some(context)]
2466 .into_iter()
2467 .flatten(),
2468 ),
2469 }
2470 }
2471
2472 fn focused_index(&self, window: &Window, cx: &App) -> Option<i32> {
2473 self.handles
2474 .iter()
2475 .position(|handle| handle.contains_focused(window, cx))
2476 .map(|i| i as i32)
2477 }
2478
2479 fn focus_index(&self, mut index: i32, window: &mut Window) {
2480 if index < 0 {
2481 index = self.handles.len() as i32 - 1;
2482 }
2483 if index >= self.handles.len() as i32 {
2484 index = 0;
2485 }
2486 window.focus(&self.handles[index as usize]);
2487 }
2488
2489 fn focus_next(&self, window: &mut Window, cx: &App) {
2490 let index_to_focus = if let Some(index) = self.focused_index(window, cx) {
2491 index + 1
2492 } else {
2493 0
2494 };
2495 self.focus_index(index_to_focus, window);
2496 }
2497
2498 fn focus_previous(&self, window: &mut Window, cx: &App) {
2499 let index_to_focus = if let Some(index) = self.focused_index(window, cx) {
2500 index - 1
2501 } else {
2502 self.handles.len() as i32 - 1
2503 };
2504 self.focus_index(index_to_focus, window);
2505 }
2506}
2507
2508struct ActionArgumentsEditor {
2509 editor: Entity<Editor>,
2510 focus_handle: FocusHandle,
2511 is_loading: bool,
2512 /// See documentation in `KeymapEditor` for why a temp dir is needed.
2513 /// This field exists because the keymap editor temp dir creation may fail,
2514 /// and rather than implement a complicated retry mechanism, we simply
2515 /// fallback to trying to create a temporary directory in this editor on
2516 /// demand. Of note is that the TempDir struct will remove the directory
2517 /// when dropped.
2518 backup_temp_dir: Option<tempfile::TempDir>,
2519}
2520
2521impl Focusable for ActionArgumentsEditor {
2522 fn focus_handle(&self, _cx: &App) -> FocusHandle {
2523 self.focus_handle.clone()
2524 }
2525}
2526
2527impl ActionArgumentsEditor {
2528 fn new(
2529 action_name: &'static str,
2530 arguments: Option<SharedString>,
2531 temp_dir: Option<&std::path::Path>,
2532 workspace: WeakEntity<Workspace>,
2533 window: &mut Window,
2534 cx: &mut Context<Self>,
2535 ) -> Self {
2536 let focus_handle = cx.focus_handle();
2537 cx.on_focus_in(&focus_handle, window, |this, window, cx| {
2538 this.editor.focus_handle(cx).focus(window);
2539 })
2540 .detach();
2541 let editor = cx.new(|cx| {
2542 let mut editor = Editor::auto_height_unbounded(1, window, cx);
2543 Self::set_editor_text(&mut editor, arguments.clone(), window, cx);
2544 editor.set_read_only(true);
2545 editor
2546 });
2547
2548 let temp_dir = temp_dir.map(|path| path.to_owned());
2549 cx.spawn_in(window, async move |this, cx| {
2550 let result = async {
2551 let (project, fs) = workspace.read_with(cx, |workspace, _cx| {
2552 (
2553 workspace.project().downgrade(),
2554 workspace.app_state().fs.clone(),
2555 )
2556 })?;
2557
2558 let file_name =
2559 project::lsp_store::json_language_server_ext::normalized_action_file_name(
2560 action_name,
2561 );
2562
2563 let (buffer, backup_temp_dir) =
2564 Self::create_temp_buffer(temp_dir, file_name.clone(), project.clone(), fs, cx)
2565 .await
2566 .context(concat!(
2567 "Failed to create temporary buffer for action arguments. ",
2568 "Auto-complete will not work"
2569 ))?;
2570
2571 let editor = cx.new_window_entity(|window, cx| {
2572 let multi_buffer = cx.new(|cx| editor::MultiBuffer::singleton(buffer, cx));
2573 let mut editor = Editor::new(
2574 editor::EditorMode::Full {
2575 scale_ui_elements_with_buffer_font_size: true,
2576 show_active_line_background: false,
2577 sized_by_content: true,
2578 },
2579 multi_buffer,
2580 project.upgrade(),
2581 window,
2582 cx,
2583 );
2584 editor.set_searchable(false);
2585 editor.disable_scrollbars_and_minimap(window, cx);
2586 editor.set_show_edit_predictions(Some(false), window, cx);
2587 editor.set_show_gutter(false, cx);
2588 Self::set_editor_text(&mut editor, arguments, window, cx);
2589 editor
2590 })?;
2591
2592 this.update_in(cx, |this, window, cx| {
2593 if this.editor.focus_handle(cx).is_focused(window) {
2594 editor.focus_handle(cx).focus(window);
2595 }
2596 this.editor = editor;
2597 this.backup_temp_dir = backup_temp_dir;
2598 this.is_loading = false;
2599 })?;
2600
2601 anyhow::Ok(())
2602 }
2603 .await;
2604 if result.is_err() {
2605 let json_language = load_json_language(workspace.clone(), cx).await;
2606 this.update(cx, |this, cx| {
2607 this.editor.update(cx, |editor, cx| {
2608 if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
2609 buffer.update(cx, |buffer, cx| {
2610 buffer.set_language(Some(json_language.clone()), cx)
2611 });
2612 }
2613 })
2614 // .context("Failed to load JSON language for editing keybinding action arguments input")
2615 })
2616 .ok();
2617 this.update(cx, |this, _cx| {
2618 this.is_loading = false;
2619 })
2620 .ok();
2621 }
2622 return result;
2623 })
2624 .detach_and_log_err(cx);
2625 Self {
2626 editor,
2627 focus_handle,
2628 is_loading: true,
2629 backup_temp_dir: None,
2630 }
2631 }
2632
2633 fn set_editor_text(
2634 editor: &mut Editor,
2635 arguments: Option<SharedString>,
2636 window: &mut Window,
2637 cx: &mut Context<Editor>,
2638 ) {
2639 if let Some(arguments) = arguments {
2640 editor.set_text(arguments, window, cx);
2641 } else {
2642 // TODO: default value from schema?
2643 editor.set_placeholder_text("Action Arguments", cx);
2644 }
2645 }
2646
2647 async fn create_temp_buffer(
2648 temp_dir: Option<std::path::PathBuf>,
2649 file_name: String,
2650 project: WeakEntity<Project>,
2651 fs: Arc<dyn Fs>,
2652 cx: &mut AsyncApp,
2653 ) -> anyhow::Result<(Entity<language::Buffer>, Option<tempfile::TempDir>)> {
2654 let (temp_file_path, temp_dir) = {
2655 let file_name = file_name.clone();
2656 async move {
2657 let temp_dir_backup = match temp_dir.as_ref() {
2658 Some(_) => None,
2659 None => {
2660 let temp_dir = paths::temp_dir();
2661 let sub_temp_dir = tempfile::Builder::new()
2662 .tempdir_in(temp_dir)
2663 .context("Failed to create temporary directory")?;
2664 Some(sub_temp_dir)
2665 }
2666 };
2667 let dir_path = temp_dir.as_deref().unwrap_or_else(|| {
2668 temp_dir_backup
2669 .as_ref()
2670 .expect("created backup tempdir")
2671 .path()
2672 });
2673 let path = dir_path.join(file_name);
2674 fs.create_file(
2675 &path,
2676 fs::CreateOptions {
2677 ignore_if_exists: true,
2678 overwrite: true,
2679 },
2680 )
2681 .await
2682 .context("Failed to create temporary file")?;
2683 anyhow::Ok((path, temp_dir_backup))
2684 }
2685 }
2686 .await
2687 .context("Failed to create backing file")?;
2688
2689 project
2690 .update(cx, |project, cx| {
2691 project.open_local_buffer(temp_file_path, cx)
2692 })?
2693 .await
2694 .context("Failed to create buffer")
2695 .map(|buffer| (buffer, temp_dir))
2696 }
2697}
2698
2699impl Render for ActionArgumentsEditor {
2700 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2701 let background_color;
2702 let border_color;
2703 let text_style = {
2704 let colors = cx.theme().colors();
2705 let settings = theme::ThemeSettings::get_global(cx);
2706 background_color = colors.editor_background;
2707 border_color = if self.is_loading {
2708 colors.border_disabled
2709 } else {
2710 colors.border_variant
2711 };
2712 TextStyleRefinement {
2713 font_size: Some(rems(0.875).into()),
2714 font_weight: Some(settings.buffer_font.weight),
2715 line_height: Some(relative(1.2)),
2716 font_style: Some(gpui::FontStyle::Normal),
2717 color: self.is_loading.then_some(colors.text_disabled),
2718 ..Default::default()
2719 }
2720 };
2721
2722 self.editor
2723 .update(cx, |editor, _| editor.set_text_style_refinement(text_style));
2724
2725 return v_flex().w_full().child(
2726 h_flex()
2727 .min_h_8()
2728 .min_w_48()
2729 .px_2()
2730 .py_1p5()
2731 .flex_grow()
2732 .rounded_lg()
2733 .bg(background_color)
2734 .border_1()
2735 .border_color(border_color)
2736 .track_focus(&self.focus_handle)
2737 .child(self.editor.clone()),
2738 );
2739 }
2740}
2741
2742struct KeyContextCompletionProvider {
2743 contexts: Vec<SharedString>,
2744}
2745
2746impl CompletionProvider for KeyContextCompletionProvider {
2747 fn completions(
2748 &self,
2749 _excerpt_id: editor::ExcerptId,
2750 buffer: &Entity<language::Buffer>,
2751 buffer_position: language::Anchor,
2752 _trigger: editor::CompletionContext,
2753 _window: &mut Window,
2754 cx: &mut Context<Editor>,
2755 ) -> gpui::Task<anyhow::Result<Vec<project::CompletionResponse>>> {
2756 let buffer = buffer.read(cx);
2757 let mut count_back = 0;
2758 for char in buffer.reversed_chars_at(buffer_position) {
2759 if char.is_ascii_alphanumeric() || char == '_' {
2760 count_back += 1;
2761 } else {
2762 break;
2763 }
2764 }
2765 let start_anchor = buffer.anchor_before(
2766 buffer_position
2767 .to_offset(&buffer)
2768 .saturating_sub(count_back),
2769 );
2770 let replace_range = start_anchor..buffer_position;
2771 gpui::Task::ready(Ok(vec![project::CompletionResponse {
2772 completions: self
2773 .contexts
2774 .iter()
2775 .map(|context| project::Completion {
2776 replace_range: replace_range.clone(),
2777 label: language::CodeLabel::plain(context.to_string(), None),
2778 new_text: context.to_string(),
2779 documentation: None,
2780 source: project::CompletionSource::Custom,
2781 icon_path: None,
2782 insert_text_mode: None,
2783 confirm: None,
2784 })
2785 .collect(),
2786 is_incomplete: false,
2787 }]))
2788 }
2789
2790 fn is_completion_trigger(
2791 &self,
2792 _buffer: &Entity<language::Buffer>,
2793 _position: language::Anchor,
2794 text: &str,
2795 _trigger_in_words: bool,
2796 _menu_is_open: bool,
2797 _cx: &mut Context<Editor>,
2798 ) -> bool {
2799 text.chars().last().map_or(false, |last_char| {
2800 last_char.is_ascii_alphanumeric() || last_char == '_'
2801 })
2802 }
2803}
2804
2805async fn load_json_language(workspace: WeakEntity<Workspace>, cx: &mut AsyncApp) -> Arc<Language> {
2806 let json_language_task = workspace
2807 .read_with(cx, |workspace, cx| {
2808 workspace
2809 .project()
2810 .read(cx)
2811 .languages()
2812 .language_for_name("JSON")
2813 })
2814 .context("Failed to load JSON language")
2815 .log_err();
2816 let json_language = match json_language_task {
2817 Some(task) => task.await.context("Failed to load JSON language").log_err(),
2818 None => None,
2819 };
2820 return json_language.unwrap_or_else(|| {
2821 Arc::new(Language::new(
2822 LanguageConfig {
2823 name: "JSON".into(),
2824 ..Default::default()
2825 },
2826 Some(tree_sitter_json::LANGUAGE.into()),
2827 ))
2828 });
2829}
2830
2831async fn load_keybind_context_language(
2832 workspace: WeakEntity<Workspace>,
2833 cx: &mut AsyncApp,
2834) -> Arc<Language> {
2835 let language_task = workspace
2836 .read_with(cx, |workspace, cx| {
2837 workspace
2838 .project()
2839 .read(cx)
2840 .languages()
2841 .language_for_name("Zed Keybind Context")
2842 })
2843 .context("Failed to load Zed Keybind Context language")
2844 .log_err();
2845 let language = match language_task {
2846 Some(task) => task
2847 .await
2848 .context("Failed to load Zed Keybind Context language")
2849 .log_err(),
2850 None => None,
2851 };
2852 return language.unwrap_or_else(|| {
2853 Arc::new(Language::new(
2854 LanguageConfig {
2855 name: "Zed Keybind Context".into(),
2856 ..Default::default()
2857 },
2858 Some(tree_sitter_rust::LANGUAGE.into()),
2859 ))
2860 });
2861}
2862
2863async fn save_keybinding_update(
2864 create: bool,
2865 existing: ProcessedBinding,
2866 action_mapping: &ActionMapping,
2867 new_args: Option<&str>,
2868 fs: &Arc<dyn Fs>,
2869 tab_size: usize,
2870) -> anyhow::Result<()> {
2871 let keymap_contents = settings::KeymapFile::load_keymap_file(fs)
2872 .await
2873 .context("Failed to load keymap file")?;
2874
2875 let existing_keystrokes = existing.keystrokes().unwrap_or_default();
2876 let existing_context = existing.context().and_then(KeybindContextString::local_str);
2877 let existing_args = existing
2878 .action()
2879 .arguments
2880 .as_ref()
2881 .map(|args| args.text.as_ref());
2882
2883 let target = settings::KeybindUpdateTarget {
2884 context: existing_context,
2885 keystrokes: existing_keystrokes,
2886 action_name: &existing.action().name,
2887 action_arguments: existing_args,
2888 };
2889
2890 let source = settings::KeybindUpdateTarget {
2891 context: action_mapping.context.as_ref().map(|a| &***a),
2892 keystrokes: &action_mapping.keystrokes,
2893 action_name: &existing.action().name,
2894 action_arguments: new_args,
2895 };
2896
2897 let operation = if !create {
2898 settings::KeybindUpdateOperation::Replace {
2899 target,
2900 target_keybind_source: existing.keybind_source().unwrap_or(KeybindSource::User),
2901 source,
2902 }
2903 } else {
2904 settings::KeybindUpdateOperation::Add {
2905 source,
2906 from: Some(target),
2907 }
2908 };
2909
2910 let (new_keybinding, removed_keybinding, source) = operation.generate_telemetry();
2911
2912 let updated_keymap_contents =
2913 settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size)
2914 .context("Failed to update keybinding")?;
2915 fs.write(
2916 paths::keymap_file().as_path(),
2917 updated_keymap_contents.as_bytes(),
2918 )
2919 .await
2920 .context("Failed to write keymap file")?;
2921
2922 telemetry::event!(
2923 "Keybinding Updated",
2924 new_keybinding = new_keybinding,
2925 removed_keybinding = removed_keybinding,
2926 source = source
2927 );
2928 Ok(())
2929}
2930
2931async fn remove_keybinding(
2932 existing: ProcessedBinding,
2933 fs: &Arc<dyn Fs>,
2934 tab_size: usize,
2935) -> anyhow::Result<()> {
2936 let Some(keystrokes) = existing.keystrokes() else {
2937 anyhow::bail!("Cannot remove a keybinding that does not exist");
2938 };
2939 let keymap_contents = settings::KeymapFile::load_keymap_file(fs)
2940 .await
2941 .context("Failed to load keymap file")?;
2942
2943 let operation = settings::KeybindUpdateOperation::Remove {
2944 target: settings::KeybindUpdateTarget {
2945 context: existing.context().and_then(KeybindContextString::local_str),
2946 keystrokes,
2947 action_name: &existing.action().name,
2948 action_arguments: existing
2949 .action()
2950 .arguments
2951 .as_ref()
2952 .map(|arguments| arguments.text.as_ref()),
2953 },
2954 target_keybind_source: existing.keybind_source().unwrap_or(KeybindSource::User),
2955 };
2956
2957 let (new_keybinding, removed_keybinding, source) = operation.generate_telemetry();
2958 let updated_keymap_contents =
2959 settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size)
2960 .context("Failed to update keybinding")?;
2961 fs.write(
2962 paths::keymap_file().as_path(),
2963 updated_keymap_contents.as_bytes(),
2964 )
2965 .await
2966 .context("Failed to write keymap file")?;
2967
2968 telemetry::event!(
2969 "Keybinding Removed",
2970 new_keybinding = new_keybinding,
2971 removed_keybinding = removed_keybinding,
2972 source = source
2973 );
2974 Ok(())
2975}
2976
2977#[derive(PartialEq, Eq, Debug, Copy, Clone)]
2978enum CloseKeystrokeResult {
2979 Partial,
2980 Close,
2981 None,
2982}
2983
2984struct KeystrokeInput {
2985 keystrokes: Vec<Keystroke>,
2986 placeholder_keystrokes: Option<Vec<Keystroke>>,
2987 outer_focus_handle: FocusHandle,
2988 inner_focus_handle: FocusHandle,
2989 intercept_subscription: Option<Subscription>,
2990 _focus_subscriptions: [Subscription; 2],
2991 search: bool,
2992 /// Handles tripe escape to stop recording
2993 close_keystrokes: Option<Vec<Keystroke>>,
2994 close_keystrokes_start: Option<usize>,
2995 previous_modifiers: Modifiers,
2996}
2997
2998impl KeystrokeInput {
2999 const KEYSTROKE_COUNT_MAX: usize = 3;
3000
3001 fn new(
3002 placeholder_keystrokes: Option<Vec<Keystroke>>,
3003 window: &mut Window,
3004 cx: &mut Context<Self>,
3005 ) -> Self {
3006 let outer_focus_handle = cx.focus_handle();
3007 let inner_focus_handle = cx.focus_handle();
3008 let _focus_subscriptions = [
3009 cx.on_focus_in(&inner_focus_handle, window, Self::on_inner_focus_in),
3010 cx.on_focus_out(&inner_focus_handle, window, Self::on_inner_focus_out),
3011 ];
3012 Self {
3013 keystrokes: Vec::new(),
3014 placeholder_keystrokes,
3015 inner_focus_handle,
3016 outer_focus_handle,
3017 intercept_subscription: None,
3018 _focus_subscriptions,
3019 search: false,
3020 close_keystrokes: None,
3021 close_keystrokes_start: None,
3022 previous_modifiers: Modifiers::default(),
3023 }
3024 }
3025
3026 fn set_keystrokes(&mut self, keystrokes: Vec<Keystroke>, cx: &mut Context<Self>) {
3027 self.keystrokes = keystrokes;
3028 self.keystrokes_changed(cx);
3029 }
3030
3031 fn dummy(modifiers: Modifiers) -> Keystroke {
3032 return Keystroke {
3033 modifiers,
3034 key: "".to_string(),
3035 key_char: None,
3036 };
3037 }
3038
3039 fn keystrokes_changed(&self, cx: &mut Context<Self>) {
3040 cx.emit(());
3041 cx.notify();
3042 }
3043
3044 fn key_context() -> KeyContext {
3045 let mut key_context = KeyContext::default();
3046 key_context.add("KeystrokeInput");
3047 key_context
3048 }
3049
3050 fn handle_possible_close_keystroke(
3051 &mut self,
3052 keystroke: &Keystroke,
3053 window: &mut Window,
3054 cx: &mut Context<Self>,
3055 ) -> CloseKeystrokeResult {
3056 let Some(keybind_for_close_action) = window
3057 .highest_precedence_binding_for_action_in_context(&StopRecording, Self::key_context())
3058 else {
3059 log::trace!("No keybinding to stop recording keystrokes in keystroke input");
3060 self.close_keystrokes.take();
3061 self.close_keystrokes_start.take();
3062 return CloseKeystrokeResult::None;
3063 };
3064 let action_keystrokes = keybind_for_close_action.keystrokes();
3065
3066 if let Some(mut close_keystrokes) = self.close_keystrokes.take() {
3067 let mut index = 0;
3068
3069 while index < action_keystrokes.len() && index < close_keystrokes.len() {
3070 if !close_keystrokes[index].should_match(&action_keystrokes[index]) {
3071 break;
3072 }
3073 index += 1;
3074 }
3075 if index == close_keystrokes.len() {
3076 if index >= action_keystrokes.len() {
3077 self.close_keystrokes_start.take();
3078 return CloseKeystrokeResult::None;
3079 }
3080 if keystroke.should_match(&action_keystrokes[index]) {
3081 if action_keystrokes.len() >= 1 && index == action_keystrokes.len() - 1 {
3082 self.stop_recording(&StopRecording, window, cx);
3083 return CloseKeystrokeResult::Close;
3084 } else {
3085 close_keystrokes.push(keystroke.clone());
3086 self.close_keystrokes = Some(close_keystrokes);
3087 return CloseKeystrokeResult::Partial;
3088 }
3089 } else {
3090 self.close_keystrokes_start.take();
3091 return CloseKeystrokeResult::None;
3092 }
3093 }
3094 } else if let Some(first_action_keystroke) = action_keystrokes.first()
3095 && keystroke.should_match(first_action_keystroke)
3096 {
3097 self.close_keystrokes = Some(vec![keystroke.clone()]);
3098 return CloseKeystrokeResult::Partial;
3099 }
3100 self.close_keystrokes_start.take();
3101 return CloseKeystrokeResult::None;
3102 }
3103
3104 fn on_modifiers_changed(
3105 &mut self,
3106 event: &ModifiersChangedEvent,
3107 _window: &mut Window,
3108 cx: &mut Context<Self>,
3109 ) {
3110 let keystrokes_len = self.keystrokes.len();
3111
3112 if event.modifiers.is_subset_of(&self.previous_modifiers) {
3113 self.previous_modifiers &= event.modifiers;
3114 cx.stop_propagation();
3115 return;
3116 }
3117
3118 if let Some(last) = self.keystrokes.last_mut()
3119 && last.key.is_empty()
3120 && keystrokes_len <= Self::KEYSTROKE_COUNT_MAX
3121 {
3122 if self.search {
3123 if self.previous_modifiers.modified() {
3124 last.modifiers |= event.modifiers;
3125 self.previous_modifiers |= event.modifiers;
3126 } else {
3127 self.keystrokes.push(Self::dummy(event.modifiers));
3128 self.previous_modifiers |= event.modifiers;
3129 }
3130 } else if !event.modifiers.modified() {
3131 self.keystrokes.pop();
3132 } else {
3133 last.modifiers = event.modifiers;
3134 }
3135
3136 self.keystrokes_changed(cx);
3137 } else if keystrokes_len < Self::KEYSTROKE_COUNT_MAX {
3138 self.keystrokes.push(Self::dummy(event.modifiers));
3139 if self.search {
3140 self.previous_modifiers |= event.modifiers;
3141 }
3142 self.keystrokes_changed(cx);
3143 }
3144 cx.stop_propagation();
3145 }
3146
3147 fn handle_keystroke(
3148 &mut self,
3149 keystroke: &Keystroke,
3150 window: &mut Window,
3151 cx: &mut Context<Self>,
3152 ) {
3153 let close_keystroke_result = self.handle_possible_close_keystroke(keystroke, window, cx);
3154 if close_keystroke_result != CloseKeystrokeResult::Close {
3155 let key_len = self.keystrokes.len();
3156 if let Some(last) = self.keystrokes.last_mut()
3157 && last.key.is_empty()
3158 && key_len <= Self::KEYSTROKE_COUNT_MAX
3159 {
3160 if self.search {
3161 last.key = keystroke.key.clone();
3162 if close_keystroke_result == CloseKeystrokeResult::Partial
3163 && self.close_keystrokes_start.is_none()
3164 {
3165 self.close_keystrokes_start = Some(self.keystrokes.len() - 1);
3166 }
3167 if self.search {
3168 self.previous_modifiers = keystroke.modifiers;
3169 }
3170 self.keystrokes_changed(cx);
3171 cx.stop_propagation();
3172 return;
3173 } else {
3174 self.keystrokes.pop();
3175 }
3176 }
3177 if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX {
3178 if close_keystroke_result == CloseKeystrokeResult::Partial
3179 && self.close_keystrokes_start.is_none()
3180 {
3181 self.close_keystrokes_start = Some(self.keystrokes.len());
3182 }
3183 self.keystrokes.push(keystroke.clone());
3184 if self.search {
3185 self.previous_modifiers = keystroke.modifiers;
3186 } else if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX {
3187 self.keystrokes.push(Self::dummy(keystroke.modifiers));
3188 }
3189 } else if close_keystroke_result != CloseKeystrokeResult::Partial {
3190 self.clear_keystrokes(&ClearKeystrokes, window, cx);
3191 }
3192 }
3193 self.keystrokes_changed(cx);
3194 cx.stop_propagation();
3195 }
3196
3197 fn on_inner_focus_in(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
3198 if self.intercept_subscription.is_none() {
3199 let listener = cx.listener(|this, event: &gpui::KeystrokeEvent, window, cx| {
3200 this.handle_keystroke(&event.keystroke, window, cx);
3201 });
3202 self.intercept_subscription = Some(cx.intercept_keystrokes(listener))
3203 }
3204 }
3205
3206 fn on_inner_focus_out(
3207 &mut self,
3208 _event: gpui::FocusOutEvent,
3209 _window: &mut Window,
3210 cx: &mut Context<Self>,
3211 ) {
3212 self.intercept_subscription.take();
3213 cx.notify();
3214 }
3215
3216 fn keystrokes(&self) -> &[Keystroke] {
3217 if let Some(placeholders) = self.placeholder_keystrokes.as_ref()
3218 && self.keystrokes.is_empty()
3219 {
3220 return placeholders;
3221 }
3222 if !self.search
3223 && self
3224 .keystrokes
3225 .last()
3226 .map_or(false, |last| last.key.is_empty())
3227 {
3228 return &self.keystrokes[..self.keystrokes.len() - 1];
3229 }
3230 return &self.keystrokes;
3231 }
3232
3233 fn render_keystrokes(&self, is_recording: bool) -> impl Iterator<Item = Div> {
3234 let keystrokes = if let Some(placeholders) = self.placeholder_keystrokes.as_ref()
3235 && self.keystrokes.is_empty()
3236 {
3237 if is_recording {
3238 &[]
3239 } else {
3240 placeholders.as_slice()
3241 }
3242 } else {
3243 &self.keystrokes
3244 };
3245 keystrokes.iter().map(move |keystroke| {
3246 h_flex().children(ui::render_keystroke(
3247 keystroke,
3248 Some(Color::Default),
3249 Some(rems(0.875).into()),
3250 ui::PlatformStyle::platform(),
3251 false,
3252 ))
3253 })
3254 }
3255
3256 fn start_recording(&mut self, _: &StartRecording, window: &mut Window, cx: &mut Context<Self>) {
3257 window.focus(&self.inner_focus_handle);
3258 self.clear_keystrokes(&ClearKeystrokes, window, cx);
3259 self.previous_modifiers = window.modifiers();
3260 cx.stop_propagation();
3261 }
3262
3263 fn stop_recording(&mut self, _: &StopRecording, window: &mut Window, cx: &mut Context<Self>) {
3264 if !self.inner_focus_handle.is_focused(window) {
3265 return;
3266 }
3267 window.focus(&self.outer_focus_handle);
3268 if let Some(close_keystrokes_start) = self.close_keystrokes_start.take()
3269 && close_keystrokes_start < self.keystrokes.len()
3270 {
3271 self.keystrokes.drain(close_keystrokes_start..);
3272 }
3273 self.close_keystrokes.take();
3274 cx.notify();
3275 }
3276
3277 fn clear_keystrokes(
3278 &mut self,
3279 _: &ClearKeystrokes,
3280 _window: &mut Window,
3281 cx: &mut Context<Self>,
3282 ) {
3283 self.keystrokes.clear();
3284 self.keystrokes_changed(cx);
3285 }
3286}
3287
3288impl EventEmitter<()> for KeystrokeInput {}
3289
3290impl Focusable for KeystrokeInput {
3291 fn focus_handle(&self, _cx: &App) -> FocusHandle {
3292 self.outer_focus_handle.clone()
3293 }
3294}
3295
3296impl Render for KeystrokeInput {
3297 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3298 let colors = cx.theme().colors();
3299 let is_focused = self.outer_focus_handle.contains_focused(window, cx);
3300 let is_recording = self.inner_focus_handle.is_focused(window);
3301
3302 let horizontal_padding = rems_from_px(64.);
3303
3304 let recording_bg_color = colors
3305 .editor_background
3306 .blend(colors.text_accent.opacity(0.1));
3307
3308 let recording_pulse = |color: Color| {
3309 Icon::new(IconName::Circle)
3310 .size(IconSize::Small)
3311 .color(Color::Error)
3312 .with_animation(
3313 "recording-pulse",
3314 Animation::new(std::time::Duration::from_secs(2))
3315 .repeat()
3316 .with_easing(gpui::pulsating_between(0.4, 0.8)),
3317 {
3318 let color = color.color(cx);
3319 move |this, delta| this.color(Color::Custom(color.opacity(delta)))
3320 },
3321 )
3322 };
3323
3324 let recording_indicator = h_flex()
3325 .h_4()
3326 .pr_1()
3327 .gap_0p5()
3328 .border_1()
3329 .border_color(colors.border)
3330 .bg(colors
3331 .editor_background
3332 .blend(colors.text_accent.opacity(0.1)))
3333 .rounded_sm()
3334 .child(recording_pulse(Color::Error))
3335 .child(
3336 Label::new("REC")
3337 .size(LabelSize::XSmall)
3338 .weight(FontWeight::SEMIBOLD)
3339 .color(Color::Error),
3340 );
3341
3342 let search_indicator = h_flex()
3343 .h_4()
3344 .pr_1()
3345 .gap_0p5()
3346 .border_1()
3347 .border_color(colors.border)
3348 .bg(colors
3349 .editor_background
3350 .blend(colors.text_accent.opacity(0.1)))
3351 .rounded_sm()
3352 .child(recording_pulse(Color::Accent))
3353 .child(
3354 Label::new("SEARCH")
3355 .size(LabelSize::XSmall)
3356 .weight(FontWeight::SEMIBOLD)
3357 .color(Color::Accent),
3358 );
3359
3360 let record_icon = if self.search {
3361 IconName::MagnifyingGlass
3362 } else {
3363 IconName::PlayFilled
3364 };
3365
3366 h_flex()
3367 .id("keystroke-input")
3368 .track_focus(&self.outer_focus_handle)
3369 .py_2()
3370 .px_3()
3371 .gap_2()
3372 .min_h_10()
3373 .w_full()
3374 .flex_1()
3375 .justify_between()
3376 .rounded_lg()
3377 .overflow_hidden()
3378 .map(|this| {
3379 if is_recording {
3380 this.bg(recording_bg_color)
3381 } else {
3382 this.bg(colors.editor_background)
3383 }
3384 })
3385 .border_1()
3386 .border_color(colors.border_variant)
3387 .when(is_focused, |parent| {
3388 parent.border_color(colors.border_focused)
3389 })
3390 .key_context(Self::key_context())
3391 .on_action(cx.listener(Self::start_recording))
3392 .on_action(cx.listener(Self::clear_keystrokes))
3393 .child(
3394 h_flex()
3395 .w(horizontal_padding)
3396 .gap_0p5()
3397 .justify_start()
3398 .flex_none()
3399 .when(is_recording, |this| {
3400 this.map(|this| {
3401 if self.search {
3402 this.child(search_indicator)
3403 } else {
3404 this.child(recording_indicator)
3405 }
3406 })
3407 }),
3408 )
3409 .child(
3410 h_flex()
3411 .id("keystroke-input-inner")
3412 .track_focus(&self.inner_focus_handle)
3413 .on_modifiers_changed(cx.listener(Self::on_modifiers_changed))
3414 .size_full()
3415 .when(!self.search, |this| {
3416 this.focus(|mut style| {
3417 style.border_color = Some(colors.border_focused);
3418 style
3419 })
3420 })
3421 .w_full()
3422 .min_w_0()
3423 .justify_center()
3424 .flex_wrap()
3425 .gap(ui::DynamicSpacing::Base04.rems(cx))
3426 .children(self.render_keystrokes(is_recording)),
3427 )
3428 .child(
3429 h_flex()
3430 .w(horizontal_padding)
3431 .gap_0p5()
3432 .justify_end()
3433 .flex_none()
3434 .map(|this| {
3435 if is_recording {
3436 this.child(
3437 IconButton::new("stop-record-btn", IconName::StopFilled)
3438 .shape(ui::IconButtonShape::Square)
3439 .map(|this| {
3440 this.tooltip(Tooltip::for_action_title(
3441 if self.search {
3442 "Stop Searching"
3443 } else {
3444 "Stop Recording"
3445 },
3446 &StopRecording,
3447 ))
3448 })
3449 .icon_color(Color::Error)
3450 .on_click(cx.listener(|this, _event, window, cx| {
3451 this.stop_recording(&StopRecording, window, cx);
3452 })),
3453 )
3454 } else {
3455 this.child(
3456 IconButton::new("record-btn", record_icon)
3457 .shape(ui::IconButtonShape::Square)
3458 .map(|this| {
3459 this.tooltip(Tooltip::for_action_title(
3460 if self.search {
3461 "Start Searching"
3462 } else {
3463 "Start Recording"
3464 },
3465 &StartRecording,
3466 ))
3467 })
3468 .when(!is_focused, |this| this.icon_color(Color::Muted))
3469 .on_click(cx.listener(|this, _event, window, cx| {
3470 this.start_recording(&StartRecording, window, cx);
3471 })),
3472 )
3473 }
3474 })
3475 .child(
3476 IconButton::new("clear-btn", IconName::Delete)
3477 .shape(ui::IconButtonShape::Square)
3478 .tooltip(Tooltip::for_action_title(
3479 "Clear Keystrokes",
3480 &ClearKeystrokes,
3481 ))
3482 .when(!is_recording || !is_focused, |this| {
3483 this.icon_color(Color::Muted)
3484 })
3485 .on_click(cx.listener(|this, _event, window, cx| {
3486 this.clear_keystrokes(&ClearKeystrokes, window, cx);
3487 })),
3488 ),
3489 )
3490 }
3491}
3492
3493fn collect_contexts_from_assets() -> Vec<SharedString> {
3494 let mut keymap_assets = vec![
3495 util::asset_str::<SettingsAssets>(settings::DEFAULT_KEYMAP_PATH),
3496 util::asset_str::<SettingsAssets>(settings::VIM_KEYMAP_PATH),
3497 ];
3498 keymap_assets.extend(
3499 BaseKeymap::OPTIONS
3500 .iter()
3501 .filter_map(|(_, base_keymap)| base_keymap.asset_path())
3502 .map(util::asset_str::<SettingsAssets>),
3503 );
3504
3505 let mut contexts = HashSet::default();
3506
3507 for keymap_asset in keymap_assets {
3508 let Ok(keymap) = KeymapFile::parse(&keymap_asset) else {
3509 continue;
3510 };
3511
3512 for section in keymap.sections() {
3513 let context_expr = §ion.context;
3514 let mut queue = Vec::new();
3515 let Ok(root_context) = gpui::KeyBindingContextPredicate::parse(context_expr) else {
3516 continue;
3517 };
3518
3519 queue.push(root_context);
3520 while let Some(context) = queue.pop() {
3521 match context {
3522 gpui::KeyBindingContextPredicate::Identifier(ident) => {
3523 contexts.insert(ident);
3524 }
3525 gpui::KeyBindingContextPredicate::Equal(ident_a, ident_b) => {
3526 contexts.insert(ident_a);
3527 contexts.insert(ident_b);
3528 }
3529 gpui::KeyBindingContextPredicate::NotEqual(ident_a, ident_b) => {
3530 contexts.insert(ident_a);
3531 contexts.insert(ident_b);
3532 }
3533 gpui::KeyBindingContextPredicate::Descendant(ctx_a, ctx_b) => {
3534 queue.push(*ctx_a);
3535 queue.push(*ctx_b);
3536 }
3537 gpui::KeyBindingContextPredicate::Not(ctx) => {
3538 queue.push(*ctx);
3539 }
3540 gpui::KeyBindingContextPredicate::And(ctx_a, ctx_b) => {
3541 queue.push(*ctx_a);
3542 queue.push(*ctx_b);
3543 }
3544 gpui::KeyBindingContextPredicate::Or(ctx_a, ctx_b) => {
3545 queue.push(*ctx_a);
3546 queue.push(*ctx_b);
3547 }
3548 }
3549 }
3550 }
3551 }
3552
3553 let mut contexts = contexts.into_iter().collect::<Vec<_>>();
3554 contexts.sort();
3555
3556 return contexts;
3557}
3558
3559impl SerializableItem for KeymapEditor {
3560 fn serialized_item_kind() -> &'static str {
3561 "KeymapEditor"
3562 }
3563
3564 fn cleanup(
3565 workspace_id: workspace::WorkspaceId,
3566 alive_items: Vec<workspace::ItemId>,
3567 _window: &mut Window,
3568 cx: &mut App,
3569 ) -> gpui::Task<gpui::Result<()>> {
3570 workspace::delete_unloaded_items(
3571 alive_items,
3572 workspace_id,
3573 "keybinding_editors",
3574 &KEYBINDING_EDITORS,
3575 cx,
3576 )
3577 }
3578
3579 fn deserialize(
3580 _project: Entity<project::Project>,
3581 workspace: WeakEntity<Workspace>,
3582 workspace_id: workspace::WorkspaceId,
3583 item_id: workspace::ItemId,
3584 window: &mut Window,
3585 cx: &mut App,
3586 ) -> gpui::Task<gpui::Result<Entity<Self>>> {
3587 window.spawn(cx, async move |cx| {
3588 if KEYBINDING_EDITORS
3589 .get_keybinding_editor(item_id, workspace_id)?
3590 .is_some()
3591 {
3592 cx.update(|window, cx| cx.new(|cx| KeymapEditor::new(workspace, window, cx)))
3593 } else {
3594 Err(anyhow!("No keybinding editor to deserialize"))
3595 }
3596 })
3597 }
3598
3599 fn serialize(
3600 &mut self,
3601 workspace: &mut Workspace,
3602 item_id: workspace::ItemId,
3603 _closing: bool,
3604 _window: &mut Window,
3605 cx: &mut ui::Context<Self>,
3606 ) -> Option<gpui::Task<gpui::Result<()>>> {
3607 let workspace_id = workspace.database_id()?;
3608 Some(cx.background_spawn(async move {
3609 KEYBINDING_EDITORS
3610 .save_keybinding_editor(item_id, workspace_id)
3611 .await
3612 }))
3613 }
3614
3615 fn should_serialize(&self, _event: &Self::Event) -> bool {
3616 false
3617 }
3618}
3619
3620mod persistence {
3621 use db::{define_connection, query, sqlez_macros::sql};
3622 use workspace::WorkspaceDb;
3623
3624 define_connection! {
3625 pub static ref KEYBINDING_EDITORS: KeybindingEditorDb<WorkspaceDb> =
3626 &[sql!(
3627 CREATE TABLE keybinding_editors (
3628 workspace_id INTEGER,
3629 item_id INTEGER UNIQUE,
3630
3631 PRIMARY KEY(workspace_id, item_id),
3632 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
3633 ON DELETE CASCADE
3634 ) STRICT;
3635 )];
3636 }
3637
3638 impl KeybindingEditorDb {
3639 query! {
3640 pub async fn save_keybinding_editor(
3641 item_id: workspace::ItemId,
3642 workspace_id: workspace::WorkspaceId
3643 ) -> Result<()> {
3644 INSERT OR REPLACE INTO keybinding_editors(item_id, workspace_id)
3645 VALUES (?, ?)
3646 }
3647 }
3648
3649 query! {
3650 pub fn get_keybinding_editor(
3651 item_id: workspace::ItemId,
3652 workspace_id: workspace::WorkspaceId
3653 ) -> Result<Option<workspace::ItemId>> {
3654 SELECT item_id
3655 FROM keybinding_editors
3656 WHERE item_id = ? AND workspace_id = ?
3657 }
3658 }
3659 }
3660}