Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement :read typed command #3966

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions book/src/generated/typable-cmd.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,4 @@
| `:append-output` | Run shell command, appending output after each selection. |
| `:pipe` | Pipe each selection to the shell command. |
| `:run-shell-command`, `:sh` | Run a shell command |
| `:read`, `:r` | Load a file into buffer |
90 changes: 89 additions & 1 deletion helix-term/src/commands/typed.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::ops::Deref;
use std::{io::BufReader, ops::Deref};

use crate::job::Job;

Expand Down Expand Up @@ -1783,6 +1783,87 @@ fn run_shell_command(
Ok(())
}

fn read_file_info_buffer(
cx: &mut compositor::Context,
args: &[Cow<str>],
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}

let (view, doc) = current!(cx.editor);

ensure!(!args.is_empty(), "file name is expected");

let filename = args.get(0).unwrap();
let path = PathBuf::from(filename.to_string());
if !path.exists() {
bail!("file doesn't exist: {}", filename);
}

let file = std::fs::File::open(path).map_err(|err| anyhow!("error reading file {}", err))?;
let mut reader = BufReader::new(file);
let contents = from_reader(&mut reader, 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);

Ok(())
}

/// Stripped down version of [`helix_view::document::from_reader`] which is adapted to use encoding_rs::Decoder::read_to_string
fn from_reader<R: std::io::Read + ?Sized>(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This duplicates a lot of code between this function and document::from_reader. Can you factor out the shared parts and share the code?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. I was following @kirawi's advice on this: #3966 (comment)

I can try working something out at the expense of dragging this PR further, or that work can be the subject of another PR. I am easy either way. It's all up to the maintainers at this point.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can use read_to_string from document now:

pub fn read_to_string<R: std::io::Read + ?Sized>(
reader: &mut R,
encoding: Option<&'static Encoding>,
) -> Result<(String, &'static Encoding, bool), Error> {

reader: &mut R,
encoding: &'static helix_core::encoding::Encoding,
) -> anyhow::Result<String> {
let mut buf = [0u8; 8192];

let (mut decoder, mut slice, read) = {
let read = reader.read(&mut buf)?;
let decoder = encoding.new_decoder();
let slice = &buf[..read];
(decoder, slice, read)
};

let mut is_empty = read == 0;
let mut buf_str = String::with_capacity(buf.len());

loop {
let mut total_read = 0usize;

loop {
let (result, read, ..) =
decoder.decode_to_string(&slice[total_read..], &mut buf_str, is_empty);

total_read += read;

match result {
helix_core::encoding::CoderResult::InputEmpty => {
debug_assert_eq!(slice.len(), total_read);
break;
}
helix_core::encoding::CoderResult::OutputFull => {
debug_assert!(slice.len() > total_read);
buf_str.reserve(buf.len())
}
}
}

if is_empty {
debug_assert_eq!(reader.read(&mut buf)?, 0);
break;
}

let read = reader.read(&mut buf)?;
slice = &buf[..read];
is_empty = read == 0;
}
Ok(buf_str)
}

pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
TypableCommand {
name: "quit",
Expand Down Expand Up @@ -2291,6 +2372,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
fun: run_shell_command,
completer: Some(completers::directory),
},
TypableCommand {
name: "read",
aliases: &["r"],
doc: "Load a file into buffer",
fun: read_file_info_buffer,
completer: Some(completers::filename),
},
];

pub static TYPABLE_COMMAND_MAP: Lazy<HashMap<&'static str, &'static TypableCommand>> =
Expand Down
27 changes: 27 additions & 0 deletions helix-term/tests/test/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,33 @@ async fn test_write_quit_fail() -> anyhow::Result<()> {
}

#[tokio::test(flavor = "multi_thread")]
async fn test_read_file() -> anyhow::Result<()> {
let mut file = tempfile::NamedTempFile::new()?;
let expected_content = String::from("some contents");
let output_file = helpers::temp_file_with_contents(&expected_content)?;
let mut command = String::new();
let cmd = format!(":r {:?}<ret><esc>:w<ret>", output_file.path());
command.push_str(&cmd);

test_key_sequence(
&mut helpers::AppBuilder::new()
.with_file(file.path(), None)
.build()?,
Some(&command),
Some(&|app| {
assert!(!app.editor.is_err(), "error: {:?}", app.editor.get_status());
}),
false,
)
.await?;

helpers::assert_file_has_content(file.as_file_mut(), expected_content.as_str())?;

Ok(())
}

#[tokio::test(flavor = "multi_thread")]
#[ignore]
async fn test_buffer_close_concurrent() -> anyhow::Result<()> {
test_key_sequences(
&mut helpers::AppBuilder::new().build()?,
Expand Down