3558 lines · 134.3 KB · Rust
RawDownload
use std::{    cmp::{self},    ops::{Not as _, Range},    sync::Arc,    time::Duration,}; mod ui_components; 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,    KeyBindingContextPredicate::{And, Descendant, Equal, Identifier, Not, NotEqual, Or},    KeyContext, KeybindingKeystroke, MouseButton, PlatformKeyboardMapper, 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::{CompletionDisplayOptions, 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::*, right_click_menu,};use ui_input::SingleLineInput;use util::ResultExt;use workspace::{    Item, ModalView, SerializableItem, Workspace, notifications::NotifyTaskExt as _,    register_serializable_item,}; pub use ui_components::*; use crate::{    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<KeybindingKeystroke>,    context: Option<SharedString>,} #[derive(Debug)]struct KeybindConflict {    first_conflict_index: usize,    remaining_conflict_amount: usize,} #[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: ConflictKeybindMapping,    has_user_conflicts: bool,} type ConflictKeybindMapping = HashMap<    Vec<KeybindingKeystroke>,    Vec<(        Option<gpui::KeyBindingContextPredicate>,        Vec<ConflictOrigin>,    )>,>; impl ConflictState {    fn new(key_bindings: &[ProcessedBinding]) -> Self {        let mut action_keybind_mapping = ConflictKeybindMapping::default();         let mut largest_index = 0;        for (index, binding) in key_bindings            .iter()            .enumerate()            .flat_map(|(index, binding)| Some(index).zip(binding.keybind_information()))        {            let mapping = binding.get_action_mapping();            let predicate = mapping                .context                .and_then(|ctx| gpui::KeyBindingContextPredicate::parse(&ctx).ok());            let entry = action_keybind_mapping                .entry(mapping.keystrokes)                .or_default();            let origin = ConflictOrigin::new(binding.source, index);            if let Some((_, origins)) =                entry                    .iter_mut()                    .find(|(other_predicate, _)| match (&predicate, other_predicate) {                        (None, None) => true,                        (Some(a), Some(b)) => normalized_ctx_eq(a, b),                        _ => false,                    })            {                origins.push(origin);            } else {                entry.push((predicate, vec![origin]));            }            largest_index = index;        }         let mut conflicts = vec![None; largest_index + 1];        let mut has_user_conflicts = false;         for entries in action_keybind_mapping.values_mut() {            for (_, indices) in entries.iter_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> {        let ActionMapping {            keystrokes,            context,        } = action_mapping;        let predicate = context            .as_deref()            .and_then(|ctx| gpui::KeyBindingContextPredicate::parse(&ctx).ok());        self.keybind_mapping.get(keystrokes).and_then(|entries| {            entries                .iter()                .find_map(|(other_predicate, indices)| {                    match (&predicate, other_predicate) {                        (None, None) => true,                        (Some(pred), Some(other)) => normalized_ctx_eq(pred, other),                        _ => false,                    }                    .then_some(indices)                })                .and_then(|indices| {                    let mut indices = indices                        .iter()                        .filter(|&conflict| Some(conflict.index) != keybind_idx);                    indices.next().map(|origin| KeybindConflict {                        first_conflict_index: origin.index,                        remaining_conflict_amount: indices.count(),                    })                })        })    }     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)        }    }}/// Helper function to check if two keystroke sequences match exactlyfn keystrokes_match_exactly(    keystrokes1: &[KeybindingKeystroke],    keystrokes2: &[KeybindingKeystroke],) -> bool {    keystrokes1.len() == keystrokes2.len()        && keystrokes1.iter().zip(keystrokes2).all(|(k1, k2)| {            k1.inner().key == k2.inner().key && k1.inner().modifiers == k2.inner().modifiers        })} 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,

Lines 1–500 of 3558.