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