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

Provide some way to create a keymap that pipes the selection to a specific shell command #1943

Closed
tmke8 opened this issue Apr 3, 2022 · 11 comments
Labels
A-helix-term Area: Helix term improvements C-enhancement Category: Improvements E-good-first-issue Call for participation: Issues suitable for new contributors E-help-wanted Call for participation: Extra attention is needed

Comments

@tmke8
Copy link

tmke8 commented Apr 3, 2022

I can map a key to shell_pipe like this:

[keys.normal]
C-s = "shell_pipe"

but I cannot specify any arguments to shell_pipe. I think there are many ways to solve this problem. One solution would be #1383, but the easiest would probably be to add a new typable command :pipe that I can then map:

[keys.normal]
C-s = ":pipe black -"

The use case is piping the content of the file to a formatter (and then saving it). So:

[keys.normal]
C-s = ["select_all", "??solution to this issue?? black -", "collapse_selection", ":w"]
@tmke8 tmke8 added the C-enhancement Category: Improvements label Apr 3, 2022
@sudormrfbin sudormrfbin added E-easy Call for participation: Experience needed to fix: Easy / not much E-help-wanted Call for participation: Extra attention is needed E-good-first-issue Call for participation: Issues suitable for new contributors labels Apr 3, 2022
@sudormrfbin
Copy link
Member

The helper for the all the shell commands is here:

fn shell(cx: &mut Context, prompt: Cow<'static, str>, behavior: ShellBehavior) {
let pipe = match behavior {
ShellBehavior::Replace | ShellBehavior::Ignore => true,
ShellBehavior::Insert | ShellBehavior::Append => false,
};
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);
},
);
}

But it also has the code for displaying the prompt interleaved with it, so the code for execution of a given command and replacing the buffer contents will have to extracted out, for example:

fn shell(cx: &mut 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());
    // ....
}

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| {
            if event != PromptEvent::Validate {
                return;
            }
            if input.is_empty() {
                return;
            }

            shell(cx, input, behavior)
        })
}

You might have to replace cx with cx.editor in some places.

@sudormrfbin sudormrfbin removed the E-easy Call for participation: Experience needed to fix: Easy / not much label Apr 3, 2022
@tmke8 tmke8 changed the title Some way to create a keymap that pipes the selection to a specific shell command Provide some way to create a keymap that pipes the selection to a specific shell command Apr 3, 2022
@kirawi kirawi added the A-helix-term Area: Helix term improvements label Apr 3, 2022
@DeviousStoat
Copy link
Contributor

I found myself needing this as well. And since it didn't seem too hard for a first PR, I took a stab at it #1972

@David-Else
Copy link
Contributor

David-Else commented Apr 6, 2022

@DeviousStoat This looks amazing!

The problems I ran into using external formatters in Neovim were:

  1. If they failed then the buffer would fill up with the error message replacing my document
  2. The cursor would go to the start of the document

It would be great if these two problems could be avoided in Helix.

I expect number 2 was just a Vim thing, but can your PR recognise an error and abort with a message?

I am really hoping to get Prettier https://prettier.io/docs/en/cli.html working in Helix for all the different file types it can act on, that would be amazing! We would need a way to turn off LSP formatting on a server by server basis. For example, the TypeScript Language Server formatting sucks, but you could use Prettier instead.

PS https://dprint.dev/ is written in Rust and copies a lot of Prettier functionality.

@DeviousStoat
Copy link
Contributor

Hey @David-Else!
I am very new to helix so others would probably give more accurate answers but from what I experienced, the shell_pipe command doesn't do anything if the shell command returned a non zero error code and it simply writes Command failed in the status bar. Since this :pipe command uses the same function, I think it works the same. Do you have an example where it didn't work like that for you?

Another improvement might be to print the stderr content to the status bar though because for now I think it is only logged and it just prints Command failed, so we have no idea what went wrong I think.

@David-Else
Copy link
Contributor

@DeviousStoat command failed is fine :) I will try this out as soon as it hits master, thanks so much for your contribution! I only started using Helix a couple of days ago myself. I think if development continues at this pace it will be the ultimate modal editor on planet earth within a few months! Most of the missing things I really need are discussed or PRs already :)

@sudormrfbin
Copy link
Member

We would need a way to turn off LSP formatting on a server by server basis.

auto-format option controls this:

# in <config_dir>/helix/languages.toml (same directory as config.toml)

[[language]]
name = "rust"
auto-format = false

@the-mikedavis
Copy link
Member

Closed by #1972.

Example:

# <config_dir>/helix/config.toml
[keys.normal]
"=" = ":pipe fmt -w 80"

is my stop-gap until the reflow command lands.

@David-Else
Copy link
Contributor

David-Else commented Apr 18, 2022

Can pipe type-able commands be used for Prettier somehow? https://prettier.io/docs/en/cli.html

@the-mikedavis
Copy link
Member

The only trouble with prettier would be specifying the language since it supports multiple. If I wanted to bind a key to format json:

[keys.normal]
"=" = ":pipe prettier --parser json"

Then I can say %= in a json document to format the whole buffer.

But there isn't a way to pass in the buffer's language currently. I believe in vim and kakoune you can do it with something like environment variables.

@jakenvac
Copy link
Contributor

@the-mikedavis did you find a workaround for this or are you currently using language specific keybinds?

@the-mikedavis
Copy link
Member

There isn't a workaround for this. If I use prettier I call out to it in a separate shell.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-helix-term Area: Helix term improvements C-enhancement Category: Improvements E-good-first-issue Call for participation: Issues suitable for new contributors E-help-wanted Call for participation: Extra attention is needed
Projects
None yet
Development

No branches or pull requests

7 participants