Add reload-sync action

Close #2816
This commit is contained in:
Junegunn Choi 2022-12-29 20:03:51 +09:00
parent 14775aa975
commit 6c37177cf5
No known key found for this signature in database
GPG Key ID: 254BC280FEF9C627
6 changed files with 124 additions and 66 deletions

View File

@ -12,63 +12,74 @@ CHANGELOG
# Send actions to the server # Send actions to the server
curl -XPOST localhost:6266 -d 'reload(seq 100)+change-prompt(hundred> )' curl -XPOST localhost:6266 -d 'reload(seq 100)+change-prompt(hundred> )'
``` ```
- Added `pos(...)` action to move the cursor to the numeric position - New event
- `first` and `last` are equivalent to `pos(1)` and `pos(-1)` respectively - Added `load` event that is triggered when the input stream is complete
```sh and the initial processing of the list is complete.
# Put the cursor on the 10th item ```sh
seq 100 | fzf --sync --bind 'start:pos(10)' # 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 # You can use it instead of 'start' event without `--sync` if asynchronous
seq 100 | fzf --sync --bind 'start:pos(-10)' # trigger is not an issue.
``` (seq 10; sleep 1; seq 11 20) | fzf --bind 'load:last'
- Added `load` event that is triggered when the input stream is complete and ```
the initial processing of the list is complete. - New actions
```sh - Added `pos(...)` action to move the cursor to the numeric position
# Change the prompt to "loaded" when the input stream is complete - `first` and `last` are equivalent to `pos(1)` and `pos(-1)` respectively
(seq 10; sleep 1; seq 11 20) | fzf --prompt 'Loading> ' --bind 'load:change-prompt:Loaded> ' ```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 # Put the cursor on the 10th to last item
# trigger is not an issue. seq 100 | fzf --sync --bind 'start:pos(-10)'
(seq 10; sleep 1; seq 11 20) | fzf --bind 'load:last' ```
``` - Added `reload-sync(...)` action which replaces the current list only after
- Added `next-selected` and `prev-selected` actions to move between selected the reload process is complete. This is useful when the command takes
items a while to produce the initial output and you don't want fzf to run against
```sh an empty list while the command is running.
# `next-selected` will move the pointer to the next selected item below the current line ```sh
# `prev-selected` will move the pointer to the previous selected item above the current line # You can still filter and select entries from the initial list for 3 seconds
seq 10 | fzf --multi --bind ctrl-n:next-selected,ctrl-p:prev-selected 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 # Both actions respect --layout option
seq 10 | fzf --multi --bind ctrl-n:next-selected,ctrl-p:prev-selected --layout reverse 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 - Added `change-query(...)` action that simply changes the query string to the
given static string. This can be useful when used with `--listen`. given static string. This can be useful when used with `--listen`.
```sh ```sh
curl localhost:6266 -d "change-query:$(date)" curl localhost:6266 -d "change-query:$(date)"
``` ```
- Added `transform-query(...)` action for transforming the query string using - Added `transform-query(...)` action for transforming the query string using
an external command an external command
```sh ```sh
# Press space to convert the query to uppercase letters # Press space to convert the query to uppercase letters
fzf --bind 'space:transform-query(tr [:lower:] [:upper:] <<< {q})' fzf --bind 'space:transform-query(tr [:lower:] [:upper:] <<< {q})'
# Bind it to 'change' event for automatic conversion # Bind it to 'change' event for automatic conversion
fzf --bind 'change:transform-query(tr [:lower:] [:upper:] <<< {q})' fzf --bind 'change:transform-query(tr [:lower:] [:upper:] <<< {q})'
# Can only type numbers # Can only type numbers
fzf --bind 'change:transform-query(sed 's/[^0-9]//g' <<< {q})' fzf --bind 'change:transform-query(sed 's/[^0-9]//g' <<< {q})'
``` ```
- `put` action can optionally take an argument string - Improvements
```sh - `put` action can optionally take an argument string
# a will put 'alpha' on the prompt, ctrl-b will put 'bravo' ```sh
fzf --bind 'a:put+put(lpha),ctrl-b:put(bravo)' # 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. - `double-click` will behave the same as `enter` unless otherwise specified,
```sh so you don't have to repeat the same action twice in `--bind` in most cases.
# No need to bind 'double-click' to the same action ```sh
fzf --bind 'enter:execute:less {}' # --bind 'double-click:execute:less {}' # 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` - Added color name `preview-label` for `--preview-label` (defaults to `label`
for `--border-label`) for `--border-label`)
- Minor bug fixes and improvements - Minor bug fixes and improvements

View File

@ -1019,6 +1019,7 @@ A key or an event can be bound to one or more of the following actions.
\fBrefresh-preview\fR \fBrefresh-preview\fR
\fBrebind(...)\fR (rebind bindings after \fBunbind\fR) \fBrebind(...)\fR (rebind bindings after \fBunbind\fR)
\fBreload(...)\fR (see below for the details) \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) \fBreplace-query\fR (replace query string with the current selection)
\fBselect\fR \fBselect\fR
\fBselect-all\fR (select all matches) \fBselect-all\fR (select all matches)
@ -1122,6 +1123,16 @@ e.g.
fzf --bind "change:reload:$RG_PREFIX {q} || true" \\ fzf --bind "change:reload:$RG_PREFIX {q} || true" \\
--ansi --disabled --query "$INITIAL_QUERY"\fR --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 .SS PREVIEW BINDING
With \fBpreview(...)\fR action, you can specify multiple different preview With \fBpreview(...)\fR action, you can specify multiple different preview

