zed-industries / zed

Code at the speed of thought – Zed is a high-performance, multiplayer code editor from the creators of Atom and Tree-sitter.
https://zed.dev
Other
47.12k stars 2.71k forks source link

Run shell command on selected region #12598

Open failable opened 3 months ago

failable commented 3 months ago

Check for existing issues

Describe the feature

Add ability to run shell command on selected region.

Use cases:

Below is an example of how it work in Emacs

Select a region and invoke the command

In Emacs, the shortcut is: M-|

Screenshot 2024-06-03 at 17 26 32

Display output in somewhere (other window, popup etc.)

Screenshot 2024-06-03 at 17 26 37

Optional insert into buffer or replace the selected region with the output

In Emacs, the shortcut is C-u M-|

Screenshot 2024-06-03 at 17 27 00

Another real world use case

Screenshot 2024-06-03 at 17 30 44 Screenshot 2024-06-03 at 17 30 55

If applicable, add mockups / screenshots to help present your vision of the feature

No response

mrnugget commented 3 months ago

That's already possible with tasks, at least showing the output:

failable commented 3 months ago

@mrnugget Yes, tasks solve some of them.

baldwindavid commented 3 months ago

Yes, it would be really nice to have the concept of a "target" for them as mentioned in #9445 . My comment in this issue I think gets at what you might be looking for here https://github.com/zed-industries/zed/issues/9445#issuecomment-2003851831 with a "target" of $ZED_SELECTED_TEXT. As of now everything is very terminal-specific, which is great, but think this could be further evolved to handle non-terminal scenarios.

Yevgnen commented 2 months ago

It would be great if there were a way to directly insert the results of a Shell command into the current file without having to copy and paste.

quinncomendant commented 2 weeks ago

In TextMate, this function is called Filter Through Command. It's the simplest, most powerful way to manipulate and analyze text because it combines a GUI editor with CLI command pipelines. I use it dozens of times every day.

It's so simple, just look at it:

SCR-20240830-rawa

@mrnugget suggests this is already possible with Zed's awesome Tasks feature, but it's not the same. Zed Tasks fall short:


Here's a few things I use it for:

failable commented 2 weeks ago

