@@ -9,7 +9,8 @@ use std::{
use client::parse_zed_link;
use command_palette_hooks::{
- CommandInterceptResult, CommandPaletteFilter, CommandPaletteInterceptor,
+ CommandInterceptItem, CommandInterceptResult, CommandPaletteFilter,
+ GlobalCommandPaletteInterceptor,
};
use fuzzy::{StringMatch, StringMatchCandidate};
@@ -81,14 +82,17 @@ impl CommandPalette {
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, 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 {
@@ -109,8 +113,12 @@ impl CommandPalette {
})
.collect();
- let delegate =
- CommandPaletteDelegate::new(cx.entity().downgrade(), commands, previous_focus_handle);
+ 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);
@@ -146,6 +154,7 @@ impl Render for CommandPalette {
pub struct CommandPaletteDelegate {
latest_query: String,
command_palette: WeakEntity<CommandPalette>,
+ workspace: WeakEntity<Workspace>,
all_commands: Vec<Command>,
commands: Vec<Command>,
matches: Vec<StringMatch>,
@@ -153,7 +162,7 @@ pub struct CommandPaletteDelegate {
previous_focus_handle: FocusHandle,
updating_matches: Option<(
Task<()>,
- postage::dispatch::Receiver<(Vec<Command>, Vec<StringMatch>)>,
+ postage::dispatch::Receiver<(Vec<Command>, Vec<StringMatch>, CommandInterceptResult)>,
)>,
}
@@ -174,11 +183,13 @@ impl Clone for Command {
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,
@@ -194,30 +205,19 @@ impl CommandPaletteDelegate {
query: String,
mut commands: Vec<Command>,
mut matches: Vec<StringMatch>,
- cx: &mut Context<Picker<Self>>,
+ intercept_result: CommandInterceptResult,
+ _: &mut Context<Picker<Self>>,
) {
self.updating_matches.take();
- self.latest_query = query.clone();
-
- 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_results = vec![CommandInterceptResult {
- action: OpenZedUrl { url: query.clone() }.boxed_clone(),
- string: query,
- positions: vec![],
- }]
- }
+ self.latest_query = query;
let mut new_matches = Vec::new();
- for CommandInterceptResult {
+ for CommandInterceptItem {
action,
string,
positions,
- } in intercept_results
+ } in intercept_result.results
{
if let Some(idx) = matches
.iter()
@@ -236,7 +236,9 @@ impl CommandPaletteDelegate {
score: 0.0,
})
}
- new_matches.append(&mut matches);
+ if !intercept_result.exclusive {
+ new_matches.append(&mut matches);
+ }
self.commands = commands;
self.matches = new_matches;
if self.matches.is_empty() {
@@ -295,12 +297,22 @@ impl PickerDelegate for CommandPaletteDelegate {
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.as_str());
+ let query = normalize_action_query(query_str);
+ let query_for_link = query_str.to_string();
async move {
commands.sort_by_key(|action| {
(
@@ -326,13 +338,34 @@ impl PickerDelegate for CommandPaletteDelegate {
)
.await;
- tx.send((commands, matches)).await.log_err();
+ 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()));
cx.spawn_in(window, async move |picker, cx| {
- let Some((commands, matches)) = rx.recv().await else {
+ let Some((commands, matches, intercept_result)) = rx.recv().await else {
return;
};
@@ -340,7 +373,7 @@ impl PickerDelegate for CommandPaletteDelegate {
.update(cx, |picker, cx| {
picker
.delegate
- .matches_updated(query, commands, matches, cx)
+ .matches_updated(query, commands, matches, intercept_result, cx)
})
.log_err();
})
@@ -361,8 +394,8 @@ impl PickerDelegate for CommandPaletteDelegate {
.background_executor()
.block_with_timeout(duration, rx.clone().recv())
{
- Ok(Some((commands, matches))) => {
- self.matches_updated(query, commands, matches, cx);
+ Ok(Some((commands, matches, interceptor_result))) => {
+ self.matches_updated(query, commands, matches, interceptor_result, cx);
true
}
_ => {
@@ -1,13 +1,15 @@
use anyhow::{Result, anyhow};
use collections::{HashMap, HashSet};
-use command_palette_hooks::CommandInterceptResult;
+use command_palette_hooks::{CommandInterceptItem, CommandInterceptResult};
use editor::{
Bias, Editor, EditorSettings, SelectionEffects, ToPoint,
actions::{SortLinesCaseInsensitive, SortLinesCaseSensitive},
display_map::ToDisplayPoint,
};
use futures::AsyncWriteExt as _;
-use gpui::{Action, App, AppContext as _, Context, Global, Keystroke, Task, Window, actions};
+use gpui::{
+ Action, App, AppContext as _, Context, Global, Keystroke, Task, WeakEntity, Window, actions,
+};
use itertools::Itertools;
use language::Point;
use multi_buffer::MultiBufferRow;
@@ -20,7 +22,7 @@ use settings::{Settings, SettingsStore};
use std::{
iter::Peekable,
ops::{Deref, Range},
- path::Path,
+ path::{Path, PathBuf},
process::Stdio,
str::Chars,
sync::OnceLock,
@@ -28,8 +30,12 @@ use std::{
};
use task::{HideStrategy, RevealStrategy, SpawnInTerminal, TaskId};
use ui::ActiveTheme;
-use util::{ResultExt, rel_path::RelPath};
-use workspace::{Item, SaveIntent, notifications::NotifyResultExt};
+use util::{
+ ResultExt,
+ paths::PathStyle,
+ rel_path::{RelPath, RelPathBuf},
+};
+use workspace::{Item, SaveIntent, Workspace, notifications::NotifyResultExt};
use workspace::{SplitDirection, notifications::DetachAndPromptErr};
use zed_actions::{OpenDocs, RevealTarget};
@@ -85,7 +91,7 @@ pub enum VimOption {
}
impl VimOption {
- fn possible_commands(query: &str) -> Vec<CommandInterceptResult> {
+ fn possible_commands(query: &str) -> Vec<CommandInterceptItem> {
let mut prefix_of_options = Vec::new();
let mut options = query.split(" ").collect::<Vec<_>>();
let prefix = options.pop().unwrap_or_default();
@@ -102,7 +108,7 @@ impl VimOption {
let mut options = prefix_of_options.clone();
options.push(possible);
- CommandInterceptResult {
+ CommandInterceptItem {
string: format!(
":set {}",
options.iter().map(|opt| opt.to_string()).join(" ")
@@ -725,6 +731,13 @@ struct VimCommand {
>,
>,
has_count: bool,
+ has_filename: bool,
+}
+
+struct ParsedQuery {
+ args: String,
+ has_bang: bool,
+ has_space: bool,
}
impl VimCommand {
@@ -760,6 +773,15 @@ impl VimCommand {
self
}
+ fn filename(
+ mut self,
+ f: impl Fn(Box<dyn Action>, String) -> Option<Box<dyn Action>> + Send + Sync + 'static,
+ ) -> Self {
+ self.args = Some(Box::new(f));
+ self.has_filename = true;
+ self
+ }
+
fn range(
mut self,
f: impl Fn(Box<dyn Action>, &CommandRange) -> Option<Box<dyn Action>> + Send + Sync + 'static,
@@ -773,14 +795,80 @@ impl VimCommand {
self
}
- fn parse(
- &self,
- query: &str,
- range: &Option<CommandRange>,
- cx: &App,
- ) -> Option<Box<dyn Action>> {
+ fn generate_filename_completions(
+ parsed_query: &ParsedQuery,
+ workspace: WeakEntity<Workspace>,
+ cx: &mut App,
+ ) -> Task<Vec<String>> {
+ let ParsedQuery {
+ args,
+ has_bang: _,
+ has_space: _,
+ } = parsed_query;
+ let Some(workspace) = workspace.upgrade() else {
+ return Task::ready(Vec::new());
+ };
+
+ let (task, args_path) = workspace.update(cx, |workspace, cx| {
+ let prefix = workspace
+ .project()
+ .read(cx)
+ .visible_worktrees(cx)
+ .map(|worktree| worktree.read(cx).abs_path().to_path_buf())
+ .next()
+ .or_else(std::env::home_dir)
+ .unwrap_or_else(|| PathBuf::from(""));
+
+ let rel_path = match RelPath::new(Path::new(&args), PathStyle::local()) {
+ Ok(path) => path.to_rel_path_buf(),
+ Err(_) => {
+ return (Task::ready(Ok(Vec::new())), RelPathBuf::new());
+ }
+ };
+
+ let rel_path = if args.ends_with(PathStyle::local().separator()) {
+ rel_path
+ } else {
+ rel_path
+ .parent()
+ .map(|rel_path| rel_path.to_rel_path_buf())
+ .unwrap_or(RelPathBuf::new())
+ };
+
+ let task = workspace.project().update(cx, |project, cx| {
+ let path = prefix
+ .join(rel_path.as_std_path())
+ .to_string_lossy()
+ .to_string();
+ project.list_directory(path, cx)
+ });
+
+ (task, rel_path)
+ });
+
+ cx.background_spawn(async move {
+ let directories = task.await.unwrap_or_default();
+ directories
+ .iter()
+ .map(|dir| {
+ let path = RelPath::new(dir.path.as_path(), PathStyle::local())
+ .map(|cow| cow.into_owned())
+ .unwrap_or(RelPathBuf::new());
+ let mut path_string = args_path
+ .join(&path)
+ .display(PathStyle::local())
+ .to_string();
+ if dir.is_dir {
+ path_string.push_str(PathStyle::local().separator());
+ }
+ path_string
+ })
+ .collect()
+ })
+ }
+
+ fn get_parsed_query(&self, query: String) -> Option<ParsedQuery> {
let rest = query
- .to_string()
.strip_prefix(self.prefix)?
.to_string()
.chars()
@@ -789,6 +877,7 @@ impl VimCommand {
.filter_map(|e| e.left())
.collect::<String>();
let has_bang = rest.starts_with('!');
+ let has_space = rest.starts_with("! ") || rest.starts_with(' ');
let args = if has_bang {
rest.strip_prefix('!')?.trim().to_string()
} else if rest.is_empty() {
@@ -796,7 +885,24 @@ impl VimCommand {
} else {
rest.strip_prefix(' ')?.trim().to_string()
};
+ Some(ParsedQuery {
+ args,
+ has_bang,
+ has_space,
+ })
+ }
+ fn parse(
+ &self,
+ query: &str,
+ range: &Option<CommandRange>,
+ cx: &App,
+ ) -> Option<Box<dyn Action>> {
+ let ParsedQuery {
+ args,
+ has_bang,
+ has_space: _,
+ } = self.get_parsed_query(query.to_string())?;
let action = if has_bang && self.bang_action.is_some() {
self.bang_action.as_ref().unwrap().boxed_clone()
} else if let Some(action) = self.action.as_ref() {
@@ -1056,18 +1162,43 @@ fn generate_commands(_: &App) -> Vec<VimCommand> {
.bang(workspace::Save {
save_intent: Some(SaveIntent::Overwrite),
})
- .args(|action, args| {
+ .filename(|action, filename| {
Some(
VimSave {
save_intent: action
.as_any()
.downcast_ref::<workspace::Save>()
.and_then(|action| action.save_intent),
- filename: args,
+ filename,
}
.boxed_clone(),
)
}),
+ VimCommand::new(("e", "dit"), editor::actions::ReloadFile)
+ .bang(editor::actions::ReloadFile)
+ .filename(|_, filename| Some(VimEdit { filename }.boxed_clone())),
+ VimCommand::new(("sp", "lit"), workspace::SplitHorizontal).filename(|_, filename| {
+ Some(
+ VimSplit {
+ vertical: false,
+ filename,
+ }
+ .boxed_clone(),
+ )
+ }),
+ VimCommand::new(("vs", "plit"), workspace::SplitVertical).filename(|_, filename| {
+ Some(
+ VimSplit {
+ vertical: true,
+ filename,
+ }
+ .boxed_clone(),
+ )
+ }),
+ VimCommand::new(("tabe", "dit"), workspace::NewFile)
+ .filename(|_action, filename| Some(VimEdit { filename }.boxed_clone())),
+ VimCommand::new(("tabnew", ""), workspace::NewFile)
+ .filename(|_action, filename| Some(VimEdit { filename }.boxed_clone())),
VimCommand::new(
("q", "uit"),
workspace::CloseActiveItem {
@@ -1164,24 +1295,6 @@ fn generate_commands(_: &App) -> Vec<VimCommand> {
save_intent: Some(SaveIntent::Overwrite),
}),
VimCommand::new(("cq", "uit"), zed_actions::Quit),
- VimCommand::new(("sp", "lit"), workspace::SplitHorizontal).args(|_, args| {
- Some(
- VimSplit {
- vertical: false,
- filename: args,
- }
- .boxed_clone(),
- )
- }),
- VimCommand::new(("vs", "plit"), workspace::SplitVertical).args(|_, args| {
- Some(
- VimSplit {
- vertical: true,
- filename: args,
- }
- .boxed_clone(),
- )
- }),
VimCommand::new(
("bd", "elete"),
workspace::CloseActiveItem {
@@ -1224,10 +1337,6 @@ fn generate_commands(_: &App) -> Vec<VimCommand> {
VimCommand::str(("ls", ""), "tab_switcher::ToggleAll"),
VimCommand::new(("new", ""), workspace::NewFileSplitHorizontal),
VimCommand::new(("vne", "w"), workspace::NewFileSplitVertical),
- VimCommand::new(("tabe", "dit"), workspace::NewFile)
- .args(|_action, args| Some(VimEdit { filename: args }.boxed_clone())),
- VimCommand::new(("tabnew", ""), workspace::NewFile)
- .args(|_action, args| Some(VimEdit { filename: args }.boxed_clone())),
VimCommand::new(("tabn", "ext"), workspace::ActivateNextItem).count(),
VimCommand::new(("tabp", "revious"), workspace::ActivatePreviousItem).count(),
VimCommand::new(("tabN", "ext"), workspace::ActivatePreviousItem).count(),
@@ -1327,9 +1436,6 @@ fn generate_commands(_: &App) -> Vec<VimCommand> {
VimCommand::new(("$", ""), EndOfDocument),
VimCommand::new(("%", ""), EndOfDocument),
VimCommand::new(("0", ""), StartOfDocument),
- VimCommand::new(("e", "dit"), editor::actions::ReloadFile)
- .bang(editor::actions::ReloadFile)
- .args(|_, args| Some(VimEdit { filename: args }.boxed_clone())),
VimCommand::new(("ex", ""), editor::actions::ReloadFile).bang(editor::actions::ReloadFile),
VimCommand::new(("cpp", "link"), editor::actions::CopyPermalinkToLine).range(act_on_range),
VimCommand::str(("opt", "ions"), "zed::OpenDefaultSettings"),
@@ -1383,18 +1489,30 @@ fn wrap_count(action: Box<dyn Action>, range: &CommandRange) -> Option<Box<dyn A
})
}
-pub fn command_interceptor(mut input: &str, cx: &App) -> Vec<CommandInterceptResult> {
- // NOTE: We also need to support passing arguments to commands like :w
- // (ideally with filename autocompletion).
+pub fn command_interceptor(
+ mut input: &str,
+ workspace: WeakEntity<Workspace>,
+ cx: &mut App,
+) -> Task<CommandInterceptResult> {
while input.starts_with(':') {
input = &input[1..];
}
let (range, query) = VimCommand::parse_range(input);
let range_prefix = input[0..(input.len() - query.len())].to_string();
- let query = query.as_str().trim();
+ let has_trailing_space = query.ends_with(" ");
+ let mut query = query.as_str().trim();
+
+ let on_matching_lines = (query.starts_with('g') || query.starts_with('v'))
+ .then(|| {
+ let (pattern, range, search, invert) = OnMatchingLines::parse(query, &range)?;
+ let start_idx = query.len() - pattern.len();
+ query = query[start_idx..].trim();
+ Some((range, search, invert))
+ })
+ .flatten();
- let action = if range.is_some() && query.is_empty() {
+ let mut action = if range.is_some() && query.is_empty() {
Some(
GoToLine {
range: range.clone().unwrap(),
@@ -1418,7 +1536,10 @@ pub fn command_interceptor(mut input: &str, cx: &App) -> Vec<CommandInterceptRes
command.positions = generate_positions(&command.string, &query);
}
}
- return commands;
+ return Task::ready(CommandInterceptResult {
+ results: commands,
+ exclusive: false,
+ });
} else if query.starts_with('s') {
let mut substitute = "substitute".chars().peekable();
let mut query = query.chars().peekable();
@@ -1438,58 +1559,138 @@ pub fn command_interceptor(mut input: &str, cx: &App) -> Vec<CommandInterceptRes
} else {
None
}
- } else if query.starts_with('g') || query.starts_with('v') {
- let mut global = "global".chars().peekable();
- let mut query = query.chars().peekable();
- let mut invert = false;
- if query.peek() == Some(&'v') {
- invert = true;
- query.next();
- }
- while global.peek().is_some_and(|char| Some(char) == query.peek()) {
- global.next();
- query.next();
- }
- if !invert && query.peek() == Some(&'!') {
- invert = true;
- query.next();
- }
- let range = range.clone().unwrap_or(CommandRange {
- start: Position::Line { row: 0, offset: 0 },
- end: Some(Position::LastLine { offset: 0 }),
- });
- OnMatchingLines::parse(query, invert, range, cx).map(|action| action.boxed_clone())
} else if query.contains('!') {
ShellExec::parse(query, range.clone())
+ } else if on_matching_lines.is_some() {
+ commands(cx)
+ .iter()
+ .find_map(|command| command.parse(query, &range, cx))
} else {
None
};
+
+ if let Some((range, search, invert)) = on_matching_lines
+ && let Some(ref inner) = action
+ {
+ action = Some(Box::new(OnMatchingLines {
+ range,
+ search,
+ action: WrappedAction(inner.boxed_clone()),
+ invert,
+ }));
+ };
+
if let Some(action) = action {
let string = input.to_string();
let positions = generate_positions(&string, &(range_prefix + query));
- return vec![CommandInterceptResult {
- action,
- string,
- positions,
- }];
+ return Task::ready(CommandInterceptResult {
+ results: vec![CommandInterceptItem {
+ action,
+ string,
+ positions,
+ }],
+ exclusive: false,
+ });
}
- for command in commands(cx).iter() {
- if let Some(action) = command.parse(query, &range, cx) {
- let mut string = ":".to_owned() + &range_prefix + command.prefix + command.suffix;
- if query.contains('!') {
- string.push('!');
- }
- let positions = generate_positions(&string, &(range_prefix + query));
-
- return vec![CommandInterceptResult {
+ let Some((mut results, filenames)) =
+ commands(cx).iter().enumerate().find_map(|(idx, command)| {
+ let action = command.parse(query, &range, cx)?;
+ let parsed_query = command.get_parsed_query(query.into())?;
+ let display_string = ":".to_owned()
+ + &range_prefix
+ + command.prefix
+ + command.suffix
+ + if parsed_query.has_bang { "!" } else { "" };
+ let space = if parsed_query.has_space { " " } else { "" };
+
+ let string = format!("{}{}{}", &display_string, &space, &parsed_query.args);
+ let positions = generate_positions(&string, &(range_prefix.clone() + query));
+
+ let results = vec![CommandInterceptItem {
action,
string,
positions,
}];
- }
+
+ let no_args_positions =
+ generate_positions(&display_string, &(range_prefix.clone() + query));
+
+ // The following are valid autocomplete scenarios:
+ // :w!filename.txt
+ // :w filename.txt
+ // :w[space]
+ if !command.has_filename
+ || (!has_trailing_space && !parsed_query.has_bang && parsed_query.args.is_empty())
+ {
+ return Some((results, None));
+ }
+
+ Some((
+ results,
+ Some((idx, parsed_query, display_string, no_args_positions)),
+ ))
+ })
+ else {
+ return Task::ready(CommandInterceptResult::default());
+ };
+
+ if let Some((cmd_idx, parsed_query, display_string, no_args_positions)) = filenames {
+ let filenames = VimCommand::generate_filename_completions(&parsed_query, workspace, cx);
+ cx.spawn(async move |cx| {
+ let filenames = filenames.await;
+ const MAX_RESULTS: usize = 100;
+ let executor = cx.background_executor().clone();
+ let mut candidates = Vec::with_capacity(filenames.len());
+
+ for (idx, filename) in filenames.iter().enumerate() {
+ candidates.push(fuzzy::StringMatchCandidate::new(idx, &filename));
+ }
+ let filenames = fuzzy::match_strings(
+ &candidates,
+ &parsed_query.args,
+ false,
+ true,
+ MAX_RESULTS,
+ &Default::default(),
+ executor,
+ )
+ .await;
+
+ for fuzzy::StringMatch {
+ candidate_id: _,
+ score: _,
+ positions,
+ string,
+ } in filenames
+ {
+ let offset = display_string.len() + 1;
+ let mut positions: Vec<_> = positions.iter().map(|&pos| pos + offset).collect();
+ positions.splice(0..0, no_args_positions.clone());
+ let string = format!("{display_string} {string}");
+ let action = match cx
+ .update(|cx| commands(cx).get(cmd_idx)?.parse(&string[1..], &range, cx))
+ {
+ Ok(Some(action)) => action,
+ _ => continue,
+ };
+ results.push(CommandInterceptItem {
+ action,
+ string,
+ positions,
+ });
+ }
+ CommandInterceptResult {
+ results,
+ exclusive: true,
+ }
+ })
+ } else {
+ Task::ready(CommandInterceptResult {
+ results,
+ exclusive: false,
+ })
}
- Vec::default()
}
fn generate_positions(string: &str, query: &str) -> Vec<usize> {
@@ -1530,19 +1731,40 @@ impl OnMatchingLines {
// but we do flip \( and \) to ( and ) (and vice-versa) in the pattern,
// and convert \0..\9 to $0..$9 in the replacement so that common idioms work.
pub(crate) fn parse(
- mut chars: Peekable<Chars>,
- invert: bool,
- range: CommandRange,
- cx: &App,
- ) -> Option<Self> {
- let delimiter = chars.next().filter(|c| {
+ query: &str,
+ range: &Option<CommandRange>,
+ ) -> Option<(String, CommandRange, String, bool)> {
+ let mut global = "global".chars().peekable();
+ let mut query_chars = query.chars().peekable();
+ let mut invert = false;
+ if query_chars.peek() == Some(&'v') {
+ invert = true;
+ query_chars.next();
+ }
+ while global
+ .peek()
+ .is_some_and(|char| Some(char) == query_chars.peek())
+ {
+ global.next();
+ query_chars.next();
+ }
+ if !invert && query_chars.peek() == Some(&'!') {
+ invert = true;
+ query_chars.next();
+ }
+ let range = range.clone().unwrap_or(CommandRange {
+ start: Position::Line { row: 0, offset: 0 },
+ end: Some(Position::LastLine { offset: 0 }),
+ });
+
+ let delimiter = query_chars.next().filter(|c| {
!c.is_alphanumeric() && *c != '"' && *c != '|' && *c != '\'' && *c != '!'
})?;
let mut search = String::new();
let mut escaped = false;
- for c in chars.by_ref() {
+ for c in query_chars.by_ref() {
if escaped {
escaped = false;
// unescape escaped parens
@@ -1563,21 +1785,7 @@ impl OnMatchingLines {
}
}
- let command: String = chars.collect();
-
- let action = WrappedAction(
- command_interceptor(&command, cx)
- .first()?
- .action
- .boxed_clone(),
- );
-
- Some(Self {
- range,
- search,
- invert,
- action,
- })
+ Some((query_chars.collect::<String>(), range, search, invert))
}
pub fn run(&self, vim: &mut Vim, window: &mut Window, cx: &mut Context<Vim>) {
@@ -2184,7 +2392,8 @@ mod test {
assert_eq!(fs.load(path).await.unwrap().replace("\r\n", "\n"), "oops\n");
assert!(!cx.has_pending_prompt());
- cx.simulate_keystrokes(": w ! enter");
+ cx.simulate_keystrokes(": w !");
+ cx.simulate_keystrokes("enter");
assert!(!cx.has_pending_prompt());
assert_eq!(fs.load(path).await.unwrap().replace("\r\n", "\n"), "@@\n");
}
@@ -2342,7 +2551,7 @@ mod test {
}
#[gpui::test]
- async fn test_w_command(cx: &mut TestAppContext) {
+ async fn test_command_write_filename(cx: &mut TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
cx.workspace(|workspace, _, cx| {