keybindings.rs

   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}