Zed Tasks fall short: You have to make a text selection first (doesn't fall back to entire document if no selection). You have to type $ZED_SELECTED_TEXT in the task prompt, or <<<"$ZED_SELECTED_TEXT" to pass it as STDIN to a command. The only output option is to a Terminal context.

@quinncomendant It seems that the operations in TextMate are quite similar to those in Emacs.

quinncomendant commented 1 week ago

Related issue: Allow tasks to receive input and send output from/to different locations

andelink commented 1 week ago

I generally +1 the request for built-in Zed support for this, but it's possible to work around this with some Perl and FZF. I'll share a script that should be a drop-in solution. Maybe it will be useful for somebody.

But first, one quick note regarding this comment by @quinncomendant:

You have to make a text selection first (doesn't fall back to entire document if no selection)

The zsh (P) parameter expansion flag is one way to solve this. With it, you can create tasks that run on either selected text or the whole file. For example, here's your jq -S task you mentioned:

Code + Demo

```jsonc { "label": "json-format", "env": { "ZEDSELECTED": "ZED_SELECTED_TEXT", "ZEDROW": "ZED_ROW", "ZEDCOL": "ZED_COLUMN" }, "command": "[[ -z ${(P)ZEDSELECTED} ]] && (jq -S '.' <<<$(<${ZED_FILE}) > ${ZED_FILE}) || perl", "args": [ "-spi", "-e", "'BEGIN{$col=$ENV{$ENV{ZEDCOL}}-1} undef $/ if $.==$ENV{ $ENV{ZEDROW} }-1; $.==$ENV{ $ENV{ZEDROW} } and s|(?<=.{$col})\\Q$ENV{ $ENV{ZEDSELECTED} }|$repl|s'", "--", "-repl=\"$(jq -Srn \"${(P)ZEDSELECTED}\")\"", "${ZED_FILE}" ], "reveal": "never", "hide": "on_success", "shell": { "with_arguments": { "program": "/usr/bin/env", "args": ["zsh", "-f"] } } } ```
Here's a video: https://github.com/user-attachments/assets/8022bc54-d219-43b7-a88f-92b6d5818c93

For the other items in your comment, I have a poor mans approximation of the Filter Through Command behavior you've described (if I understand it correctly). It's basically a generalized version of the above task definition, but put in an actual file to clean things up a bit. Giving us tasks like this:

Small task definitions

```jsonc [ { "label": "filter-through-command", "command": "~/bin/zed-filter-through-command", "reveal": "always", "hide": "on_success", "shell": { "program": "sh" } }, { "label": "filter-through-jq", "command": "~/bin/zed-filter-through-command", "args": ["jq", "-S"], // same as above but defaulting to jq -S "reveal": "always", "hide": "on_success", "shell": { "program": "sh" } } ] ```

It uses FZF to allow user input for what command to run and shows a preview of the output. Also works for just selection or whole file input.

First half is your curl API call, second half is a pipelined multiline regex-replace:

https://github.com/user-attachments/assets/05cda871-2cd2-42a9-a561-31f971c97edc

This one shows the jq default and preview scrolling:

https://github.com/user-attachments/assets/df49e8a0-e7e2-4e6b-8a07-dfe16d002820

You could do I think most everything you've described eariler with this, or with minor modifications. But anyways, here's the script itself, if you or anyone is interested:.

zed-filter-through-command

https://gist.github.com/andelink/00af4840d16038371d2e6ce78c704c66 ```zsh #!/usr/bin/env zsh -f setopt errexit setopt pipefail if [[ ! $(command -v fzf) ]] { echo "\e[31;1mfzf not found\e[m: brew install fzf" >&2 exit 1 } # Export for FZF child processes export OUTFILE=${ZED_FILE} if [[ -n ${ZED_SELECTED_TEXT} ]] { OUTFILE="$(mktemp -p /tmp "${${(%):-%N}:t:r}.XXXXX")" } HEADER="${ZED_FILE}${ZED_SELECTED_TEXT:+:${ZED_ROW}:${ZED_COLUMN}}" BORDER_LABEL=' ENTER write output / CTRL-Y copy output / CTRL-T copy command / CTRL-Q clear command / ESC or CTRL-C abort ' PREVIEW_LABEL=' Preview ([alt] up/down, page up/down) ' FZFCMD='{ [[ -n \${ZED_SELECTED_TEXT} ]] && <<<\${ZED_SELECTED_TEXT} || <<<\$(<${ZED_FILE}) } | ${(@)${=${(Q):-{q}}}}' { [[ -n ${ZED_SELECTED_TEXT} ]] && <<<"${ZED_SELECTED_TEXT}" || <<<$(<${ZED_FILE}) } | fzf \ --wrap \ --read0 \ --no-info \ --no-bold \ --no-mouse \ --disabled \ --reverse \ --no-separator \ --header-first \ --pointer '' \ --border bottom \ --border-label-pos 0:bottom \ --preview-window right,60%,wrap \ --query "${*:-cat}" \ --header "${HEADER}" \ --border-label "${BORDER_LABEL}" \ --preview-label "${PREVIEW_LABEL}" \ --preview "eval \"${FZFCMD}\"" \ --bind "ctrl-t:execute-silent:pbcopy <<< {q}" \ --bind "ctrl-y:execute-silent:eval \"${FZFCMD} | pbcopy\"" \ --bind "enter:execute-silent:eval \"${FZFCMD} >${OUTFILE}\"" \ --bind enter:+accept-or-print-query \ --bind ctrl-q:clear-query \ --bind up:preview-up \ --bind down:preview-down \ --bind scroll-up:preview-up \ --bind scroll-down:preview-down \ --bind page-up:preview-page-up \ --bind page-down:preview-page-down \ --bind alt-up:preview-top \ --bind alt-down:preview-bottom \ --color header:italic,label:italic,bg:-1,bg+:-1,fg:-1,fg+:-1,border:-1 \ > /dev/null || RC=$? # Code 130: FZF was aborted # No selection: FZF wrote to the output file [[ ${RC:-0} -eq 130 || -z ${ZED_SELECTED_TEXT} ]] && exit # Assume if the user got to this point, they intended to write something if [[ ! -s ${OUTFILE} ]] { echo "\e[31;1mNo output to write\e[m" >&2 exit 1 } # Overwrite selection with command output PERLCMD=( 'BEGIN{$col=$ENV{ZED_COLUMN}-1; undef $/ if $ENV{ZED_ROW}==1}' 'undef $/ if $.==$ENV{ZED_ROW}-1;' '$.==$ENV{ZED_ROW} and s|(?<=.{$col})\Q$ENV{ZED_SELECTED_TEXT}|$repl|s' ) perl -spi \ -e "${PERLCMD[*]}" \ -- -repl="$(<${OUTFILE})" \ ${ZED_FILE} ```