diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index d4bfe38d56..68467ff4b0 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -198,26 +198,29 @@ impl CommandPaletteDelegate { ) { self.updating_matches.take(); - let mut intercept_result = CommandPaletteInterceptor::try_global(cx) - .and_then(|interceptor| interceptor.intercept(&query, cx)); + let mut intercept_results = CommandPaletteInterceptor::try_global(cx) + .map(|interceptor| interceptor.intercept(&query, cx)) + .unwrap_or_default(); if parse_zed_link(&query, cx).is_some() { - intercept_result = Some(CommandInterceptResult { + intercept_results = vec![CommandInterceptResult { action: OpenZedUrl { url: query.clone() }.boxed_clone(), string: query.clone(), positions: vec![], - }) + }] } - if let Some(CommandInterceptResult { + let mut new_matches = Vec::new(); + + for CommandInterceptResult { action, string, positions, - }) = intercept_result + } in intercept_results { if let Some(idx) = matches .iter() - .position(|m| commands[m.candidate_id].action.type_id() == action.type_id()) + .position(|m| commands[m.candidate_id].action.partial_eq(&*action)) { matches.remove(idx); } @@ -225,18 +228,16 @@ impl CommandPaletteDelegate { name: string.clone(), action, }); - matches.insert( - 0, - StringMatch { - candidate_id: commands.len() - 1, - string, - positions, - score: 0.0, - }, - ) + new_matches.push(StringMatch { + candidate_id: commands.len() - 1, + string, + positions, + score: 0.0, + }) } + new_matches.append(&mut matches); self.commands = commands; - self.matches = matches; + self.matches = new_matches; if self.matches.is_empty() { self.selected_ix = 0; } else { diff --git a/crates/command_palette_hooks/src/command_palette_hooks.rs b/crates/command_palette_hooks/src/command_palette_hooks.rs index 3ea94bd10f..df64d53874 100644 --- a/crates/command_palette_hooks/src/command_palette_hooks.rs +++ b/crates/command_palette_hooks/src/command_palette_hooks.rs @@ -108,7 +108,7 @@ pub struct CommandInterceptResult { /// An interceptor for the command palette. #[derive(Default)] pub struct CommandPaletteInterceptor( - Option Option>>, + Option Vec>>, ); #[derive(Default)] @@ -132,10 +132,12 @@ impl CommandPaletteInterceptor { } /// Intercepts the given query from the command palette. - pub fn intercept(&self, query: &str, cx: &App) -> Option { - let handler = self.0.as_ref()?; - - (handler)(query, cx) + pub fn intercept(&self, query: &str, cx: &App) -> Vec { + if let Some(handler) = self.0.as_ref() { + (handler)(query, cx) + } else { + Vec::new() + } } /// Clears the global interceptor. @@ -146,7 +148,7 @@ impl CommandPaletteInterceptor { /// Sets the global interceptor. /// /// This will override the previous interceptor, if it exists. - pub fn set(&mut self, handler: Box Option>) { + pub fn set(&mut self, handler: Box Vec>) { self.0 = Some(handler); } } diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index bbd579218a..76c7b464e0 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -8,6 +8,7 @@ use editor::{ Bias, Editor, ToPoint, }; use gpui::{actions, impl_internal_actions, Action, App, Context, Global, Window}; +use itertools::Itertools; use language::Point; use multi_buffer::MultiBufferRow; use regex::Regex; @@ -64,6 +65,95 @@ pub struct WithCount { action: WrappedAction, } +#[derive(Clone, Deserialize, JsonSchema, PartialEq)] +pub enum VimOption { + Wrap(bool), + Number(bool), + RelativeNumber(bool), +} + +impl VimOption { + fn possible_commands(query: &str) -> Vec { + let mut prefix_of_options = Vec::new(); + let mut options = query.split(" ").collect::>(); + let prefix = options.pop().unwrap_or_default(); + for option in options { + if let Some(opt) = Self::from(option) { + prefix_of_options.push(opt) + } else { + return vec![]; + } + } + + Self::possibilities(&prefix) + .map(|possible| { + let mut options = prefix_of_options.clone(); + options.push(possible); + + CommandInterceptResult { + string: format!( + "set {}", + options.iter().map(|opt| opt.to_string()).join(" ") + ), + action: VimSet { options }.boxed_clone(), + positions: vec![], + } + }) + .collect() + } + + fn possibilities(query: &str) -> impl Iterator + '_ { + [ + (None, VimOption::Wrap(true)), + (None, VimOption::Wrap(false)), + (None, VimOption::Number(true)), + (None, VimOption::Number(false)), + (None, VimOption::RelativeNumber(true)), + (None, VimOption::RelativeNumber(false)), + (Some("rnu"), VimOption::RelativeNumber(true)), + (Some("nornu"), VimOption::RelativeNumber(false)), + ] + .into_iter() + .filter(move |(prefix, option)| prefix.unwrap_or(option.to_string()).starts_with(query)) + .map(|(_, option)| option) + } + + fn from(option: &str) -> Option { + match option { + "wrap" => Some(Self::Wrap(true)), + "nowrap" => Some(Self::Wrap(false)), + + "number" => Some(Self::Number(true)), + "nu" => Some(Self::Number(true)), + "nonumber" => Some(Self::Number(false)), + "nonu" => Some(Self::Number(false)), + + "relativenumber" => Some(Self::RelativeNumber(true)), + "rnu" => Some(Self::RelativeNumber(true)), + "norelativenumber" => Some(Self::RelativeNumber(false)), + "nornu" => Some(Self::RelativeNumber(false)), + + _ => None, + } + } + + fn to_string(&self) -> &'static str { + match self { + VimOption::Wrap(true) => "wrap", + VimOption::Wrap(false) => "nowrap", + VimOption::Number(true) => "number", + VimOption::Number(false) => "nonumber", + VimOption::RelativeNumber(true) => "relativenumber", + VimOption::RelativeNumber(false) => "norelativenumber", + } + } +} + +#[derive(Clone, Deserialize, JsonSchema, PartialEq)] +pub struct VimSet { + options: Vec, +} + #[derive(Debug)] struct WrappedAction(Box); @@ -76,7 +166,8 @@ impl_internal_actions!( WithRange, WithCount, OnMatchingLines, - ShellExec + ShellExec, + VimSet, ] ); @@ -100,6 +191,26 @@ impl Deref for WrappedAction { } pub fn register(editor: &mut Editor, cx: &mut Context) { + // Vim::action(editor, cx, |vim, action: &StartOfLine, window, cx| { + Vim::action(editor, cx, |vim, action: &VimSet, window, cx| { + for option in action.options.iter() { + vim.update_editor(window, cx, |_, editor, _, cx| match option { + VimOption::Wrap(true) => { + editor + .set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx); + } + VimOption::Wrap(false) => { + editor.set_soft_wrap_mode(language::language_settings::SoftWrap::None, cx); + } + VimOption::Number(enabled) => { + editor.set_show_line_numbers(*enabled, cx); + } + VimOption::RelativeNumber(enabled) => { + editor.set_relative_line_number(Some(*enabled), cx); + } + }); + } + }); Vim::action(editor, cx, |vim, _: &VisualCommand, window, cx| { let Some(workspace) = vim.workspace(window) else { return; @@ -808,7 +919,7 @@ fn wrap_count(action: Box, range: &CommandRange) -> Option Option { +pub fn command_interceptor(mut input: &str, cx: &App) -> Vec { // NOTE: We also need to support passing arguments to commands like :w // (ideally with filename autocompletion). while input.starts_with(':') { @@ -834,6 +945,8 @@ pub fn command_interceptor(mut input: &str, cx: &App) -> Option Option Option Vec { @@ -982,7 +1095,12 @@ impl OnMatchingLines { let command: String = chars.collect(); - let action = WrappedAction(command_interceptor(&command, cx)?.action); + let action = WrappedAction( + command_interceptor(&command, cx) + .first()? + .action + .boxed_clone(), + ); Some(Self { range,