vim: Add :w <filename>
command (#29256)
Closes https://github.com/zed-industries/zed/issues/10920 Release Notes: - vim: Adds support for `:w[rite] <filename>` --------- Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
This commit is contained in:
parent
196586e352
commit
5a38bbbd22
1 changed files with 137 additions and 15 deletions
|
@ -11,6 +11,7 @@ use gpui::{Action, App, AppContext as _, Context, Global, Window, actions, impl_
|
|||
use itertools::Itertools;
|
||||
use language::Point;
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use project::ProjectPath;
|
||||
use regex::Regex;
|
||||
use schemars::JsonSchema;
|
||||
use search::{BufferSearchBar, SearchOptions};
|
||||
|
@ -19,15 +20,17 @@ use std::{
|
|||
io::Write,
|
||||
iter::Peekable,
|
||||
ops::{Deref, Range},
|
||||
path::Path,
|
||||
process::Stdio,
|
||||
str::Chars,
|
||||
sync::OnceLock,
|
||||
sync::{Arc, OnceLock},
|
||||
time::Instant,
|
||||
};
|
||||
use task::{HideStrategy, RevealStrategy, SpawnInTerminal, TaskId};
|
||||
use ui::ActiveTheme;
|
||||
use util::ResultExt;
|
||||
use workspace::{SaveIntent, notifications::NotifyResultExt};
|
||||
use workspace::notifications::DetachAndPromptErr;
|
||||
use workspace::{Item, SaveIntent, notifications::NotifyResultExt};
|
||||
use zed_actions::{OpenDocs, RevealTarget};
|
||||
|
||||
use crate::{
|
||||
|
@ -157,6 +160,12 @@ pub struct VimSet {
|
|||
#[derive(Debug)]
|
||||
struct WrappedAction(Box<dyn Action>);
|
||||
|
||||
#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
|
||||
struct VimSave {
|
||||
pub save_intent: Option<SaveIntent>,
|
||||
pub filename: String,
|
||||
}
|
||||
|
||||
actions!(vim, [VisualCommand, CountCommand, ShellCommand]);
|
||||
impl_internal_actions!(
|
||||
vim,
|
||||
|
@ -168,6 +177,7 @@ impl_internal_actions!(
|
|||
OnMatchingLines,
|
||||
ShellExec,
|
||||
VimSet,
|
||||
VimSave,
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -229,6 +239,47 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
|
|||
})
|
||||
});
|
||||
|
||||
Vim::action(editor, cx, |vim, action: &VimSave, window, cx| {
|
||||
vim.update_editor(window, cx, |_, editor, window, cx| {
|
||||
let Some(project) = editor.project.clone() else {
|
||||
return;
|
||||
};
|
||||
let Some(worktree) = project.read(cx).visible_worktrees(cx).next() else {
|
||||
return;
|
||||
};
|
||||
let project_path = ProjectPath {
|
||||
worktree_id: worktree.read(cx).id(),
|
||||
path: Arc::from(Path::new(&action.filename)),
|
||||
};
|
||||
|
||||
if project.read(cx).entry_for_path(&project_path, cx).is_some() && action.save_intent != Some(SaveIntent::Overwrite) {
|
||||
let answer = window.prompt(
|
||||
gpui::PromptLevel::Critical,
|
||||
&format!("{} already exists. Do you want to replace it?", project_path.path.to_string_lossy()),
|
||||
Some(
|
||||
"A file or folder with the same name already exists. Replacing it will overwrite its current contents.",
|
||||
),
|
||||
&["Replace", "Cancel"],
|
||||
cx);
|
||||
cx.spawn_in(window, async move |editor, cx| {
|
||||
if answer.await.ok() != Some(0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let _ = editor.update_in(cx, |editor, window, cx|{
|
||||
editor
|
||||
.save_as(project, project_path, window, cx)
|
||||
.detach_and_prompt_err("Failed to :w", window, cx, |_, _, _| None);
|
||||
});
|
||||
}).detach();
|
||||
} else {
|
||||
editor
|
||||
.save_as(project, project_path, window, cx)
|
||||
.detach_and_prompt_err("Failed to :w", window, cx, |_, _, _| None);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Vim::action(editor, cx, |vim, _: &CountCommand, window, cx| {
|
||||
let Some(workspace) = vim.workspace(window) else {
|
||||
return;
|
||||
|
@ -364,6 +415,9 @@ struct VimCommand {
|
|||
action: Option<Box<dyn Action>>,
|
||||
action_name: Option<&'static str>,
|
||||
bang_action: Option<Box<dyn Action>>,
|
||||
args: Option<
|
||||
Box<dyn Fn(Box<dyn Action>, String) -> Option<Box<dyn Action>> + Send + Sync + 'static>,
|
||||
>,
|
||||
range: Option<
|
||||
Box<
|
||||
dyn Fn(Box<dyn Action>, &CommandRange) -> Option<Box<dyn Action>>
|
||||
|
@ -400,6 +454,14 @@ impl VimCommand {
|
|||
self
|
||||
}
|
||||
|
||||
fn args(
|
||||
mut self,
|
||||
f: impl Fn(Box<dyn Action>, String) -> Option<Box<dyn Action>> + Send + Sync + 'static,
|
||||
) -> Self {
|
||||
self.args = Some(Box::new(f));
|
||||
self
|
||||
}
|
||||
|
||||
fn range(
|
||||
mut self,
|
||||
f: impl Fn(Box<dyn Action>, &CommandRange) -> Option<Box<dyn Action>> + Send + Sync + 'static,
|
||||
|
@ -415,19 +477,27 @@ impl VimCommand {
|
|||
|
||||
fn parse(
|
||||
&self,
|
||||
mut query: &str,
|
||||
query: &str,
|
||||
range: &Option<CommandRange>,
|
||||
cx: &App,
|
||||
) -> Option<Box<dyn Action>> {
|
||||
let has_bang = query.ends_with('!');
|
||||
if has_bang {
|
||||
query = &query[..query.len() - 1];
|
||||
}
|
||||
|
||||
let suffix = query.strip_prefix(self.prefix)?;
|
||||
if !self.suffix.starts_with(suffix) {
|
||||
return None;
|
||||
}
|
||||
let rest = query
|
||||
.to_string()
|
||||
.strip_prefix(self.prefix)?
|
||||
.to_string()
|
||||
.chars()
|
||||
.zip_longest(self.suffix.to_string().chars())
|
||||
.skip_while(|e| e.clone().both().map(|(s, q)| s == q).unwrap_or(false))
|
||||
.filter_map(|e| e.left())
|
||||
.collect::<String>();
|
||||
let has_bang = rest.starts_with('!');
|
||||
let args = if has_bang {
|
||||
rest.strip_prefix('!')?.trim().to_string()
|
||||
} else if rest.is_empty() {
|
||||
"".into()
|
||||
} else {
|
||||
rest.strip_prefix(' ')?.trim().to_string()
|
||||
};
|
||||
|
||||
let action = if has_bang && self.bang_action.is_some() {
|
||||
self.bang_action.as_ref().unwrap().boxed_clone()
|
||||
|
@ -438,8 +508,14 @@ impl VimCommand {
|
|||
} else {
|
||||
return None;
|
||||
};
|
||||
|
||||
if let Some(range) = range {
|
||||
if !args.is_empty() {
|
||||
// if command does not accept args and we have args then we should do no action
|
||||
if let Some(args_fn) = &self.args {
|
||||
args_fn.deref()(action, args)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else if let Some(range) = range {
|
||||
self.range.as_ref().and_then(|f| f(action, range))
|
||||
} else {
|
||||
Some(action)
|
||||
|
@ -680,6 +756,18 @@ fn generate_commands(_: &App) -> Vec<VimCommand> {
|
|||
)
|
||||
.bang(workspace::Save {
|
||||
save_intent: Some(SaveIntent::Overwrite),
|
||||
})
|
||||
.args(|action, args| {
|
||||
Some(
|
||||
VimSave {
|
||||
save_intent: action
|
||||
.as_any()
|
||||
.downcast_ref::<workspace::Save>()
|
||||
.and_then(|action| action.save_intent),
|
||||
filename: args,
|
||||
}
|
||||
.boxed_clone(),
|
||||
)
|
||||
}),
|
||||
VimCommand::new(
|
||||
("q", "uit"),
|
||||
|
@ -1035,7 +1123,7 @@ pub fn command_interceptor(mut input: &str, cx: &App) -> Vec<CommandInterceptRes
|
|||
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.ends_with('!') {
|
||||
if query.contains('!') {
|
||||
string.push('!');
|
||||
}
|
||||
let positions = generate_positions(&string, &(range_prefix + query));
|
||||
|
@ -1808,6 +1896,7 @@ mod test {
|
|||
cx.shared_state().await.assert_eq("k\nk\nˇk\n4\n4\n3\n2\n1");
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_active_item(
|
||||
workspace: &mut Workspace,
|
||||
expected_path: &str,
|
||||
|
@ -1890,6 +1979,39 @@ mod test {
|
|||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_w_command(cx: &mut TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
cx.workspace(|workspace, _, cx| {
|
||||
assert_active_item(workspace, path!("/root/dir/file.rs"), "", cx);
|
||||
});
|
||||
|
||||
cx.simulate_keystrokes(": w space other.rs");
|
||||
cx.simulate_keystrokes("enter");
|
||||
|
||||
cx.workspace(|workspace, _, cx| {
|
||||
assert_active_item(workspace, path!("/root/other.rs"), "", cx);
|
||||
});
|
||||
|
||||
cx.simulate_keystrokes(": w space dir/file.rs");
|
||||
cx.simulate_keystrokes("enter");
|
||||
|
||||
cx.simulate_prompt_answer("Replace");
|
||||
cx.run_until_parked();
|
||||
|
||||
cx.workspace(|workspace, _, cx| {
|
||||
assert_active_item(workspace, path!("/root/dir/file.rs"), "", cx);
|
||||
});
|
||||
|
||||
cx.simulate_keystrokes(": w ! space other.rs");
|
||||
cx.simulate_keystrokes("enter");
|
||||
|
||||
cx.workspace(|workspace, _, cx| {
|
||||
assert_active_item(workspace, path!("/root/other.rs"), "", cx);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_command_matching_lines(cx: &mut TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue