vim: :set support (#24209)

Closes #21147 

Release Notes:

- vim: First version of `:set` with support for `[no]wrap`,
`[no]number`, `[no]relativenumber`

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
This commit is contained in:
Max Bucknell 2025-02-10 20:55:40 -08:00 committed by GitHub
parent 2e7bb11b7d
commit 37785a54d5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 152 additions and 31 deletions

View file

@ -198,26 +198,29 @@ impl CommandPaletteDelegate {
) { ) {
self.updating_matches.take(); self.updating_matches.take();
let mut intercept_result = CommandPaletteInterceptor::try_global(cx) let mut intercept_results = CommandPaletteInterceptor::try_global(cx)
.and_then(|interceptor| interceptor.intercept(&query, cx)); .map(|interceptor| interceptor.intercept(&query, cx))
.unwrap_or_default();
if parse_zed_link(&query, cx).is_some() { if parse_zed_link(&query, cx).is_some() {
intercept_result = Some(CommandInterceptResult { intercept_results = vec![CommandInterceptResult {
action: OpenZedUrl { url: query.clone() }.boxed_clone(), action: OpenZedUrl { url: query.clone() }.boxed_clone(),
string: query.clone(), string: query.clone(),
positions: vec![], positions: vec![],
}) }]
} }
if let Some(CommandInterceptResult { let mut new_matches = Vec::new();
for CommandInterceptResult {
action, action,
string, string,
positions, positions,
}) = intercept_result } in intercept_results
{ {
if let Some(idx) = matches if let Some(idx) = matches
.iter() .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); matches.remove(idx);
} }
@ -225,18 +228,16 @@ impl CommandPaletteDelegate {
name: string.clone(), name: string.clone(),
action, action,
}); });
matches.insert( new_matches.push(StringMatch {
0, candidate_id: commands.len() - 1,
StringMatch { string,
candidate_id: commands.len() - 1, positions,
string, score: 0.0,
positions, })
score: 0.0,
},
)
} }
new_matches.append(&mut matches);
self.commands = commands; self.commands = commands;
self.matches = matches; self.matches = new_matches;
if self.matches.is_empty() { if self.matches.is_empty() {
self.selected_ix = 0; self.selected_ix = 0;
} else { } else {

View file

@ -108,7 +108,7 @@ pub struct CommandInterceptResult {
/// An interceptor for the command palette. /// An interceptor for the command palette.
#[derive(Default)] #[derive(Default)]
pub struct CommandPaletteInterceptor( pub struct CommandPaletteInterceptor(
Option<Box<dyn Fn(&str, &App) -> Option<CommandInterceptResult>>>, Option<Box<dyn Fn(&str, &App) -> Vec<CommandInterceptResult>>>,
); );
#[derive(Default)] #[derive(Default)]
@ -132,10 +132,12 @@ impl CommandPaletteInterceptor {
} }
/// Intercepts the given query from the command palette. /// Intercepts the given query from the command palette.
pub fn intercept(&self, query: &str, cx: &App) -> Option<CommandInterceptResult> { pub fn intercept(&self, query: &str, cx: &App) -> Vec<CommandInterceptResult> {
let handler = self.0.as_ref()?; if let Some(handler) = self.0.as_ref() {
(handler)(query, cx)
(handler)(query, cx) } else {
Vec::new()
}
} }
/// Clears the global interceptor. /// Clears the global interceptor.
@ -146,7 +148,7 @@ impl CommandPaletteInterceptor {
/// Sets the global interceptor. /// Sets the global interceptor.
/// ///
/// This will override the previous interceptor, if it exists. /// This will override the previous interceptor, if it exists.
pub fn set(&mut self, handler: Box<dyn Fn(&str, &App) -> Option<CommandInterceptResult>>) { pub fn set(&mut self, handler: Box<dyn Fn(&str, &App) -> Vec<CommandInterceptResult>>) {
self.0 = Some(handler); self.0 = Some(handler);
} }
} }

View file

@ -8,6 +8,7 @@ use editor::{
Bias, Editor, ToPoint, Bias, Editor, ToPoint,
}; };
use gpui::{actions, impl_internal_actions, Action, App, Context, Global, Window}; use gpui::{actions, impl_internal_actions, Action, App, Context, Global, Window};
use itertools::Itertools;
use language::Point; use language::Point;
use multi_buffer::MultiBufferRow; use multi_buffer::MultiBufferRow;
use regex::Regex; use regex::Regex;
@ -64,6 +65,95 @@ pub struct WithCount {
action: WrappedAction, action: WrappedAction,
} }
#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
pub enum VimOption {
Wrap(bool),
Number(bool),
RelativeNumber(bool),
}
impl VimOption {
fn possible_commands(query: &str) -> Vec<CommandInterceptResult> {
let mut prefix_of_options = Vec::new();
let mut options = query.split(" ").collect::<Vec<_>>();
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<Item = Self> + '_ {
[
(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<Self> {
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<VimOption>,
}
#[derive(Debug)] #[derive(Debug)]
struct WrappedAction(Box<dyn Action>); struct WrappedAction(Box<dyn Action>);
@ -76,7 +166,8 @@ impl_internal_actions!(
WithRange, WithRange,
WithCount, WithCount,
OnMatchingLines, OnMatchingLines,
ShellExec ShellExec,
VimSet,
] ]
); );
@ -100,6 +191,26 @@ impl Deref for WrappedAction {
} }
pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) { pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
// 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| { Vim::action(editor, cx, |vim, _: &VisualCommand, window, cx| {
let Some(workspace) = vim.workspace(window) else { let Some(workspace) = vim.workspace(window) else {
return; return;
@ -808,7 +919,7 @@ fn wrap_count(action: Box<dyn Action>, range: &CommandRange) -> Option<Box<dyn A
}) })
} }
pub fn command_interceptor(mut input: &str, cx: &App) -> Option<CommandInterceptResult> { pub fn command_interceptor(mut input: &str, cx: &App) -> Vec<CommandInterceptResult> {
// NOTE: We also need to support passing arguments to commands like :w // NOTE: We also need to support passing arguments to commands like :w
// (ideally with filename autocompletion). // (ideally with filename autocompletion).
while input.starts_with(':') { while input.starts_with(':') {
@ -834,6 +945,8 @@ pub fn command_interceptor(mut input: &str, cx: &App) -> Option<CommandIntercept
} }
.boxed_clone(), .boxed_clone(),
) )
} else if query.starts_with("se ") || query.starts_with("set ") {
return VimOption::possible_commands(query.split_once(" ").unwrap().1);
} else if query.starts_with('s') { } else if query.starts_with('s') {
let mut substitute = "substitute".chars().peekable(); let mut substitute = "substitute".chars().peekable();
let mut query = query.chars().peekable(); let mut query = query.chars().peekable();
@ -886,11 +999,11 @@ pub fn command_interceptor(mut input: &str, cx: &App) -> Option<CommandIntercept
if let Some(action) = action { if let Some(action) = action {
let string = input.to_string(); let string = input.to_string();
let positions = generate_positions(&string, &(range_prefix + query)); let positions = generate_positions(&string, &(range_prefix + query));
return Some(CommandInterceptResult { return vec![CommandInterceptResult {
action, action,
string, string,
positions, positions,
}); }];
} }
for command in commands(cx).iter() { for command in commands(cx).iter() {
@ -901,14 +1014,14 @@ pub fn command_interceptor(mut input: &str, cx: &App) -> Option<CommandIntercept
} }
let positions = generate_positions(&string, &(range_prefix + query)); let positions = generate_positions(&string, &(range_prefix + query));
return Some(CommandInterceptResult { return vec![CommandInterceptResult {
action, action,
string, string,
positions, positions,
}); }];
} }
} }
None return Vec::default();
} }
fn generate_positions(string: &str, query: &str) -> Vec<usize> { fn generate_positions(string: &str, query: &str) -> Vec<usize> {
@ -982,7 +1095,12 @@ impl OnMatchingLines {
let command: String = chars.collect(); 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 { Some(Self {
range, range,