@@ -131,7 +131,7 @@ pub struct Command {
133macro_rules! commands {
133macro_rules! commands {
134− ( $($name:ident, $doc:literal),* ) => {
134+ ( $($name:ident, $doc:literal,)* ) => {
136 #[allow(non_upper_case_globals)]
136 #[allow(non_upper_case_globals)]
137 pub const $name: Self = Self {
137 pub const $name: Self = Self {
@@ -302,7 +302,12 @@ impl Command {
302 surround_delete, "Surround delete",
302 surround_delete, "Surround delete",
303 select_textobject_around, "Select around object",
303 select_textobject_around, "Select around object",
304 select_textobject_inner, "Select inside object",
304 select_textobject_inner, "Select inside object",
305+ shell_pipe, "Pipe selections through shell command",
306+ shell_pipe_to, "Pipe selections into shell command, ignoring command output",
307+ shell_insert_output, "Insert output of shell command before each selection",
308+ shell_append_output, "Append output of shell command after each selection",
309+ shell_keep_pipe, "Filter selections with shell predicate",
305− suspend, "Suspend"
310+ suspend, "Suspend",
@@ -4292,6 +4297,133 @@ fn surround_delete(cx: &mut Context) {
4300+#[derive(Eq, PartialEq)]
4301+enum ShellBehavior {
4309+fn shell_pipe(cx: &mut Context) {
4310+ shell(cx, "pipe:", ShellBehavior::Replace);
4313+fn shell_pipe_to(cx: &mut Context) {
4314+ shell(cx, "pipe-to:", ShellBehavior::Ignore);
4317+fn shell_insert_output(cx: &mut Context) {
4318+ shell(cx, "insert-output:", ShellBehavior::Insert);
4321+fn shell_append_output(cx: &mut Context) {
4322+ shell(cx, "append-output:", ShellBehavior::Append);
4325+fn shell_keep_pipe(cx: &mut Context) {
4326+ shell(cx, "keep-pipe:", ShellBehavior::Filter);
4329+fn shell(cx: &mut Context, prompt: &str, behavior: ShellBehavior) {
4330+ use std::io::Write;
4331+ use std::process::{Command, Stdio};
4332+ if cx.editor.config.shell.is_empty() {
4333+ cx.editor.set_error("No shell set".to_owned());
4336+ let pipe = match behavior {
4337+ ShellBehavior::Replace | ShellBehavior::Ignore | ShellBehavior::Filter => true,
4338+ ShellBehavior::Insert | ShellBehavior::Append => false,
4340+ let prompt = Prompt::new(
4343+ |_input: &str| Vec::new(),
4344+ move |cx: &mut compositor::Context, input: &str, event: PromptEvent| {
4345+ let shell = &cx.editor.config.shell;
4346+ if event == PromptEvent::Validate {
4347+ let (view, doc) = current!(cx.editor);
4348+ let selection = doc.selection(view.id);
4349+ let mut error: Option<&str> = None;
4351+ Transaction::change_by_selection(doc.text(), selection, |range| {
4353+ match Command::new(&shell[0])
4356+ .stdin(Stdio::piped())
4357+ .stdout(Stdio::piped())
4358+ .stderr(Stdio::piped())
4361+ Ok(p) => process = p,
4363+ log::error!("Failed to start shell: {}", e);
4364+ error = Some("Failed to start shell");
4365+ return (0, 0, None);
4369+ let stdin = process.stdin.as_mut().unwrap();
4370+ let fragment = range.fragment(doc.text().slice(..));
4371+ stdin.write_all(fragment.as_bytes()).unwrap();
4374+ let output = process.wait_with_output().unwrap();
4375+ if behavior != ShellBehavior::Filter {
4376+ if !output.status.success() {
4377+ let stderr = output.stderr;
4378+ if !stderr.is_empty() {
4381+ String::from_utf8_lossy(&stderr)
4384+ error = Some("Command failed");
4385+ return (0, 0, None);
4387+ let stdout = output.stdout;
4389+ match Tendril::try_from_byte_slice(&stdout) {
4390+ Ok(t) => tendril = t,
4392+ error = Some("Process did not output valid UTF-8");
4393+ return (0, 0, None);
4396+ let (from, to) = match behavior {
4397+ ShellBehavior::Replace => (range.from(), range.to()),
4398+ ShellBehavior::Insert => (range.from(), range.from()),
4399+ ShellBehavior::Append => (range.to(), range.to()),
4400+ _ => (range.from(), range.from()),
4402+ (from, to, Some(tendril))
4404+ // if the process exits successfully, keep the selection, otherwise delete it.
4405+ let keep = output.status.success();
4408+ if keep { range.from() } else { range.to() },
4414+ if let Some(error) = error {
4415+ cx.editor.set_error(error.to_owned());
4416+ } else if behavior != ShellBehavior::Ignore {
4417+ doc.apply(&transaction, view.id);
4418+ doc.append_changes_to_history(view.id);
4424+ cx.push_layer(Box::new(prompt));
4295fn suspend(_cx: &mut Context) {
4427fn suspend(_cx: &mut Context) {
4296 #[cfg(not(windows))]
4428 #[cfg(not(windows))]
4297 signal_hook::low_level::raise(signal_hook::consts::signal::SIGTSTP).unwrap();
4429 signal_hook::low_level::raise(signal_hook::consts::signal::SIGTSTP).unwrap();