Skip to content

Commit 84f24e4

Browse files
authored
vim: Add :<range>w <filename> command (#41256)
Release Notes: - Adds support for `:[range]w {file}` - This writes the lines in the range to the specified - Adds support for `:[range]w` - This replaces the current file with the selected lines
1 parent 03fad4b commit 84f24e4

File tree

2 files changed

+197
-11
lines changed

2 files changed

+197
-11
lines changed

crates/language/src/buffer.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2055,6 +2055,11 @@ impl Buffer {
20552055
}
20562056
}
20572057

2058+
/// Marks the buffer as having a conflict regardless of current buffer state.
2059+
pub fn set_conflict(&mut self) {
2060+
self.has_conflict = true;
2061+
}
2062+
20582063
/// Checks if the buffer and its file have both changed since the buffer
20592064
/// was last saved or reloaded.
20602065
pub fn has_conflict(&self) -> bool {

crates/vim/src/command.rs

Lines changed: 192 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ pub struct VimSet {
189189
#[derive(Clone, PartialEq, Action)]
190190
#[action(namespace = vim, no_json, no_register)]
191191
struct VimSave {
192+
pub range: Option<CommandRange>,
192193
pub save_intent: Option<SaveIntent>,
193194
pub filename: String,
194195
}
@@ -324,6 +325,134 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
324325
});
325326

