From 2eca2901f31083af85ffd6f299c64ad80a8bfaf5 Mon Sep 17 00:00:00 2001 From: Thomas <74479846+DeviousStoat@users.noreply.github.com> Date: Sun, 17 Apr 2022 05:03:47 +0200 Subject: [PATCH] Pipe typable command (#1972) Co-authored-by: DeviousStoat --- book/src/generated/typable-cmd.md | 1 + helix-term/src/commands.rs | 94 ++++++++++++++++--------------- helix-term/src/commands/typed.rs | 17 ++++++ 3 files changed, 67 insertions(+), 45 deletions(-) diff --git a/book/src/generated/typable-cmd.md b/book/src/generated/typable-cmd.md index bb5da3bb..0b591ba4 100644 --- a/book/src/generated/typable-cmd.md +++ b/book/src/generated/typable-cmd.md @@ -61,3 +61,4 @@ | `:tree-sitter-subtree`, `:ts-subtree` | Display tree sitter subtree under cursor, primarily for debugging queries. | | `:config-reload` | Refreshes helix's config. | | `:config-open` | Open the helix config.toml file. | +| `:pipe` | Pipe each selection to the shell command. | diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 886ee62d..90226c53 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -4150,19 +4150,19 @@ enum ShellBehavior { } fn shell_pipe(cx: &mut Context) { - shell(cx, "pipe:".into(), ShellBehavior::Replace); + shell_prompt(cx, "pipe:".into(), ShellBehavior::Replace); } fn shell_pipe_to(cx: &mut Context) { - shell(cx, "pipe-to:".into(), ShellBehavior::Ignore); + shell_prompt(cx, "pipe-to:".into(), ShellBehavior::Ignore); } fn shell_insert_output(cx: &mut Context) { - shell(cx, "insert-output:".into(), ShellBehavior::Insert); + shell_prompt(cx, "insert-output:".into(), ShellBehavior::Insert); } fn shell_append_output(cx: &mut Context) { - shell(cx, "append-output:".into(), ShellBehavior::Append); + shell_prompt(cx, "append-output:".into(), ShellBehavior::Append); } fn shell_keep_pipe(cx: &mut Context) { @@ -4256,65 +4256,69 @@ fn shell_impl( Ok((tendril, output.status.success())) } -fn shell(cx: &mut Context, prompt: Cow<'static, str>, behavior: ShellBehavior) { +fn shell(cx: &mut compositor::Context, cmd: &str, behavior: &ShellBehavior) { let pipe = match behavior { ShellBehavior::Replace | ShellBehavior::Ignore => true, ShellBehavior::Insert | ShellBehavior::Append => false, }; + let config = cx.editor.config(); + let shell = &config.shell; + 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); + let (output, success) = match shell_impl(shell, cmd, pipe.then(|| fragment.as_bytes())) { + Ok(result) => result, + Err(err) => { + cx.editor.set_error(err.to_string()); + return; + } + }; + + if !success { + cx.editor.set_error("Command failed"); + return; + } + + 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()), + }; + changes.push((from, to, Some(output))); + } + + if behavior != &ShellBehavior::Ignore { + let transaction = Transaction::change(doc.text(), changes.into_iter()); + doc.apply(&transaction, view.id); + } + + // after replace cursor may be out of bounds, do this to + // make sure cursor is in view and update scroll as well + view.ensure_cursor_in_view(doc, config.scrolloff); +} + +fn shell_prompt(cx: &mut Context, prompt: Cow<'static, str>, behavior: ShellBehavior) { ui::prompt( cx, prompt, Some('|'), ui::completers::none, move |cx, input: &str, event: PromptEvent| { - let config = cx.editor.config(); - let shell = &config.shell; 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); - let (output, success) = - match shell_impl(shell, input, pipe.then(|| fragment.as_bytes())) { - Ok(result) => result, - Err(err) => { - cx.editor.set_error(err.to_string()); - return; - } - }; - - if !success { - cx.editor.set_error("Command failed"); - return; - } - - 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()), - }; - changes.push((from, to, Some(output))); - } - - if behavior != ShellBehavior::Ignore { - let transaction = Transaction::change(doc.text(), changes.into_iter()); - doc.apply(&transaction, view.id); - } - // after replace cursor may be out of bounds, do this to - // make sure cursor is in view and update scroll as well - view.ensure_cursor_in_view(doc, config.scrolloff); + shell(cx, input, &behavior); }, ); } diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index d158388f..9a5298bb 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -1067,6 +1067,16 @@ fn refresh_config( Ok(()) } +fn pipe( + cx: &mut compositor::Context, + args: &[Cow], + _event: PromptEvent, +) -> anyhow::Result<()> { + ensure!(!args.is_empty(), "Shell command required"); + shell(cx, &args.join(" "), &ShellBehavior::Replace); + Ok(()) +} + pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ TypableCommand { name: "quit", @@ -1495,6 +1505,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ fun: open_config, completer: None, }, + TypableCommand { + name: "pipe", + aliases: &[], + doc: "Pipe each selection to the shell command.", + fun: pipe, + completer: None, + }, ]; pub static TYPABLE_COMMAND_MAP: Lazy> =