mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
vim: Add vim command filename autocomplete (#36332)
Release Notes: - Adds filename autocomplete for vim commands: - write - edit - split - vsplit - tabedit - tabnew - Makes command palette interceptor async <img width="1382" height="634" alt="image" src="https://github.com/user-attachments/assets/e7bf01c5-e9cd-4a7d-b38c-12fc3df5069f" /> --------- Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
This commit is contained in:
parent
908ae95cf8
commit
0cbab311a1
7 changed files with 426 additions and 184 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -3755,6 +3755,7 @@ dependencies = [
|
|||
"collections",
|
||||
"derive_more",
|
||||
"gpui",
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
|
|
@ -18743,6 +18744,7 @@ dependencies = [
|
|||
"editor",
|
||||
"env_logger 0.11.8",
|
||||
"futures 0.3.31",
|
||||
"fuzzy",
|
||||
"git_ui",
|
||||
"gpui",
|
||||
"indoc",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
_ => {
|
||||
|
|
|
|||
|
|
@ -17,3 +17,4 @@ collections.workspace = true
|
|||
derive_more.workspace = true
|
||||
gpui.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
workspace.workspace = true
|
||||
|
|
|
|||
|
|
@ -2,16 +2,16 @@
|
|||
|
||||
#![deny(missing_docs)]
|
||||
|
||||
use std::any::TypeId;
|
||||
use std::{any::TypeId, rc::Rc};
|
||||
|
||||
use collections::HashSet;
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use gpui::{Action, App, BorrowAppContext, Global};
|
||||
use gpui::{Action, App, BorrowAppContext, Global, Task, WeakEntity};
|
||||
use workspace::Workspace;
|
||||
|
||||
/// Initializes the command palette hooks.
|
||||
pub fn init(cx: &mut App) {
|
||||
cx.set_global(GlobalCommandPaletteFilter::default());
|
||||
cx.set_global(GlobalCommandPaletteInterceptor::default());
|
||||
}
|
||||
|
||||
/// A filter for the command palette.
|
||||
|
|
@ -94,7 +94,7 @@ impl CommandPaletteFilter {
|
|||
|
||||
/// The result of intercepting a command palette command.
|
||||
#[derive(Debug)]
|
||||
pub struct CommandInterceptResult {
|
||||
pub struct CommandInterceptItem {
|
||||
/// The action produced as a result of the interception.
|
||||
pub action: Box<dyn Action>,
|
||||
/// The display string to show in the command palette for this result.
|
||||
|
|
@ -104,50 +104,50 @@ pub struct CommandInterceptResult {
|
|||
pub positions: Vec<usize>,
|
||||
}
|
||||
|
||||
/// An interceptor for the command palette.
|
||||
#[derive(Default)]
|
||||
pub struct CommandPaletteInterceptor(
|
||||
Option<Box<dyn Fn(&str, &App) -> Vec<CommandInterceptResult>>>,
|
||||
);
|
||||
/// The result of intercepting a command palette command.
|
||||
#[derive(Default, Debug)]
|
||||
pub struct CommandInterceptResult {
|
||||
/// The items
|
||||
pub results: Vec<CommandInterceptItem>,
|
||||
/// Whether or not to continue to show the normal matches
|
||||
pub exclusive: bool,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct GlobalCommandPaletteInterceptor(CommandPaletteInterceptor);
|
||||
/// An interceptor for the command palette.
|
||||
#[derive(Clone)]
|
||||
pub struct GlobalCommandPaletteInterceptor(
|
||||
Rc<dyn Fn(&str, WeakEntity<Workspace>, &mut App) -> Task<CommandInterceptResult>>,
|
||||
);
|
||||
|
||||
impl Global for GlobalCommandPaletteInterceptor {}
|
||||
|
||||
impl CommandPaletteInterceptor {
|
||||
/// Returns the global [`CommandPaletteInterceptor`], if one is set.
|
||||
pub fn try_global(cx: &App) -> Option<&CommandPaletteInterceptor> {
|
||||
cx.try_global::<GlobalCommandPaletteInterceptor>()
|
||||
.map(|interceptor| &interceptor.0)
|
||||
}
|
||||
|
||||
/// Updates the global [`CommandPaletteInterceptor`] using the given closure.
|
||||
pub fn update_global<F, R>(cx: &mut App, update: F) -> R
|
||||
where
|
||||
F: FnOnce(&mut Self, &mut App) -> R,
|
||||
{
|
||||
cx.update_global(|this: &mut GlobalCommandPaletteInterceptor, cx| update(&mut this.0, cx))
|
||||
}
|
||||
|
||||
/// Intercepts the given query from the command palette.
|
||||
pub fn intercept(&self, query: &str, cx: &App) -> Vec<CommandInterceptResult> {
|
||||
if let Some(handler) = self.0.as_ref() {
|
||||
(handler)(query, cx)
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Clears the global interceptor.
|
||||
pub fn clear(&mut self) {
|
||||
self.0 = None;
|
||||
}
|
||||
|
||||
impl GlobalCommandPaletteInterceptor {
|
||||
/// Sets the global interceptor.
|
||||
///
|
||||
/// This will override the previous interceptor, if it exists.
|
||||
pub fn set(&mut self, handler: Box<dyn Fn(&str, &App) -> Vec<CommandInterceptResult>>) {
|
||||
self.0 = Some(handler);
|
||||
pub fn set(
|
||||
cx: &mut App,
|
||||
interceptor: impl Fn(&str, WeakEntity<Workspace>, &mut App) -> Task<CommandInterceptResult>
|
||||
+ 'static,
|
||||
) {
|
||||
cx.set_global(Self(Rc::new(interceptor)));
|
||||
}
|
||||
|
||||
/// Clears the global interceptor.
|
||||
pub fn clear(cx: &mut App) {
|
||||
if cx.has_global::<Self>() {
|
||||
cx.remove_global::<Self>();
|
||||
}
|
||||
}
|
||||
|
||||
/// Intercepts the given query from the command palette.
|
||||
pub fn intercept(
|
||||
query: &str,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
cx: &mut App,
|
||||
) -> Option<Task<CommandInterceptResult>> {
|
||||
let interceptor = cx.try_global::<Self>()?;
|
||||
let handler = interceptor.0.clone();
|
||||
Some(handler(query, workspace, cx))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ db.workspace = true
|
|||
editor.workspace = true
|
||||
env_logger.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
itertools.workspace = true
|
||||
language.workspace = 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 action = if range.is_some() && query.is_empty() {
|
||||
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 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));
|
||||
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 { "" };
|
||||
|
||||
return vec![CommandInterceptResult {
|
||||
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| {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use crate::{ToggleMarksView, ToggleRegistersView, UseSystemClipboard, Vim, VimAd
|
|||
use crate::{motion::Motion, object::Object};
|
||||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use command_palette_hooks::{CommandPaletteFilter, CommandPaletteInterceptor};
|
||||
use command_palette_hooks::{CommandPaletteFilter, GlobalCommandPaletteInterceptor};
|
||||
use db::{
|
||||
sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection},
|
||||
sqlez_macros::sql,
|
||||
|
|
@ -718,9 +718,7 @@ impl VimGlobals {
|
|||
CommandPaletteFilter::update_global(cx, |filter, _| {
|
||||
filter.show_namespace(Vim::NAMESPACE);
|
||||
});
|
||||
CommandPaletteInterceptor::update_global(cx, |interceptor, _| {
|
||||
interceptor.set(Box::new(command_interceptor));
|
||||
});
|
||||
GlobalCommandPaletteInterceptor::set(cx, command_interceptor);
|
||||
for window in cx.windows() {
|
||||
if let Some(workspace) = window.downcast::<Workspace>() {
|
||||
workspace
|
||||
|
|
@ -735,9 +733,7 @@ impl VimGlobals {
|
|||
} else {
|
||||
KeyBinding::set_vim_mode(cx, false);
|
||||
*Vim::globals(cx) = VimGlobals::default();
|
||||
CommandPaletteInterceptor::update_global(cx, |interceptor, _| {
|
||||
interceptor.clear();
|
||||
});
|
||||
GlobalCommandPaletteInterceptor::clear(cx);
|
||||
CommandPaletteFilter::update_global(cx, |filter, _| {
|
||||
filter.hide_namespace(Vim::NAMESPACE);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue