1use std::{
2 ops::{Not, Range},
3 sync::Arc,
4};
5
6use anyhow::{Context as _, anyhow};
7use collections::HashSet;
8use editor::{CompletionProvider, Editor, EditorEvent};
9use feature_flags::FeatureFlagViewExt;
10use fs::Fs;
11use fuzzy::{StringMatch, StringMatchCandidate};
12use gpui::{
13 AppContext as _, AsyncApp, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
14 Global, KeyContext, Keystroke, ModifiersChangedEvent, ScrollStrategy, StyledText, Subscription,
15 WeakEntity, actions, div, transparent_black,
16};
17use language::{Language, LanguageConfig, ToOffset as _};
18use settings::{BaseKeymap, KeybindSource, KeymapFile, SettingsAssets};
19
20use util::ResultExt;
21
22use ui::{
23 ActiveTheme as _, App, BorrowAppContext, ContextMenu, ParentElement as _, Render, SharedString,
24 Styled as _, Tooltip, Window, prelude::*, right_click_menu,
25};
26use workspace::{Item, ModalView, SerializableItem, Workspace, register_serializable_item};
27
28use crate::{
29 SettingsUiFeatureFlag,
30 keybindings::persistence::KEYBINDING_EDITORS,
31 ui_components::table::{Table, TableInteractionState},
32};
33
34actions!(
35 zed,
36 [
37 /// Opens the keymap editor.
38 OpenKeymapEditor
39 ]
40);
41
42const KEYMAP_EDITOR_NAMESPACE: &'static str = "keymap_editor";
43actions!(
44 keymap_editor,
45 [
46 /// Edits the selected key binding.
47 EditBinding,
48 /// Copies the action name to clipboard.
49 CopyAction,
50 /// Copies the context predicate to clipboard.
51 CopyContext
52 ]
53);
54
55pub fn init(cx: &mut App) {
56 let keymap_event_channel = KeymapEventChannel::new();
57 cx.set_global(keymap_event_channel);
58
59 cx.on_action(|_: &OpenKeymapEditor, cx| {
60 workspace::with_active_or_new_workspace(cx, move |workspace, window, cx| {
61 let existing = workspace
62 .active_pane()
63 .read(cx)
64 .items()
65 .find_map(|item| item.downcast::<KeymapEditor>());
66
67 if let Some(existing) = existing {
68 workspace.activate_item(&existing, true, true, window, cx);
69 } else {
70 let keymap_editor =
71 cx.new(|cx| KeymapEditor::new(workspace.weak_handle(), window, cx));
72 workspace.add_item_to_active_pane(Box::new(keymap_editor), None, true, window, cx);
73 }
74 });
75 });
76
77 cx.observe_new(|_workspace: &mut Workspace, window, cx| {
78 let Some(window) = window else { return };
79
80 let keymap_ui_actions = [std::any::TypeId::of::<OpenKeymapEditor>()];
81
82 command_palette_hooks::CommandPaletteFilter::update_global(cx, |filter, _cx| {
83 filter.hide_action_types(&keymap_ui_actions);
84 filter.hide_namespace(KEYMAP_EDITOR_NAMESPACE);
85 });
86
87 cx.observe_flag::<SettingsUiFeatureFlag, _>(
88 window,
89 move |is_enabled, _workspace, _, cx| {
90 if is_enabled {
91 command_palette_hooks::CommandPaletteFilter::update_global(
92 cx,
93 |filter, _cx| {
94 filter.show_action_types(keymap_ui_actions.iter());
95 filter.show_namespace(KEYMAP_EDITOR_NAMESPACE);
96 },
97 );
98 } else {
99 command_palette_hooks::CommandPaletteFilter::update_global(
100 cx,
101 |filter, _cx| {
102 filter.hide_action_types(&keymap_ui_actions);
103 filter.hide_namespace(KEYMAP_EDITOR_NAMESPACE);
104 },
105 );
106 }
107 },
108 )
109 .detach();
110 })
111 .detach();
112
113 register_serializable_item::<KeymapEditor>(cx);
114}
115
116pub struct KeymapEventChannel {}
117
118impl Global for KeymapEventChannel {}
119
120impl KeymapEventChannel {
121 fn new() -> Self {
122 Self {}
123 }
124
125 pub fn trigger_keymap_changed(cx: &mut App) {
126 let Some(_event_channel) = cx.try_global::<Self>() else {
127 // don't panic if no global defined. This usually happens in tests
128 return;
129 };
130 cx.update_global(|_event_channel: &mut Self, _| {
131 /* triggers observers in KeymapEditors */
132 });
133 }
134}
135
136struct KeymapEditor {
137 workspace: WeakEntity<Workspace>,
138 focus_handle: FocusHandle,
139 _keymap_subscription: Subscription,
140 keybindings: Vec<ProcessedKeybinding>,
141 // corresponds 1 to 1 with keybindings
142 string_match_candidates: Arc<Vec<StringMatchCandidate>>,
143 matches: Vec<StringMatch>,
144 table_interaction_state: Entity<TableInteractionState>,
145 filter_editor: Entity<Editor>,
146 selected_index: Option<usize>,
147}
148
149impl EventEmitter<()> for KeymapEditor {}
150
151impl Focusable for KeymapEditor {
152 fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
153 return self.filter_editor.focus_handle(cx);
154 }
155}
156
157impl KeymapEditor {
158 fn new(workspace: WeakEntity<Workspace>, window: &mut Window, cx: &mut Context<Self>) -> Self {
159 let focus_handle = cx.focus_handle();
160
161 let _keymap_subscription =
162 cx.observe_global::<KeymapEventChannel>(Self::update_keybindings);
163 let table_interaction_state = TableInteractionState::new(window, cx);
164
165 let filter_editor = cx.new(|cx| {
166 let mut editor = Editor::single_line(window, cx);
167 editor.set_placeholder_text("Filter action names…", cx);
168 editor
169 });
170
171 cx.subscribe(&filter_editor, |this, _, e: &EditorEvent, cx| {
172 if !matches!(e, EditorEvent::BufferEdited) {
173 return;
174 }
175
176 this.update_matches(cx);
177 })
178 .detach();
179
180 let mut this = Self {
181 workspace,
182 keybindings: vec![],
183 string_match_candidates: Arc::new(vec![]),
184 matches: vec![],
185 focus_handle: focus_handle.clone(),
186 _keymap_subscription,
187 table_interaction_state,
188 filter_editor,
189 selected_index: None,
190 };
191
192 this.update_keybindings(cx);
193
194 this
195 }
196
197 fn current_query(&self, cx: &mut Context<Self>) -> String {
198 self.filter_editor.read(cx).text(cx)
199 }
200
201 fn update_matches(&self, cx: &mut Context<Self>) {
202 let query = self.current_query(cx);
203
204 cx.spawn(async move |this, cx| Self::process_query(this, query, cx).await)
205 .detach();
206 }
207
208 async fn process_query(
209 this: WeakEntity<Self>,
210 query: String,
211 cx: &mut AsyncApp,
212 ) -> anyhow::Result<()> {
213 let query = command_palette::normalize_action_query(&query);
214 let (string_match_candidates, keybind_count) = this.read_with(cx, |this, _| {
215 (this.string_match_candidates.clone(), this.keybindings.len())
216 })?;
217 let executor = cx.background_executor().clone();
218 let mut matches = fuzzy::match_strings(
219 &string_match_candidates,
220 &query,
221 true,
222 true,
223 keybind_count,
224 &Default::default(),
225 executor,
226 )
227 .await;
228 this.update(cx, |this, cx| {
229 if query.is_empty() {
230 // apply default sort
231 // sorts by source precedence, and alphabetically by action name within each source
232 matches.sort_by_key(|match_item| {
233 let keybind = &this.keybindings[match_item.candidate_id];
234 let source = keybind.source.as_ref().map(|s| s.0);
235 use KeybindSource::*;
236 let source_precedence = match source {
237 Some(User) => 0,
238 Some(Vim) => 1,
239 Some(Base) => 2,
240 Some(Default) => 3,
241 None => 4,
242 };
243 return (source_precedence, keybind.action.as_ref());
244 });
245 }
246 this.selected_index.take();
247 this.scroll_to_item(0, ScrollStrategy::Top, cx);
248 this.matches = matches;
249 cx.notify();
250 })
251 }
252
253 fn process_bindings(
254 json_language: Arc<Language>,
255 rust_language: Arc<Language>,
256 cx: &mut App,
257 ) -> (Vec<ProcessedKeybinding>, Vec<StringMatchCandidate>) {
258 let key_bindings_ptr = cx.key_bindings();
259 let lock = key_bindings_ptr.borrow();
260 let key_bindings = lock.bindings();
261 let mut unmapped_action_names =
262 HashSet::from_iter(cx.all_action_names().into_iter().copied());
263 let action_documentation = cx.action_documentation();
264
265 let mut processed_bindings = Vec::new();
266 let mut string_match_candidates = Vec::new();
267
268 for key_binding in key_bindings {
269 let source = key_binding.meta().map(settings::KeybindSource::from_meta);
270
271 let keystroke_text = ui::text_for_keystrokes(key_binding.keystrokes(), cx);
272 let ui_key_binding = Some(
273 ui::KeyBinding::new_from_gpui(key_binding.clone(), cx)
274 .vim_mode(source == Some(settings::KeybindSource::Vim)),
275 );
276
277 let context = key_binding
278 .predicate()
279 .map(|predicate| {
280 KeybindContextString::Local(predicate.to_string().into(), rust_language.clone())
281 })
282 .unwrap_or(KeybindContextString::Global);
283
284 let source = source.map(|source| (source, source.name().into()));
285
286 let action_name = key_binding.action().name();
287 unmapped_action_names.remove(&action_name);
288 let action_input = key_binding
289 .action_input()
290 .map(|input| SyntaxHighlightedText::new(input, json_language.clone()));
291 let action_docs = action_documentation.get(action_name).copied();
292
293 let index = processed_bindings.len();
294 let string_match_candidate = StringMatchCandidate::new(index, &action_name);
295 processed_bindings.push(ProcessedKeybinding {
296 keystroke_text: keystroke_text.into(),
297 ui_key_binding,
298 action: action_name.into(),
299 action_input,
300 action_docs,
301 context: Some(context),
302 source,
303 });
304 string_match_candidates.push(string_match_candidate);
305 }
306
307 let empty = SharedString::new_static("");
308 for action_name in unmapped_action_names.into_iter() {
309 let index = processed_bindings.len();
310 let string_match_candidate = StringMatchCandidate::new(index, &action_name);
311 processed_bindings.push(ProcessedKeybinding {
312 keystroke_text: empty.clone(),
313 ui_key_binding: None,
314 action: action_name.into(),
315 action_input: None,
316 action_docs: action_documentation.get(action_name).copied(),
317 context: None,
318 source: None,
319 });
320 string_match_candidates.push(string_match_candidate);
321 }
322
323 (processed_bindings, string_match_candidates)
324 }
325
326 fn update_keybindings(&mut self, cx: &mut Context<KeymapEditor>) {
327 let workspace = self.workspace.clone();
328 cx.spawn(async move |this, cx| {
329 let json_language = Self::load_json_language(workspace.clone(), cx).await;
330 let rust_language = Self::load_rust_language(workspace.clone(), cx).await;
331
332 let query = this.update(cx, |this, cx| {
333 let (key_bindings, string_match_candidates) =
334 Self::process_bindings(json_language, rust_language, cx);
335 this.keybindings = key_bindings;
336 this.string_match_candidates = Arc::new(string_match_candidates);
337 this.matches = this
338 .string_match_candidates
339 .iter()
340 .enumerate()
341 .map(|(ix, candidate)| StringMatch {
342 candidate_id: ix,
343 score: 0.0,
344 positions: vec![],
345 string: candidate.string.clone(),
346 })
347 .collect();
348 this.current_query(cx)
349 })?;
350 // calls cx.notify
351 Self::process_query(this, query, cx).await
352 })
353 .detach_and_log_err(cx);
354 }
355
356 async fn load_json_language(
357 workspace: WeakEntity<Workspace>,
358 cx: &mut AsyncApp,
359 ) -> Arc<Language> {
360 let json_language_task = workspace
361 .read_with(cx, |workspace, cx| {
362 workspace
363 .project()
364 .read(cx)
365 .languages()
366 .language_for_name("JSON")
367 })
368 .context("Failed to load JSON language")
369 .log_err();
370 let json_language = match json_language_task {
371 Some(task) => task.await.context("Failed to load JSON language").log_err(),
372 None => None,
373 };
374 return json_language.unwrap_or_else(|| {
375 Arc::new(Language::new(
376 LanguageConfig {
377 name: "JSON".into(),
378 ..Default::default()
379 },
380 Some(tree_sitter_json::LANGUAGE.into()),
381 ))
382 });
383 }
384
385 async fn load_rust_language(
386 workspace: WeakEntity<Workspace>,
387 cx: &mut AsyncApp,
388 ) -> Arc<Language> {
389 let rust_language_task = workspace
390 .read_with(cx, |workspace, cx| {
391 workspace
392 .project()
393 .read(cx)
394 .languages()
395 .language_for_name("Rust")
396 })
397 .context("Failed to load Rust language")
398 .log_err();
399 let rust_language = match rust_language_task {
400 Some(task) => task.await.context("Failed to load Rust language").log_err(),
401 None => None,
402 };
403 return rust_language.unwrap_or_else(|| {
404 Arc::new(Language::new(
405 LanguageConfig {
406 name: "Rust".into(),
407 ..Default::default()
408 },
409 Some(tree_sitter_rust::LANGUAGE.into()),
410 ))
411 });
412 }
413
414 fn dispatch_context(&self, _window: &Window, _cx: &Context<Self>) -> KeyContext {
415 let mut dispatch_context = KeyContext::new_with_defaults();
416 dispatch_context.add("KeymapEditor");
417 dispatch_context.add("menu");
418
419 dispatch_context
420 }
421
422 fn scroll_to_item(&self, index: usize, strategy: ScrollStrategy, cx: &mut App) {
423 let index = usize::min(index, self.matches.len().saturating_sub(1));
424 self.table_interaction_state.update(cx, |this, _cx| {
425 this.scroll_handle.scroll_to_item(index, strategy);
426 });
427 }
428
429 fn focus_search(
430 &mut self,
431 _: &search::FocusSearch,
432 window: &mut Window,
433 cx: &mut Context<Self>,
434 ) {
435 if !self
436 .filter_editor
437 .focus_handle(cx)
438 .contains_focused(window, cx)
439 {
440 window.focus(&self.filter_editor.focus_handle(cx));
441 } else {
442 self.filter_editor.update(cx, |editor, cx| {
443 editor.select_all(&Default::default(), window, cx);
444 });
445 }
446 self.selected_index.take();
447 }
448
449 fn selected_binding(&self) -> Option<&ProcessedKeybinding> {
450 self.selected_index
451 .and_then(|match_index| self.matches.get(match_index))
452 .map(|r#match| r#match.candidate_id)
453 .and_then(|keybind_index| self.keybindings.get(keybind_index))
454 }
455
456 fn select_next(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) {
457 if let Some(selected) = self.selected_index {
458 let selected = selected + 1;
459 if selected >= self.matches.len() {
460 self.select_last(&Default::default(), window, cx);
461 } else {
462 self.selected_index = Some(selected);
463 self.scroll_to_item(selected, ScrollStrategy::Center, cx);
464 cx.notify();
465 }
466 } else {
467 self.select_first(&Default::default(), window, cx);
468 }
469 }
470
471 fn select_previous(
472 &mut self,
473 _: &menu::SelectPrevious,
474 window: &mut Window,
475 cx: &mut Context<Self>,
476 ) {
477 if let Some(selected) = self.selected_index {
478 if selected == 0 {
479 return;
480 }
481
482 let selected = selected - 1;
483
484 if selected >= self.matches.len() {
485 self.select_last(&Default::default(), window, cx);
486 } else {
487 self.selected_index = Some(selected);
488 self.scroll_to_item(selected, ScrollStrategy::Center, cx);
489 cx.notify();
490 }
491 } else {
492 self.select_last(&Default::default(), window, cx);
493 }
494 }
495
496 fn select_first(
497 &mut self,
498 _: &menu::SelectFirst,
499 _window: &mut Window,
500 cx: &mut Context<Self>,
501 ) {
502 if self.matches.get(0).is_some() {
503 self.selected_index = Some(0);
504 self.scroll_to_item(0, ScrollStrategy::Center, cx);
505 cx.notify();
506 }
507 }
508
509 fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
510 if self.matches.last().is_some() {
511 let index = self.matches.len() - 1;
512 self.selected_index = Some(index);
513 self.scroll_to_item(index, ScrollStrategy::Center, cx);
514 cx.notify();
515 }
516 }
517
518 fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
519 self.edit_selected_keybinding(window, cx);
520 }
521
522 fn edit_selected_keybinding(&mut self, window: &mut Window, cx: &mut Context<Self>) {
523 let Some(keybind) = self.selected_binding() else {
524 return;
525 };
526 self.workspace
527 .update(cx, |workspace, cx| {
528 let fs = workspace.app_state().fs.clone();
529 workspace.toggle_modal(window, cx, |window, cx| {
530 let modal = KeybindingEditorModal::new(keybind.clone(), fs, window, cx);
531 window.focus(&modal.focus_handle(cx));
532 modal
533 });
534 })
535 .log_err();
536 }
537
538 fn edit_binding(&mut self, _: &EditBinding, window: &mut Window, cx: &mut Context<Self>) {
539 self.edit_selected_keybinding(window, cx);
540 }
541
542 fn copy_context_to_clipboard(
543 &mut self,
544 _: &CopyContext,
545 _window: &mut Window,
546 cx: &mut Context<Self>,
547 ) {
548 let context = self
549 .selected_binding()
550 .and_then(|binding| binding.context.as_ref())
551 .and_then(KeybindContextString::local_str)
552 .map(|context| context.to_string());
553 let Some(context) = context else {
554 return;
555 };
556 cx.write_to_clipboard(gpui::ClipboardItem::new_string(context.clone()));
557 }
558
559 fn copy_action_to_clipboard(
560 &mut self,
561 _: &CopyAction,
562 _window: &mut Window,
563 cx: &mut Context<Self>,
564 ) {
565 let action = self
566 .selected_binding()
567 .map(|binding| binding.action.to_string());
568 let Some(action) = action else {
569 return;
570 };
571 cx.write_to_clipboard(gpui::ClipboardItem::new_string(action.clone()));
572 }
573}
574
575#[derive(Clone)]
576struct ProcessedKeybinding {
577 keystroke_text: SharedString,
578 ui_key_binding: Option<ui::KeyBinding>,
579 action: SharedString,
580 action_input: Option<SyntaxHighlightedText>,
581 action_docs: Option<&'static str>,
582 context: Option<KeybindContextString>,
583 source: Option<(KeybindSource, SharedString)>,
584}
585
586#[derive(Clone, Debug, IntoElement)]
587enum KeybindContextString {
588 Global,
589 Local(SharedString, Arc<Language>),
590}
591
592impl KeybindContextString {
593 const GLOBAL: SharedString = SharedString::new_static("<global>");
594
595 pub fn local(&self) -> Option<&SharedString> {
596 match self {
597 KeybindContextString::Global => None,
598 KeybindContextString::Local(name, _) => Some(name),
599 }
600 }
601
602 pub fn local_str(&self) -> Option<&str> {
603 match self {
604 KeybindContextString::Global => None,
605 KeybindContextString::Local(name, _) => Some(name),
606 }
607 }
608}
609
610impl RenderOnce for KeybindContextString {
611 fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
612 match self {
613 KeybindContextString::Global => StyledText::new(KeybindContextString::GLOBAL.clone())
614 .with_highlights([(
615 0..KeybindContextString::GLOBAL.len(),
616 gpui::HighlightStyle::color(_cx.theme().colors().text_muted),
617 )])
618 .into_any_element(),
619 KeybindContextString::Local(name, language) => {
620 SyntaxHighlightedText::new(name, language).into_any_element()
621 }
622 }
623 }
624}
625
626impl Item for KeymapEditor {
627 type Event = ();
628
629 fn tab_content_text(&self, _detail: usize, _cx: &App) -> ui::SharedString {
630 "Keymap Editor".into()
631 }
632}
633
634impl Render for KeymapEditor {
635 fn render(&mut self, window: &mut Window, cx: &mut ui::Context<Self>) -> impl ui::IntoElement {
636 let row_count = self.matches.len();
637 let theme = cx.theme();
638
639 v_flex()
640 .id("keymap-editor")
641 .track_focus(&self.focus_handle)
642 .key_context(self.dispatch_context(window, cx))
643 .on_action(cx.listener(Self::select_next))
644 .on_action(cx.listener(Self::select_previous))
645 .on_action(cx.listener(Self::select_first))
646 .on_action(cx.listener(Self::select_last))
647 .on_action(cx.listener(Self::focus_search))
648 .on_action(cx.listener(Self::confirm))
649 .on_action(cx.listener(Self::edit_binding))
650 .on_action(cx.listener(Self::copy_action_to_clipboard))
651 .on_action(cx.listener(Self::copy_context_to_clipboard))
652 .size_full()
653 .p_2()
654 .gap_1()
655 .bg(theme.colors().editor_background)
656 .child(
657 h_flex()
658 .key_context({
659 let mut context = KeyContext::new_with_defaults();
660 context.add("BufferSearchBar");
661 context
662 })
663 .h_8()
664 .pl_2()
665 .pr_1()
666 .py_1()
667 .border_1()
668 .border_color(theme.colors().border)
669 .rounded_lg()
670 .child(self.filter_editor.clone()),
671 )
672 .child(
673 Table::new()
674 .interactable(&self.table_interaction_state)
675 .striped()
676 .column_widths([rems(16.), rems(16.), rems(16.), rems(32.), rems(8.)])
677 .header(["Action", "Arguments", "Keystrokes", "Context", "Source"])
678 .uniform_list(
679 "keymap-editor-table",
680 row_count,
681 cx.processor(move |this, range: Range<usize>, _window, _cx| {
682 range
683 .filter_map(|index| {
684 let candidate_id = this.matches.get(index)?.candidate_id;
685 let binding = &this.keybindings[candidate_id];
686
687 let action = div()
688 .child(binding.action.clone())
689 .id(("keymap action", index))
690 .tooltip({
691 let action_name = binding.action.clone();
692 let action_docs = binding.action_docs;
693 move |_, cx| {
694 let action_tooltip = Tooltip::new(
695 command_palette::humanize_action_name(
696 &action_name,
697 ),
698 );
699 let action_tooltip = match action_docs {
700 Some(docs) => action_tooltip.meta(docs),
701 None => action_tooltip,
702 };
703 cx.new(|_| action_tooltip).into()
704 }
705 })
706 .into_any_element();
707 let keystrokes = binding.ui_key_binding.clone().map_or(
708 binding.keystroke_text.clone().into_any_element(),
709 IntoElement::into_any_element,
710 );
711 let action_input = binding
712 .action_input
713 .clone()
714 .map_or(gpui::Empty.into_any_element(), |input| {
715 input.into_any_element()
716 });
717 let context = binding
718 .context
719 .clone()
720 .map_or(gpui::Empty.into_any_element(), |context| {
721 context.into_any_element()
722 });
723 let source = binding
724 .source
725 .clone()
726 .map(|(_source, name)| name)
727 .unwrap_or_default()
728 .into_any_element();
729 Some([action, action_input, keystrokes, context, source])
730 })
731 .collect()
732 }),
733 )
734 .map_row(
735 cx.processor(|this, (row_index, row): (usize, Div), _window, cx| {
736 let is_selected = this.selected_index == Some(row_index);
737 let row = row
738 .id(("keymap-table-row", row_index))
739 .on_click(cx.listener(move |this, _event, _window, _cx| {
740 this.selected_index = Some(row_index);
741 }))
742 .border_2()
743 .border_color(transparent_black())
744 .when(is_selected, |row| {
745 row.border_color(cx.theme().colors().panel_focused_border)
746 });
747
748 right_click_menu(("keymap-table-row-menu", row_index))
749 .trigger({
750 let this = cx.weak_entity();
751 move |is_menu_open: bool, _window, cx| {
752 if is_menu_open {
753 this.update(cx, |this, cx| {
754 if this.selected_index != Some(row_index) {
755 this.selected_index = Some(row_index);
756 cx.notify();
757 }
758 })
759 .ok();
760 }
761 row
762 }
763 })
764 .menu({
765 let this = cx.weak_entity();
766 move |window, cx| build_keybind_context_menu(&this, window, cx)
767 })
768 .into_any_element()
769 }),
770 ),
771 )
772 }
773}
774
775#[derive(Debug, Clone, IntoElement)]
776struct SyntaxHighlightedText {
777 text: SharedString,
778 language: Arc<Language>,
779}
780
781impl SyntaxHighlightedText {
782 pub fn new(text: impl Into<SharedString>, language: Arc<Language>) -> Self {
783 Self {
784 text: text.into(),
785 language,
786 }
787 }
788}
789
790impl RenderOnce for SyntaxHighlightedText {
791 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
792 let text_style = window.text_style();
793 let syntax_theme = cx.theme().syntax();
794
795 let text = self.text.clone();
796
797 let highlights = self
798 .language
799 .highlight_text(&text.as_ref().into(), 0..text.len());
800 let mut runs = Vec::with_capacity(highlights.len());
801 let mut offset = 0;
802
803 for (highlight_range, highlight_id) in highlights {
804 // Add un-highlighted text before the current highlight
805 if highlight_range.start > offset {
806 runs.push(text_style.to_run(highlight_range.start - offset));
807 }
808
809 let mut run_style = text_style.clone();
810 if let Some(highlight_style) = highlight_id.style(syntax_theme) {
811 run_style = run_style.highlight(highlight_style);
812 }
813 // add the highlighted range
814 runs.push(run_style.to_run(highlight_range.len()));
815 offset = highlight_range.end;
816 }
817
818 // Add any remaining un-highlighted text
819 if offset < text.len() {
820 runs.push(text_style.to_run(text.len() - offset));
821 }
822
823 return StyledText::new(text).with_runs(runs);
824 }
825}
826
827struct KeybindingEditorModal {
828 editing_keybind: ProcessedKeybinding,
829 keybind_editor: Entity<KeystrokeInput>,
830 context_editor: Entity<Editor>,
831 fs: Arc<dyn Fs>,
832 error: Option<String>,
833}
834
835impl ModalView for KeybindingEditorModal {}
836
837impl EventEmitter<DismissEvent> for KeybindingEditorModal {}
838
839impl Focusable for KeybindingEditorModal {
840 fn focus_handle(&self, cx: &App) -> FocusHandle {
841 self.keybind_editor.focus_handle(cx)
842 }
843}
844
845impl KeybindingEditorModal {
846 pub fn new(
847 editing_keybind: ProcessedKeybinding,
848 fs: Arc<dyn Fs>,
849 window: &mut Window,
850 cx: &mut App,
851 ) -> Self {
852 let keybind_editor = cx.new(KeystrokeInput::new);
853
854 let context_editor = cx.new(|cx| {
855 let mut editor = Editor::single_line(window, cx);
856
857 if let Some(context) = editing_keybind
858 .context
859 .as_ref()
860 .and_then(KeybindContextString::local)
861 {
862 editor.set_text(context.clone(), window, cx);
863 } else {
864 editor.set_placeholder_text("Keybinding context", cx);
865 }
866
867 cx.spawn(async |editor, cx| {
868 let contexts = cx
869 .background_spawn(async { collect_contexts_from_assets() })
870 .await;
871
872 editor
873 .update(cx, |editor, _cx| {
874 editor.set_completion_provider(Some(std::rc::Rc::new(
875 KeyContextCompletionProvider { contexts },
876 )));
877 })
878 .context("Failed to load completions for keybinding context")
879 })
880 .detach_and_log_err(cx);
881
882 editor
883 });
884 Self {
885 editing_keybind,
886 fs,
887 keybind_editor,
888 context_editor,
889 error: None,
890 }
891 }
892
893 fn save(&mut self, cx: &mut Context<Self>) {
894 let existing_keybind = self.editing_keybind.clone();
895 let fs = self.fs.clone();
896 let new_keystrokes = self
897 .keybind_editor
898 .read_with(cx, |editor, _| editor.keystrokes().to_vec());
899 if new_keystrokes.is_empty() {
900 self.error = Some("Keystrokes cannot be empty".to_string());
901 cx.notify();
902 return;
903 }
904 let tab_size = cx.global::<settings::SettingsStore>().json_tab_size();
905 let new_context = self
906 .context_editor
907 .read_with(cx, |editor, cx| editor.text(cx));
908 let new_context = new_context.is_empty().not().then_some(new_context);
909 let new_context_err = new_context.as_deref().and_then(|context| {
910 gpui::KeyBindingContextPredicate::parse(context)
911 .context("Failed to parse key context")
912 .err()
913 });
914 if let Some(err) = new_context_err {
915 // TODO: store and display as separate error
916 // TODO: also, should be validating on keystroke
917 self.error = Some(err.to_string());
918 cx.notify();
919 return;
920 }
921
922 cx.spawn(async move |this, cx| {
923 if let Err(err) = save_keybinding_update(
924 existing_keybind,
925 &new_keystrokes,
926 new_context.as_deref(),
927 &fs,
928 tab_size,
929 )
930 .await
931 {
932 this.update(cx, |this, cx| {
933 this.error = Some(err.to_string());
934 cx.notify();
935 })
936 .log_err();
937 } else {
938 this.update(cx, |_this, cx| {
939 cx.emit(DismissEvent);
940 })
941 .ok();
942 }
943 })
944 .detach();
945 }
946}
947
948impl Render for KeybindingEditorModal {
949 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
950 let theme = cx.theme().colors();
951
952 return v_flex()
953 .w(rems(34.))
954 .elevation_3(cx)
955 .child(
956 v_flex()
957 .p_3()
958 .gap_2()
959 .child(
960 v_flex().child(Label::new("Edit Keystroke")).child(
961 Label::new("Input the desired keystroke for the selected action.")
962 .color(Color::Muted),
963 ),
964 )
965 .child(self.keybind_editor.clone()),
966 )
967 .child(
968 v_flex()
969 .p_3()
970 .gap_3()
971 .child(
972 v_flex().child(Label::new("Edit Keystroke")).child(
973 Label::new("Input the desired keystroke for the selected action.")
974 .color(Color::Muted),
975 ),
976 )
977 .child(
978 div()
979 .w_full()
980 .border_color(cx.theme().colors().border_variant)
981 .border_1()
982 .py_2()
983 .px_3()
984 .min_h_8()
985 .rounded_md()
986 .bg(theme.editor_background)
987 .child(self.context_editor.clone()),
988 ),
989 )
990 .child(
991 h_flex()
992 .p_2()
993 .w_full()
994 .gap_1()
995 .justify_end()
996 .border_t_1()
997 .border_color(cx.theme().colors().border_variant)
998 .child(
999 Button::new("cancel", "Cancel")
1000 .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
1001 )
1002 .child(
1003 Button::new("save-btn", "Save").on_click(
1004 cx.listener(|this, _event, _window, cx| Self::save(this, cx)),
1005 ),
1006 ),
1007 )
1008 .when_some(self.error.clone(), |this, error| {
1009 this.child(
1010 div()
1011 .bg(theme.background)
1012 .border_color(theme.border)
1013 .border_2()
1014 .rounded_md()
1015 .child(error),
1016 )
1017 });
1018 }
1019}
1020
1021struct KeyContextCompletionProvider {
1022 contexts: Vec<SharedString>,
1023}
1024
1025impl CompletionProvider for KeyContextCompletionProvider {
1026 fn completions(
1027 &self,
1028 _excerpt_id: editor::ExcerptId,
1029 buffer: &Entity<language::Buffer>,
1030 buffer_position: language::Anchor,
1031 _trigger: editor::CompletionContext,
1032 _window: &mut Window,
1033 cx: &mut Context<Editor>,
1034 ) -> gpui::Task<anyhow::Result<Vec<project::CompletionResponse>>> {
1035 let buffer = buffer.read(cx);
1036 let mut count_back = 0;
1037 for char in buffer.reversed_chars_at(buffer_position) {
1038 if char.is_ascii_alphanumeric() || char == '_' {
1039 count_back += 1;
1040 } else {
1041 break;
1042 }
1043 }
1044 let start_anchor = buffer.anchor_before(
1045 buffer_position
1046 .to_offset(&buffer)
1047 .saturating_sub(count_back),
1048 );
1049 let replace_range = start_anchor..buffer_position;
1050 gpui::Task::ready(Ok(vec![project::CompletionResponse {
1051 completions: self
1052 .contexts
1053 .iter()
1054 .map(|context| project::Completion {
1055 replace_range: replace_range.clone(),
1056 label: language::CodeLabel::plain(context.to_string(), None),
1057 new_text: context.to_string(),
1058 documentation: None,
1059 source: project::CompletionSource::Custom,
1060 icon_path: None,
1061 insert_text_mode: None,
1062 confirm: None,
1063 })
1064 .collect(),
1065 is_incomplete: false,
1066 }]))
1067 }
1068
1069 fn is_completion_trigger(
1070 &self,
1071 _buffer: &Entity<language::Buffer>,
1072 _position: language::Anchor,
1073 text: &str,
1074 _trigger_in_words: bool,
1075 _menu_is_open: bool,
1076 _cx: &mut Context<Editor>,
1077 ) -> bool {
1078 text.chars().last().map_or(false, |last_char| {
1079 last_char.is_ascii_alphanumeric() || last_char == '_'
1080 })
1081 }
1082}
1083
1084async fn save_keybinding_update(
1085 existing: ProcessedKeybinding,
1086 new_keystrokes: &[Keystroke],
1087 new_context: Option<&str>,
1088 fs: &Arc<dyn Fs>,
1089 tab_size: usize,
1090) -> anyhow::Result<()> {
1091 let keymap_contents = settings::KeymapFile::load_keymap_file(fs)
1092 .await
1093 .context("Failed to load keymap file")?;
1094
1095 let existing_keystrokes = existing
1096 .ui_key_binding
1097 .as_ref()
1098 .map(|keybinding| keybinding.keystrokes.as_slice())
1099 .unwrap_or_default();
1100
1101 let existing_context = existing
1102 .context
1103 .as_ref()
1104 .and_then(KeybindContextString::local_str);
1105
1106 let input = existing
1107 .action_input
1108 .as_ref()
1109 .map(|input| input.text.as_ref());
1110
1111 let operation = if existing.ui_key_binding.is_some() {
1112 settings::KeybindUpdateOperation::Replace {
1113 target: settings::KeybindUpdateTarget {
1114 context: existing_context,
1115 keystrokes: existing_keystrokes,
1116 action_name: &existing.action,
1117 use_key_equivalents: false,
1118 input,
1119 },
1120 target_keybind_source: existing
1121 .source
1122 .map(|(source, _name)| source)
1123 .unwrap_or(KeybindSource::User),
1124 source: settings::KeybindUpdateTarget {
1125 context: new_context,
1126 keystrokes: new_keystrokes,
1127 action_name: &existing.action,
1128 use_key_equivalents: false,
1129 input,
1130 },
1131 }
1132 } else {
1133 anyhow::bail!("Adding new bindings not implemented yet");
1134 };
1135 let updated_keymap_contents =
1136 settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size)
1137 .context("Failed to update keybinding")?;
1138 fs.atomic_write(paths::keymap_file().clone(), updated_keymap_contents)
1139 .await
1140 .context("Failed to write keymap file")?;
1141 Ok(())
1142}
1143
1144struct KeystrokeInput {
1145 keystrokes: Vec<Keystroke>,
1146 focus_handle: FocusHandle,
1147}
1148
1149impl KeystrokeInput {
1150 fn new(cx: &mut Context<Self>) -> Self {
1151 let focus_handle = cx.focus_handle();
1152 Self {
1153 keystrokes: Vec::new(),
1154 focus_handle,
1155 }
1156 }
1157
1158 fn on_modifiers_changed(
1159 &mut self,
1160 event: &ModifiersChangedEvent,
1161 _window: &mut Window,
1162 cx: &mut Context<Self>,
1163 ) {
1164 if let Some(last) = self.keystrokes.last_mut()
1165 && last.key.is_empty()
1166 {
1167 if !event.modifiers.modified() {
1168 self.keystrokes.pop();
1169 } else {
1170 last.modifiers = event.modifiers;
1171 }
1172 } else {
1173 self.keystrokes.push(Keystroke {
1174 modifiers: event.modifiers,
1175 key: "".to_string(),
1176 key_char: None,
1177 });
1178 }
1179 cx.stop_propagation();
1180 cx.notify();
1181 }
1182
1183 fn on_key_down(
1184 &mut self,
1185 event: &gpui::KeyDownEvent,
1186 _window: &mut Window,
1187 cx: &mut Context<Self>,
1188 ) {
1189 if event.is_held {
1190 return;
1191 }
1192 if let Some(last) = self.keystrokes.last_mut()
1193 && last.key.is_empty()
1194 {
1195 *last = event.keystroke.clone();
1196 } else {
1197 self.keystrokes.push(event.keystroke.clone());
1198 }
1199 cx.stop_propagation();
1200 cx.notify();
1201 }
1202
1203 fn on_key_up(
1204 &mut self,
1205 event: &gpui::KeyUpEvent,
1206 _window: &mut Window,
1207 cx: &mut Context<Self>,
1208 ) {
1209 if let Some(last) = self.keystrokes.last_mut()
1210 && !last.key.is_empty()
1211 && last.modifiers == event.keystroke.modifiers
1212 {
1213 self.keystrokes.push(Keystroke {
1214 modifiers: event.keystroke.modifiers,
1215 key: "".to_string(),
1216 key_char: None,
1217 });
1218 }
1219 cx.stop_propagation();
1220 cx.notify();
1221 }
1222
1223 fn keystrokes(&self) -> &[Keystroke] {
1224 if self
1225 .keystrokes
1226 .last()
1227 .map_or(false, |last| last.key.is_empty())
1228 {
1229 return &self.keystrokes[..self.keystrokes.len() - 1];
1230 }
1231 return &self.keystrokes;
1232 }
1233}
1234
1235impl Focusable for KeystrokeInput {
1236 fn focus_handle(&self, _cx: &App) -> FocusHandle {
1237 self.focus_handle.clone()
1238 }
1239}
1240
1241impl Render for KeystrokeInput {
1242 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1243 let colors = cx.theme().colors();
1244
1245 return h_flex()
1246 .id("keybinding_input")
1247 .track_focus(&self.focus_handle)
1248 .on_modifiers_changed(cx.listener(Self::on_modifiers_changed))
1249 .on_key_down(cx.listener(Self::on_key_down))
1250 .on_key_up(cx.listener(Self::on_key_up))
1251 .focus(|mut style| {
1252 style.border_color = Some(colors.border_focused);
1253 style
1254 })
1255 .py_2()
1256 .px_3()
1257 .gap_2()
1258 .min_h_8()
1259 .w_full()
1260 .justify_between()
1261 .bg(colors.editor_background)
1262 .border_1()
1263 .rounded_md()
1264 .flex_1()
1265 .overflow_hidden()
1266 .child(
1267 h_flex()
1268 .w_full()
1269 .min_w_0()
1270 .justify_center()
1271 .flex_wrap()
1272 .gap(ui::DynamicSpacing::Base04.rems(cx))
1273 .children(self.keystrokes.iter().map(|keystroke| {
1274 h_flex().children(ui::render_keystroke(
1275 keystroke,
1276 None,
1277 Some(rems(0.875).into()),
1278 ui::PlatformStyle::platform(),
1279 false,
1280 ))
1281 })),
1282 )
1283 .child(
1284 h_flex()
1285 .gap_0p5()
1286 .flex_none()
1287 .child(
1288 IconButton::new("backspace-btn", IconName::Delete)
1289 .tooltip(Tooltip::text("Delete Keystroke"))
1290 .on_click(cx.listener(|this, _event, _window, cx| {
1291 this.keystrokes.pop();
1292 cx.notify();
1293 })),
1294 )
1295 .child(
1296 IconButton::new("clear-btn", IconName::Eraser)
1297 .tooltip(Tooltip::text("Clear Keystrokes"))
1298 .on_click(cx.listener(|this, _event, _window, cx| {
1299 this.keystrokes.clear();
1300 cx.notify();
1301 })),
1302 ),
1303 );
1304 }
1305}
1306
1307fn build_keybind_context_menu(
1308 this: &WeakEntity<KeymapEditor>,
1309 window: &mut Window,
1310 cx: &mut App,
1311) -> Entity<ContextMenu> {
1312 ContextMenu::build(window, cx, |menu, _window, cx| {
1313 let Some(this) = this.upgrade() else {
1314 return menu;
1315 };
1316 let selected_binding = this.read_with(cx, |this, _cx| this.selected_binding().cloned());
1317 let Some(selected_binding) = selected_binding else {
1318 return menu;
1319 };
1320
1321 let selected_binding_has_context = selected_binding
1322 .context
1323 .as_ref()
1324 .and_then(KeybindContextString::local)
1325 .is_some();
1326
1327 menu.action("Edit Binding", Box::new(EditBinding))
1328 .action("Copy action", Box::new(CopyAction))
1329 .action_disabled_when(
1330 !selected_binding_has_context,
1331 "Copy Context",
1332 Box::new(CopyContext),
1333 )
1334 })
1335}
1336
1337fn collect_contexts_from_assets() -> Vec<SharedString> {
1338 let mut keymap_assets = vec![
1339 util::asset_str::<SettingsAssets>(settings::DEFAULT_KEYMAP_PATH),
1340 util::asset_str::<SettingsAssets>(settings::VIM_KEYMAP_PATH),
1341 ];
1342 keymap_assets.extend(
1343 BaseKeymap::OPTIONS
1344 .iter()
1345 .filter_map(|(_, base_keymap)| base_keymap.asset_path())
1346 .map(util::asset_str::<SettingsAssets>),
1347 );
1348
1349 let mut contexts = HashSet::default();
1350
1351 for keymap_asset in keymap_assets {
1352 let Ok(keymap) = KeymapFile::parse(&keymap_asset) else {
1353 continue;
1354 };
1355
1356 for section in keymap.sections() {
1357 let context_expr = §ion.context;
1358 let mut queue = Vec::new();
1359 let Ok(root_context) = gpui::KeyBindingContextPredicate::parse(context_expr) else {
1360 continue;
1361 };
1362
1363 queue.push(root_context);
1364 while let Some(context) = queue.pop() {
1365 match context {
1366 gpui::KeyBindingContextPredicate::Identifier(ident) => {
1367 contexts.insert(ident);
1368 }
1369 gpui::KeyBindingContextPredicate::Equal(ident_a, ident_b) => {
1370 contexts.insert(ident_a);
1371 contexts.insert(ident_b);
1372 }
1373 gpui::KeyBindingContextPredicate::NotEqual(ident_a, ident_b) => {
1374 contexts.insert(ident_a);
1375 contexts.insert(ident_b);
1376 }
1377 gpui::KeyBindingContextPredicate::Child(ctx_a, ctx_b) => {
1378 queue.push(*ctx_a);
1379 queue.push(*ctx_b);
1380 }
1381 gpui::KeyBindingContextPredicate::Not(ctx) => {
1382 queue.push(*ctx);
1383 }
1384 gpui::KeyBindingContextPredicate::And(ctx_a, ctx_b) => {
1385 queue.push(*ctx_a);
1386 queue.push(*ctx_b);
1387 }
1388 gpui::KeyBindingContextPredicate::Or(ctx_a, ctx_b) => {
1389 queue.push(*ctx_a);
1390 queue.push(*ctx_b);
1391 }
1392 }
1393 }
1394 }
1395 }
1396
1397 let mut contexts = contexts.into_iter().collect::<Vec<_>>();
1398 contexts.sort();
1399
1400 return contexts;
1401}
1402
1403impl SerializableItem for KeymapEditor {
1404 fn serialized_item_kind() -> &'static str {
1405 "KeymapEditor"
1406 }
1407
1408 fn cleanup(
1409 workspace_id: workspace::WorkspaceId,
1410 alive_items: Vec<workspace::ItemId>,
1411 _window: &mut Window,
1412 cx: &mut App,
1413 ) -> gpui::Task<gpui::Result<()>> {
1414 workspace::delete_unloaded_items(
1415 alive_items,
1416 workspace_id,
1417 "keybinding_editors",
1418 &KEYBINDING_EDITORS,
1419 cx,
1420 )
1421 }
1422
1423 fn deserialize(
1424 _project: Entity<project::Project>,
1425 workspace: WeakEntity<Workspace>,
1426 workspace_id: workspace::WorkspaceId,
1427 item_id: workspace::ItemId,
1428 window: &mut Window,
1429 cx: &mut App,
1430 ) -> gpui::Task<gpui::Result<Entity<Self>>> {
1431 window.spawn(cx, async move |cx| {
1432 if KEYBINDING_EDITORS
1433 .get_keybinding_editor(item_id, workspace_id)?
1434 .is_some()
1435 {
1436 cx.update(|window, cx| cx.new(|cx| KeymapEditor::new(workspace, window, cx)))
1437 } else {
1438 Err(anyhow!("No keybinding editor to deserialize"))
1439 }
1440 })
1441 }
1442
1443 fn serialize(
1444 &mut self,
1445 workspace: &mut Workspace,
1446 item_id: workspace::ItemId,
1447 _closing: bool,
1448 _window: &mut Window,
1449 cx: &mut ui::Context<Self>,
1450 ) -> Option<gpui::Task<gpui::Result<()>>> {
1451 let workspace_id = workspace.database_id()?;
1452 Some(cx.background_spawn(async move {
1453 KEYBINDING_EDITORS
1454 .save_keybinding_editor(item_id, workspace_id)
1455 .await
1456 }))
1457 }
1458
1459 fn should_serialize(&self, _event: &Self::Event) -> bool {
1460 false
1461 }
1462}
1463
1464mod persistence {
1465 use db::{define_connection, query, sqlez_macros::sql};
1466 use workspace::WorkspaceDb;
1467
1468 define_connection! {
1469 pub static ref KEYBINDING_EDITORS: KeybindingEditorDb<WorkspaceDb> =
1470 &[sql!(
1471 CREATE TABLE keybinding_editors (
1472 workspace_id INTEGER,
1473 item_id INTEGER UNIQUE,
1474
1475 PRIMARY KEY(workspace_id, item_id),
1476 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
1477 ON DELETE CASCADE
1478 ) STRICT;
1479 )];
1480 }
1481
1482 impl KeybindingEditorDb {
1483 query! {
1484 pub async fn save_keybinding_editor(
1485 item_id: workspace::ItemId,
1486 workspace_id: workspace::WorkspaceId
1487 ) -> Result<()> {
1488 INSERT OR REPLACE INTO keybinding_editors(item_id, workspace_id)
1489 VALUES (?, ?)
1490 }
1491 }
1492
1493 query! {
1494 pub fn get_keybinding_editor(
1495 item_id: workspace::ItemId,
1496 workspace_id: workspace::WorkspaceId
1497 ) -> Result<Option<workspace::ItemId>> {
1498 SELECT item_id
1499 FROM keybinding_editors
1500 WHERE item_id = ? AND workspace_id = ?
1501 }
1502 }
1503 }
1504}