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_nucleo::{StringMatch, StringMatchCandidate};use gpui::{ Action, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, ParentElement, Render, Styled, Task, TaskExt, WeakEntity, Window,};use persistence::CommandPaletteDB;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, and converts/// underscores to spaces.////// This improves the likelihood of a match by either humanized name or keymap-style name./// Underscores are converted to spaces because `humanize_action_name` converts them to spaces/// when building the search candidates (e.g. `terminal_panel::Toggle` -> `terminal panel: toggle`).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() { let normalized_char = if char == '_' { ' ' } else { char }; match (last_char, normalized_char) { (Some(':'), ':') => continue, (Some(last_char), c) if last_char.is_whitespace() && c.is_whitespace() => { continue; } _ => { last_char = Some(normalized_char); } } result.push(normalized_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| { // One-shot action; there's nothing to reopen. let picker = Picker::uniform_list(delegate, window, cx) .reopenable(false, cx) .show_scrollbar(true); 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") .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, cx: &App) -> &mut VecDeque<String> { self.history.get_or_insert_with(|| { CommandPaletteDB::global(cx) .list_recent_queries() .unwrap_or_default() .into_iter() .collect() }) } fn add(&mut self, query: String, cx: &App) { if let Some(pos) = self.history(cx).iter().position(|h| h == &query) { self.history(cx).remove(pos); } self.history(cx).push_back(query); self.cursor = None; self.prefix = None; } fn validate_cursor(&mut self, current_query: &str, cx: &App) -> Option<usize> { if let Some(pos) = self.cursor { if self.history(cx).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, cx: &App) -> Option<&str> { if self.validate_cursor(current_query, cx).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(cx).len()); for i in (0..start_index).rev() { if self .history(cx) .get(i) .is_some_and(|e| e.starts_with(&prefix)) { self.cursor = Some(i); return self.history(cx).get(i).map(|s| s.as_str()); } } None } fn next(&mut self, current_query: &str, cx: &App) -> Option<&str> { let selected = self.validate_cursor(current_query, cx)?; let prefix = self.prefix.clone().unwrap_or_default(); for i in (selected + 1)..self.history(cx).len() { if self .history(cx) .get(i) .is_some_and(|e| e.starts_with(&prefix)) { self.cursor = Some(i); return self.history(cx).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: string.into(), 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, cx: &App) -> HashMap<String, u16> { if let Ok(commands) = CommandPaletteDB::global(cx).list_commands_used() { commands .into_iter() .map(|command| (command.command_name, command.invocations)) .collect() } else { HashMap::new() } } fn selected_command(&self) -> Option<&Command> { if self.matches.is_empty() { return None; } 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 name() -> &'static str { "command palette" } 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, cx) .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, cx).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.as_ref().to_owned(); } 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(cx); 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_nucleo::match_strings_async( &candidates, &query, fuzzy_nucleo::Case::Smart, fuzzy_nucleo::LengthPenalty::On, 10000, &Default::default(), executor, ) .await; let intercept_result = if is_zed_link { CommandInterceptResult { results: vec![CommandInterceptItem { action: OpenZedUrl { url: query_for_link.clone().into(), } .boxed_clone(), string: query_for_link,Lines 1–500 of 1326. Load more
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_nucleo::{StringMatch, StringMatchCandidate}; use gpui::{ Action, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, ParentElement, Render, Styled, Task, TaskExt, WeakEntity, Window, }; use persistence::CommandPaletteDB; 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, and converts /// underscores to spaces. /// /// This improves the likelihood of a match by either humanized name or keymap-style name. /// Underscores are converted to spaces because `humanize_action_name` converts them to spaces /// when building the search candidates (e.g. `terminal_panel::Toggle` -> `terminal panel: toggle`). 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() { let normalized_char = if char == '_' { ' ' } else { char }; match (last_char, normalized_char) { (Some(':'), ':') => continue, (Some(last_char), c) if last_char.is_whitespace() && c.is_whitespace() => { continue; } _ => { last_char = Some(normalized_char); } } result.push(normalized_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| { // One-shot action; there's nothing to reopen. let picker = Picker::uniform_list(delegate, window, cx) .reopenable(false, cx) .show_scrollbar(true); 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") .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, cx: &App) -> &mut VecDeque<String> { self.history.get_or_insert_with(|| { CommandPaletteDB::global(cx) .list_recent_queries() .unwrap_or_default() .into_iter() .collect() }) } fn add(&mut self, query: String, cx: &App) { if let Some(pos) = self.history(cx).iter().position(|h| h == &query) { self.history(cx).remove(pos); } self.history(cx).push_back(query); self.cursor = None; self.prefix = None; } fn validate_cursor(&mut self, current_query: &str, cx: &App) -> Option<usize> { if let Some(pos) = self.cursor { if self.history(cx).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, cx: &App) -> Option<&str> { if self.validate_cursor(current_query, cx).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(cx).len()); for i in (0..start_index).rev() { if self .history(cx) .get(i) .is_some_and(|e| e.starts_with(&prefix)) { self.cursor = Some(i); return self.history(cx).get(i).map(|s| s.as_str()); } } None } fn next(&mut self, current_query: &str, cx: &App) -> Option<&str> { let selected = self.validate_cursor(current_query, cx)?; let prefix = self.prefix.clone().unwrap_or_default(); for i in (selected + 1)..self.history(cx).len() { if self .history(cx) .get(i) .is_some_and(|e| e.starts_with(&prefix)) { self.cursor = Some(i); return self.history(cx).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: string.into(), 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, cx: &App) -> HashMap<String, u16> { if let Ok(commands) = CommandPaletteDB::global(cx).list_commands_used() { commands .into_iter() .map(|command| (command.command_name, command.invocations)) .collect() } else { HashMap::new() } } fn selected_command(&self) -> Option<&Command> { if self.matches.is_empty() { return None; } 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 name() -> &'static str { "command palette" } 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, cx) .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, cx).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.as_ref().to_owned(); } 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(cx); 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_nucleo::match_strings_async( &candidates, &query, fuzzy_nucleo::Case::Smart, fuzzy_nucleo::LengthPenalty::On, 10000, &Default::default(), executor, ) .await; let intercept_result = if is_zed_link { CommandInterceptResult { results: vec![CommandInterceptItem { action: OpenZedUrl { url: query_for_link.clone().into(), } .boxed_clone(), string: query_for_link,