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