@@ -135,7 +135,7 @@ pub struct Command {
135135}
136136
137137macro_rules! commands {
138− ( $($name:ident, $doc:literal),* ) => {
138+ ( $($name:ident, $doc:literal,)* ) => {
139139 $(
140140 #[allow(non_upper_case_globals)]
141141 pub const $name: Self = Self {
@@ -317,7 +317,12 @@ impl Command {
317317 dap_variables, "List variables",
318318 dap_terminate, "End debug session",
319319 dap_switch_thread, "Switch current thread",
320− suspend, "Suspend"
320+ shell_pipe, "Pipe selections through shell command",
321+ shell_pipe_to, "Pipe selections into shell command, ignoring command output",
322+ shell_insert_output, "Insert output of shell command before each selection",
323+ shell_append_output, "Append output of shell command after each selection",
324+ shell_keep_pipe, "Filter selections with shell predicate",
325+ suspend, "Suspend",
321326 );
322327}
323328
@@ -1089,7 +1094,7 @@ fn select_all(cx: &mut Context) {
10891094}
10901095
10911096fn select_regex(cx: &mut Context) {
1092− let prompt = ui::regex_prompt(cx, "select:".to_string(), move |view, doc, _, regex| {
1097+ let prompt = ui::regex_prompt(cx, "select:".into(), move |view, doc, _, regex| {
10931098 let text = doc.text().slice(..);
10941099 if let Some(selection) = selection::select_on_matches(text, doc.selection(view.id), ®ex)
10951100 {
@@ -1101,7 +1106,7 @@ fn select_regex(cx: &mut Context) {
11011106}
11021107
11031108fn split_selection(cx: &mut Context) {
1104− let prompt = ui::regex_prompt(cx, "split:".to_string(), move |view, doc, _, regex| {
1109+ let prompt = ui::regex_prompt(cx, "split:".into(), move |view, doc, _, regex| {
11051110 let text = doc.text().slice(..);
11061111 let selection = selection::split_on_matches(text, doc.selection(view.id), ®ex);
11071112 doc.set_selection(view.id, selection);
@@ -1167,15 +1172,11 @@ fn search(cx: &mut Context) {
11671172 // feed chunks into the regex yet
11681173 let contents = doc.text().slice(..).to_string();
11691174
1170− let prompt = ui::regex_prompt(
1171− cx,
1172− "search:".to_string(),
1173− move |view, doc, registers, regex| {
1174− search_impl(doc, view, &contents, ®ex, false);
1175− // TODO: only store on enter (accept), not update
1176− registers.write('/', vec![regex.as_str().to_string()]);
1177− },
1178− );
1175+ let prompt = ui::regex_prompt(cx, "search:".into(), move |view, doc, registers, regex| {
1176+ search_impl(doc, view, &contents, ®ex, false);
1177+ // TODO: only store on enter (accept), not update
1178+ registers.write('/', vec![regex.as_str().to_string()]);
1179+ });
11791180
11801181 cx.push_layer(Box::new(prompt));
11811182}
@@ -2400,7 +2401,7 @@ mod cmd {
24002401
24012402fn command_mode(cx: &mut Context) {
24022403 let mut prompt = Prompt::new(
2403− ":".to_owned(),
2404+ ":".into(),
24042405 Some(':'),
24052406 |input: &str| {
24062407 // we use .this over split_whitespace() because we care about empty segments
@@ -4009,7 +4010,7 @@ fn join_selections(cx: &mut Context) {
40094010
40104011fn keep_selections(cx: &mut Context) {
40114012 // keep selections matching regex
4012− let prompt = ui::regex_prompt(cx, "keep:".to_string(), move |view, doc, _, regex| {
4013+ let prompt = ui::regex_prompt(cx, "keep:".into(), move |view, doc, _, regex| {
40134014 let text = doc.text().slice(..);
40144015
40154016 if let Some(selection) = selection::keep_matches(text, doc.selection(view.id), ®ex) {
@@ -4487,6 +4488,128 @@ fn surround_delete(cx: &mut Context) {
44874488 })
44884489}
44894490
4491+#[derive(Eq, PartialEq)]
4492+enum ShellBehavior {
4493+ Replace,
4494+ Ignore,
4495+ Insert,
4496+ Append,
4497+ Filter,
4498+}
4499+
4500+fn shell_pipe(cx: &mut Context) {
4501+ shell(cx, "pipe:".into(), ShellBehavior::Replace);
4502+}
4503+
4504+fn shell_pipe_to(cx: &mut Context) {
4505+ shell(cx, "pipe-to:".into(), ShellBehavior::Ignore);
4506+}
4507+
4508+fn shell_insert_output(cx: &mut Context) {
4509+ shell(cx, "insert-output:".into(), ShellBehavior::Insert);
4510+}
4511+
4512+fn shell_append_output(cx: &mut Context) {
4513+ shell(cx, "append-output:".into(), ShellBehavior::Append);
4514+}
4515+
4516+fn shell_keep_pipe(cx: &mut Context) {
4517+ shell(cx, "keep-pipe:".into(), ShellBehavior::Filter);
4518+}
4519+
4520+fn shell(cx: &mut Context, prompt: Cow<'static, str>, behavior: ShellBehavior) {
4521+ use std::io::Write;
4522+ use std::process::{Command, Stdio};
4523+ if cx.editor.config.shell.is_empty() {
4524+ cx.editor.set_error("No shell set".to_owned());
4525+ return;
4526+ }
4527+ let pipe = match behavior {
4528+ ShellBehavior::Replace | ShellBehavior::Ignore | ShellBehavior::Filter => true,
4529+ ShellBehavior::Insert | ShellBehavior::Append => false,
4530+ };
4531+ let prompt = Prompt::new(
4532+ prompt,
4533+ Some('|'),
4534+ |_input: &str| Vec::new(),
4535+ move |cx: &mut compositor::Context, input: &str, event: PromptEvent| {
4536+ if event != PromptEvent::Validate {
4537+ return;
4538+ }
4539+ let shell = &cx.editor.config.shell;
4540+ let (view, doc) = current!(cx.editor);
4541+ let selection = doc.selection(view.id);
4542+
4543+ let mut changes = Vec::with_capacity(selection.len());
4544+
4545+ for range in selection.ranges() {
4546+ let mut process = match Command::new(&shell[0])
4547+ .args(&shell[1..])
4548+ .arg(input)
4549+ .stdin(Stdio::piped())
4550+ .stdout(Stdio::piped())
4551+ .stderr(Stdio::piped())
4552+ .spawn()
4553+ {
4554+ Ok(process) => process,
4555+ Err(e) => {
4556+ log::error!("Failed to start shell: {}", e);
4557+ cx.editor.set_error("Failed to start shell".to_owned());
4558+ return;
4559+ }
4560+ };
4561+ if pipe {
4562+ let stdin = process.stdin.as_mut().unwrap();
4563+ let fragment = range.fragment(doc.text().slice(..));
4564+ stdin.write_all(fragment.as_bytes()).unwrap();
4565+ }
4566+ let output = process.wait_with_output().unwrap();
4567+
4568+ if behavior != ShellBehavior::Filter {
4569+ if !output.status.success() {
4570+ if !output.stderr.is_empty() {
4571+ log::error!("Shell error: {}", String::from_utf8_lossy(&output.stderr));
4572+ }
4573+ cx.editor.set_error("Command failed".to_owned());
4574+ return;
4575+ }
4576+ let tendril = match Tendril::try_from_byte_slice(&output.stdout) {
4577+ Ok(tendril) => tendril,
4578+ Err(_) => {
4579+ cx.editor
4580+ .set_error("Process did not output valid UTF-8".to_owned());
4581+ return;
4582+ }
4583+ };
4584+ let (from, to) = match behavior {
4585+ ShellBehavior::Replace => (range.from(), range.to()),
4586+ ShellBehavior::Insert => (range.from(), range.from()),
4587+ ShellBehavior::Append => (range.to(), range.to()),
4588+ _ => (range.from(), range.from()),
4589+ };
4590+ changes.push((from, to, Some(tendril)));
4591+ } else {
4592+ // if the process exits successfully, keep the selection, otherwise delete it.
4593+ let keep = output.status.success();
4594+ changes.push((
4595+ range.from(),
4596+ if keep { range.from() } else { range.to() },
4597+ None,
4598+ ));
4599+ }
4600+ }
4601+
4602+ if behavior != ShellBehavior::Ignore {
4603+ let transaction = Transaction::change(doc.text(), changes.into_iter());
4604+ doc.apply(&transaction, view.id);
4605+ doc.append_changes_to_history(view.id);
4606+ }
4607+ },
4608+ );
4609+
4610+ cx.push_layer(Box::new(prompt));
4611+}
4612+
44904613fn suspend(_cx: &mut Context) {
44914614 #[cfg(not(windows))]
44924615 signal_hook::low_level::raise(signal_hook::consts::signal::SIGTSTP).unwrap();