diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index a5fb9d6d..09a86215 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -2323,6 +2323,14 @@ default), which cancel the wait and discard the pending actions instead of performing their usual role. The remaining actions of such a binding still run, so a binding like \fBesc:cancel+first\fR is possible. +Asynchronous \fBbg\-transform\-*\fR actions are not affected; their results are +applied as soon as they arrive, even while waiting. For the same reason, +\fBwait\fR does not pair with them: in +\fBbg\-transform\-query(...)+wait\fR, the background command completes only +after \fBwait\fR has already been evaluated, so the search its result +eventually triggers is not waited for. Use the synchronous +\fBtransform\-query(...)\fR variant instead when chaining with \fBwait\fR. + If the search takes long enough, fzf indicates that it is waiting by dimming the input, hiding the cursor, and showing \fB(..)\fR on the info line. This visual feedback is debounced so that quick searches do not cause flickering. diff --git a/src/terminal.go b/src/terminal.go index 37c41595..41d17cb3 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -1890,10 +1890,10 @@ func (t *Terminal) UpdateList(result MatchResult) { t.searchInProgress = false // If waiting, unblock so main loop can execute pending actions. // Note: any final result unblocks the wait, not just the one for the - // search that armed it. Back-to-back searches (--listen, reload+wait) - // can therefore unblock early and run the pending actions on the - // previous result set. Accepted; a per-search generation token - // through the matcher isn't worth the complexity. + // search that armed it. Back-to-back searches (--listen, bg-transform + // callbacks, reload+wait) can therefore unblock early and run the + // pending actions on the previous result set. Accepted; a per-search + // generation token through the matcher isn't worth the complexity. if t.waitBlocked { t.unblockWait() wakeUp = len(t.pendingActions) > 0 @@ -6523,6 +6523,10 @@ func (t *Terminal) Loop() error { var newCommand *commandSpec var reloadSync bool var denylist []int32 + // True while running bg-transform callbacks. Declared outside the loop + // because callbacks invoke the doActions closure of the iteration that + // scheduled them, not the one executing actAsync. + inBgCallback := false req := func(evts ...util.EventType) { for _, event := range evts { events = append(events, event) @@ -6677,8 +6681,13 @@ func (t *Terminal) Loop() error { currentIndex := t.currentIndex() for i, action := range actions { if action.t == actWait { - // Already waiting; ignore so input can't reset the blocked state + // Already waiting. Actions parsed from a bg-transform + // result join the current wait; user input can't reset + // the blocked state if t.waitBlocked { + if inBgCallback { + t.pendingActions = append(t.pendingActions, actions[i+1:]...) + } return true } // Block if search is in progress or will be triggered @@ -6761,8 +6770,12 @@ func (t *Terminal) Loop() error { callback(a.a) } } + // Actions that run even while wait/track-blocked: bg-transform + // callbacks and their parsed actions (results of processes + // started before the block). + passthrough := inBgCallback || a.t == actAsync // When wait-blocked, only allow abort/cancel - if t.waitBlocked { + if t.waitBlocked && !passthrough { if a.t == actAbort || a.t == actCancel { t.unblockWait() t.pendingActions = nil @@ -6771,7 +6784,7 @@ func (t *Terminal) Loop() error { return true } // When track-blocked, only allow abort/cancel and track-disabling actions - if t.trackBlocked && a.t != actToggleTrack && a.t != actToggleTrackCurrent && a.t != actUntrackCurrent { + if t.trackBlocked && !passthrough && a.t != actToggleTrack && a.t != actToggleTrackCurrent && a.t != actUntrackCurrent { if a.t == actAbort || a.t == actCancel { t.unblockTrack() req(reqPrompt, reqInfo) @@ -6782,11 +6795,13 @@ func (t *Terminal) Loop() error { switch a.t { case actIgnore, actStart, actClick: case actAsync: + inBgCallback = true for _, callback := range callbacks { if t.bgVersion == callback.version { callback.callback() } } + inBgCallback = false case actBecome: valid, list := t.buildPlusList(a.a, false) if valid { diff --git a/test/test_core.rb b/test/test_core.rb index eacf88fd..ab6e800e 100644 --- a/test/test_core.rb +++ b/test/test_core.rb @@ -1185,6 +1185,30 @@ class TestCore < TestInteractive tmux.until { |lines| assert_equal 1, lines.match_count } end + def test_wait_action_bg_transform + # A bg-transform result is unrelated to the wait, so it's applied while the + # wait is still blocking rather than dropped. The long read keeps the wait + # blocked so the (instant) bg-transform completes during the block; the + # header must show while '(..)' is still displayed. + tmux.send_keys %((seq 100; sleep 2) | #{FZF} --bind 'start:bg-transform-header(echo hello)+search(5)+wait'), :Enter + tmux.until { |lines| assert(lines.any_include?('(..)') && lines.any_include?('hello')) } + end + + def test_wait_action_bg_transform_actions + # Actions parsed from a generic bg-transform result are also applied + # while wait-blocked + tmux.send_keys %((seq 100; sleep 2) | #{FZF} --bind 'start:bg-transform(echo change-header:hello)+search(5)+wait'), :Enter + tmux.until { |lines| assert(lines.any_include?('(..)') && lines.any_include?('hello')) } + end + + def test_wait_action_bg_transform_join + # A 'wait' in a bg-transform result body joins the ongoing wait; the + # actions after it run when the wait unblocks instead of being dropped + tmux.send_keys %((seq 100; sleep 2) | #{FZF} --bind 'start:bg-transform(echo change-header{hello}+wait+change-footer{world})+search(5)+wait'), :Enter + tmux.until { |lines| assert(lines.any_include?('(..)') && lines.any_include?('hello') && !lines.any_include?('world')) } + tmux.until { |lines| assert(lines.any_include?('world') && !lines.any_include?('(..)')) } + end + def test_wait_action_query_change # Query-editing actions must also make wait block; accept must run on the # results of the new query, not the stale ones @@ -1227,6 +1251,15 @@ class TestCore < TestInteractive tmux.until { |lines| assert_equal '> 5', lines[-3] } end + def test_track_blocked_bg_transform + # A bg-transform result completing while track-blocked is applied, not + # dropped. The header must show while '+T*' is still displayed. + tmux.send_keys "seq 100 | #{FZF} --track --id-nth .. --bind 'ctrl-r:bg-transform-header(echo hello)+reload(sleep 2; seq 100)'", :Enter + tmux.until { |lines| assert_includes lines[-2], '+T' } + tmux.send_keys 'C-r' + tmux.until { |lines| assert(lines.any_include?('+T*') && lines.any_include?('hello')) } + end + def test_clear_selection tmux.send_keys %(seq 100 | #{FZF} --multi --bind space:clear-selection), :Enter tmux.until { |lines| assert_equal 100, lines.match_count }