View File

@ -213,15 +213,6 @@ func Run(opts *Options, version string, revision string) {
clearSelection := util.Once(false) clearSelection := util.Once(false)
ticks := 0 ticks := 0
var nextCommand *string 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) eventBox.Watch(EvtReadNew)
total := 0 total := 0
query := []rune{} query := []rune{}
@ -236,6 +227,25 @@ func Run(opts *Options, version string, revision string) {
terminal.startChan <- fitpad{-1, -1} 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 { for {
delay := true delay := true
ticks++ ticks++
@ -267,7 +277,13 @@ func Run(opts *Options, version string, revision string) {
} else { } else {
reading = reading && evt == EvtReadNew reading = reading && evt == EvtReadNew
} }
snapshot, count := chunkList.Snapshot() if useSnapshot && evt == EvtReadFin {
useSnapshot = false
prevSnapshot = nil
}
if !useSnapshot {
snapshot, count = chunkList.Snapshot()
}
total = count total = count
terminal.UpdateCount(total, !reading, value.(*string)) terminal.UpdateCount(total, !reading, value.(*string))
if opts.Sync { if opts.Sync {
@ -286,6 +302,9 @@ func Run(opts *Options, version string, revision string) {
case searchRequest: case searchRequest:
sort = val.sort sort = val.sort
command = val.command command = val.command
if command != nil {
useSnapshot = val.sync
}
} }
if command != nil { if command != nil {
if reading { if reading {
@ -296,7 +315,9 @@ func Run(opts *Options, version string, revision string) {
} }
break break
} }
snapshot, _ := chunkList.Snapshot() if !useSnapshot {
snapshot, _ = chunkList.Snapshot()
}
reset := clearCache() reset := clearCache()
matcher.Reset(snapshot, input(reset), true, !reading, sort, reset) matcher.Reset(snapshot, input(reset), true, !reading, sort, reset)
delay = false delay = false

View File

@ -892,7 +892,7 @@ const (
func init() { func init() {
executeRegexp = regexp.MustCompile( 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("[,:]+") splitRegexp = regexp.MustCompile("[,:]+")
actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+") actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+")
} }
@ -1185,6 +1185,8 @@ func isExecuteAction(str string) actionType {
switch prefix { switch prefix {
case "reload": case "reload":
return actReload return actReload
case "reload-sync":
return actReloadSync
case "unbind": case "unbind":
return actUnbind return actUnbind
case "rebind": case "rebind":

View File

@ -335,6 +335,7 @@ const (
actFirst actFirst
actLast actLast
actReload actReload
actReloadSync
actDisableSearch actDisableSearch
actEnableSearch actEnableSearch
actSelect actSelect
@ -353,6 +354,7 @@ type placeholderFlags struct {
type searchRequest struct { type searchRequest struct {
sort bool sort bool
sync bool
command *string command *string
} }
@ -2540,6 +2542,7 @@ func (t *Terminal) Loop() {
}() }()
for looping { for looping {
var newCommand *string var newCommand *string
var reloadSync bool
changed := false changed := false
beof := false beof := false
queryChanged := false queryChanged := false
@ -3030,7 +3033,7 @@ func (t *Terminal) Loop() {
} }
} }
} }
case actReload: case actReload, actReloadSync:
t.failed = nil t.failed = nil
valid, list := t.buildPlusList(a.a, false) valid, list := t.buildPlusList(a.a, false)
@ -3044,6 +3047,7 @@ func (t *Terminal) Loop() {
if valid { if valid {
command := t.replacePlaceholder(a.a, false, string(t.input), list) command := t.replacePlaceholder(a.a, false, string(t.input), list)
newCommand = &command newCommand = &command
reloadSync = a.t == actReloadSync
t.reading = true t.reading = true
} }
case actUnbind: case actUnbind:
@ -3173,7 +3177,7 @@ func (t *Terminal) Loop() {
t.mutex.Unlock() // Must be unlocked before touching reqBox t.mutex.Unlock() // Must be unlocked before touching reqBox
if changed || newCommand != nil { 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 { for _, event := range events {
t.reqBox.Set(event, nil) t.reqBox.Set(event, nil)

View File

@ -2161,6 +2161,15 @@ class TestGoFZF < TestBase
tmux.until { |lines| assert_includes lines[1], '4' } tmux.until { |lines| assert_includes lines[1], '4' }
end 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 def test_scroll_off
tmux.send_keys "seq 1000 | #{FZF} --scroll-off=3 --bind l:last", :Enter tmux.send_keys "seq 1000 | #{FZF} --scroll-off=3 --bind l:last", :Enter
tmux.until { |lines| assert_equal 1000, lines.item_count } tmux.until { |lines| assert_equal 1000, lines.item_count }