1217 lines · 39.5 KB · Rust
RawDownload
mod persistence; use std::{    cmp::{self, Reverse},    collections::{HashMap, VecDeque},    sync::Arc,    time::Duration,}; use client::parse_zed_link;use command_palette_hooks::{    CommandInterceptItem, CommandInterceptResult, CommandPaletteFilter,    GlobalCommandPaletteInterceptor,}; use fuzzy::{StringMatch, StringMatchCandidate};use gpui::{    Action, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,    ParentElement, Render, Styled, Task, WeakEntity, Window,};use persistence::COMMAND_PALETTE_HISTORY;use picker::Direction;use picker::{Picker, PickerDelegate};use postage::{sink::Sink, stream::Stream};use settings::Settings;use ui::{HighlightedLabel, KeyBinding, ListItem, ListItemSpacing, prelude::*};use util::ResultExt;use workspace::{ModalView, Workspace, WorkspaceSettings};use zed_actions::{OpenZedUrl, command_palette::Toggle}; pub fn init(cx: &mut App) {    command_palette_hooks::init(cx);    cx.observe_new(CommandPalette::register).detach();} impl ModalView for CommandPalette {} pub struct CommandPalette {    picker: Entity<Picker<CommandPaletteDelegate>>,} /// Removes subsequent whitespace characters and double colons from the query.////// This improves the likelihood of a match by either humanized name or keymap-style name.pub fn normalize_action_query(input: &str) -> String {    let mut result = String::with_capacity(input.len());    let mut last_char = None;     for char in input.trim().chars() {        match (last_char, char) {            (Some(':'), ':') => continue,            (Some(last_char), char) if last_char.is_whitespace() && char.is_whitespace() => {                continue;            }            _ => {                last_char = Some(char);            }        }        result.push(char);    }     result} impl CommandPalette {    fn register(        workspace: &mut Workspace,        _window: Option<&mut Window>,        _: &mut Context<Workspace>,    ) {        workspace.register_action(|workspace, _: &Toggle, window, cx| {            Self::toggle(workspace, "", window, cx)        });    }     pub fn toggle(        workspace: &mut Workspace,        query: &str,        window: &mut Window,        cx: &mut Context<Workspace>,    ) {        let Some(previous_focus_handle) = window.focused(cx) else {            return;        };         let entity = cx.weak_entity();        workspace.toggle_modal(window, cx, move |window, cx| {            CommandPalette::new(previous_focus_handle, query, entity, window, cx)        });    }     fn new(        previous_focus_handle: FocusHandle,        query: &str,        entity: WeakEntity<Workspace>,        window: &mut Window,        cx: &mut Context<Self>,    ) -> Self {        let filter = CommandPaletteFilter::try_global(cx);         let commands = window            .available_actions(cx)            .into_iter()            .filter_map(|action| {                if filter.is_some_and(|filter| filter.is_hidden(&*action)) {                    return None;                }                 Some(Command {                    name: humanize_action_name(action.name()),                    action,                })            })            .collect();         let delegate = CommandPaletteDelegate::new(            cx.entity().downgrade(),            entity,            commands,            previous_focus_handle,        );         let picker = cx.new(|cx| {            let picker = Picker::uniform_list(delegate, window, cx);            picker.set_query(query, window, cx);            picker        });        Self { picker }    }     pub fn set_query(&mut self, query: &str, window: &mut Window, cx: &mut Context<Self>) {        self.picker            .update(cx, |picker, cx| picker.set_query(query, window, cx))    }} impl EventEmitter<DismissEvent> for CommandPalette {} impl Focusable for CommandPalette {    fn focus_handle(&self, cx: &App) -> FocusHandle {        self.picker.focus_handle(cx)    }} impl Render for CommandPalette {    fn render(&mut self, _window: &mut Window, _: &mut Context<Self>) -> impl IntoElement {        v_flex()            .key_context("CommandPalette")            .w(rems(34.))            .child(self.picker.clone())    }} pub struct CommandPaletteDelegate {    latest_query: String,    command_palette: WeakEntity<CommandPalette>,    workspace: WeakEntity<Workspace>,    all_commands: Vec<Command>,    commands: Vec<Command>,    matches: Vec<StringMatch>,    selected_ix: usize,    previous_focus_handle: FocusHandle,    updating_matches: Option<(        Task<()>,        postage::dispatch::Receiver<(Vec<Command>, Vec<StringMatch>, CommandInterceptResult)>,    )>,    query_history: QueryHistory,} struct Command {    name: String,    action: Box<dyn Action>,} #[derive(Default)]struct QueryHistory {    history: Option<VecDeque<String>>,    cursor: Option<usize>,    prefix: Option<String>,} impl QueryHistory {    fn history(&mut self) -> &mut VecDeque<String> {        self.history.get_or_insert_with(|| {            COMMAND_PALETTE_HISTORY                .list_recent_queries()                .unwrap_or_default()                .into_iter()                .collect()        })    }     fn add(&mut self, query: String) {        if let Some(pos) = self.history().iter().position(|h| h == &query) {            self.history().remove(pos);        }        self.history().push_back(query);        self.cursor = None;        self.prefix = None;    }     fn validate_cursor(&mut self, current_query: &str) -> Option<usize> {        if let Some(pos) = self.cursor {            if self.history().get(pos).map(|s| s.as_str()) != Some(current_query) {                self.cursor = None;                self.prefix = None;            }        }        self.cursor    }     fn previous(&mut self, current_query: &str) -> Option<&str> {        if self.validate_cursor(current_query).is_none() {            self.prefix = Some(current_query.to_string());        }         let prefix = self.prefix.clone().unwrap_or_default();        let start_index = self.cursor.unwrap_or(self.history().len());         for i in (0..start_index).rev() {            if self                .history()                .get(i)                .is_some_and(|e| e.starts_with(&prefix))            {                self.cursor = Some(i);                return self.history().get(i).map(|s| s.as_str());            }        }        None    }     fn next(&mut self, current_query: &str) -> Option<&str> {        let selected = self.validate_cursor(current_query)?;        let prefix = self.prefix.clone().unwrap_or_default();         for i in (selected + 1)..self.history().len() {            if self                .history()                .get(i)                .is_some_and(|e| e.starts_with(&prefix))            {                self.cursor = Some(i);                return self.history().get(i).map(|s| s.as_str());            }        }        None    }     fn reset_cursor(&mut self) {        self.cursor = None;        self.prefix = None;    }     fn is_navigating(&self) -> bool {        self.cursor.is_some()    }} impl Clone for Command {    fn clone(&self) -> Self {        Self {            name: self.name.clone(),            action: self.action.boxed_clone(),        }    }} impl CommandPaletteDelegate {    fn new(        command_palette: WeakEntity<CommandPalette>,        workspace: WeakEntity<Workspace>,        commands: Vec<Command>,        previous_focus_handle: FocusHandle,    ) -> Self {        Self {            command_palette,            workspace,            all_commands: commands.clone(),            matches: vec![],            commands,            selected_ix: 0,            previous_focus_handle,            latest_query: String::new(),            updating_matches: None,            query_history: Default::default(),        }    }     fn matches_updated(        &mut self,        query: String,        mut commands: Vec<Command>,        mut matches: Vec<StringMatch>,        intercept_result: CommandInterceptResult,        _: &mut Context<Picker<Self>>,    ) {        self.updating_matches.take();        self.latest_query = query;         let mut new_matches = Vec::new();         for CommandInterceptItem {            action,            string,            positions,        } in intercept_result.results        {            if let Some(idx) = matches                .iter()                .position(|m| commands[m.candidate_id].action.partial_eq(&*action))            {                matches.remove(idx);            }            commands.push(Command {                name: string.clone(),                action,            });            new_matches.push(StringMatch {                candidate_id: commands.len() - 1,                string,                positions,                score: 0.0,            })        }        if !intercept_result.exclusive {            new_matches.append(&mut matches);        }        self.commands = commands;        self.matches = new_matches;        if self.matches.is_empty() {            self.selected_ix = 0;        } else {            self.selected_ix = cmp::min(self.selected_ix, self.matches.len() - 1);        }    }     /// Hit count for each command in the palette.    /// We only account for commands triggered directly via command palette and not by e.g. keystrokes because    /// if a user already knows a keystroke for a command, they are unlikely to use a command palette to look for it.    fn hit_counts(&self) -> HashMap<String, u16> {        if let Ok(commands) = COMMAND_PALETTE_HISTORY.list_commands_used() {            commands                .into_iter()                .map(|command| (command.command_name, command.invocations))                .collect()        } else {            HashMap::new()        }    }     fn selected_command(&self) -> Option<&Command> {        let action_ix = self            .matches            .get(self.selected_ix)            .map(|m| m.candidate_id)            .unwrap_or(self.selected_ix);        // this gets called in headless tests where there are no commands loaded        // so we need to return an Option here        self.commands.get(action_ix)    }     #[cfg(any(test, feature = "test-support"))]    pub fn seed_history(&mut self, queries: &[&str]) {        self.query_history.history = Some(queries.iter().map(|s| s.to_string()).collect());    }} impl PickerDelegate for CommandPaletteDelegate {    type ListItem = ListItem;     fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {        "Execute a command...".into()    }     fn select_history(        &mut self,        direction: Direction,        query: &str,        _window: &mut Window,        _cx: &mut App,    ) -> Option<String> {        match direction {            Direction::Up => {                let should_use_history =                    self.selected_ix == 0 || self.query_history.is_navigating();                if should_use_history {                    if let Some(query) = self.query_history.previous(query).map(|s| s.to_string()) {                        return Some(query);                    }                }            }            Direction::Down => {                if self.query_history.is_navigating() {                    if let Some(query) = self.query_history.next(query).map(|s| s.to_string()) {                        return Some(query);                    } else {                        let prefix = self.query_history.prefix.take().unwrap_or_default();                        self.query_history.reset_cursor();                        return Some(prefix);                    }                }            }        }        None    }     fn match_count(&self) -> usize {        self.matches.len()    }     fn selected_index(&self) -> usize {        self.selected_ix    }     fn set_selected_index(        &mut self,        ix: usize,        _window: &mut Window,        _: &mut Context<Picker<Self>>,    ) {        self.selected_ix = ix;    }     fn update_matches(        &mut self,        mut query: String,        window: &mut Window,        cx: &mut Context<Picker<Self>>,    ) -> gpui::Task<()> {        let settings = WorkspaceSettings::get_global(cx);        if let Some(alias) = settings.command_aliases.get(&query) {            query = alias.to_string();        }         let workspace = self.workspace.clone();         let intercept_task = GlobalCommandPaletteInterceptor::intercept(&query, workspace, cx);         let (mut tx, mut rx) = postage::dispatch::channel(1);         let query_str = query.as_str();        let is_zed_link = parse_zed_link(query_str, cx).is_some();         let task = cx.background_spawn({            let mut commands = self.all_commands.clone();            let hit_counts = self.hit_counts();            let executor = cx.background_executor().clone();            let query = normalize_action_query(query_str);            let query_for_link = query_str.to_string();            async move {                commands.sort_by_key(|action| {                    (                        Reverse(hit_counts.get(&action.name).cloned()),                        action.name.clone(),                    )                });                 let candidates = commands                    .iter()                    .enumerate()                    .map(|(ix, command)| StringMatchCandidate::new(ix, &command.name))                    .collect::<Vec<_>>();                 let matches = fuzzy::match_strings(                    &candidates,                    &query,                    true,                    true,                    10000,                    &Default::default(),                    executor,                )                .await;                 let intercept_result = if is_zed_link {                    CommandInterceptResult {                        results: vec![CommandInterceptItem {                            action: OpenZedUrl {                                url: query_for_link.clone(),                            }                            .boxed_clone(),                            string: query_for_link,                            positions: vec![],                        }],                        exclusive: false,                    }                } else if let Some(task) = intercept_task {                    task.await                } else {                    CommandInterceptResult::default()                };                 tx.send((commands, matches, intercept_result))                    .await                    .log_err();            }        });         self.updating_matches = Some((task, rx.clone()));

Lines 1–500 of 1217.