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