diff --git a/CHANGELOG.md b/CHANGELOG.md index aa5e7eb6..cc8c9c4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,63 +12,74 @@ CHANGELOG # Send actions to the server curl -XPOST localhost:6266 -d 'reload(seq 100)+change-prompt(hundred> )' ``` -- Added `pos(...)` action to move the cursor to the numeric position - - `first` and `last` are equivalent to `pos(1)` and `pos(-1)` respectively - ```sh - # Put the cursor on the 10th item - seq 100 | fzf --sync --bind 'start:pos(10)' +- New event + - Added `load` event that is triggered when the input stream is complete + and the initial processing of the list is complete. + ```sh + # Change the prompt to "loaded" when the input stream is complete + (seq 10; sleep 1; seq 11 20) | fzf --prompt 'Loading> ' --bind 'load:change-prompt:Loaded> ' - # Put the cursor on the 10th to last item - seq 100 | fzf --sync --bind 'start:pos(-10)' - ``` -- Added `load` event that is triggered when the input stream is complete and - the initial processing of the list is complete. - ```sh - # Change the prompt to "loaded" when the input stream is complete - (seq 10; sleep 1; seq 11 20) | fzf --prompt 'Loading> ' --bind 'load:change-prompt:Loaded> ' + # You can use it instead of 'start' event without `--sync` if asynchronous + # trigger is not an issue. + (seq 10; sleep 1; seq 11 20) | fzf --bind 'load:last' + ``` +- New actions + - Added `pos(...)` action to move the cursor to the numeric position + - `first` and `last` are equivalent to `pos(1)` and `pos(-1)` respectively + ```sh + # Put the cursor on the 10th item + seq 100 | fzf --sync --bind 'start:pos(10)' - # You can use it instead of 'start' event without `--sync` if asynchronous - # trigger is not an issue. - (seq 10; sleep 1; seq 11 20) | fzf --bind 'load:last' - ``` -- Added `next-selected` and `prev-selected` actions to move between selected - items - ```sh - # `next-selected` will move the pointer to the next selected item below the current line - # `prev-selected` will move the pointer to the previous selected item above the current line - seq 10 | fzf --multi --bind ctrl-n:next-selected,ctrl-p:prev-selected + # Put the cursor on the 10th to last item + seq 100 | fzf --sync --bind 'start:pos(-10)' + ``` + - Added `reload-sync(...)` action which replaces the current list only after + the reload process is complete. This is useful when the command takes + a while to produce the initial output and you don't want fzf to run against + an empty list while the command is running. + ```sh + # You can still filter and select entries from the initial list for 3 seconds + seq 100 | fzf --bind 'load:reload-sync(sleep 3; seq 1000)+unbind(load)' + ``` + - Added `next-selected` and `prev-selected` actions to move between selected + items + ```sh + # `next-selected` will move the pointer to the next selected item below the current line + # `prev-selected` will move the pointer to the previous selected item above the current line + seq 10 | fzf --multi --bind ctrl-n:next-selected,ctrl-p:prev-selected - # Both actions respect --layout option - seq 10 | fzf --multi --bind ctrl-n:next-selected,ctrl-p:prev-selected --layout reverse - ``` -- Added `change-query(...)` action that simply changes the query string to the - given static string. This can be useful when used with `--listen`. - ```sh - curl localhost:6266 -d "change-query:$(date)" - ``` -- Added `transform-query(...)` action for transforming the query string using - an external command - ```sh - # Press space to convert the query to uppercase letters - fzf --bind 'space:transform-query(tr [:lower:] [:upper:] <<< {q})' + # Both actions respect --layout option + seq 10 | fzf --multi --bind ctrl-n:next-selected,ctrl-p:prev-selected --layout reverse + ``` + - Added `change-query(...)` action that simply changes the query string to the + given static string. This can be useful when used with `--listen`. + ```sh + curl localhost:6266 -d "change-query:$(date)" + ``` + - Added `transform-query(...)` action for transforming the query string using + an external command + ```sh + # Press space to convert the query to uppercase letters + fzf --bind 'space:transform-query(tr [:lower:] [:upper:] <<< {q})' - # Bind it to 'change' event for automatic conversion - fzf --bind 'change:transform-query(tr [:lower:] [:upper:] <<< {q})' + # Bind it to 'change' event for automatic conversion + fzf --bind 'change:transform-query(tr [:lower:] [:upper:] <<< {q})' - # Can only type numbers - fzf --bind 'change:transform-query(sed 's/[^0-9]//g' <<< {q})' - ``` -- `put` action can optionally take an argument string - ```sh - # a will put 'alpha' on the prompt, ctrl-b will put 'bravo' - fzf --bind 'a:put+put(lpha),ctrl-b:put(bravo)' - ``` -- `double-click` will behave the same as `enter` unless otherwise specified, - so you don't have to repeat the same action twice in `--bind` in most cases. - ```sh - # No need to bind 'double-click' to the same action - fzf --bind 'enter:execute:less {}' # --bind 'double-click:execute:less {}' - ``` + # Can only type numbers + fzf --bind 'change:transform-query(sed 's/[^0-9]//g' <<< {q})' + ``` +- Improvements + - `put` action can optionally take an argument string + ```sh + # a will put 'alpha' on the prompt, ctrl-b will put 'bravo' + fzf --bind 'a:put+put(lpha),ctrl-b:put(bravo)' + ``` + - `double-click` will behave the same as `enter` unless otherwise specified, + so you don't have to repeat the same action twice in `--bind` in most cases. + ```sh + # No need to bind 'double-click' to the same action + fzf --bind 'enter:execute:less {}' # --bind 'double-click:execute:less {}' + ``` - Added color name `preview-label` for `--preview-label` (defaults to `label` for `--border-label`) - Minor bug fixes and improvements diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 5983dcfb..61d583f6 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -1019,6 +1019,7 @@ A key or an event can be bound to one or more of the following actions. \fBrefresh-preview\fR \fBrebind(...)\fR (rebind bindings after \fBunbind\fR) \fBreload(...)\fR (see below for the details) + \fBreload-sync(...)\fR (see below for the details) \fBreplace-query\fR (replace query string with the current selection) \fBselect\fR \fBselect-all\fR (select all matches) @@ -1122,6 +1123,16 @@ e.g. fzf --bind "change:reload:$RG_PREFIX {q} || true" \\ --ansi --disabled --query "$INITIAL_QUERY"\fR +\fBreload-sync(...)\fR is a synchronous version of \fBreload\fR that replaces +the list only when the command is complete. This is useful when the command +takes a while to produce the initial output and you don't want fzf to run +against an empty list while the command is running. + + +e.g. + \fB# You can still filter and select entries from the initial list for 3 seconds + seq 100 | fzf --bind 'load:reload-sync(sleep 3; seq 1000)+unbind(load)'\fR + .SS PREVIEW BINDING With \fBpreview(...)\fR action, you can specify multiple different preview diff --git a/src/core.go b/src/core.go index 2ddddc35..c6ff3dcf 100644 --- a/src/core.go +++ b/src/core.go @@ -213,15 +213,6 @@ func Run(opts *Options, version string, revision string) { clearSelection := util.Once(false) ticks := 0 var nextCommand *string - restart := func(command string) { - reading = true - clearCache = util.Once(true) - clearSelection = util.Once(true) - chunkList.Clear() - itemIndex = 0 - header = make([]string, 0, opts.HeaderLines) - go reader.restart(command) - } eventBox.Watch(EvtReadNew) total := 0 query := []rune{} @@ -236,6 +227,25 @@ func Run(opts *Options, version string, revision string) { terminal.startChan <- fitpad{-1, -1} } } + + useSnapshot := false + var snapshot []*Chunk + var prevSnapshot []*Chunk + var count int + restart := func(command string) { + reading = true + clearCache = util.Once(true) + clearSelection = util.Once(true) + // We should not update snapshot if reload is triggered again while + // the previous reload is in progress + if useSnapshot && prevSnapshot != nil { + snapshot, count = chunkList.Snapshot() + } + chunkList.Clear() + itemIndex = 0 + header = make([]string, 0, opts.HeaderLines) + go reader.restart(command) + } for { delay := true ticks++ @@ -267,7 +277,13 @@ func Run(opts *Options, version string, revision string) { } else { reading = reading && evt == EvtReadNew } - snapshot, count := chunkList.Snapshot() + if useSnapshot && evt == EvtReadFin { + useSnapshot = false + prevSnapshot = nil + } + if !useSnapshot { + snapshot, count = chunkList.Snapshot() + } total = count terminal.UpdateCount(total, !reading, value.(*string)) if opts.Sync { @@ -286,6 +302,9 @@ func Run(opts *Options, version string, revision string) { case searchRequest: sort = val.sort command = val.command + if command != nil { + useSnapshot = val.sync + } } if command != nil { if reading { @@ -296,7 +315,9 @@ func Run(opts *Options, version string, revision string) { } break } - snapshot, _ := chunkList.Snapshot() + if !useSnapshot { + snapshot, _ = chunkList.Snapshot() + } reset := clearCache() matcher.Reset(snapshot, input(reset), true, !reading, sort, reset) delay = false diff --git a/src/options.go b/src/options.go index 08b48258..79a4a0ad 100644 --- a/src/options.go +++ b/src/options.go @@ -892,7 +892,7 @@ const ( func init() { executeRegexp = regexp.MustCompile( - `(?si)[:+](execute(?:-multi|-silent)?|reload|preview|change-query|change-prompt|change-preview-window|change-preview|(?:re|un)bind|pos|put|transform-query)`) + `(?si)[:+](execute(?:-multi|-silent)?|reload(?:-sync)?|preview|change-query|change-prompt|change-preview-window|change-preview|(?:re|un)bind|pos|put|transform-query)`) splitRegexp = regexp.MustCompile("[,:]+") actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+") } @@ -1185,6 +1185,8 @@ func isExecuteAction(str string) actionType { switch prefix { case "reload": return actReload + case "reload-sync": + return actReloadSync case "unbind": return actUnbind case "rebind": diff --git a/src/terminal.go b/src/terminal.go index 02b1a5df..7e900c96 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -335,6 +335,7 @@ const ( actFirst actLast actReload + actReloadSync actDisableSearch actEnableSearch actSelect @@ -353,6 +354,7 @@ type placeholderFlags struct { type searchRequest struct { sort bool + sync bool command *string } @@ -2540,6 +2542,7 @@ func (t *Terminal) Loop() { }() for looping { var newCommand *string + var reloadSync bool changed := false beof := false queryChanged := false @@ -3030,7 +3033,7 @@ func (t *Terminal) Loop() { } } } - case actReload: + case actReload, actReloadSync: t.failed = nil valid, list := t.buildPlusList(a.a, false) @@ -3044,6 +3047,7 @@ func (t *Terminal) Loop() { if valid { command := t.replacePlaceholder(a.a, false, string(t.input), list) newCommand = &command + reloadSync = a.t == actReloadSync t.reading = true } case actUnbind: @@ -3173,7 +3177,7 @@ func (t *Terminal) Loop() { t.mutex.Unlock() // Must be unlocked before touching reqBox if changed || newCommand != nil { - t.eventBox.Set(EvtSearchNew, searchRequest{sort: t.sort, command: newCommand}) + t.eventBox.Set(EvtSearchNew, searchRequest{sort: t.sort, sync: reloadSync, command: newCommand}) } for _, event := range events { t.reqBox.Set(event, nil) diff --git a/test/test_go.rb b/test/test_go.rb index a4d2e2a8..18999da3 100755 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -2161,6 +2161,15 @@ class TestGoFZF < TestBase tmux.until { |lines| assert_includes lines[1], '4' } end + def test_reload_sync + tmux.send_keys "seq 100 | #{FZF} --bind 'load:reload-sync(sleep 1; seq 1000)+unbind(load)'", :Enter + tmux.until { |lines| assert_equal 100, lines.item_count } + tmux.send_keys '00' + tmux.until { |lines| assert_equal 1, lines.match_count } + # After 1 second + tmux.until { |lines| assert_equal 10, lines.match_count } + end + def test_scroll_off tmux.send_keys "seq 1000 | #{FZF} --scroll-off=3 --bind l:last", :Enter tmux.until { |lines| assert_equal 1000, lines.item_count }