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        cx: &mut App,
 253    ) -> (Vec<ProcessedKeybinding>, Vec<StringMatchCandidate>) {
 254        let key_bindings_ptr = cx.key_bindings();
 255        let lock = key_bindings_ptr.borrow();
 256        let key_bindings = lock.bindings();
 257        let mut unmapped_action_names = HashSet::from_iter(cx.all_action_names());
 258
 259        let mut processed_bindings = Vec::new();
 260        let mut string_match_candidates = Vec::new();
 261
 262        for key_binding in key_bindings {
 263            let source = key_binding.meta().map(settings::KeybindSource::from_meta);
 264
 265            let keystroke_text = ui::text_for_keystrokes(key_binding.keystrokes(), cx);
 266            let ui_key_binding = Some(
 267                ui::KeyBinding::new_from_gpui(key_binding.clone(), cx)
 268                    .vim_mode(source == Some(settings::KeybindSource::Vim)),
 269            );
 270
 271            let context = key_binding
 272                .predicate()
 273                .map(|predicate| KeybindContextString::Local(predicate.to_string().into()))
 274                .unwrap_or(KeybindContextString::Global);
 275
 276            let source = source.map(|source| (source, source.name().into()));
 277
 278            let action_name = key_binding.action().name();
 279            unmapped_action_names.remove(&action_name);
 280            let action_input = key_binding
 281                .action_input()
 282                .map(|input| SyntaxHighlightedText::new(input, json_language.clone()));
 283
 284            let index = processed_bindings.len();
 285            let string_match_candidate = StringMatchCandidate::new(index, &action_name);
 286            processed_bindings.push(ProcessedKeybinding {
 287                keystroke_text: keystroke_text.into(),
 288                ui_key_binding,
 289                action: action_name.into(),
 290                action_input,
 291                context: Some(context),
 292                source,
 293            });
 294            string_match_candidates.push(string_match_candidate);
 295        }
 296
 297        let empty = SharedString::new_static("");
 298        for action_name in unmapped_action_names.into_iter() {
 299            let index = processed_bindings.len();
 300            let string_match_candidate = StringMatchCandidate::new(index, &action_name);
 301            processed_bindings.push(ProcessedKeybinding {
 302                keystroke_text: empty.clone(),
 303                ui_key_binding: None,
 304                action: (*action_name).into(),
 305                action_input: None,
 306                context: None,
 307                source: None,
 308            });
 309            string_match_candidates.push(string_match_candidate);
 310        }
 311
 312        (processed_bindings, string_match_candidates)
 313    }
 314
 315    fn update_keybindings(&mut self, cx: &mut Context<KeymapEditor>) {
 316        let workspace = self.workspace.clone();
 317        cx.spawn(async move |this, cx| {
 318            let json_language = Self::load_json_language(workspace, cx).await;
 319
 320            let query = this.update(cx, |this, cx| {
 321                let (key_bindings, string_match_candidates) =
 322                    Self::process_bindings(json_language.clone(), cx);
 323                this.keybindings = key_bindings;
 324                this.string_match_candidates = Arc::new(string_match_candidates);
 325                this.matches = this
 326                    .string_match_candidates
 327                    .iter()
 328                    .enumerate()
 329                    .map(|(ix, candidate)| StringMatch {
 330                        candidate_id: ix,
 331                        score: 0.0,
 332                        positions: vec![],
 333                        string: candidate.string.clone(),
 334                    })
 335                    .collect();
 336                this.current_query(cx)
 337            })?;
 338            // calls cx.notify
 339            Self::process_query(this, query, cx).await
 340        })
 341        .detach_and_log_err(cx);
 342    }
 343
 344    async fn load_json_language(
 345        workspace: WeakEntity<Workspace>,
 346        cx: &mut AsyncApp,
 347    ) -> Arc<Language> {
 348        let json_language_task = workspace
 349            .read_with(cx, |workspace, cx| {
 350                workspace
 351                    .project()
 352                    .read(cx)
 353                    .languages()
 354                    .language_for_name("JSON")
 355            })
 356            .context("Failed to load JSON language")
 357            .log_err();
 358        let json_language = match json_language_task {
 359            Some(task) => task.await.context("Failed to load JSON language").log_err(),
 360            None => None,
 361        };
 362        return json_language.unwrap_or_else(|| {
 363            Arc::new(Language::new(
 364                LanguageConfig {
 365                    name: "JSON".into(),
 366                    ..Default::default()
 367                },
 368                Some(tree_sitter_json::LANGUAGE.into()),
 369            ))
 370        });
 371    }
 372
 373    fn dispatch_context(&self, _window: &Window, _cx: &Context<Self>) -> KeyContext {
 374        let mut dispatch_context = KeyContext::new_with_defaults();
 375        dispatch_context.add("KeymapEditor");
 376        dispatch_context.add("menu");
 377
 378        dispatch_context
 379    }
 380
 381    fn scroll_to_item(&self, index: usize, strategy: ScrollStrategy, cx: &mut App) {
 382        let index = usize::min(index, self.matches.len().saturating_sub(1));
 383        self.table_interaction_state.update(cx, |this, _cx| {
 384            this.scroll_handle.scroll_to_item(index, strategy);
 385        });
 386    }
 387
 388    fn focus_search(
 389        &mut self,
 390        _: &search::FocusSearch,
 391        window: &mut Window,
 392        cx: &mut Context<Self>,
 393    ) {
 394        if !self
 395            .filter_editor
 396            .focus_handle(cx)
 397            .contains_focused(window, cx)
 398        {
 399            window.focus(&self.filter_editor.focus_handle(cx));
 400        } else {
 401            self.filter_editor.update(cx, |editor, cx| {
 402                editor.select_all(&Default::default(), window, cx);
 403            });
 404        }
 405        self.selected_index.take();
 406    }
 407
 408    fn selected_binding(&self) -> Option<&ProcessedKeybinding> {
 409        self.selected_index
 410            .and_then(|match_index| self.matches.get(match_index))
 411            .map(|r#match| r#match.candidate_id)
 412            .and_then(|keybind_index| self.keybindings.get(keybind_index))
 413    }
 414
 415    fn select_next(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) {
 416        if let Some(selected) = self.selected_index {
 417            let selected = selected + 1;
 418            if selected >= self.matches.len() {
 419                self.select_last(&Default::default(), window, cx);
 420            } else {
 421                self.selected_index = Some(selected);
 422                self.scroll_to_item(selected, ScrollStrategy::Center, cx);
 423                cx.notify();
 424            }
 425        } else {
 426            self.select_first(&Default::default(), window, cx);
 427        }
 428    }
 429
 430    fn select_previous(
 431        &mut self,
 432        _: &menu::SelectPrevious,
 433        window: &mut Window,
 434        cx: &mut Context<Self>,
 435    ) {
 436        if let Some(selected) = self.selected_index {
 437            if selected == 0 {
 438                return;
 439            }
 440
 441            let selected = selected - 1;
 442
 443            if selected >= self.matches.len() {
 444                self.select_last(&Default::default(), window, cx);
 445            } else {
 446                self.selected_index = Some(selected);
 447                self.scroll_to_item(selected, ScrollStrategy::Center, cx);
 448                cx.notify();
 449            }
 450        } else {
 451            self.select_last(&Default::default(), window, cx);
 452        }
 453    }
 454
 455    fn select_first(
 456        &mut self,
 457        _: &menu::SelectFirst,
 458        _window: &mut Window,
 459        cx: &mut Context<Self>,
 460    ) {
 461        if self.matches.get(0).is_some() {
 462            self.selected_index = Some(0);
 463            self.scroll_to_item(0, ScrollStrategy::Center, cx);
 464            cx.notify();
 465        }
 466    }
 467
 468    fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
 469        if self.matches.last().is_some() {
 470            let index = self.matches.len() - 1;
 471            self.selected_index = Some(index);
 472            self.scroll_to_item(index, ScrollStrategy::Center, cx);
 473            cx.notify();
 474        }
 475    }
 476
 477    fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
 478        self.edit_selected_keybinding(window, cx);
 479    }
 480
 481    fn edit_selected_keybinding(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 482        let Some(keybind) = self.selected_binding() else {
 483            return;
 484        };
 485        self.workspace
 486            .update(cx, |workspace, cx| {
 487                let fs = workspace.app_state().fs.clone();
 488                workspace.toggle_modal(window, cx, |window, cx| {
 489                    let modal = KeybindingEditorModal::new(keybind.clone(), fs, window, cx);
 490                    window.focus(&modal.focus_handle(cx));
 491                    modal
 492                });
 493            })
 494            .log_err();
 495    }
 496
 497    fn edit_binding(&mut self, _: &EditBinding, window: &mut Window, cx: &mut Context<Self>) {
 498        self.edit_selected_keybinding(window, cx);
 499    }
 500
 501    fn copy_context_to_clipboard(
 502        &mut self,
 503        _: &CopyContext,
 504        _window: &mut Window,
 505        cx: &mut Context<Self>,
 506    ) {
 507        let context = self
 508            .selected_binding()
 509            .and_then(|binding| binding.context.as_ref())
 510            .and_then(KeybindContextString::local_str)
 511            .map(|context| context.to_string());
 512        let Some(context) = context else {
 513            return;
 514        };
 515        cx.write_to_clipboard(gpui::ClipboardItem::new_string(context.clone()));
 516    }
 517
 518    fn copy_action_to_clipboard(
 519        &mut self,
 520        _: &CopyAction,
 521        _window: &mut Window,
 522        cx: &mut Context<Self>,
 523    ) {
 524        let action = self
 525            .selected_binding()
 526            .map(|binding| binding.action.to_string());
 527        let Some(action) = action else {
 528            return;
 529        };
 530        cx.write_to_clipboard(gpui::ClipboardItem::new_string(action.clone()));
 531    }
 532}
 533
 534#[derive(Clone)]
 535struct ProcessedKeybinding {
 536    keystroke_text: SharedString,
 537    ui_key_binding: Option<ui::KeyBinding>,
 538    action: SharedString,
 539    action_input: Option<SyntaxHighlightedText>,
 540    context: Option<KeybindContextString>,
 541    source: Option<(KeybindSource, SharedString)>,
 542}
 543
 544#[derive(Clone, Debug, IntoElement)]
 545enum KeybindContextString {
 546    Global,
 547    Local(SharedString),
 548}
 549
 550impl KeybindContextString {
 551    const GLOBAL: SharedString = SharedString::new_static("<global>");
 552
 553    pub fn local(&self) -> Option<&SharedString> {
 554        match self {
 555            KeybindContextString::Global => None,
 556            KeybindContextString::Local(name) => Some(name),
 557        }
 558    }
 559
 560    pub fn local_str(&self) -> Option<&str> {
 561        match self {
 562            KeybindContextString::Global => None,
 563            KeybindContextString::Local(name) => Some(name),
 564        }
 565    }
 566}
 567
 568impl RenderOnce for KeybindContextString {
 569    fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
 570        match self {
 571            KeybindContextString::Global => KeybindContextString::GLOBAL.clone(),
 572            KeybindContextString::Local(name) => name,
 573        }
 574    }
 575}
 576
 577impl Item for KeymapEditor {
 578    type Event = ();
 579
 580    fn tab_content_text(&self, _detail: usize, _cx: &App) -> ui::SharedString {
 581        "Keymap Editor".into()
 582    }
 583}
 584
 585impl Render for KeymapEditor {
 586    fn render(&mut self, window: &mut Window, cx: &mut ui::Context<Self>) -> impl ui::IntoElement {
 587        let row_count = self.matches.len();
 588        let theme = cx.theme();
 589
 590        v_flex()
 591            .key_context(self.dispatch_context(window, cx))
 592            .on_action(cx.listener(Self::select_next))
 593            .on_action(cx.listener(Self::select_previous))
 594            .on_action(cx.listener(Self::select_first))
 595            .on_action(cx.listener(Self::select_last))
 596            .on_action(cx.listener(Self::focus_search))
 597            .on_action(cx.listener(Self::confirm))
 598            .on_action(cx.listener(Self::edit_binding))
 599            .on_action(cx.listener(Self::copy_action_to_clipboard))
 600            .on_action(cx.listener(Self::copy_context_to_clipboard))
 601            .size_full()
 602            .bg(theme.colors().editor_background)
 603            .id("keymap-editor")
 604            .track_focus(&self.focus_handle)
 605            .pt_4()
 606            .px_4()
 607            .gap_4()
 608            .child(
 609                h_flex()
 610                    .key_context({
 611                        let mut context = KeyContext::new_with_defaults();
 612                        context.add("BufferSearchBar");
 613                        context
 614                    })
 615                    .h_8()
 616                    .pl_2()
 617                    .pr_1()
 618                    .py_1()
 619                    .border_1()
 620                    .border_color(theme.colors().border)
 621                    .rounded_lg()
 622                    .child(self.filter_editor.clone()),
 623            )
 624            .child(
 625                Table::new()
 626                    .interactable(&self.table_interaction_state)
 627                    .striped()
 628                    .column_widths([rems(16.), rems(16.), rems(16.), rems(32.), rems(8.)])
 629                    .header(["Action", "Arguments", "Keystrokes", "Context", "Source"])
 630                    .uniform_list(
 631                        "keymap-editor-table",
 632                        row_count,
 633                        cx.processor(move |this, range: Range<usize>, _window, _cx| {
 634                            range
 635                                .filter_map(|index| {
 636                                    let candidate_id = this.matches.get(index)?.candidate_id;
 637                                    let binding = &this.keybindings[candidate_id];
 638
 639                                    let action = binding.action.clone().into_any_element();
 640                                    let keystrokes = binding.ui_key_binding.clone().map_or(
 641                                        binding.keystroke_text.clone().into_any_element(),
 642                                        IntoElement::into_any_element,
 643                                    );
 644                                    let action_input = binding
 645                                        .action_input
 646                                        .clone()
 647                                        .map_or(gpui::Empty.into_any_element(), |input| {
 648                                            input.into_any_element()
 649                                        });
 650                                    let context = binding
 651                                        .context
 652                                        .clone()
 653                                        .map_or(gpui::Empty.into_any_element(), |context| {
 654                                            context.into_any_element()
 655                                        });
 656                                    let source = binding
 657                                        .source
 658                                        .clone()
 659                                        .map(|(_source, name)| name)
 660                                        .unwrap_or_default()
 661                                        .into_any_element();
 662                                    Some([action, action_input, keystrokes, context, source])
 663                                })
 664                                .collect()
 665                        }),
 666                    )
 667                    .map_row(
 668                        cx.processor(|this, (row_index, row): (usize, Div), _window, cx| {
 669                            let is_selected = this.selected_index == Some(row_index);
 670                            let row = row
 671                                .id(("keymap-table-row", row_index))
 672                                .on_click(cx.listener(move |this, _event, _window, _cx| {
 673                                    this.selected_index = Some(row_index);
 674                                }))
 675                                .border_2()
 676                                .border_color(transparent_black())
 677                                .when(is_selected, |row| {
 678                                    row.border_color(cx.theme().colors().panel_focused_border)
 679                                });
 680
 681                            right_click_menu(("keymap-table-row-menu", row_index))
 682                                .trigger({
 683                                    let this = cx.weak_entity();
 684                                    move |is_menu_open: bool, _window, cx| {
 685                                        if is_menu_open {
 686                                            this.update(cx, |this, cx| {
 687                                                if this.selected_index != Some(row_index) {
 688                                                    this.selected_index = Some(row_index);
 689                                                    cx.notify();
 690                                                }
 691                                            })
 692                                            .ok();
 693                                        }
 694                                        row
 695                                    }
 696                                })
 697                                .menu({
 698                                    let this = cx.weak_entity();
 699                                    move |window, cx| build_keybind_context_menu(&this, window, cx)
 700                                })
 701                                .into_any_element()
 702                        }),
 703                    ),
 704            )
 705    }
 706}
 707
 708#[derive(Debug, Clone, IntoElement)]
 709struct SyntaxHighlightedText {
 710    text: SharedString,
 711    language: Arc<Language>,
 712}
 713
 714impl SyntaxHighlightedText {
 715    pub fn new(text: impl Into<SharedString>, language: Arc<Language>) -> Self {
 716        Self {
 717            text: text.into(),
 718            language,
 719        }
 720    }
 721}
 722
 723impl RenderOnce for SyntaxHighlightedText {
 724    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
 725        let text_style = window.text_style();
 726        let syntax_theme = cx.theme().syntax();
 727
 728        let text = self.text.clone();
 729
 730        let highlights = self
 731            .language
 732            .highlight_text(&text.as_ref().into(), 0..text.len());
 733        let mut runs = Vec::with_capacity(highlights.len());
 734        let mut offset = 0;
 735
 736        for (highlight_range, highlight_id) in highlights {
 737            // Add un-highlighted text before the current highlight
 738            if highlight_range.start > offset {
 739                runs.push(text_style.to_run(highlight_range.start - offset));
 740            }
 741
 742            let mut run_style = text_style.clone();
 743            if let Some(highlight_style) = highlight_id.style(syntax_theme) {
 744                run_style = run_style.highlight(highlight_style);
 745            }
 746            // add the highlighted range
 747            runs.push(run_style.to_run(highlight_range.len()));
 748            offset = highlight_range.end;
 749        }
 750
 751        // Add any remaining un-highlighted text
 752        if offset < text.len() {
 753            runs.push(text_style.to_run(text.len() - offset));
 754        }
 755
 756        return StyledText::new(text).with_runs(runs);
 757    }
 758}
 759
 760struct KeybindingEditorModal {
 761    editing_keybind: ProcessedKeybinding,
 762    keybind_editor: Entity<KeystrokeInput>,
 763    fs: Arc<dyn Fs>,
 764    error: Option<String>,
 765}
 766
 767impl ModalView for KeybindingEditorModal {}
 768
 769impl EventEmitter<DismissEvent> for KeybindingEditorModal {}
 770
 771impl Focusable for KeybindingEditorModal {
 772    fn focus_handle(&self, cx: &App) -> FocusHandle {
 773        self.keybind_editor.focus_handle(cx)
 774    }
 775}
 776
 777impl KeybindingEditorModal {
 778    pub fn new(
 779        editing_keybind: ProcessedKeybinding,
 780        fs: Arc<dyn Fs>,
 781        _window: &mut Window,
 782        cx: &mut App,
 783    ) -> Self {
 784        let keybind_editor = cx.new(KeystrokeInput::new);
 785        Self {
 786            editing_keybind,
 787            fs,
 788            keybind_editor,
 789            error: None,
 790        }
 791    }
 792}
 793
 794impl Render for KeybindingEditorModal {
 795    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 796        let theme = cx.theme().colors();
 797
 798        return v_flex()
 799            .w(rems(36.))
 800            .elevation_3(cx)
 801            .child(
 802                v_flex()
 803                    .pt_2()
 804                    .px_4()
 805                    .pb_4()
 806                    .gap_2()
 807                    .child(Label::new("Input desired keystroke, then hit save"))
 808                    .child(self.keybind_editor.clone()),
 809            )
 810            .child(
 811                h_flex()
 812                    .p_2()
 813                    .w_full()
 814                    .gap_1()
 815                    .justify_end()
 816                    .border_t_1()
 817                    .border_color(cx.theme().colors().border_variant)
 818                    .child(
 819                        Button::new("cancel", "Cancel")
 820                            .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
 821                    )
 822                    .child(
 823                        Button::new("save-btn", "Save Keybinding").on_click(cx.listener(
 824                            |this, _event, _window, cx| {
 825                                let existing_keybind = this.editing_keybind.clone();
 826                                let fs = this.fs.clone();
 827                                let new_keystrokes = this
 828                                    .keybind_editor
 829                                    .read_with(cx, |editor, _| editor.keystrokes.clone());
 830                                if new_keystrokes.is_empty() {
 831                                    this.error = Some("Keystrokes cannot be empty".to_string());
 832                                    cx.notify();
 833                                    return;
 834                                }
 835                                let tab_size =
 836                                    cx.global::<settings::SettingsStore>().json_tab_size();
 837                                cx.spawn(async move |this, cx| {
 838                                    if let Err(err) = save_keybinding_update(
 839                                        existing_keybind,
 840                                        &new_keystrokes,
 841                                        &fs,
 842                                        tab_size,
 843                                    )
 844                                    .await
 845                                    {
 846                                        this.update(cx, |this, cx| {
 847                                            this.error = Some(err.to_string());
 848                                            cx.notify();
 849                                        })
 850                                        .log_err();
 851                                    }
 852                                })
 853                                .detach();
 854                            },
 855                        )),
 856                    ),
 857            )
 858            .when_some(self.error.clone(), |this, error| {
 859                this.child(
 860                    div()
 861                        .bg(theme.background)
 862                        .border_color(theme.border)
 863                        .border_2()
 864                        .rounded_md()
 865                        .child(error),
 866                )
 867            });
 868    }
 869}
 870
 871async fn save_keybinding_update(
 872    existing: ProcessedKeybinding,
 873    new_keystrokes: &[Keystroke],
 874    fs: &Arc<dyn Fs>,
 875    tab_size: usize,
 876) -> anyhow::Result<()> {
 877    let keymap_contents = settings::KeymapFile::load_keymap_file(fs)
 878        .await
 879        .context("Failed to load keymap file")?;
 880
 881    let existing_keystrokes = existing
 882        .ui_key_binding
 883        .as_ref()
 884        .map(|keybinding| keybinding.keystrokes.as_slice())
 885        .unwrap_or_default();
 886
 887    let context = existing
 888        .context
 889        .as_ref()
 890        .and_then(KeybindContextString::local_str);
 891
 892    let input = existing
 893        .action_input
 894        .as_ref()
 895        .map(|input| input.text.as_ref());
 896
 897    let operation = if existing.ui_key_binding.is_some() {
 898        settings::KeybindUpdateOperation::Replace {
 899            target: settings::KeybindUpdateTarget {
 900                context,
 901                keystrokes: existing_keystrokes,
 902                action_name: &existing.action,
 903                use_key_equivalents: false,
 904                input,
 905            },
 906            target_source: existing
 907                .source
 908                .map(|(source, _name)| source)
 909                .unwrap_or(KeybindSource::User),
 910            source: settings::KeybindUpdateTarget {
 911                context,
 912                keystrokes: new_keystrokes,
 913                action_name: &existing.action,
 914                use_key_equivalents: false,
 915                input,
 916            },
 917        }
 918    } else {
 919        anyhow::bail!("Adding new bindings not implemented yet");
 920    };
 921    let updated_keymap_contents =
 922        settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size)
 923            .context("Failed to update keybinding")?;
 924    fs.atomic_write(paths::keymap_file().clone(), updated_keymap_contents)
 925        .await
 926        .context("Failed to write keymap file")?;
 927    Ok(())
 928}
 929
 930struct KeystrokeInput {
 931    keystrokes: Vec<Keystroke>,
 932    focus_handle: FocusHandle,
 933}
 934
 935impl KeystrokeInput {
 936    fn new(cx: &mut Context<Self>) -> Self {
 937        let focus_handle = cx.focus_handle();
 938        Self {
 939            keystrokes: Vec::new(),
 940            focus_handle,
 941        }
 942    }
 943
 944    fn on_modifiers_changed(
 945        &mut self,
 946        event: &ModifiersChangedEvent,
 947        _window: &mut Window,
 948        cx: &mut Context<Self>,
 949    ) {
 950        if let Some(last) = self.keystrokes.last_mut()
 951            && last.key.is_empty()
 952        {
 953            if !event.modifiers.modified() {
 954                self.keystrokes.pop();
 955            } else {
 956                last.modifiers = event.modifiers;
 957            }
 958        } else {
 959            self.keystrokes.push(Keystroke {
 960                modifiers: event.modifiers,
 961                key: "".to_string(),
 962                key_char: None,
 963            });
 964        }
 965        cx.stop_propagation();
 966        cx.notify();
 967    }
 968
 969    fn on_key_down(
 970        &mut self,
 971        event: &gpui::KeyDownEvent,
 972        _window: &mut Window,
 973        cx: &mut Context<Self>,
 974    ) {
 975        if event.is_held {
 976            return;
 977        }
 978        if let Some(last) = self.keystrokes.last_mut()
 979            && last.key.is_empty()
 980        {
 981            *last = event.keystroke.clone();
 982        } else {
 983            self.keystrokes.push(event.keystroke.clone());
 984        }
 985        cx.stop_propagation();
 986        cx.notify();
 987    }
 988
 989    fn on_key_up(
 990        &mut self,
 991        event: &gpui::KeyUpEvent,
 992        _window: &mut Window,
 993        cx: &mut Context<Self>,
 994    ) {
 995        if let Some(last) = self.keystrokes.last_mut()
 996            && !last.key.is_empty()
 997            && last.modifiers == event.keystroke.modifiers
 998        {
 999            self.keystrokes.push(Keystroke {
1000                modifiers: event.keystroke.modifiers,
1001                key: "".to_string(),
1002                key_char: None,
1003            });
1004        }
1005        cx.stop_propagation();
1006        cx.notify();
1007    }
1008}
1009
1010impl Focusable for KeystrokeInput {
1011    fn focus_handle(&self, _cx: &App) -> FocusHandle {
1012        self.focus_handle.clone()
1013    }
1014}
1015
1016impl Render for KeystrokeInput {
1017    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1018        let colors = cx.theme().colors();
1019
1020        return h_flex()
1021            .id("keybinding_input")
1022            .track_focus(&self.focus_handle)
1023            .on_modifiers_changed(cx.listener(Self::on_modifiers_changed))
1024            .on_key_down(cx.listener(Self::on_key_down))
1025            .on_key_up(cx.listener(Self::on_key_up))
1026            .focus(|mut style| {
1027                style.border_color = Some(colors.border_focused);
1028                style
1029            })
1030            .py_2()
1031            .px_3()
1032            .gap_2()
1033            .min_h_8()
1034            .w_full()
1035            .justify_between()
1036            .bg(colors.editor_background)
1037            .border_1()
1038            .rounded_md()
1039            .flex_1()
1040            .overflow_hidden()
1041            .child(
1042                h_flex()
1043                    .w_full()
1044                    .min_w_0()
1045                    .justify_center()
1046                    .flex_wrap()
1047                    .gap(ui::DynamicSpacing::Base04.rems(cx))
1048                    .children(self.keystrokes.iter().map(|keystroke| {
1049                        h_flex().children(ui::render_keystroke(
1050                            keystroke,
1051                            None,
1052                            Some(rems(0.875).into()),
1053                            ui::PlatformStyle::platform(),
1054                            false,
1055                        ))
1056                    })),
1057            )
1058            .child(
1059                h_flex()
1060                    .gap_0p5()
1061                    .flex_none()
1062                    .child(
1063                        IconButton::new("backspace-btn", IconName::Delete)
1064                            .tooltip(Tooltip::text("Delete Keystroke"))
1065                            .on_click(cx.listener(|this, _event, _window, cx| {
1066                                this.keystrokes.pop();
1067                                cx.notify();
1068                            })),
1069                    )
1070                    .child(
1071                        IconButton::new("clear-btn", IconName::Eraser)
1072                            .tooltip(Tooltip::text("Clear Keystrokes"))
1073                            .on_click(cx.listener(|this, _event, _window, cx| {
1074                                this.keystrokes.clear();
1075                                cx.notify();
1076                            })),
1077                    ),
1078            );
1079    }
1080}
1081
1082fn build_keybind_context_menu(
1083    this: &WeakEntity<KeymapEditor>,
1084    window: &mut Window,
1085    cx: &mut App,
1086) -> Entity<ContextMenu> {
1087    ContextMenu::build(window, cx, |menu, _window, cx| {
1088        let Some(this) = this.upgrade() else {
1089            return menu;
1090        };
1091        let selected_binding = this.read_with(cx, |this, _cx| this.selected_binding().cloned());
1092        let Some(selected_binding) = selected_binding else {
1093            return menu;
1094        };
1095
1096        let selected_binding_has_context = selected_binding
1097            .context
1098            .as_ref()
1099            .and_then(KeybindContextString::local)
1100            .is_some();
1101
1102        menu.action("Edit Binding", Box::new(EditBinding))
1103            .action("Copy action", Box::new(CopyAction))
1104            .action_disabled_when(
1105                !selected_binding_has_context,
1106                "Copy Context",
1107                Box::new(CopyContext),
1108            )
1109    })
1110}
1111
1112impl SerializableItem for KeymapEditor {
1113    fn serialized_item_kind() -> &'static str {
1114        "KeymapEditor"
1115    }
1116
1117    fn cleanup(
1118        workspace_id: workspace::WorkspaceId,
1119        alive_items: Vec<workspace::ItemId>,
1120        _window: &mut Window,
1121        cx: &mut App,
1122    ) -> gpui::Task<gpui::Result<()>> {
1123        workspace::delete_unloaded_items(
1124            alive_items,
1125            workspace_id,
1126            "keybinding_editors",
1127            &KEYBINDING_EDITORS,
1128            cx,
1129        )
1130    }
1131
1132    fn deserialize(
1133        _project: Entity<project::Project>,
1134        workspace: WeakEntity<Workspace>,
1135        workspace_id: workspace::WorkspaceId,
1136        item_id: workspace::ItemId,
1137        window: &mut Window,
1138        cx: &mut App,
1139    ) -> gpui::Task<gpui::Result<Entity<Self>>> {
1140        window.spawn(cx, async move |cx| {
1141            if KEYBINDING_EDITORS
1142                .get_keybinding_editor(item_id, workspace_id)?
1143                .is_some()
1144            {
1145                cx.update(|window, cx| cx.new(|cx| KeymapEditor::new(workspace, window, cx)))
1146            } else {
1147                Err(anyhow!("No keybinding editor to deserialize"))
1148            }
1149        })
1150    }
1151
1152    fn serialize(
1153        &mut self,
1154        workspace: &mut Workspace,
1155        item_id: workspace::ItemId,
1156        _closing: bool,
1157        _window: &mut Window,
1158        cx: &mut ui::Context<Self>,
1159    ) -> Option<gpui::Task<gpui::Result<()>>> {
1160        let workspace_id = workspace.database_id()?;
1161        Some(cx.background_spawn(async move {
1162            KEYBINDING_EDITORS
1163                .save_keybinding_editor(item_id, workspace_id)
1164                .await
1165        }))
1166    }
1167
1168    fn should_serialize(&self, _event: &Self::Event) -> bool {
1169        false
1170    }
1171}
1172
1173mod persistence {
1174    use db::{define_connection, query, sqlez_macros::sql};
1175    use workspace::WorkspaceDb;
1176
1177    define_connection! {
1178        pub static ref KEYBINDING_EDITORS: KeybindingEditorDb<WorkspaceDb> =
1179            &[sql!(
1180                CREATE TABLE keybinding_editors (
1181                    workspace_id INTEGER,
1182                    item_id INTEGER UNIQUE,
1183
1184                    PRIMARY KEY(workspace_id, item_id),
1185                    FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
1186                    ON DELETE CASCADE
1187                ) STRICT;
1188            )];
1189    }
1190
1191    impl KeybindingEditorDb {
1192        query! {
1193            pub async fn save_keybinding_editor(
1194                item_id: workspace::ItemId,
1195                workspace_id: workspace::WorkspaceId
1196            ) -> Result<()> {
1197                INSERT OR REPLACE INTO keybinding_editors(item_id, workspace_id)
1198                VALUES (?, ?)
1199            }
1200        }
1201
1202        query! {
1203            pub fn get_keybinding_editor(
1204                item_id: workspace::ItemId,
1205                workspace_id: workspace::WorkspaceId
1206            ) -> Result<Option<workspace::ItemId>> {
1207                SELECT item_id
1208                FROM keybinding_editors
1209                WHERE item_id = ? AND workspace_id = ?
1210            }
1211        }
1212    }
1213}