on master help sign in
Shell commands (#547)
e772808Omnikar
book/src/keymap.md+10
@@ -122,6 +122,16 @@ in reverse, or searching via smartcase.
122| `[D` | Go to first diagnostic in document | `goto_first_diag` |
122| `[D` | Go to first diagnostic in document | `goto_first_diag` |
123| `]D` | Go to last diagnostic in document | `goto_last_diag` |
123| `]D` | Go to last diagnostic in document | `goto_last_diag` |
124
124
125+### Shell
126+
127+| Key | Description | Command |
128+| ------ | ----------- | ------- |
129+| `\|` | Pipe each selection through shell command, replacing with output | `shell_pipe` |
130+| `A-\|` | Pipe each selection into shell command, ignoring output | `shell_pipe_to` |
131+| `!` | Run shell command, inserting output before each selection | `shell_insert_output` |
132+| `A-!` | Run shell command, appending output after each selection | `shell_append_output` |
133+| `$` | Pipe each selection into shell command, removing if the command exits >0 | `shell_keep_pipe` |
134+
125## Select / extend mode
135## Select / extend mode
126
136
127I'm still pondering whether to keep this mode or not. It changes movement
137I'm still pondering whether to keep this mode or not. It changes movement
helix-term/src/commands.rs+134−2
@@ -131,7 +131,7 @@ pub struct Command {
131}
131}
132
132
133macro_rules! commands {
133macro_rules! commands {
134− ( $($name:ident, $doc:literal),* ) => {
134+ ( $($name:ident, $doc:literal,)* ) => {
135 $(
135 $(
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",
306 );
311 );
307}
312}
308
313
@@ -4292,6 +4297,133 @@ fn surround_delete(cx: &mut Context) {
4292 })
4297 })
4293}
4298}
4294
4299
4300+#[derive(Eq, PartialEq)]
4301+enum ShellBehavior {
4302+ Replace,
4303+ Ignore,
4304+ Insert,
4305+ Append,
4306+ Filter,
4307+}
4308+
4309+fn shell_pipe(cx: &mut Context) {
4310+ shell(cx, "pipe:", ShellBehavior::Replace);
4311+}
4312+
4313+fn shell_pipe_to(cx: &mut Context) {
4314+ shell(cx, "pipe-to:", ShellBehavior::Ignore);
4315+}
4316+
4317+fn shell_insert_output(cx: &mut Context) {
4318+ shell(cx, "insert-output:", ShellBehavior::Insert);
4319+}
4320+
4321+fn shell_append_output(cx: &mut Context) {
4322+ shell(cx, "append-output:", ShellBehavior::Append);
4323+}
4324+
4325+fn shell_keep_pipe(cx: &mut Context) {
4326+ shell(cx, "keep-pipe:", ShellBehavior::Filter);
4327+}
4328+
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());
4334+ return;
4335+ }
4336+ let pipe = match behavior {
4337+ ShellBehavior::Replace | ShellBehavior::Ignore | ShellBehavior::Filter => true,
4338+ ShellBehavior::Insert | ShellBehavior::Append => false,
4339+ };
4340+ let prompt = Prompt::new(
4341+ prompt.to_owned(),
4342+ Some('|'),
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;
4350+ let transaction =
4351+ Transaction::change_by_selection(doc.text(), selection, |range| {
4352+ let mut process;
4353+ match Command::new(&shell[0])
4354+ .args(&shell[1..])
4355+ .arg(input)
4356+ .stdin(Stdio::piped())
4357+ .stdout(Stdio::piped())
4358+ .stderr(Stdio::piped())
4359+ .spawn()
4360+ {
4361+ Ok(p) => process = p,
4362+ Err(e) => {
4363+ log::error!("Failed to start shell: {}", e);
4364+ error = Some("Failed to start shell");
4365+ return (0, 0, None);
4366+ }
4367+ }
4368+ if pipe {
4369+ let stdin = process.stdin.as_mut().unwrap();
4370+ let fragment = range.fragment(doc.text().slice(..));
4371+ stdin.write_all(fragment.as_bytes()).unwrap();
4372+ }
4373+
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() {
4379+ log::error!(
4380+ "Shell error: {}",
4381+ String::from_utf8_lossy(&stderr)
4382+ );
4383+ }
4384+ error = Some("Command failed");
4385+ return (0, 0, None);
4386+ }
4387+ let stdout = output.stdout;
4388+ let tendril;
4389+ match Tendril::try_from_byte_slice(&stdout) {
4390+ Ok(t) => tendril = t,
4391+ Err(_) => {
4392+ error = Some("Process did not output valid UTF-8");
4393+ return (0, 0, None);
4394+ }
4395+ }
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()),
4401+ };
4402+ (from, to, Some(tendril))
4403+ } else {
4404+ // if the process exits successfully, keep the selection, otherwise delete it.
4405+ let keep = output.status.success();
4406+ (
4407+ range.from(),
4408+ if keep { range.from() } else { range.to() },
4409+ None,
4410+ )
4411+ }
4412+ });
4413+
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);
4419+ }
4420+ }
4421+ },
4422+ );
4423+
4424+ cx.push_layer(Box::new(prompt));
4425+}
4426+
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();
helix-term/src/keymap.rs+5
@@ -508,6 +508,11 @@ impl Default for Keymaps {
508 },
508 },
509
509
510 "\"" => select_register,
510 "\"" => select_register,
511+ "|" => shell_pipe,
512+ "A-|" => shell_pipe_to,
513+ "!" => shell_insert_output,
514+ "A-!" => shell_append_output,
515+ "$" => shell_keep_pipe,
511 "C-z" => suspend,
516 "C-z" => suspend,
512 });
517 });
513 let mut select = normal.clone();
518 let mut select = normal.clone();
helix-view/src/editor.rs+7
@@ -33,6 +33,8 @@ pub struct Config {
33 pub scroll_lines: isize,
33 pub scroll_lines: isize,
34 /// Mouse support. Defaults to true.
34 /// Mouse support. Defaults to true.
35 pub mouse: bool,
35 pub mouse: bool,
36+ /// Shell to use for shell commands. Defaults to ["cmd", "/C"] on Windows and ["sh", "-c"] otherwise.
37+ pub shell: Vec<String>,
36 /// Line number mode.
38 /// Line number mode.
37 pub line_number: LineNumber,
39 pub line_number: LineNumber,
38 /// Middle click paste support. Defaults to true
40 /// Middle click paste support. Defaults to true
@@ -55,6 +57,11 @@ impl Default for Config {
55 scrolloff: 5,
57 scrolloff: 5,
56 scroll_lines: 3,
58 scroll_lines: 3,
57 mouse: true,
59 mouse: true,
60+ shell: if cfg!(windows) {
61+ vec!["cmd".to_owned(), "/C".to_owned()]
62+ } else {
63+ vec!["sh".to_owned(), "-c".to_owned()]
64+ },
58 line_number: LineNumber::Absolute,
65 line_number: LineNumber::Absolute,
59 middle_click_paste: true,
66 middle_click_paste: true,
60 }
67 }