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