From 30baff907d5eb8c7ef3881a0b0cceac088b506e3 Mon Sep 17 00:00:00 2001
From: Sean Perry <shalehperry@gmail.com>
Date: Wed, 17 Apr 2024 15:57:57 -0700
Subject: [PATCH] Implement read command (#10447)

Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
Co-authored-by: Ibrahim Dursun <ibrahim@dursun.cc>
---
 book/src/generated/typable-cmd.md |  1 +
 helix-term/src/commands/typed.rs  | 43 ++++++++++++++++++++++++++++++-
 helix-term/tests/test/commands.rs | 24 +++++++++++++++++
 3 files changed, 67 insertions(+), 1 deletion(-)

diff --git a/book/src/generated/typable-cmd.md b/book/src/generated/typable-cmd.md
index dbb8b5f3..cf1b550d 100644
--- a/book/src/generated/typable-cmd.md
+++ b/book/src/generated/typable-cmd.md
@@ -87,3 +87,4 @@
 | `:redraw` | Clear and re-render the whole UI |
 | `:move` | Move the current buffer and its corresponding file to a different path |
 | `:yank-diagnostic` | Yank diagnostic(s) under primary cursor to register, or clipboard by default |
+| `:read`, `:r` | Load a file into buffer |
diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs
index acbd8a8c..f38ae6bb 100644
--- a/helix-term/src/commands/typed.rs
+++ b/helix-term/src/commands/typed.rs
@@ -1,4 +1,5 @@
 use std::fmt::Write;
+use std::io::BufReader;
 use std::ops::Deref;
 
 use crate::job::Job;
@@ -8,7 +9,7 @@ use super::*;
 use helix_core::fuzzy::fuzzy_match;
 use helix_core::indent::MAX_INDENT;
 use helix_core::{line_ending, shellwords::Shellwords};
-use helix_view::document::DEFAULT_LANGUAGE_NAME;
+use helix_view::document::{read_to_string, DEFAULT_LANGUAGE_NAME};
 use helix_view::editor::{CloseError, ConfigEvent};
 use serde_json::Value;
 use ui::completers::{self, Completer};
@@ -2454,6 +2455,39 @@ fn yank_diagnostic(
     Ok(())
 }
 
+fn read(cx: &mut compositor::Context, args: &[Cow<str>], event: PromptEvent) -> anyhow::Result<()> {
+    if event != PromptEvent::Validate {
+        return Ok(());
+    }
+
+    let scrolloff = cx.editor.config().scrolloff;
+    let (view, doc) = current!(cx.editor);
+
+    ensure!(!args.is_empty(), "file name is expected");
+    ensure!(args.len() == 1, "only the file name is expected");
+
+    let filename = args.get(0).unwrap();
+    let path = PathBuf::from(filename.to_string());
+    ensure!(
+        path.exists() && path.is_file(),
+        "path is not a file: {:?}",
+        path
+    );
+
+    let file = std::fs::File::open(path).map_err(|err| anyhow!("error opening file: {}", err))?;
+    let mut reader = BufReader::new(file);
+    let (contents, _, _) = read_to_string(&mut reader, Some(doc.encoding()))
+        .map_err(|err| anyhow!("error reading file: {}", err))?;
+    let contents = Tendril::from(contents);
+    let selection = doc.selection(view.id);
+    let transaction = Transaction::insert(doc.text(), selection, contents);
+    doc.apply(&transaction, view.id);
+    doc.append_changes_to_history(view);
+    view.ensure_cursor_in_view(doc, scrolloff);
+
+    Ok(())
+}
+
 pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
     TypableCommand {
         name: "quit",
@@ -3068,6 +3102,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
         fun: yank_diagnostic,
         signature: CommandSignature::all(completers::register),
     },
+    TypableCommand {
+        name: "read",
+        aliases: &["r"],
+        doc: "Load a file into buffer",
+        fun: read,
+        signature: CommandSignature::positional(&[completers::filename]),
+    },
 ];
 
 pub static TYPABLE_COMMAND_MAP: Lazy<HashMap<&'static str, &'static TypableCommand>> =
diff --git a/helix-term/tests/test/commands.rs b/helix-term/tests/test/commands.rs
index 351f07fe..6b904aa3 100644
--- a/helix-term/tests/test/commands.rs
+++ b/helix-term/tests/test/commands.rs
@@ -640,3 +640,27 @@ async fn test_join_selections_space() -> anyhow::Result<()> {
 
     Ok(())
 }
+
+#[tokio::test(flavor = "multi_thread")]
+async fn test_read_file() -> anyhow::Result<()> {
+    let mut file = tempfile::NamedTempFile::new()?;
+    let contents_to_read = "some contents";
+    let output_file = helpers::temp_file_with_contents(contents_to_read)?;
+
+    test_key_sequence(
+        &mut helpers::AppBuilder::new()
+            .with_file(file.path(), None)
+            .build()?,
+        Some(&format!(":r {:?}<ret><esc>:w<ret>", output_file.path())),
+        Some(&|app| {
+            assert!(!app.editor.is_err(), "error: {:?}", app.editor.get_status());
+        }),
+        false,
+    )
+    .await?;
+
+    let expected_contents = LineFeedHandling::Native.apply(contents_to_read);
+    helpers::assert_file_has_content(&mut file, &expected_contents)?;
+
+    Ok(())
+}