diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index f479a7a0..797a5e99 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -4322,9 +4322,46 @@ fn shell_keep_pipe(cx: &mut Context) { shell(cx, "keep-pipe:".into(), ShellBehavior::Filter); } -fn shell(cx: &mut Context, prompt: Cow<'static, str>, behavior: ShellBehavior) { +fn shell_impl( + shell: &[String], + cmd: &str, + input: Option<&[u8]>, +) -> anyhow::Result<(Tendril, bool)> { use std::io::Write; use std::process::{Command, Stdio}; + let mut process = match Command::new(&shell[0]) + .args(&shell[1..]) + .arg(cmd) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + { + Ok(process) => process, + Err(e) => { + log::error!("Failed to start shell: {}", e); + return Err(e.into()); + } + }; + if let Some(input) = input { + let mut stdin = process.stdin.take().unwrap(); + stdin.write_all(input)?; + } + let output = process.wait_with_output()?; + + if !output.status.success() { + if !output.stderr.is_empty() { + log::error!("Shell error: {}", String::from_utf8_lossy(&output.stderr)); + } + bail!("Command failed"); + } + + let tendril = Tendril::try_from_byte_slice(&output.stdout) + .map_err(|_| anyhow!("Process did not output valid UTF-8"))?; + Ok((tendril, output.status.success())) +} + +fn shell(cx: &mut Context, prompt: Cow<'static, str>, behavior: ShellBehavior) { if cx.editor.config.shell.is_empty() { cx.editor.set_error("No shell set".to_owned()); return; @@ -4338,67 +4375,40 @@ fn shell(cx: &mut Context, prompt: Cow<'static, str>, behavior: ShellBehavior) { Some('|'), |_input: &str| Vec::new(), move |cx: &mut compositor::Context, input: &str, event: PromptEvent| { + let shell = &cx.editor.config.shell; if event != PromptEvent::Validate { return; } - let shell = &cx.editor.config.shell; let (view, doc) = current!(cx.editor); let selection = doc.selection(view.id); let mut changes = Vec::with_capacity(selection.len()); + let text = doc.text().slice(..); for range in selection.ranges() { - let mut process = match Command::new(&shell[0]) - .args(&shell[1..]) - .arg(input) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - { - Ok(process) => process, - Err(e) => { - log::error!("Failed to start shell: {}", e); - cx.editor.set_error("Failed to start shell".to_owned()); - return; - } - }; - if pipe { - let stdin = process.stdin.as_mut().unwrap(); - let fragment = range.fragment(doc.text().slice(..)); - stdin.write_all(fragment.as_bytes()).unwrap(); - } - let output = process.wait_with_output().unwrap(); - - if behavior != ShellBehavior::Filter { - if !output.status.success() { - if !output.stderr.is_empty() { - log::error!("Shell error: {}", String::from_utf8_lossy(&output.stderr)); - } - cx.editor.set_error("Command failed".to_owned()); - return; - } - let tendril = match Tendril::try_from_byte_slice(&output.stdout) { - Ok(tendril) => tendril, - Err(_) => { - cx.editor - .set_error("Process did not output valid UTF-8".to_owned()); + let fragment = range.fragment(text); + let (output, success) = + match shell_impl(shell, input, pipe.then(|| fragment.as_bytes())) { + Ok(result) => result, + Err(err) => { + cx.editor.set_error(err.to_string()); return; } }; + + if behavior != ShellBehavior::Filter { let (from, to) = match behavior { ShellBehavior::Replace => (range.from(), range.to()), ShellBehavior::Insert => (range.from(), range.from()), ShellBehavior::Append => (range.to(), range.to()), _ => (range.from(), range.from()), }; - changes.push((from, to, Some(tendril))); + changes.push((from, to, Some(output))); } else { // if the process exits successfully, keep the selection, otherwise delete it. - let keep = output.status.success(); changes.push(( range.from(), - if keep { range.from() } else { range.to() }, + if success { range.from() } else { range.to() }, None, )); }