3139 lines · 116.7 KB · Rust
RawDownload
use std::{    cmp::{self},    ops::{Not as _, Range},    sync::Arc,    time::Duration,}; use anyhow::{Context as _, anyhow};use collections::{HashMap, HashSet};use editor::{CompletionProvider, Editor, EditorEvent};use fs::Fs;use fuzzy::{StringMatch, StringMatchCandidate};use gpui::{    Action, AppContext as _, AsyncApp, Axis, ClickEvent, Context, DismissEvent, Entity,    EventEmitter, FocusHandle, Focusable, Global, IsZero, KeyContext, Keystroke, MouseButton,    Point, ScrollStrategy, ScrollWheelEvent, Stateful, StyledText, Subscription, Task,    TextStyleRefinement, WeakEntity, actions, anchored, deferred, div,};use language::{Language, LanguageConfig, ToOffset as _};use notifications::status_toast::{StatusToast, ToastIcon};use project::Project;use settings::{BaseKeymap, KeybindSource, KeymapFile, Settings as _, SettingsAssets};use ui::{    ActiveTheme as _, App, Banner, BorrowAppContext, ContextMenu, IconButtonShape, Indicator,    Modal, ModalFooter, ModalHeader, ParentElement as _, Render, Section, SharedString,    Styled as _, Tooltip, Window, prelude::*,};use ui_input::SingleLineInput;use util::ResultExt;use workspace::{    Item, ModalView, SerializableItem, Workspace, notifications::NotifyTaskExt as _,    register_serializable_item,}; use crate::{    keybindings::persistence::KEYBINDING_EDITORS,    ui_components::{        keystroke_input::{ClearKeystrokes, KeystrokeInput, StartRecording, StopRecording},        table::{ColumnWidths, ResizeBehavior, Table, TableInteractionState},    },}; const NO_ACTION_ARGUMENTS_TEXT: SharedString = SharedString::new_static("<no arguments>"); actions!(    zed,    [        /// Opens the keymap editor.        OpenKeymapEditor    ]); actions!(    keymap_editor,    [        /// Edits the selected key binding.        EditBinding,        /// Creates a new key binding for the selected action.        CreateBinding,        /// Deletes the selected key binding.        DeleteBinding,        /// Copies the action name to clipboard.        CopyAction,        /// Copies the context predicate to clipboard.        CopyContext,        /// Toggles Conflict Filtering        ToggleConflictFilter,        /// Toggle Keystroke search        ToggleKeystrokeSearch,        /// Toggles exact matching for keystroke search        ToggleExactKeystrokeMatching,        /// Shows matching keystrokes for the currently selected binding        ShowMatchingKeybinds    ]); pub fn init(cx: &mut App) {    let keymap_event_channel = KeymapEventChannel::new();    cx.set_global(keymap_event_channel);     cx.on_action(|_: &OpenKeymapEditor, cx| {        workspace::with_active_or_new_workspace(cx, move |workspace, window, cx| {            workspace                .with_local_workspace(window, cx, |workspace, window, cx| {                    let existing = workspace                        .active_pane()                        .read(cx)                        .items()                        .find_map(|item| item.downcast::<KeymapEditor>());                     if let Some(existing) = existing {                        workspace.activate_item(&existing, true, true, window, cx);                    } else {                        let keymap_editor =                            cx.new(|cx| KeymapEditor::new(workspace.weak_handle(), window, cx));                        workspace.add_item_to_active_pane(                            Box::new(keymap_editor),                            None,                            true,                            window,                            cx,                        );                    }                })                .detach();        })    });     register_serializable_item::<KeymapEditor>(cx);} pub struct KeymapEventChannel {} impl Global for KeymapEventChannel {} impl KeymapEventChannel {    fn new() -> Self {        Self {}    }     pub fn trigger_keymap_changed(cx: &mut App) {        let Some(_event_channel) = cx.try_global::<Self>() else {            // don't panic if no global defined. This usually happens in tests            return;        };        cx.update_global(|_event_channel: &mut Self, _| {            /* triggers observers in KeymapEditors */        });    }} #[derive(Default, PartialEq)]enum SearchMode {    #[default]    Normal,    KeyStroke {        exact_match: bool,    },} impl SearchMode {    fn invert(&self) -> Self {        match self {            SearchMode::Normal => SearchMode::KeyStroke { exact_match: false },            SearchMode::KeyStroke { .. } => SearchMode::Normal,        }    }     fn exact_match(&self) -> bool {        match self {            SearchMode::Normal => false,            SearchMode::KeyStroke { exact_match } => *exact_match,        }    }} #[derive(Default, PartialEq, Copy, Clone)]enum FilterState {    #[default]    All,    Conflicts,} impl FilterState {    fn invert(&self) -> Self {        match self {            FilterState::All => FilterState::Conflicts,            FilterState::Conflicts => FilterState::All,        }    }} #[derive(Debug, Default, PartialEq, Eq, Clone, Hash)]struct ActionMapping {    keystrokes: Vec<Keystroke>,    context: Option<SharedString>,} #[derive(Debug)]struct KeybindConflict {    first_conflict_index: usize,    remaining_conflict_amount: usize,} impl KeybindConflict {    fn from_iter<'a>(mut indices: impl Iterator<Item = &'a ConflictOrigin>) -> Option<Self> {        indices.next().map(|origin| Self {            first_conflict_index: origin.index,            remaining_conflict_amount: indices.count(),        })    }} #[derive(Clone, Copy, PartialEq)]struct ConflictOrigin {    override_source: KeybindSource,    overridden_source: Option<KeybindSource>,    index: usize,} impl ConflictOrigin {    fn new(source: KeybindSource, index: usize) -> Self {        Self {            override_source: source,            index,            overridden_source: None,        }    }     fn with_overridden_source(self, source: KeybindSource) -> Self {        Self {            overridden_source: Some(source),            ..self        }    }     fn get_conflict_with(&self, other: &Self) -> Option<Self> {        if self.override_source == KeybindSource::User            && other.override_source == KeybindSource::User        {            Some(                Self::new(KeybindSource::User, other.index)                    .with_overridden_source(self.override_source),            )        } else if self.override_source > other.override_source {            Some(other.with_overridden_source(self.override_source))        } else {            None        }    }     fn is_user_keybind_conflict(&self) -> bool {        self.override_source == KeybindSource::User            && self.overridden_source == Some(KeybindSource::User)    }} #[derive(Default)]struct ConflictState {    conflicts: Vec<Option<ConflictOrigin>>,    keybind_mapping: HashMap<ActionMapping, Vec<ConflictOrigin>>,    has_user_conflicts: bool,} impl ConflictState {    fn new(key_bindings: &[ProcessedBinding]) -> Self {        let mut action_keybind_mapping: HashMap<_, Vec<ConflictOrigin>> = HashMap::default();         let mut largest_index = 0;        for (index, binding) in key_bindings            .iter()            .enumerate()            .flat_map(|(index, binding)| Some(index).zip(binding.keybind_information()))        {            action_keybind_mapping                .entry(binding.get_action_mapping())                .or_default()                .push(ConflictOrigin::new(binding.source, index));            largest_index = index;        }         let mut conflicts = vec![None; largest_index + 1];        let mut has_user_conflicts = false;         for indices in action_keybind_mapping.values_mut() {            indices.sort_unstable_by_key(|origin| origin.override_source);            let Some((fst, snd)) = indices.get(0).zip(indices.get(1)) else {                continue;            };             for origin in indices.iter() {                conflicts[origin.index] =                    origin.get_conflict_with(if origin == fst { &snd } else { &fst })            }             has_user_conflicts |= fst.override_source == KeybindSource::User                && snd.override_source == KeybindSource::User;        }         Self {            conflicts,            keybind_mapping: action_keybind_mapping,            has_user_conflicts,        }    }     fn conflicting_indices_for_mapping(        &self,        action_mapping: &ActionMapping,        keybind_idx: Option<usize>,    ) -> Option<KeybindConflict> {        self.keybind_mapping            .get(action_mapping)            .and_then(|indices| {                KeybindConflict::from_iter(                    indices                        .iter()                        .filter(|&conflict| Some(conflict.index) != keybind_idx),                )            })    }     fn conflict_for_idx(&self, idx: usize) -> Option<ConflictOrigin> {        self.conflicts.get(idx).copied().flatten()    }     fn has_user_conflict(&self, candidate_idx: usize) -> bool {        self.conflict_for_idx(candidate_idx)            .is_some_and(|conflict| conflict.is_user_keybind_conflict())    }     fn any_user_binding_conflicts(&self) -> bool {        self.has_user_conflicts    }} struct KeymapEditor {    workspace: WeakEntity<Workspace>,    focus_handle: FocusHandle,    _keymap_subscription: Subscription,    keybindings: Vec<ProcessedBinding>,    keybinding_conflict_state: ConflictState,    filter_state: FilterState,    search_mode: SearchMode,    search_query_debounce: Option<Task<()>>,    // corresponds 1 to 1 with keybindings    string_match_candidates: Arc<Vec<StringMatchCandidate>>,    matches: Vec<StringMatch>,    table_interaction_state: Entity<TableInteractionState>,    filter_editor: Entity<Editor>,    keystroke_editor: Entity<KeystrokeInput>,    selected_index: Option<usize>,    context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,    previous_edit: Option<PreviousEdit>,    humanized_action_names: HumanizedActionNameCache,    current_widths: Entity<ColumnWidths<6>>,    show_hover_menus: bool,    /// In order for the JSON LSP to run in the actions arguments editor, we    /// require a backing file In order to avoid issues (primarily log spam)    /// with drop order between the buffer, file, worktree, etc, we create a    /// temporary directory for these backing files in the keymap editor struct    /// instead of here. This has the added benefit of only having to create a    /// worktree and directory once, although the perf improvement is negligible.    action_args_temp_dir_worktree: Option<Entity<project::Worktree>>,    action_args_temp_dir: Option<tempfile::TempDir>,} enum PreviousEdit {    /// When deleting, we want to maintain the same scroll position    ScrollBarOffset(Point<Pixels>),    /// When editing or creating, because the new keybinding could be in a different position in the sort order    /// we store metadata about the new binding (either the modified version or newly created one)    /// and upon reload, we search for this binding in the list of keybindings, and if we find the one that matches    /// this metadata, we set the selected index to it and scroll to it,    /// and if we don't find it, we scroll to 0 and don't set a selected index    Keybinding {        action_mapping: ActionMapping,        action_name: &'static str,        /// The scrollbar position to fallback to if we don't find the keybinding during a refresh        /// this can happen if there's a filter applied to the search and the keybinding modification        /// filters the binding from the search results        fallback: Point<Pixels>,    },} impl EventEmitter<()> for KeymapEditor {} impl Focusable for KeymapEditor {    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {        if self.selected_index.is_some() {            self.focus_handle.clone()        } else {            self.filter_editor.focus_handle(cx)        }    }} impl KeymapEditor {    fn new(workspace: WeakEntity<Workspace>, window: &mut Window, cx: &mut Context<Self>) -> Self {        let _keymap_subscription =            cx.observe_global_in::<KeymapEventChannel>(window, Self::on_keymap_changed);        let table_interaction_state = TableInteractionState::new(window, cx);         let keystroke_editor = cx.new(|cx| {            let mut keystroke_editor = KeystrokeInput::new(None, window, cx);            keystroke_editor.set_search(true);            keystroke_editor        });         let filter_editor = cx.new(|cx| {            let mut editor = Editor::single_line(window, cx);            editor.set_placeholder_text("Filter action names…", cx);            editor        });         cx.subscribe(&filter_editor, |this, _, e: &EditorEvent, cx| {            if !matches!(e, EditorEvent::BufferEdited) {                return;            }             this.on_query_changed(cx);        })        .detach();         cx.subscribe(&keystroke_editor, |this, _, _, cx| {            if matches!(this.search_mode, SearchMode::Normal) {                return;            }             this.on_query_changed(cx);        })        .detach();         cx.spawn({            let workspace = workspace.clone();            async move |this, cx| {                let temp_dir = tempfile::tempdir_in(paths::temp_dir())?;                let worktree = workspace                    .update(cx, |ws, cx| {                        ws.project()                            .update(cx, |p, cx| p.create_worktree(temp_dir.path(), false, cx))                    })?                    .await?;                this.update(cx, |this, _| {                    this.action_args_temp_dir = Some(temp_dir);                    this.action_args_temp_dir_worktree = Some(worktree);                })            }        })        .detach();         let mut this = Self {            workspace,            keybindings: vec![],            keybinding_conflict_state: ConflictState::default(),            filter_state: FilterState::default(),            search_mode: SearchMode::default(),            string_match_candidates: Arc::new(vec![]),            matches: vec![],            focus_handle: cx.focus_handle(),            _keymap_subscription,            table_interaction_state,            filter_editor,            keystroke_editor,            selected_index: None,            context_menu: None,            previous_edit: None,            search_query_debounce: None,            humanized_action_names: HumanizedActionNameCache::new(cx),            show_hover_menus: true,            action_args_temp_dir: None,            action_args_temp_dir_worktree: None,            current_widths: cx.new(|cx| ColumnWidths::new(cx)),        };         this.on_keymap_changed(window, cx);         this    }     fn current_action_query(&self, cx: &App) -> String {        self.filter_editor.read(cx).text(cx)    }     fn current_keystroke_query(&self, cx: &App) -> Vec<Keystroke> {        match self.search_mode {            SearchMode::KeyStroke { .. } => self                .keystroke_editor                .read(cx)                .keystrokes()                .iter()                .cloned()                .collect(),            SearchMode::Normal => Default::default(),        }    }     fn on_query_changed(&mut self, cx: &mut Context<Self>) {        let action_query = self.current_action_query(cx);        let keystroke_query = self.current_keystroke_query(cx);        let exact_match = self.search_mode.exact_match();         let timer = cx.background_executor().timer(Duration::from_secs(1));        self.search_query_debounce = Some(cx.background_spawn({            let action_query = action_query.clone();            let keystroke_query = keystroke_query.clone();            async move {                timer.await;                 let keystroke_query = keystroke_query                    .into_iter()                    .map(|keystroke| keystroke.unparse())                    .collect::<Vec<String>>()                    .join(" ");                 telemetry::event!(                    "Keystroke Search Completed",                    action_query = action_query,                    keystroke_query = keystroke_query,                    keystroke_exact_match = exact_match

Lines 1–500 of 3139.

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t