diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bfb9427..4a8cb3a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,11 +3,6 @@ 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' diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index df2fba5f..a64b5e0d 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -789,16 +789,13 @@ 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 items (or the current item if no selection +space-separated list of the selected lines (or the current line 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 @@ -810,13 +807,14 @@ 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 -pass a large number of items and the length of the evaluated string may +multi-select a large number of items and the length of the evaluated string may exceed \fBARG_MAX\fR. e.g. - \fB# See the sum of all the matched numbers + \fB# Press CTRL\-A to select 100K items and see the sum of all the numbers. # This won't work properly without 'f' flag due to ARG_MAX limit. - seq 100000 | fzf \-\-preview "awk '{sum+=\\$1} END {print sum}' {*f}"\fR + seq 100000 | fzf \-\-multi \-\-bind ctrl\-a:select\-all \\ + \-\-preview "awk '{sum+=\\$1} END {print sum}' {+f}"\fR Also, diff --git a/src/terminal.go b/src/terminal.go index fe33b340..85a51112 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -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,7 +692,6 @@ func processExecution(action actionType) bool { type placeholderFlags struct { plus bool - asterisk bool preserveSpace bool number bool forceUpdate bool @@ -714,7 +713,7 @@ type searchRequest struct { type previewRequest struct { template string scrollOffset int - list [3][]*Item + list []*Item env []string query string } @@ -4100,8 +4099,6 @@ 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': @@ -4125,16 +4122,19 @@ func parsePlaceholder(match string) (bool, string, placeholderFlags) { return false, matchWithoutFlags, flags } -func hasPreviewFlags(template string) (slot bool, plus bool, asterisk bool, forceUpdate bool) { +func hasPreviewFlags(template string) (slot bool, plus 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 [3][]*Item // current, select, and all matched items + allItems []*Item lastAction actionType prompt string executor *util.Executor } func (t *Terminal) replacePlaceholderInInitialCommand(template string) (string, []string) { - return t.replacePlaceholder(template, false, string(t.input), [3][]*Item{nil, nil, nil}) + return t.replacePlaceholder(template, false, string(t.input), []*Item{nil, nil}) } -func (t *Terminal) replacePlaceholder(template string, forcePlus bool, input string, list [3][]*Item) (string, []string) { +func (t *Terminal) replacePlaceholder(template string, forcePlus bool, input string, list []*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, "", [3][]*Item{{t.currentItem()}, nil, nil}) + replaced, tempFiles := t.replacePlaceholder(t.activePreviewOpts.scroll, false, "", []*Item{t.currentItem(), nil}) removeFiles(tempFiles) offsetExpr := offsetTrimCharsRegex.ReplaceAllString(replaced, "") @@ -4209,9 +4209,14 @@ func (t *Terminal) evaluateScrollOffset() int { func replacePlaceholder(params replacePlaceholderParams) (string, []string) { tempFiles := []string{} - current := params.allItems[0] - selected := params.allItems[1] - matched := params.allItems[2] + current := params.allItems[:1] + selected := params.allItems[1:] + if current[0] == nil { + current = []*Item{} + } + if selected[0] == nil { + selected = []*Item{} + } // replace placeholders one by one replaced := placeholder.ReplaceAllStringFunc(params.template, func(match string) string { @@ -4307,9 +4312,7 @@ func replacePlaceholder(params replacePlaceholderParams) (string, []string) { // apply 'replace' function over proper set of items and return result items := current - if flags.asterisk { - items = matched - } else if flags.plus || params.forcePlus { + if flags.plus || params.forcePlus { items = selected } replacements := make([]string, len(items)) @@ -4543,15 +4546,11 @@ func (t *Terminal) currentItem() *Item { return nil } -func (t *Terminal) buildPlusList(template string, forcePlus bool) (bool, [3][]*Item) { +func (t *Terminal) buildPlusList(template string, forcePlus bool) (bool, []*Item) { current := t.currentItem() - 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} + slot, plus, forceUpdate := hasPreviewFlags(template) + if !(!slot || forceUpdate || (forcePlus || plus) && len(t.selected) > 0) { + return current != nil, []*Item{current, current} } // We would still want to update preview window even if there is no match if @@ -4562,26 +4561,17 @@ func (t *Terminal) buildPlusList(template string, forcePlus bool) (bool, [3][]*I current = &minItem } - 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 - } - } - var sels []*Item if len(t.selected) == 0 { - sels = []*Item{current} - } else if len(t.selected) > 0 { - sels = make([]*Item, len(t.selected)) + sels = []*Item{current, current} + } else { + sels = make([]*Item, len(t.selected)+1) + sels[0] = current for i, sel := range t.sortSelected() { - sels[i] = sel.item + sels[i+1] = sel.item } } - return true, [3][]*Item{{current}, sels, all} + return true, sels } func (t *Terminal) selectItem(item *Item) bool { @@ -4841,8 +4831,7 @@ func (t *Terminal) Loop() error { stop := false t.previewBox.WaitFor(reqPreviewReady) for { - requested := false - var items [3][]*Item + var items []*Item var commandTemplate string var env []string var query string @@ -4860,7 +4849,6 @@ func (t *Terminal) Loop() error { items = request.list env = request.env query = request.query - requested = true } } events.Clear() @@ -4868,7 +4856,7 @@ func (t *Terminal) Loop() error { if stop { break } - if !requested { + if items == nil { continue } version++ @@ -6408,7 +6396,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 { @@ -6597,7 +6585,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++ } diff --git a/test/test_preview.rb b/test/test_preview.rb index e2147735..576e36ec 100644 --- a/test/test_preview.rb +++ b/test/test_preview.rb @@ -189,16 +189,6 @@ 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 ' }