326327
Vim::action(editor, cx, |vim, action: &VimSave, window, cx| {
328+
if let Some(range) = &action.range {
329+
vim.update_editor(cx, |vim, editor, cx| {
330+
let Some(range) = range.buffer_range(vim, editor, window, cx).ok() else {
331+
return;
332+
};
333+
let Some((line_ending, text, whole_buffer)) = editor.buffer().update(cx, |multi, cx| {
334+
Some(multi.as_singleton()?.update(cx, |buffer, _| {
335+
(
336+
buffer.line_ending(),
337+
buffer.as_rope().slice_rows(range.start.0..range.end.0 + 1),
338+
range.start.0 == 0 && range.end.0 + 1 >= buffer.row_count(),
339+
)
340+
}))
341+
}) else {
342+
return;
343+
};
344+
345+
let filename = action.filename.clone();
346+
let filename = if filename.is_empty() {
347+
let Some(file) = editor
348+
.buffer()
349+
.read(cx)
350+
.as_singleton()
351+
.and_then(|buffer| buffer.read(cx).file())
352+
else {
353+
let _ = window.prompt(
354+
gpui::PromptLevel::Warning,
355+
"No file name",
356+
Some("Partial buffer write requires file name."),
357+
&["Cancel"],
358+
cx,
359+
);
360+
return;
361+
};
362+
file.path().display(file.path_style(cx)).to_string()
363+
} else {
364+
filename
365+
};
366+
367+
if action.filename.is_empty() {
368+
if whole_buffer {
369+
if let Some(workspace) = vim.workspace(window) {
370+
workspace.update(cx, |workspace, cx| {
371+
workspace
372+
.save_active_item(
373+
action.save_intent.unwrap_or(SaveIntent::Save),
374+
window,
375+
cx,
376+
)
377+
.detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
378+
});
379+
}
380+
return;
381+
}
382+
if Some(SaveIntent::Overwrite) != action.save_intent {
383+
let _ = window.prompt(
384+
gpui::PromptLevel::Warning,
385+
"Use ! to write partial buffer",
386+
Some("Overwriting the current file with selected buffer content requires '!'."),
387+
&["Cancel"],
388+
cx,
389+
);
390+
return;
391+
}
392+
editor.buffer().update(cx, |multi, cx| {
393+
if let Some(buffer) = multi.as_singleton() {
394+
buffer.update(cx, |buffer, _| buffer.set_conflict());
395+
}
396+
});
397+
};
398+
399+
editor.project().unwrap().update(cx, |project, cx| {
400+
let worktree = project.visible_worktrees(cx).next().unwrap();
401+
402+
worktree.update(cx, |worktree, cx| {
403+
let path_style = worktree.path_style();
404+
let Some(path) = RelPath::new(Path::new(&filename), path_style).ok() else {
405+
return;
406+
};
407+
408+
let rx = (worktree.entry_for_path(&path).is_some() && Some(SaveIntent::Overwrite) != action.save_intent).then(|| {
409+
window.prompt(
410+
gpui::PromptLevel::Warning,
411+
&format!("{path:?} already exists. Do you want to replace it?"),
412+
Some(
413+
"A file or folder with the same name already exists. Replacing it will overwrite its current contents.",
414+
),
415+
&["Replace", "Cancel"],
416+
cx
417+
)
418+
});
419+
let filename = filename.clone();
420+
cx.spawn_in(window, async move |this, cx| {
421+
if let Some(rx) = rx
422+
&& Ok(0) != rx.await
423+
{
424+
return;
425+
}
426+
427+
let _ = this.update_in(cx, |worktree, window, cx| {
428+
let Some(path) = RelPath::new(Path::new(&filename), path_style).ok() else {
429+
return;
430+
};
431+
worktree
432+
.write_file(path.into_arc(), text.clone(), line_ending, cx)
433+
.detach_and_prompt_err("Failed to write lines", window, cx, |_, _, _| None);
434+
});
435+
})
436+
.detach();
437+
});
438+
});
439+
});
440+
return;
441+
}
442+
if action.filename.is_empty() {
443+
if let Some(workspace) = vim.workspace(window) {
444+
workspace.update(cx, |workspace, cx| {
445+
workspace
446+
.save_active_item(
447+
action.save_intent.unwrap_or(SaveIntent::Save),
448+
window,
449+
cx,
450+
)
451+
.detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
452+
});
453+
}
454+
return;
455+
}
327456
vim.update_editor(cx, |_, editor, cx| {
328457
let Some(project) = editor.project().cloned() else {
329458
return;
@@ -1175,24 +1304,34 @@ fn generate_commands(_: &App) -> Vec<VimCommand> {
11751304
vec![
11761305
VimCommand::new(
11771306
("w", "rite"),
1178-
workspace::Save {
1307+
VimSave {
11791308
save_intent: Some(SaveIntent::Save),
1309+
filename: "".into(),
1310+
range: None,
11801311
},
11811312
)
1182-
.bang(workspace::Save {
1313+
.bang(VimSave {
11831314
save_intent: Some(SaveIntent::Overwrite),
1315+
filename: "".into(),
1316+
range: None,
11841317
})
11851318
.filename(|action, filename| {
11861319
Some(
11871320
VimSave {
11881321
save_intent: action
11891322
.as_any()
1190-
.downcast_ref::<workspace::Save>()
1323+
.downcast_ref::<VimSave>()
11911324
.and_then(|action| action.save_intent),
11921325
filename,
1326+
range: None,
11931327
}
11941328
.boxed_clone(),
11951329
)
1330+
})
1331+
.range(|action, range| {
1332+
let mut action: VimSave = action.as_any().downcast_ref::<VimSave>().unwrap().clone();
1333+
action.range.replace(range.clone());
1334+
Some(Box::new(action))
11961335
}),
11971336
VimCommand::new(("e", "dit"), editor::actions::ReloadFile)
11981337
.bang(editor::actions::ReloadFile)
@@ -1692,12 +1831,12 @@ pub fn command_interceptor(
16921831
let mut positions: Vec<_> = positions.iter().map(|&pos| pos + offset).collect();
16931832
positions.splice(0..0, no_args_positions.clone());
16941833
let string = format!("{display_string} {string}");
1695-
let action = match cx
1696-
.update(|cx| commands(cx).get(cmd_idx)?.parse(&string[1..], &range, cx))
1697-
{
1698-
Ok(Some(action)) => action,
1699-
_ => continue,
1700-
};
1834+
let (range, query) = VimCommand::parse_range(&string[1..]);
1835+
let action =
1836+
match cx.update(|cx| commands(cx).get(cmd_idx)?.parse(&query, &range, cx)) {
1837+
Ok(Some(action)) => action,
1838+
_ => continue,
1839+
};
17011840
results.push(CommandInterceptItem {
17021841
action,
17031842
string,
@@ -2302,7 +2441,7 @@ impl ShellExec {
23022441

23032442
#[cfg(test)]
23042443
mod test {
2305-
use std::path::Path;
2444+
use std::path::{Path, PathBuf};
23062445

23072446
use crate::{
23082447
VimAddon,
@@ -2314,7 +2453,7 @@ mod test {
23142453
use indoc::indoc;
23152454
use settings::Settings;
23162455
use util::path;
2317-
use workspace::Workspace;
2456+
use workspace::{OpenOptions, Workspace};
23182457

23192458
#[gpui::test]
23202459
async fn test_command_basics(cx: &mut TestAppContext) {
@@ -2619,6 +2758,48 @@ mod test {
26192758
});
26202759
}
26212760

2761+
#[gpui::test]
2762+
async fn test_command_write_range(cx: &mut TestAppContext) {
2763+
let mut cx = VimTestContext::new(cx, true).await;
2764+
2765+
cx.workspace(|workspace, _, cx| {
2766+
assert_active_item(workspace, path!("/root/dir/file.rs"), "", cx);
2767+
});
2768+
2769+
cx.set_state(
2770+
indoc! {"
2771+
The quick
2772+
brown« fox
2773+
jumpsˇ» over
2774+
the lazy dog
2775+
"},
2776+
Mode::Visual,
2777+
);
2778+
2779+
cx.simulate_keystrokes(": w space dir/other.rs");
2780+
cx.simulate_keystrokes("enter");
2781+
2782+
let other = path!("/root/dir/other.rs");
2783+
2784+
let _ = cx
2785+
.workspace(|workspace, window, cx| {
2786+
workspace.open_abs_path(PathBuf::from(other), OpenOptions::default(), window, cx)
2787+
})
2788+
.await;
2789+
2790+
cx.workspace(|workspace, _, cx| {
2791+
assert_active_item(
2792+
workspace,
2793+
other,
2794+
indoc! {"
2795+
brown fox
2796+
jumps over
2797+
"},
2798+
cx,
2799+
);
2800+
});
2801+
}
2802+
26222803
#[gpui::test]
26232804
async fn test_command_matching_lines(cx: &mut TestAppContext) {
26242805
let mut cx = NeovimBackedTestContext::new(cx).await;

0 commit comments

Comments
 (0)