aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorOmnikar2021-08-31 09:13:16 +0000
committerGitHub2021-08-31 09:13:16 +0000
commite772808a5b0417e4d074eb9683d79376f83dae2d (patch)
treebd0b8f7011543d3592b51e5c0c260842118fade9
parentdbfd054562ba97a73128c49fae0cea95deabec5d (diff)
Shell commands (#547)
* Implement shell interaction commands * Use slice instead of iterator for shell invocation * Default to `sh` instead of `$SHELL` for shell commands * Enforce trailing comma in `commands` macro * Use `|` register for shell commands * Move shell config to `editor` and use in command * Update shell command prompts * Remove clone of shell config * Change shell function names to match prompts * Log stderr contents upon external command error * Remove `unwrap` calls on potential common errors `shell` will no longer panic if: * The user-configured shell cannot be found * The shell command does not output UTF-8 * Remove redundant `pipe` parameter * Rename `ShellBehavior::None` to `Ignore` * Display error when shell command is used and `shell = []` * Document shell commands in `keymap.md`
-rw-r--r--book/src/keymap.md10
-rw-r--r--helix-term/src/commands.rs136
-rw-r--r--helix-term/src/keymap.rs5
-rw-r--r--helix-view/src/editor.rs7
4 files changed, 156 insertions, 2 deletions
diff --git a/book/src/keymap.md b/book/src/keymap.md
index 61378863..d85fb936 100644
--- a/book/src/keymap.md
+++ b/book/src/keymap.md
@@ -122,6 +122,16 @@ in reverse, or searching via smartcase.
| `[D` | Go to first diagnostic in document | `goto_first_diag` |
| `]D` | Go to last diagnostic in document | `goto_last_diag` |
+### Shell
+
+| Key | Description | Command |
+| ------ | ----------- | ------- |
+| `\|` | Pipe each selection through shell command, replacing with output | `shell_pipe` |
+| `A-\|` | Pipe each selection into shell command, ignoring output | `shell_pipe_to` |
+| `!` | Run shell command, inserting output before each selection | `shell_insert_output` |
+| `A-!` | Run shell command, appending output after each selection | `shell_append_output` |
+| `$` | Pipe each selection into shell command, removing if the command exits >0 | `shell_keep_pipe` |
+
## Select / extend mode
I'm still pondering whether to keep this mode or not. It changes movement
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
index d21bbe42..6437bf52 100644
--- a/helix-term/src/commands.rs
+++ b/helix-term/src/commands.rs
@@ -131,7 +131,7 @@ pub struct Command {
}
macro_rules! commands {
- ( $($name:ident, $doc:literal),* ) => {
+ ( $($name:ident, $doc:literal,)* ) => {
$(
#[allow(non_upper_case_globals)]
pub const $name: Self = Self {
@@ -302,7 +302,12 @@ impl Command {
surround_delete, "Surround delete",
select_textobject_around, "Select around object",
select_textobject_inner, "Select inside object",
- suspend, "Suspend"
+ shell_pipe, "Pipe selections through shell command",
+ shell_pipe_to, "Pipe selections into shell command, ignoring command output",
+ shell_insert_output, "Insert output of shell command before each selection",
+ shell_append_output, "Append output of shell command after each selection",
+ shell_keep_pipe, "Filter selections with shell predicate",
+ suspend, "Suspend",
);
}
@@ -4292,6 +4297,133 @@ fn surround_delete(cx: &mut Context) {
})
}
+#[derive(Eq, PartialEq)]
+enum ShellBehavior {
+ Replace,
+ Ignore,
+ Insert,
+ Append,
+ Filter,
+}
+
+fn shell_pipe(cx: &mut Context) {
+ shell(cx, "pipe:", ShellBehavior::Replace);
+}
+
+fn shell_pipe_to(cx: &mut Context) {
+ shell(cx, "pipe-to:", ShellBehavior::Ignore);
+}
+
+fn shell_insert_output(cx: &mut Context) {
+ shell(cx, "insert-output:", ShellBehavior::Insert);
+}
+
+fn shell_append_output(cx: &mut Context) {
+ shell(cx, "append-output:", ShellBehavior::Append);
+}
+
+fn shell_keep_pipe(cx: &mut Context) {
+ shell(cx, "keep-pipe:", ShellBehavior::Filter);
+}
+
+fn shell(cx: &mut Context, prompt: &str, behavior: ShellBehavior) {
+ use std::io::Write;
+ use std::process::{Command, Stdio};
+ if cx.editor.config.shell.is_empty() {
+ cx.editor.set_error("No shell set".to_owned());
+ return;
+ }
+ let pipe = match behavior {
+ ShellBehavior::Replace | ShellBehavior::Ignore | ShellBehavior::Filter => true,
+ ShellBehavior::Insert | ShellBehavior::Append => false,
+ };
+ let prompt = Prompt::new(
+ prompt.to_owned(),
+ Some('|'),
+ |_input: &str| Vec::new(),
+ move |cx: &mut compositor::Context, input: &str, event: PromptEvent| {
+ let shell = &cx.editor.config.shell;
+ if event == PromptEvent::Validate {
+ let (view, doc) = current!(cx.editor);
+ let selection = doc.selection(view.id);
+ let mut error: Option<&str> = None;
+ let transaction =
+ Transaction::change_by_selection(doc.text(), selection, |range| {
+ let mut process;
+ match Command::new(&shell[0])
+ .args(&shell[1..])
+ .arg(input)
+ .stdin(Stdio::piped())
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .spawn()
+ {
+ Ok(p) => process = p,
+ Err(e) => {
+ log::error!("Failed to start shell: {}", e);
+ error = Some("Failed to start shell");
+ return (0, 0, None);
+ }
+ }
+ 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() {
+ let stderr = output.stderr;
+ if !stderr.is_empty() {
+ log::error!(
+ "Shell error: {}",
+ String::from_utf8_lossy(&stderr)
+ );
+ }
+ error = Some("Command failed");
+ return (0, 0, None);
+ }
+ let stdout = output.stdout;
+ let tendril;
+ match Tendril::try_from_byte_slice(&stdout) {
+ Ok(t) => tendril = t,
+ Err(_) => {
+ error = Some("Process did not output valid UTF-8");
+ return (0, 0, None);
+ }
+ }
+ 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()),
+ };
+ (from, to, Some(tendril))
+ } else {
+ // if the process exits successfully, keep the selection, otherwise delete it.
+ let keep = output.status.success();
+ (
+ range.from(),
+ if keep { range.from() } else { range.to() },
+ None,
+ )
+ }
+ });
+
+ if let Some(error) = error {
+ cx.editor.set_error(error.to_owned());
+ } else if behavior != ShellBehavior::Ignore {
+ doc.apply(&transaction, view.id);
+ doc.append_changes_to_history(view.id);
+ }
+ }
+ },
+ );
+
+ cx.push_layer(Box::new(prompt));
+}
+
fn suspend(_cx: &mut Context) {
#[cfg(not(windows))]
signal_hook::low_level::raise(signal_hook::consts::signal::SIGTSTP).unwrap();
diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs
index 492dc292..f3e160b1 100644
--- a/helix-term/src/keymap.rs
+++ b/helix-term/src/keymap.rs
@@ -508,6 +508,11 @@ impl Default for Keymaps {
},
"\"" => select_register,
+ "|" => shell_pipe,
+ "A-|" => shell_pipe_to,
+ "!" => shell_insert_output,
+ "A-!" => shell_append_output,
+ "$" => shell_keep_pipe,
"C-z" => suspend,
});
let mut select = normal.clone();
diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs
index 18cb9106..e5ff93ad 100644
--- a/helix-view/src/editor.rs
+++ b/helix-view/src/editor.rs
@@ -33,6 +33,8 @@ pub struct Config {
pub scroll_lines: isize,
/// Mouse support. Defaults to true.
pub mouse: bool,
+ /// Shell to use for shell commands. Defaults to ["cmd", "/C"] on Windows and ["sh", "-c"] otherwise.
+ pub shell: Vec<String>,
/// Line number mode.
pub line_number: LineNumber,
/// Middle click paste support. Defaults to true
@@ -55,6 +57,11 @@ impl Default for Config {
scrolloff: 5,
scroll_lines: 3,
mouse: true,
+ shell: if cfg!(windows) {
+ vec!["cmd".to_owned(), "/C".to_owned()]
+ } else {
+ vec!["sh".to_owned(), "-c".to_owned()]
+ },
line_number: LineNumber::Absolute,
middle_click_paste: true,
}