vim: Add :r[ead] [name] command (#45332)
This adds the following Vim commands:
- `:r[ead] [name]`
- `:{range}r[ead] [name]`
The most important parts of this feature are outlined
[here](https://vimhelp.org/insert.txt.html#%3Ar).
The only intentional difference between this and Vim is that Vim only
allows `:read` (no filename) for buffers with a file attached. I am
allowing it for all buffers because I think that could be useful.
Release Notes:
- vim: Added the [`:r[ead] [name]` Vim
command](https://vimhelp.org/insert.txt.html#:read)
---------
Co-authored-by: Ben Kunkle <ben@zed.dev>
This commit is contained in:
@@ -230,6 +230,14 @@ struct VimEdit {
|
||||
pub filename: String,
|
||||
}
|
||||
|
||||
/// Pastes the specified file's contents.
|
||||
#[derive(Clone, PartialEq, Action)]
|
||||
#[action(namespace = vim, no_json, no_register)]
|
||||
struct VimRead {
|
||||
pub range: Option<CommandRange>,
|
||||
pub filename: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Action)]
|
||||
#[action(namespace = vim, no_json, no_register)]
|
||||
struct VimNorm {
|
||||
@@ -643,6 +651,107 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
|
||||
});
|
||||
});
|
||||
|
||||
Vim::action(editor, cx, |vim, action: &VimRead, window, cx| {
|
||||
vim.update_editor(cx, |vim, editor, cx| {
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
let end = if let Some(range) = action.range.clone() {
|
||||
let Some(multi_range) = range.buffer_range(vim, editor, window, cx).log_err()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
match &range.start {
|
||||
// inserting text above the first line uses the command ":0r {name}"
|
||||
Position::Line { row: 0, offset: 0 } if range.end.is_none() => {
|
||||
snapshot.clip_point(Point::new(0, 0), Bias::Right)
|
||||
}
|
||||
_ => snapshot.clip_point(Point::new(multi_range.end.0 + 1, 0), Bias::Right),
|
||||
}
|
||||
} else {
|
||||
let end_row = editor
|
||||
.selections
|
||||
.newest::<Point>(&editor.display_snapshot(cx))
|
||||
.range()
|
||||
.end
|
||||
.row;
|
||||
snapshot.clip_point(Point::new(end_row + 1, 0), Bias::Right)
|
||||
};
|
||||
let is_end_of_file = end == snapshot.max_point();
|
||||
let edit_range = snapshot.anchor_before(end)..snapshot.anchor_before(end);
|
||||
|
||||
let mut text = if is_end_of_file {
|
||||
String::from('\n')
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
let mut task = None;
|
||||
if action.filename.is_empty() {
|
||||
text.push_str(
|
||||
&editor
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.map(|buffer| buffer.read(cx).text())
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
} else {
|
||||
if let Some(project) = editor.project().cloned() {
|
||||
project.update(cx, |project, cx| {
|
||||
let Some(worktree) = project.visible_worktrees(cx).next() else {
|
||||
return;
|
||||
};
|
||||
let path_style = worktree.read(cx).path_style();
|
||||
let Some(path) =
|
||||
RelPath::new(Path::new(&action.filename), path_style).log_err()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
task =
|
||||
Some(worktree.update(cx, |worktree, cx| worktree.load_file(&path, cx)));
|
||||
});
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
cx.spawn_in(window, async move |editor, cx| {
|
||||
if let Some(task) = task {
|
||||
text.push_str(
|
||||
&task
|
||||
.await
|
||||
.log_err()
|
||||
.map(|loaded_file| loaded_file.text)
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
}
|
||||
|
||||
if !text.is_empty() && !is_end_of_file {
|
||||
text.push('\n');
|
||||
}
|
||||
|
||||
let _ = editor.update_in(cx, |editor, window, cx| {
|
||||
editor.transact(window, cx, |editor, window, cx| {
|
||||
editor.edit([(edit_range.clone(), text)], cx);
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
editor.change_selections(Default::default(), window, cx, |s| {
|
||||
let point = if is_end_of_file {
|
||||
Point::new(
|
||||
edit_range.start.to_point(&snapshot).row.saturating_add(1),
|
||||
0,
|
||||
)
|
||||
} else {
|
||||
Point::new(edit_range.start.to_point(&snapshot).row, 0)
|
||||
};
|
||||
s.select_ranges([point..point]);
|
||||
})
|
||||
});
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
});
|
||||
|
||||
Vim::action(editor, cx, |vim, action: &VimNorm, window, cx| {
|
||||
let keystrokes = action
|
||||
.command
|
||||
@@ -1338,6 +1447,27 @@ fn generate_commands(_: &App) -> Vec<VimCommand> {
|
||||
VimCommand::new(("e", "dit"), editor::actions::ReloadFile)
|
||||
.bang(editor::actions::ReloadFile)
|
||||
.filename(|_, filename| Some(VimEdit { filename }.boxed_clone())),
|
||||
VimCommand::new(
|
||||
("r", "ead"),
|
||||
VimRead {
|
||||
range: None,
|
||||
filename: "".into(),
|
||||
},
|
||||
)
|
||||
.filename(|_, filename| {
|
||||
Some(
|
||||
VimRead {
|
||||
range: None,
|
||||
filename,
|
||||
}
|
||||
.boxed_clone(),
|
||||
)
|
||||
})
|
||||
.range(|action, range| {
|
||||
let mut action: VimRead = action.as_any().downcast_ref::<VimRead>().unwrap().clone();
|
||||
action.range.replace(range.clone());
|
||||
Some(Box::new(action))
|
||||
}),
|
||||
VimCommand::new(("sp", "lit"), workspace::SplitHorizontal).filename(|_, filename| {
|
||||
Some(
|
||||
VimSplit {
|
||||
@@ -2575,6 +2705,76 @@ mod test {
|
||||
assert_eq!(fs.load(path).await.unwrap().replace("\r\n", "\n"), "@@\n");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_command_read(cx: &mut TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
|
||||
let path = Path::new(path!("/root/dir/other.rs"));
|
||||
fs.as_fake().insert_file(path, "1\n2\n3".into()).await;
|
||||
|
||||
cx.workspace(|workspace, _, cx| {
|
||||
assert_active_item(workspace, path!("/root/dir/file.rs"), "", cx);
|
||||
});
|
||||
|
||||
// File without trailing newline
|
||||
cx.set_state("one\ntwo\nthreeˇ", Mode::Normal);
|
||||
cx.simulate_keystrokes(": r space d i r / o t h e r . r s");
|
||||
cx.simulate_keystrokes("enter");
|
||||
cx.assert_state("one\ntwo\nthree\nˇ1\n2\n3", Mode::Normal);
|
||||
|
||||
cx.set_state("oneˇ\ntwo\nthree", Mode::Normal);
|
||||
cx.simulate_keystrokes(": r space d i r / o t h e r . r s");
|
||||
cx.simulate_keystrokes("enter");
|
||||
cx.assert_state("one\nˇ1\n2\n3\ntwo\nthree", Mode::Normal);
|
||||
|
||||
cx.set_state("one\nˇtwo\nthree", Mode::Normal);
|
||||
cx.simulate_keystrokes(": 0 r space d i r / o t h e r . r s");
|
||||
cx.simulate_keystrokes("enter");
|
||||
cx.assert_state("ˇ1\n2\n3\none\ntwo\nthree", Mode::Normal);
|
||||
|
||||
cx.set_state("one\n«ˇtwo\nthree\nfour»\nfive", Mode::Visual);
|
||||
cx.simulate_keystrokes(": r space d i r / o t h e r . r s");
|
||||
cx.simulate_keystrokes("enter");
|
||||
cx.run_until_parked();
|
||||
cx.assert_state("one\ntwo\nthree\nfour\nˇ1\n2\n3\nfive", Mode::Normal);
|
||||
|
||||
// Empty filename
|
||||
cx.set_state("oneˇ\ntwo\nthree", Mode::Normal);
|
||||
cx.simulate_keystrokes(": r");
|
||||
cx.simulate_keystrokes("enter");
|
||||
cx.assert_state("one\nˇone\ntwo\nthree\ntwo\nthree", Mode::Normal);
|
||||
|
||||
// File with trailing newline
|
||||
fs.as_fake().insert_file(path, "1\n2\n3\n".into()).await;
|
||||
cx.set_state("one\ntwo\nthreeˇ", Mode::Normal);
|
||||
cx.simulate_keystrokes(": r space d i r / o t h e r . r s");
|
||||
cx.simulate_keystrokes("enter");
|
||||
cx.assert_state("one\ntwo\nthree\nˇ1\n2\n3\n", Mode::Normal);
|
||||
|
||||
cx.set_state("oneˇ\ntwo\nthree", Mode::Normal);
|
||||
cx.simulate_keystrokes(": r space d i r / o t h e r . r s");
|
||||
cx.simulate_keystrokes("enter");
|
||||
cx.assert_state("one\nˇ1\n2\n3\n\ntwo\nthree", Mode::Normal);
|
||||
|
||||
cx.set_state("one\n«ˇtwo\nthree\nfour»\nfive", Mode::Visual);
|
||||
cx.simulate_keystrokes(": r space d i r / o t h e r . r s");
|
||||
cx.simulate_keystrokes("enter");
|
||||
cx.assert_state("one\ntwo\nthree\nfour\nˇ1\n2\n3\n\nfive", Mode::Normal);
|
||||
|
||||
cx.set_state("«one\ntwo\nthreeˇ»", Mode::Visual);
|
||||
cx.simulate_keystrokes(": r space d i r / o t h e r . r s");
|
||||
cx.simulate_keystrokes("enter");
|
||||
cx.assert_state("one\ntwo\nthree\nˇ1\n2\n3\n", Mode::Normal);
|
||||
|
||||
// Empty file
|
||||
fs.as_fake().insert_file(path, "".into()).await;
|
||||
cx.set_state("ˇone\ntwo\nthree", Mode::Normal);
|
||||
cx.simulate_keystrokes(": r space d i r / o t h e r . r s");
|
||||
cx.simulate_keystrokes("enter");
|
||||
cx.assert_state("one\nˇtwo\nthree", Mode::Normal);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_command_quit(cx: &mut TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
Reference in New Issue
Block a user