Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add async_run_shell_command to avoid blocking UI #3029

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions book/src/generated/typable-cmd.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,4 @@
| `:append-output` | Run shell command, appending output after each selection. |
| `:pipe` | Pipe each selection to the shell command. |
| `:run-shell-command`, `:sh` | Run a shell command |
| `:async-run-shell-command`, `:async` | Run a async shell command |
1 change: 1 addition & 0 deletions book/src/keymap.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@
| ------ | ----------- | ------- |
| <code>&#124;</code> | Pipe each selection through shell command, replacing with output | `shell_pipe` |
| <code>Alt-&#124;</code> | Pipe each selection into shell command, ignoring output | `shell_pipe_to` |
| Alt-> | Pipe each selection into an async shell command, ignoring output | `async_shell_pipe_to` |
| `!` | Run shell command, inserting output before each selection | `shell_insert_output` |
| `Alt-!` | Run shell command, appending output after each selection | `shell_append_output` |
| `$` | Pipe each selection into shell command, keep selections where command returned 0 | `shell_keep_pipe` |
Expand Down
77 changes: 77 additions & 0 deletions helix-term/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,7 @@ impl MappableCommand {
dap_disable_exceptions, "Disable exception breakpoints",
shell_pipe, "Pipe selections through shell command",
shell_pipe_to, "Pipe selections into shell command ignoring output",
async_shell_pipe_to, "Pipe selections into async shell command ignoring output",
shell_insert_output, "Insert shell command output before selections",
shell_append_output, "Append shell command output after selections",
shell_keep_pipe, "Filter selections with shell predicate",
Expand Down Expand Up @@ -4483,6 +4484,40 @@ fn shell_pipe_to(cx: &mut Context) {
shell_prompt(cx, "pipe-to:".into(), ShellBehavior::Ignore);
}

fn async_shell_pipe_to(cx: &mut Context) {
ui::prompt(
cx,
"pipe-to-async:".into(),
Some('|'),
ui::completers::none,
move |cx, input: &str, event: PromptEvent| {
if event != PromptEvent::Validate {
return;
}
if input.is_empty() {
return;
}

let (view, doc) = current!(cx.editor);
let selection = doc.selection(view.id);

let mut changes = Vec::with_capacity(selection.len());
let text = doc.text().slice(..);

for range in selection.ranges() {
let fragment = range.fragment(text);
changes.extend_from_slice(fragment.as_bytes());
}
let shell = cx.editor.config().shell.clone();
let cmd = input.to_string();
cx.jobs.spawn(async move {
async_shell_impl(&shell, &cmd, Some(&changes)).await?;
Ok(())
});
},
)
}

fn shell_insert_output(cx: &mut Context) {
shell_prompt(cx, "insert-output:".into(), ShellBehavior::Insert);
}
Expand Down Expand Up @@ -4588,6 +4623,48 @@ fn shell_impl(
Ok((tendril, output.status.success()))
}

async fn async_shell_impl(
shell: &[String],
cmd: &str,
input: Option<&[u8]>,
) -> anyhow::Result<(Tendril, bool)> {
use std::process::Stdio;
use tokio::io::AsyncWriteExt;
use tokio::process::Command;

ensure!(!shell.is_empty(), "No shell set");

let mut process = match Command::new(&shell[0])
.args(&shell[1..])
.arg(cmd)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
{
Ok(process) => process,
Err(e) => {
log::error!("Failed to start shell: {}", e);
return Err(e.into());
}
};
if let Some(input) = input {
let mut stdin = process.stdin.take().unwrap();
stdin.write_all(input).await?;
drop(stdin);
}
let output = process.wait_with_output().await?;

if !output.stderr.is_empty() {
log::error!("Shell error: {}", String::from_utf8_lossy(&output.stderr));
}

let str = std::str::from_utf8(&output.stdout)
.map_err(|_| anyhow!("Process did not output valid UTF-8"))?;
let tendril = Tendril::from(str);
Ok((tendril, output.status.success()))
}

fn shell(cx: &mut compositor::Context, cmd: &str, behavior: &ShellBehavior) {
let pipe = match behavior {
ShellBehavior::Replace | ShellBehavior::Ignore => true,
Expand Down
46 changes: 46 additions & 0 deletions helix-term/src/commands/typed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1515,6 +1515,45 @@ fn run_shell_command(
Ok(())
}

fn async_run_shell_command(
cx: &mut compositor::Context,
args: &[Cow<str>],
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}

let shell = cx.editor.config().shell.clone();
let cmd = args.join(" ");
let callback = async move {
let (output, success) = async_shell_impl(&shell, &cmd, None).await?;

let call: job::Callback =
Box::new(move |editor: &mut Editor, compositor: &mut Compositor| {
if success {
editor.set_status(format!("Command {} succeed", &cmd));
} else {
editor.set_error(format!("Command {} failed", &cmd));
}
if !output.is_empty() {
let contents = ui::Markdown::new(
format!("```sh {}\n{}\n```", &cmd, output),
editor.syn_loader.clone(),
);
let popup = Popup::new("shell", contents).position(Some(
helix_core::Position::new(editor.cursor().0.unwrap_or_default().row, 2),
));
compositor.replace_or_push("shell", popup);
}
});
Ok(call)
};

cx.jobs.callback(callback);
Ok(())
}

pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
TypableCommand {
name: "quit",
Expand Down Expand Up @@ -1995,6 +2034,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
fun: run_shell_command,
completer: Some(completers::directory),
},
TypableCommand {
name: "async-run-shell-command",
aliases: &["async"],
doc: "Run a async shell command",
fun: async_run_shell_command,
completer: Some(completers::directory),
},
];

pub static TYPABLE_COMMAND_MAP: Lazy<HashMap<&'static str, &'static TypableCommand>> =
Expand Down
1 change: 1 addition & 0 deletions helix-term/src/keymap/default.rs
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ pub fn default() -> HashMap<Mode, Keymap> {
"\"" => select_register,
"|" => shell_pipe,
"A-|" => shell_pipe_to,
"A->" => async_shell_pipe_to,
"!" => shell_insert_output,
"A-!" => shell_append_output,
"$" => shell_keep_pipe,
Expand Down