mirror of
https://github.com/junegunn/fzf.git
synced 2025-11-08 11:23:47 -05:00
Add {*} placeholder flag
This commit is contained in:
@@ -3,6 +3,11 @@ CHANGELOG
|
||||
|
||||
0.63.0
|
||||
------
|
||||
- Added `{*}` placeholder flag that evaluates to all matched items.
|
||||
```bash
|
||||
seq 10000 | fzf --preview "awk '{sum += \$1} END {print sum}' {*f}"
|
||||
```
|
||||
- Use this with caution, as it can make fzf sluggish for large lists.
|
||||
- Added background variants of transform actions with `bg-` prefix that run asynchronously in the background
|
||||
```sh
|
||||
GETTER='curl -s http://metaphorpsum.com/sentences/1'
|
||||
|
||||
@@ -789,13 +789,16 @@ fzf also exports \fB$FZF_PREVIEW_TOP\fR and \fB$FZF_PREVIEW_LEFT\fR so that
|
||||
the preview command can determine the position of the preview window.
|
||||
|
||||
A placeholder expression starting with \fB+\fR flag will be replaced to the
|
||||
space-separated list of the selected lines (or the current line if no selection
|
||||
space-separated list of the selected items (or the current item if no selection
|
||||
was made) individually quoted.
|
||||
|
||||
e.g.
|
||||
\fBfzf \-\-multi \-\-preview='head \-10 {+}'
|
||||
git log \-\-oneline | fzf \-\-multi \-\-preview 'git show {+1}'\fR
|
||||
|
||||
Similarly, a placeholder expression starting with \fB*\fR flag will be replaced
|
||||
to the space-separated list of all matched items individually quoted.
|
||||
|
||||
Each expression expands to a quoted string, so that it's safe to pass it as an
|
||||
argument to an external command. So you should not manually add quotes around
|
||||
the curly braces. But if you don't want this behavior, you can put
|
||||
@@ -807,14 +810,13 @@ from the replacement string. To preserve the whitespace, use the \fBs\fR flag.
|
||||
|
||||
A placeholder expression with \fBf\fR flag is replaced to the path of
|
||||
a temporary file that holds the evaluated list. This is useful when you
|
||||
multi-select a large number of items and the length of the evaluated string may
|
||||
pass a large number of items and the length of the evaluated string may
|
||||
exceed \fBARG_MAX\fR.
|
||||
|
||||
e.g.
|
||||
\fB# Press CTRL\-A to select 100K items and see the sum of all the numbers.
|
||||
\fB# See the sum of all the matched numbers
|
||||
# This won't work properly without 'f' flag due to ARG_MAX limit.
|
||||
seq 100000 | fzf \-\-multi \-\-bind ctrl\-a:select\-all \\
|
||||
\-\-preview "awk '{sum+=\\$1} END {print sum}' {+f}"\fR
|
||||
seq 100000 | fzf \-\-preview "awk '{sum+=\\$1} END {print sum}' {*f}"\fR
|
||||
|
||||
Also,
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ const maxFocusEvents = 10000
|
||||
const blockDuration = 1 * time.Second
|
||||
|
||||
func init() {
|
||||
placeholder = regexp.MustCompile(`\\?(?:{[+sfr]*[0-9,-.]*}|{q(?::s?[0-9,-.]+)?}|{fzf:(?:query|action|prompt)}|{\+?f?nf?})`)
|
||||
placeholder = regexp.MustCompile(`\\?(?:{[+*sfr]*[0-9,-.]*}|{q(?::s?[0-9,-.]+)?}|{fzf:(?:query|action|prompt)}|{\+?f?nf?})`)
|
||||
whiteSuffix = regexp.MustCompile(`\s*$`)
|
||||
offsetComponentRegex = regexp.MustCompile(`([+-][0-9]+)|(-?/[1-9][0-9]*)`)
|
||||
offsetTrimCharsRegex = regexp.MustCompile(`[^0-9/+-]`)
|
||||
@@ -692,6 +692,7 @@ func processExecution(action actionType) bool {
|
||||
|
||||
type placeholderFlags struct {
|
||||
plus bool
|
||||
asterisk bool
|
||||
preserveSpace bool
|
||||
number bool
|
||||
forceUpdate bool
|
||||
@@ -713,7 +714,7 @@ type searchRequest struct {
|
||||
type previewRequest struct {
|
||||
template string
|
||||
scrollOffset int
|
||||
list []*Item
|
||||
list [3][]*Item
|
||||
env []string
|
||||
query string
|
||||
}
|
||||
@@ -4099,6 +4100,8 @@ func parsePlaceholder(match string) (bool, string, placeholderFlags) {
|
||||
trimmed := ""
|
||||
for _, char := range match[1:] {
|
||||
switch char {
|
||||
case '*':
|
||||
flags.asterisk = true
|
||||
case '+':
|
||||
flags.plus = true
|
||||
case 's':
|
||||
@@ -4122,19 +4125,16 @@ func parsePlaceholder(match string) (bool, string, placeholderFlags) {
|
||||
return false, matchWithoutFlags, flags
|
||||
}
|
||||
|
||||
func hasPreviewFlags(template string) (slot bool, plus bool, forceUpdate bool) {
|
||||
func hasPreviewFlags(template string) (slot bool, plus bool, asterisk bool, forceUpdate bool) {
|
||||
for _, match := range placeholder.FindAllString(template, -1) {
|
||||
escaped, _, flags := parsePlaceholder(match)
|
||||
if escaped {
|
||||
continue
|
||||
}
|
||||
if flags.plus {
|
||||
plus = true
|
||||
}
|
||||
if flags.forceUpdate {
|
||||
forceUpdate = true
|
||||
}
|
||||
slot = true
|
||||
plus = plus || flags.plus
|
||||
asterisk = asterisk || flags.asterisk
|
||||
forceUpdate = forceUpdate || flags.forceUpdate
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -4146,17 +4146,17 @@ type replacePlaceholderParams struct {
|
||||
printsep string
|
||||
forcePlus bool
|
||||
query string
|
||||
allItems []*Item
|
||||
allItems [3][]*Item // current, select, and all matched items
|
||||
lastAction actionType
|
||||
prompt string
|
||||
executor *util.Executor
|
||||
}
|
||||
|
||||
func (t *Terminal) replacePlaceholderInInitialCommand(template string) (string, []string) {
|
||||
return t.replacePlaceholder(template, false, string(t.input), []*Item{nil, nil})
|
||||
return t.replacePlaceholder(template, false, string(t.input), [3][]*Item{nil, nil, nil})
|
||||
}
|
||||
|
||||
func (t *Terminal) replacePlaceholder(template string, forcePlus bool, input string, list []*Item) (string, []string) {
|
||||
func (t *Terminal) replacePlaceholder(template string, forcePlus bool, input string, list [3][]*Item) (string, []string) {
|
||||
return replacePlaceholder(replacePlaceholderParams{
|
||||
template: template,
|
||||
stripAnsi: t.ansi,
|
||||
@@ -4177,7 +4177,7 @@ func (t *Terminal) evaluateScrollOffset() int {
|
||||
}
|
||||
|
||||
// We only need the current item to calculate the scroll offset
|
||||
replaced, tempFiles := t.replacePlaceholder(t.activePreviewOpts.scroll, false, "", []*Item{t.currentItem(), nil})
|
||||
replaced, tempFiles := t.replacePlaceholder(t.activePreviewOpts.scroll, false, "", [3][]*Item{{t.currentItem()}, nil, nil})
|
||||
removeFiles(tempFiles)
|
||||
offsetExpr := offsetTrimCharsRegex.ReplaceAllString(replaced, "")
|
||||
|
||||
@@ -4209,14 +4209,9 @@ func (t *Terminal) evaluateScrollOffset() int {
|
||||
|
||||
func replacePlaceholder(params replacePlaceholderParams) (string, []string) {
|
||||
tempFiles := []string{}
|
||||
current := params.allItems[:1]
|
||||
selected := params.allItems[1:]
|
||||
if current[0] == nil {
|
||||
current = []*Item{}
|
||||
}
|
||||
if selected[0] == nil {
|
||||
selected = []*Item{}
|
||||
}
|
||||
current := params.allItems[0]
|
||||
selected := params.allItems[1]
|
||||
matched := params.allItems[2]
|
||||
|
||||
// replace placeholders one by one
|
||||
replaced := placeholder.ReplaceAllStringFunc(params.template, func(match string) string {
|
||||
@@ -4312,7 +4307,9 @@ func replacePlaceholder(params replacePlaceholderParams) (string, []string) {
|
||||
// apply 'replace' function over proper set of items and return result
|
||||
|
||||
items := current
|
||||
if flags.plus || params.forcePlus {
|
||||
if flags.asterisk {
|
||||
items = matched
|
||||
} else if flags.plus || params.forcePlus {
|
||||
items = selected
|
||||
}
|
||||
replacements := make([]string, len(items))
|
||||
@@ -4546,11 +4543,15 @@ func (t *Terminal) currentItem() *Item {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Terminal) buildPlusList(template string, forcePlus bool) (bool, []*Item) {
|
||||
func (t *Terminal) buildPlusList(template string, forcePlus bool) (bool, [3][]*Item) {
|
||||
current := t.currentItem()
|
||||
slot, plus, forceUpdate := hasPreviewFlags(template)
|
||||
if !(!slot || forceUpdate || (forcePlus || plus) && len(t.selected) > 0) {
|
||||
return current != nil, []*Item{current, current}
|
||||
slot, plus, asterisk, forceUpdate := hasPreviewFlags(template)
|
||||
if !(!slot || forceUpdate || asterisk || (forcePlus || plus) && len(t.selected) > 0) {
|
||||
if current == nil {
|
||||
// Invalid
|
||||
return false, [3][]*Item{nil, nil, nil}
|
||||
}
|
||||
return true, [3][]*Item{{current}, {current}, nil}
|
||||
}
|
||||
|
||||
// We would still want to update preview window even if there is no match if
|
||||
@@ -4561,17 +4562,26 @@ func (t *Terminal) buildPlusList(template string, forcePlus bool) (bool, []*Item
|
||||
current = &minItem
|
||||
}
|
||||
|
||||
var sels []*Item
|
||||
if len(t.selected) == 0 {
|
||||
sels = []*Item{current, current}
|
||||
} else {
|
||||
sels = make([]*Item, len(t.selected)+1)
|
||||
sels[0] = current
|
||||
for i, sel := range t.sortSelected() {
|
||||
sels[i+1] = sel.item
|
||||
var all []*Item
|
||||
if asterisk {
|
||||
cnt := t.merger.Length()
|
||||
all = make([]*Item, cnt)
|
||||
for i := 0; i < cnt; i++ {
|
||||
item := t.merger.Get(i).item
|
||||
all[i] = item
|
||||
}
|
||||
}
|
||||
return true, sels
|
||||
|
||||
var sels []*Item
|
||||
if len(t.selected) == 0 {
|
||||
sels = []*Item{current}
|
||||
} else if len(t.selected) > 0 {
|
||||
sels = make([]*Item, len(t.selected))
|
||||
for i, sel := range t.sortSelected() {
|
||||
sels[i] = sel.item
|
||||
}
|
||||
}
|
||||
return true, [3][]*Item{{current}, sels, all}
|
||||
}
|
||||
|
||||
func (t *Terminal) selectItem(item *Item) bool {
|
||||
@@ -4831,7 +4841,8 @@ func (t *Terminal) Loop() error {
|
||||
stop := false
|
||||
t.previewBox.WaitFor(reqPreviewReady)
|
||||
for {
|
||||
var items []*Item
|
||||
requested := false
|
||||
var items [3][]*Item
|
||||
var commandTemplate string
|
||||
var env []string
|
||||
var query string
|
||||
@@ -4849,6 +4860,7 @@ func (t *Terminal) Loop() error {
|
||||
items = request.list
|
||||
env = request.env
|
||||
query = request.query
|
||||
requested = true
|
||||
}
|
||||
}
|
||||
events.Clear()
|
||||
@@ -4856,7 +4868,7 @@ func (t *Terminal) Loop() error {
|
||||
if stop {
|
||||
break
|
||||
}
|
||||
if items == nil {
|
||||
if !requested {
|
||||
continue
|
||||
}
|
||||
version++
|
||||
@@ -6396,7 +6408,7 @@ func (t *Terminal) Loop() error {
|
||||
// We run the command even when there's no match
|
||||
// 1. If the template doesn't have any slots
|
||||
// 2. If the template has {q}
|
||||
slot, _, forceUpdate := hasPreviewFlags(a.a)
|
||||
slot, _, _, forceUpdate := hasPreviewFlags(a.a)
|
||||
valid = !slot || forceUpdate
|
||||
}
|
||||
if valid {
|
||||
@@ -6585,7 +6597,7 @@ func (t *Terminal) Loop() error {
|
||||
}
|
||||
|
||||
if queryChanged && t.canPreview() && len(t.previewOpts.command) > 0 {
|
||||
_, _, forceUpdate := hasPreviewFlags(t.previewOpts.command)
|
||||
_, _, _, forceUpdate := hasPreviewFlags(t.previewOpts.command)
|
||||
if forceUpdate {
|
||||
t.version++
|
||||
}
|
||||
|
||||
@@ -189,6 +189,16 @@ class TestPreview < TestInteractive
|
||||
tmux.until { |lines| assert_includes lines[1], ' {//1 10/1 10 /123//0 9} ' }
|
||||
end
|
||||
|
||||
def test_preview_asterisk
|
||||
tmux.send_keys %(seq 5 | #{FZF} --multi --preview 'echo {} / {+} / {*}'), :Enter
|
||||
tmux.until { |lines| assert_equal 5, lines.match_count }
|
||||
tmux.until { |lines| assert_includes lines[1], ' 1 / 1 / 1 2 3 4 5 ' }
|
||||
tmux.send_keys :BTab
|
||||
tmux.until { |lines| assert_includes lines[1], ' 2 / 1 / 1 2 3 4 5 ' }
|
||||
tmux.send_keys :BTab
|
||||
tmux.until { |lines| assert_includes lines[1], ' 3 / 1 2 / 1 2 3 4 5 ' }
|
||||
end
|
||||
|
||||
def test_preview_file
|
||||
tmux.send_keys %[(echo foo bar; echo bar foo) | #{FZF} --multi --preview 'cat {+f} {+f2} {+nf} {+fn}' --print0], :Enter
|
||||
tmux.until { |lines| assert_includes lines[1], ' foo barbar00 ' }
|
||||
|
||||
Reference in New Issue
Block a user