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`
This commit is contained in:
parent
dbfd054562
commit
e772808a5b
4 changed files with 156 additions and 2 deletions
|
@ -122,6 +122,16 @@ in reverse, or searching via smartcase.
|
||||||
| `[D` | Go to first diagnostic in document | `goto_first_diag` |
|
| `[D` | Go to first diagnostic in document | `goto_first_diag` |
|
||||||
| `]D` | Go to last diagnostic in document | `goto_last_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
|
## Select / extend mode
|
||||||
|
|
||||||
I'm still pondering whether to keep this mode or not. It changes movement
|
I'm still pondering whether to keep this mode or not. It changes movement
|
||||||
|
|
|
@ -131,7 +131,7 @@ pub struct Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! commands {
|
macro_rules! commands {
|
||||||
( $($name:ident, $doc:literal),* ) => {
|
( $($name:ident, $doc:literal,)* ) => {
|
||||||
$(
|
$(
|
||||||
#[allow(non_upper_case_globals)]
|
#[allow(non_upper_case_globals)]
|
||||||
pub const $name: Self = Self {
|
pub const $name: Self = Self {
|
||||||
|
@ -302,7 +302,12 @@ impl Command {
|
||||||
surround_delete, "Surround delete",
|
surround_delete, "Surround delete",
|
||||||
select_textobject_around, "Select around object",
|
select_textobject_around, "Select around object",
|
||||||
select_textobject_inner, "Select inside 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) {
|
fn suspend(_cx: &mut Context) {
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
signal_hook::low_level::raise(signal_hook::consts::signal::SIGTSTP).unwrap();
|
signal_hook::low_level::raise(signal_hook::consts::signal::SIGTSTP).unwrap();
|
||||||
|
|
|
@ -508,6 +508,11 @@ impl Default for Keymaps {
|
||||||
},
|
},
|
||||||
|
|
||||||
"\"" => select_register,
|
"\"" => select_register,
|
||||||
|
"|" => shell_pipe,
|
||||||
|
"A-|" => shell_pipe_to,
|
||||||
|
"!" => shell_insert_output,
|
||||||
|
"A-!" => shell_append_output,
|
||||||
|
"$" => shell_keep_pipe,
|
||||||
"C-z" => suspend,
|
"C-z" => suspend,
|
||||||
});
|
});
|
||||||
let mut select = normal.clone();
|
let mut select = normal.clone();
|
||||||
|
|
|
@ -33,6 +33,8 @@ pub struct Config {
|
||||||
pub scroll_lines: isize,
|
pub scroll_lines: isize,
|
||||||
/// Mouse support. Defaults to true.
|
/// Mouse support. Defaults to true.
|
||||||
pub mouse: bool,
|
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.
|
/// Line number mode.
|
||||||
pub line_number: LineNumber,
|
pub line_number: LineNumber,
|
||||||
/// Middle click paste support. Defaults to true
|
/// Middle click paste support. Defaults to true
|
||||||
|
@ -55,6 +57,11 @@ impl Default for Config {
|
||||||
scrolloff: 5,
|
scrolloff: 5,
|
||||||
scroll_lines: 3,
|
scroll_lines: 3,
|
||||||
mouse: true,
|
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,
|
line_number: LineNumber::Absolute,
|
||||||
middle_click_paste: true,
|
middle_click_paste: true,
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue