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 itertools::Itertools;
|
||||||
use language::Point;
|
use language::Point;
|
||||||
use multi_buffer::MultiBufferRow;
|
use multi_buffer::MultiBufferRow;
|
||||||
|
use project::ProjectPath;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use search::{BufferSearchBar, SearchOptions};
|
use search::{BufferSearchBar, SearchOptions};
|
||||||
|
@ -19,15 +20,17 @@ use std::{
|
||||||
io::Write,
|
io::Write,
|
||||||
iter::Peekable,
|
iter::Peekable,
|
||||||
ops::{Deref, Range},
|
ops::{Deref, Range},
|
||||||
|
path::Path,
|
||||||
process::Stdio,
|
process::Stdio,
|
||||||
str::Chars,
|
str::Chars,
|
||||||
sync::OnceLock,
|
sync::{Arc, OnceLock},
|
||||||
time::Instant,
|
time::Instant,
|
||||||
};
|
};
|
||||||
use task::{HideStrategy, RevealStrategy, SpawnInTerminal, TaskId};
|
use task::{HideStrategy, RevealStrategy, SpawnInTerminal, TaskId};
|
||||||
use ui::ActiveTheme;
|
use ui::ActiveTheme;
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use workspace::{SaveIntent, notifications::NotifyResultExt};
|
use workspace::notifications::DetachAndPromptErr;
|
||||||
|
use workspace::{Item, SaveIntent, notifications::NotifyResultExt};
|
||||||
use zed_actions::{OpenDocs, RevealTarget};
|
use zed_actions::{OpenDocs, RevealTarget};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -157,6 +160,12 @@ pub struct VimSet {
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct WrappedAction(Box<dyn Action>);
|
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]);
|
actions!(vim, [VisualCommand, CountCommand, ShellCommand]);
|
||||||
impl_internal_actions!(
|
impl_internal_actions!(
|
||||||
vim,
|
vim,
|
||||||
|
@ -168,6 +177,7 @@ impl_internal_actions!(
|
||||||
OnMatchingLines,
|
OnMatchingLines,
|
||||||
ShellExec,
|
ShellExec,
|
||||||
VimSet,
|
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| {
|
Vim::action(editor, cx, |vim, _: &CountCommand, window, cx| {
|
||||||
let Some(workspace) = vim.workspace(window) else {
|
let Some(workspace) = vim.workspace(window) else {
|
||||||
return;
|
return;
|
||||||
|
@ -364,6 +415,9 @@ struct VimCommand {
|
||||||
action: Option<Box<dyn Action>>,
|
action: Option<Box<dyn Action>>,
|
||||||
action_name: Option<&'static str>,
|
action_name: Option<&'static str>,
|
||||||
bang_action: Option<Box<dyn Action>>,
|
bang_action: Option<Box<dyn Action>>,
|
||||||
|
args: Option<
|
||||||
|
Box<dyn Fn(Box<dyn Action>, String) -> Option<Box<dyn Action>> + Send + Sync + 'static>,
|
||||||
|
>,
|
||||||
range: Option<
|
range: Option<
|
||||||
Box<
|
Box<
|
||||||
dyn Fn(Box<dyn Action>, &CommandRange) -> Option<Box<dyn Action>>
|
dyn Fn(Box<dyn Action>, &CommandRange) -> Option<Box<dyn Action>>
|
||||||
|
@ -400,6 +454,14 @@ impl VimCommand {
|
||||||
self
|
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(
|
fn range(
|
||||||
mut self,
|
mut self,
|
||||||
f: impl Fn(Box<dyn Action>, &CommandRange) -> Option<Box<dyn Action>> + Send + Sync + 'static,
|
f: impl Fn(Box<dyn Action>, &CommandRange) -> Option<Box<dyn Action>> + Send + Sync + 'static,
|
||||||
|
@ -415,19 +477,27 @@ impl VimCommand {
|
||||||
|
|
||||||
fn parse(
|
fn parse(
|
||||||
&self,
|
&self,
|
||||||
mut query: &str,
|
query: &str,
|
||||||
range: &Option<CommandRange>,
|
range: &Option<CommandRange>,
|
||||||
cx: &App,
|
cx: &App,
|
||||||
) -> Option<Box<dyn Action>> {
|
) -> Option<Box<dyn Action>> {
|
||||||
let has_bang = query.ends_with('!');
|
let rest = query
|
||||||
if has_bang {
|
.to_string()
|
||||||
query = &query[..query.len() - 1];
|
.strip_prefix(self.prefix)?
|
||||||
}
|
.to_string()
|
||||||
|
.chars()
|
||||||
let suffix = query.strip_prefix(self.prefix)?;
|
.zip_longest(self.suffix.to_string().chars())
|
||||||
if !self.suffix.starts_with(suffix) {
|
.skip_while(|e| e.clone().both().map(|(s, q)| s == q).unwrap_or(false))
|
||||||
return None;
|
.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() {
|
let action = if has_bang && self.bang_action.is_some() {
|
||||||
self.bang_action.as_ref().unwrap().boxed_clone()
|
self.bang_action.as_ref().unwrap().boxed_clone()
|
||||||
|
@ -438,8 +508,14 @@ impl VimCommand {
|
||||||
} else {
|
} else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
|
if !args.is_empty() {
|
||||||
if let Some(range) = range {
|
// 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))
|
self.range.as_ref().and_then(|f| f(action, range))
|
||||||
} else {
|
} else {
|
||||||
Some(action)
|
Some(action)
|
||||||
|
@ -680,6 +756,18 @@ fn generate_commands(_: &App) -> Vec<VimCommand> {
|
||||||
)
|
)
|
||||||
.bang(workspace::Save {
|
.bang(workspace::Save {
|
||||||
save_intent: Some(SaveIntent::Overwrite),
|
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(
|
VimCommand::new(
|
||||||
("q", "uit"),
|
("q", "uit"),
|
||||||
|
@ -1035,7 +1123,7 @@ pub fn command_interceptor(mut input: &str, cx: &App) -> Vec<CommandInterceptRes
|
||||||
for command in commands(cx).iter() {
|
for command in commands(cx).iter() {
|
||||||
if let Some(action) = command.parse(query, &range, cx) {
|
if let Some(action) = command.parse(query, &range, cx) {
|
||||||
let mut string = ":".to_owned() + &range_prefix + command.prefix + command.suffix;
|
let mut string = ":".to_owned() + &range_prefix + command.prefix + command.suffix;
|
||||||
if query.ends_with('!') {
|
if query.contains('!') {
|
||||||
string.push('!');
|
string.push('!');
|
||||||
}
|
}
|
||||||
let positions = generate_positions(&string, &(range_prefix + query));
|
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");
|
cx.shared_state().await.assert_eq("k\nk\nˇk\n4\n4\n3\n2\n1");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
fn assert_active_item(
|
fn assert_active_item(
|
||||||
workspace: &mut Workspace,
|
workspace: &mut Workspace,
|
||||||
expected_path: &str,
|
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]
|
#[gpui::test]
|
||||||
async fn test_command_matching_lines(cx: &mut TestAppContext) {
|
async fn test_command_matching_lines(cx: &mut TestAppContext) {
|
||||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue