@@ -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;