mirror of
https://github.com/junegunn/fzf.git
synced 2026-08-02 06:00:32 +08:00
Add 'wait' action to block until search completion (#4825)
Blocks execution of subsequent actions until the current search completes, so motion actions operate on the complete result set. # Move to the best match only after the search is done fzf --bind 'start:change-query(foo)+wait+best' - Blocks when a search is in progress, will be triggered by preceding actions, or the query was edited in the same binding; no-op otherwise - Initial input load counts as a search in progress, so 'start:wait' blocks until the input is fully loaded and searched - User input ignored while blocked; keys bound to abort/cancel cancel the wait and discard pending actions instead - Debounced feedback after 200ms: dimmed prompt, hidden cursor, and (..) on the info line
This commit is contained in:
@@ -11,6 +11,17 @@ CHANGELOG
|
|||||||
(seq 100; sleep 1; seq 100) | fzf --query 1 \
|
(seq 100; sleep 1; seq 100) | fzf --query 1 \
|
||||||
--bind 'result:transform-header(echo result: $FZF_MATCH_COUNT),result-final:transform-footer(echo final: $FZF_MATCH_COUNT)'
|
--bind 'result:transform-header(echo result: $FZF_MATCH_COUNT),result-final:transform-footer(echo final: $FZF_MATCH_COUNT)'
|
||||||
```
|
```
|
||||||
|
- Added `wait` action to block subsequent actions until search completes (#4825)
|
||||||
|
- Useful for chaining query-changing actions with motion actions to ensure operations on complete results
|
||||||
|
```sh
|
||||||
|
# Wait for search to complete before moving to the best match
|
||||||
|
fzf --bind 'start:change-query(foo)+wait+best'
|
||||||
|
```
|
||||||
|
- The initial loading of the input is also considered a search in progress, so `start:wait` can be used to wait until the input is fully loaded
|
||||||
|
```sh
|
||||||
|
# Move to the last item after the input is fully loaded
|
||||||
|
(seq 1000; sleep 1; seq 1001 2000) | fzf --bind 'start:wait+last'
|
||||||
|
```
|
||||||
- Bound `alt-left` to `backward-word` and `alt-right` to `forward-word` by default (#4833)
|
- Bound `alt-left` to `backward-word` and `alt-right` to `forward-word` by default (#4833)
|
||||||
|
|
||||||
0.73.1
|
0.73.1
|
||||||
|
|||||||
@@ -2146,6 +2146,7 @@ A key or an event can be bound to one or more of the following actions.
|
|||||||
\fBunix\-line\-discard\fR \fIctrl\-u\fR
|
\fBunix\-line\-discard\fR \fIctrl\-u\fR
|
||||||
\fBunix\-word\-rubout\fR \fIctrl\-w\fR
|
\fBunix\-word\-rubout\fR \fIctrl\-w\fR
|
||||||
\fBuntrack\-current\fR (stop tracking the current item; no-op if global tracking is enabled)
|
\fBuntrack\-current\fR (stop tracking the current item; no-op if global tracking is enabled)
|
||||||
|
\fBwait\fR (block action execution until search completes)
|
||||||
\fBup\fR \fIctrl\-k up\fR
|
\fBup\fR \fIctrl\-k up\fR
|
||||||
\fBup\-match\fR \fIctrl\-p\fR \fIalt\-up\fR (move to the match above the cursor)
|
\fBup\-match\fR \fIctrl\-p\fR \fIalt\-up\fR (move to the match above the cursor)
|
||||||
\fBup\-selected\fR (move to the selected item above the cursor)
|
\fBup\-selected\fR (move to the selected item above the cursor)
|
||||||
@@ -2298,6 +2299,38 @@ chain multiple transform actions where later ones depend on earlier results,
|
|||||||
prefer using the \fBbg\fR variant. To cancel currently running background
|
prefer using the \fBbg\fR variant. To cancel currently running background
|
||||||
transform processes, use \fBbg\-cancel\fR action.
|
transform processes, use \fBbg\-cancel\fR action.
|
||||||
|
|
||||||
|
.SS WAITING FOR SEARCH COMPLETION
|
||||||
|
|
||||||
|
The \fBwait\fR action blocks the execution of subsequent actions until the
|
||||||
|
current search completes. This is useful when chaining query\-changing actions
|
||||||
|
with motion actions like \fBbest\fR or \fBfirst\fR, ensuring that the motion
|
||||||
|
action operates on the complete search results rather than stale data.
|
||||||
|
|
||||||
|
e.g.
|
||||||
|
\fBfzf \-\-bind 'start:change\-query(foo)+wait+best'\fR
|
||||||
|
|
||||||
|
In this example, \fBchange\-query(foo)\fR starts an asynchronous search for
|
||||||
|
the new query, \fBwait\fR blocks until the search completes, and \fBbest\fR
|
||||||
|
then moves the cursor to the best match in the complete result set.
|
||||||
|
|
||||||
|
The initial loading of the input is also considered a search in progress, so
|
||||||
|
\fBstart:wait\fR can be used to block until the input is fully loaded and
|
||||||
|
searched.
|
||||||
|
|
||||||
|
While waiting, user input is ignored, except for keys bound to \fBabort\fR or
|
||||||
|
\fBcancel\fR (\fIctrl\-c\fR, \fIctrl\-g\fR, \fIctrl\-q\fR, and \fIesc\fR by
|
||||||
|
default), which cancel the wait and discard the pending actions instead of
|
||||||
|
performing their usual role.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Note that when searches are triggered in rapid succession (e.g. via
|
||||||
|
\fB\-\-listen\fR), \fBwait\fR may unblock on the completion of an earlier
|
||||||
|
search. Also, if the input source never completes, \fBwait\fR will block until
|
||||||
|
cancelled.
|
||||||
|
|
||||||
.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
|
||||||
|
|||||||
@@ -186,11 +186,12 @@ func _() {
|
|||||||
_ = x[actExclude-175]
|
_ = x[actExclude-175]
|
||||||
_ = x[actExcludeMulti-176]
|
_ = x[actExcludeMulti-176]
|
||||||
_ = x[actAsync-177]
|
_ = x[actAsync-177]
|
||||||
|
_ = x[actWait-178]
|
||||||
}
|
}
|
||||||
|
|
||||||
const _actionType_name = "actIgnoreactStartactClickactInvalidactBracketedPasteBeginactBracketedPasteEndactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactBackwardSubWordactCancelactChangeBorderLabelactChangeGhostactChangeHeaderactChangeHeaderLinesactChangeFooteractChangeHeaderLabelactChangeFooterLabelactChangeInputLabelactChangeListLabelactChangeMultiactChangeNthactChangeWithNthactChangePointeractChangePreviewactChangePreviewLabelactChangePreviewWindowactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactForwardSubWordactKillLineactKillWordactKillSubWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactBackwardKillSubWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleWrapWordactToggleMultiLineactToggleHscrollactToggleRawactEnableRawactDisableRawactTrackCurrentactToggleInputactHideInputactShowInputactUntrackCurrentactDownactDownMatchactUpactUpMatchactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTogglePreviewWrapWordactTransformactTransformBorderLabelactTransformGhostactTransformHeaderactTransformHeaderLinesactTransformFooteractTransformHeaderLabelactTransformFooterLabelactTransformInputLabelactTransformListLabelactTransformNthactTransformWithNthactTransformPointeractTransformPreviewLabelactTransformPromptactTransformQueryactTransformSearchactTriggeractBgTransformactBgTransformBorderLabelactBgTransformGhostactBgTransformHeaderactBgTransformHeaderLinesactBgTransformFooteractBgTransformHeaderLabelactBgTransformFooterLabelactBgTransformInputLabelactBgTransformListLabelactBgTransformNthactBgTransformWithNthactBgTransformPointeractBgTransformPreviewLabelactBgTransformPromptactBgTransformQueryactBgTransformSearchactBgCancelactSearchactPreviewactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactBestactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactToggleBindactBecomeactShowHeaderactHideHeaderactBellactExcludeactExcludeMultiactAsync"
|
const _actionType_name = "actIgnoreactStartactClickactInvalidactBracketedPasteBeginactBracketedPasteEndactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactBackwardSubWordactCancelactChangeBorderLabelactChangeGhostactChangeHeaderactChangeHeaderLinesactChangeFooteractChangeHeaderLabelactChangeFooterLabelactChangeInputLabelactChangeListLabelactChangeMultiactChangeNthactChangeWithNthactChangePointeractChangePreviewactChangePreviewLabelactChangePreviewWindowactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactForwardSubWordactKillLineactKillWordactKillSubWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactBackwardKillSubWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleWrapWordactToggleMultiLineactToggleHscrollactToggleRawactEnableRawactDisableRawactTrackCurrentactToggleInputactHideInputactShowInputactUntrackCurrentactDownactDownMatchactUpactUpMatchactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTogglePreviewWrapWordactTransformactTransformBorderLabelactTransformGhostactTransformHeaderactTransformHeaderLinesactTransformFooteractTransformHeaderLabelactTransformFooterLabelactTransformInputLabelactTransformListLabelactTransformNthactTransformWithNthactTransformPointeractTransformPreviewLabelactTransformPromptactTransformQueryactTransformSearchactTriggeractBgTransformactBgTransformBorderLabelactBgTransformGhostactBgTransformHeaderactBgTransformHeaderLinesactBgTransformFooteractBgTransformHeaderLabelactBgTransformFooterLabelactBgTransformInputLabelactBgTransformListLabelactBgTransformNthactBgTransformWithNthactBgTransformPointeractBgTransformPreviewLabelactBgTransformPromptactBgTransformQueryactBgTransformSearchactBgCancelactSearchactPreviewactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactBestactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactToggleBindactBecomeactShowHeaderactHideHeaderactBellactExcludeactExcludeMultiactAsyncactWait"
|
||||||
|
|
||||||
var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 57, 77, 84, 92, 110, 118, 127, 144, 165, 180, 201, 225, 240, 258, 267, 287, 301, 316, 336, 351, 371, 391, 410, 428, 442, 454, 470, 486, 502, 523, 545, 560, 574, 588, 601, 618, 626, 639, 655, 667, 675, 689, 703, 720, 731, 742, 756, 774, 791, 798, 817, 839, 851, 865, 874, 889, 901, 914, 925, 936, 948, 962, 983, 998, 1011, 1028, 1046, 1062, 1074, 1086, 1099, 1114, 1128, 1140, 1152, 1169, 1176, 1188, 1193, 1203, 1212, 1223, 1234, 1247, 1262, 1273, 1286, 1301, 1308, 1321, 1334, 1351, 1366, 1379, 1393, 1407, 1423, 1443, 1467, 1479, 1502, 1519, 1537, 1560, 1578, 1601, 1624, 1646, 1667, 1682, 1701, 1720, 1744, 1762, 1779, 1797, 1807, 1821, 1846, 1865, 1885, 1910, 1930, 1955, 1980, 2004, 2027, 2044, 2065, 2086, 2112, 2132, 2151, 2171, 2182, 2191, 2201, 2214, 2230, 2242, 2256, 2272, 2290, 2310, 2332, 2346, 2361, 2369, 2375, 2389, 2404, 2414, 2430, 2445, 2455, 2462, 2470, 2477, 2486, 2499, 2515, 2530, 2539, 2550, 2559, 2568, 2581, 2590, 2603, 2616, 2623, 2633, 2648, 2656}
|
var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 57, 77, 84, 92, 110, 118, 127, 144, 165, 180, 201, 225, 240, 258, 267, 287, 301, 316, 336, 351, 371, 391, 410, 428, 442, 454, 470, 486, 502, 523, 545, 560, 574, 588, 601, 618, 626, 639, 655, 667, 675, 689, 703, 720, 731, 742, 756, 774, 791, 798, 817, 839, 851, 865, 874, 889, 901, 914, 925, 936, 948, 962, 983, 998, 1011, 1028, 1046, 1062, 1074, 1086, 1099, 1114, 1128, 1140, 1152, 1169, 1176, 1188, 1193, 1203, 1212, 1223, 1234, 1247, 1262, 1273, 1286, 1301, 1308, 1321, 1334, 1351, 1366, 1379, 1393, 1407, 1423, 1443, 1467, 1479, 1502, 1519, 1537, 1560, 1578, 1601, 1624, 1646, 1667, 1682, 1701, 1720, 1744, 1762, 1779, 1797, 1807, 1821, 1846, 1865, 1885, 1910, 1930, 1955, 1980, 2004, 2027, 2044, 2065, 2086, 2112, 2132, 2151, 2171, 2182, 2191, 2201, 2214, 2230, 2242, 2256, 2272, 2290, 2310, 2332, 2346, 2361, 2369, 2375, 2389, 2404, 2414, 2430, 2445, 2455, 2462, 2470, 2477, 2486, 2499, 2515, 2530, 2539, 2550, 2559, 2568, 2581, 2590, 2603, 2616, 2623, 2633, 2648, 2656, 2663}
|
||||||
|
|
||||||
func (i actionType) String() string {
|
func (i actionType) String() string {
|
||||||
if i < 0 || i >= actionType(len(_actionType_index)-1) {
|
if i < 0 || i >= actionType(len(_actionType_index)-1) {
|
||||||
|
|||||||
@@ -1958,6 +1958,8 @@ func parseActionList(masked string, original string, prevActions []*action, putA
|
|||||||
} else {
|
} else {
|
||||||
return nil, errors.New("unable to put non-printable character")
|
return nil, errors.New("unable to put non-printable character")
|
||||||
}
|
}
|
||||||
|
case "wait":
|
||||||
|
appendAction(actWait)
|
||||||
case "bell":
|
case "bell":
|
||||||
appendAction(actBell)
|
appendAction(actBell)
|
||||||
case "exclude":
|
case "exclude":
|
||||||
|
|||||||
+129
-10
@@ -321,6 +321,10 @@ type Terminal struct {
|
|||||||
trackBlocked bool
|
trackBlocked bool
|
||||||
trackSync bool
|
trackSync bool
|
||||||
trackKeyCache map[int32]bool
|
trackKeyCache map[int32]bool
|
||||||
|
waitBlocked bool
|
||||||
|
waitBlockedAt time.Time
|
||||||
|
pendingActions []*action
|
||||||
|
searchInProgress bool
|
||||||
pendingSelections map[string]selectedItem
|
pendingSelections map[string]selectedItem
|
||||||
targetIndex int32
|
targetIndex int32
|
||||||
delimiter Delimiter
|
delimiter Delimiter
|
||||||
@@ -720,6 +724,7 @@ const (
|
|||||||
actExclude
|
actExclude
|
||||||
actExcludeMulti
|
actExcludeMulti
|
||||||
actAsync
|
actAsync
|
||||||
|
actWait
|
||||||
)
|
)
|
||||||
|
|
||||||
func (a actionType) Name() string {
|
func (a actionType) Name() string {
|
||||||
@@ -1164,7 +1169,10 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
|
|||||||
lastAction: actStart,
|
lastAction: actStart,
|
||||||
lastFocus: minItem.Index(),
|
lastFocus: minItem.Index(),
|
||||||
lastActivity: time.Now(),
|
lastActivity: time.Now(),
|
||||||
numLinesCache: make(map[int32]numLinesCacheValue)}
|
// The initial load counts as a search in progress ('start:wait').
|
||||||
|
// Set before the reader starts so the first final result clears it.
|
||||||
|
searchInProgress: true,
|
||||||
|
numLinesCache: make(map[int32]numLinesCacheValue)}
|
||||||
if opts.AcceptNth != nil {
|
if opts.AcceptNth != nil {
|
||||||
t.acceptNth = opts.AcceptNth(t.delimiter)
|
t.acceptNth = opts.AcceptNth(t.delimiter)
|
||||||
}
|
}
|
||||||
@@ -1876,6 +1884,21 @@ func (t *Terminal) UpdateProgress(progress float32) {
|
|||||||
func (t *Terminal) UpdateList(result MatchResult) {
|
func (t *Terminal) UpdateList(result MatchResult) {
|
||||||
merger := result.merger
|
merger := result.merger
|
||||||
t.mutex.Lock()
|
t.mutex.Lock()
|
||||||
|
waitWasBlocked := t.waitBlocked
|
||||||
|
wakeUp := false
|
||||||
|
if result.final() {
|
||||||
|
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.
|
||||||
|
if t.waitBlocked {
|
||||||
|
t.unblockWait()
|
||||||
|
wakeUp = len(t.pendingActions) > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
prevIndex := minItem.Index()
|
prevIndex := minItem.Index()
|
||||||
newRevision := merger.Revision()
|
newRevision := merger.Revision()
|
||||||
if t.revision.compatible(newRevision) && t.track != trackDisabled {
|
if t.revision.compatible(newRevision) && t.track != trackDisabled {
|
||||||
@@ -2041,9 +2064,19 @@ func (t *Terminal) UpdateList(result MatchResult) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
updateList := !t.trackBlocked && !t.pendingReqList
|
updateList := !t.trackBlocked && !t.pendingReqList
|
||||||
updatePrompt := trackWasBlocked && !t.trackBlocked
|
updatePrompt := (trackWasBlocked && !t.trackBlocked) || (waitWasBlocked && !t.waitBlocked)
|
||||||
t.mutex.Unlock()
|
t.mutex.Unlock()
|
||||||
|
|
||||||
|
// Wake up the main loop to execute pending actions after wait unblocks.
|
||||||
|
// Send from a goroutine; UpdateList runs inside the event box callback,
|
||||||
|
// and an inline send on a full channel would deadlock with the main loop
|
||||||
|
// blocking on eventBox.Set while trying to drain the channel.
|
||||||
|
if wakeUp {
|
||||||
|
go func() {
|
||||||
|
t.serverInputChan <- []*action{{t: actIgnore}}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
t.reqBox.Set(reqInfo, nil)
|
t.reqBox.Set(reqInfo, nil)
|
||||||
if updateList {
|
if updateList {
|
||||||
t.reqBox.Set(reqList, nil)
|
t.reqBox.Set(reqList, nil)
|
||||||
@@ -3259,7 +3292,7 @@ func (t *Terminal) printPrompt() {
|
|||||||
color := tui.ColInput
|
color := tui.ColInput
|
||||||
if t.paused {
|
if t.paused {
|
||||||
color = tui.ColDisabled
|
color = tui.ColDisabled
|
||||||
} else if t.trackBlocked {
|
} else if t.trackBlocked || t.waitFeedback() {
|
||||||
color = color.WithAttr(tui.Dim)
|
color = color.WithAttr(tui.Dim)
|
||||||
}
|
}
|
||||||
w.CPrint(color, string(before))
|
w.CPrint(color, string(before))
|
||||||
@@ -3351,9 +3384,6 @@ func (t *Terminal) printInfoImpl() {
|
|||||||
output += fmt.Sprintf(" (%d/%d)", len(t.selected), t.multi)
|
output += fmt.Sprintf(" (%d/%d)", len(t.selected), t.multi)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if t.progress > 0 && t.progress < 100 {
|
|
||||||
output += fmt.Sprintf(" (%d%%)", t.progress)
|
|
||||||
}
|
|
||||||
if t.toggleSort {
|
if t.toggleSort {
|
||||||
if t.sort {
|
if t.sort {
|
||||||
output += " +S"
|
output += " +S"
|
||||||
@@ -3374,6 +3404,14 @@ func (t *Terminal) printInfoImpl() {
|
|||||||
output += " +t"
|
output += " +t"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if t.waitFeedback() {
|
||||||
|
output += " (..)"
|
||||||
|
}
|
||||||
|
// Keep the search progress at the end so the other indicators don't shift
|
||||||
|
// as it appears and disappears.
|
||||||
|
if t.progress > 0 && t.progress < 100 {
|
||||||
|
output += fmt.Sprintf(" (%d%%)", t.progress)
|
||||||
|
}
|
||||||
if t.failed != nil && t.count == 0 {
|
if t.failed != nil && t.count == 0 {
|
||||||
output = fmt.Sprintf("[Command failed: %s]", *t.failed)
|
output = fmt.Sprintf("[Command failed: %s]", *t.failed)
|
||||||
}
|
}
|
||||||
@@ -5780,12 +5818,26 @@ func (t *Terminal) unblockTrack() {
|
|||||||
t.trackBlocked = false
|
t.trackBlocked = false
|
||||||
t.trackKey = ""
|
t.trackKey = ""
|
||||||
t.trackKeyCache = nil
|
t.trackKeyCache = nil
|
||||||
if !t.inputless {
|
// Keep the cursor hidden if the wait feedback is still showing it
|
||||||
|
if !t.inputless && !t.waitFeedback() {
|
||||||
t.tui.ShowCursor()
|
t.tui.ShowCursor()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Debounce visual feedback so quick searches don't cause flashing
|
||||||
|
func (t *Terminal) waitFeedback() bool {
|
||||||
|
return t.waitBlocked && time.Since(t.waitBlockedAt) > progressMinDuration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Terminal) unblockWait() {
|
||||||
|
t.waitBlocked = false
|
||||||
|
// Restore the cursor unless it's still hidden for another reason
|
||||||
|
if !t.inputless && !t.trackBlocked {
|
||||||
|
t.tui.ShowCursor()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (t *Terminal) addClickHeaderWord(env []string) []string {
|
func (t *Terminal) addClickHeaderWord(env []string) []string {
|
||||||
/*
|
/*
|
||||||
* echo $'HL1\nHL2' | fzf --header-lines 3 --header $'H1\nH2' --header-lines-border --bind 'click-header:preview:env | grep FZF_CLICK'
|
* echo $'HL1\nHL2' | fzf --header-lines 3 --header $'H1\nH2' --header-lines-border --bind 'click-header:preview:env | grep FZF_CLICK'
|
||||||
@@ -6411,6 +6463,10 @@ func (t *Terminal) Loop() error {
|
|||||||
t.printFooter()
|
t.printFooter()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Hide the cursor while the debounced waiting feedback is shown
|
||||||
|
if !t.inputless && !t.trackBlocked && t.waitFeedback() {
|
||||||
|
t.tui.HideCursor()
|
||||||
|
}
|
||||||
t.flush()
|
t.flush()
|
||||||
t.mutex.Unlock()
|
t.mutex.Unlock()
|
||||||
t.uiMutex.Unlock()
|
t.uiMutex.Unlock()
|
||||||
@@ -6613,12 +6669,51 @@ func (t *Terminal) Loop() error {
|
|||||||
|
|
||||||
var doAction func(*action) bool
|
var doAction func(*action) bool
|
||||||
doActions := func(actions []*action) bool {
|
doActions := func(actions []*action) bool {
|
||||||
|
// Snapshot to detect query edits in this batch that trigger a
|
||||||
|
// search at loop end without setting 'changed'. String copy:
|
||||||
|
// edits mutate t.input in place.
|
||||||
|
queryBefore := string(t.input)
|
||||||
|
wasBlocked := t.waitBlocked
|
||||||
for iter := 0; iter <= maxFocusEvents; iter++ {
|
for iter := 0; iter <= maxFocusEvents; iter++ {
|
||||||
currentIndex := t.currentIndex()
|
currentIndex := t.currentIndex()
|
||||||
for _, action := range actions {
|
for i, action := range actions {
|
||||||
|
if action.t == actWait {
|
||||||
|
// Already waiting; ignore so input can't reset the blocked state
|
||||||
|
if t.waitBlocked {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// Block if search is in progress or will be triggered
|
||||||
|
if changed || newCommand != nil || t.searchInProgress || queryBefore != string(t.input) {
|
||||||
|
t.waitBlocked = true
|
||||||
|
t.waitBlockedAt = time.Now()
|
||||||
|
t.pendingActions = actions[i+1:]
|
||||||
|
// Show the waiting feedback only if the search takes long enough,
|
||||||
|
// so that quick searches don't cause flickering.
|
||||||
|
go func() {
|
||||||
|
timer := time.NewTimer(progressMinDuration)
|
||||||
|
<-timer.C
|
||||||
|
t.mutex.Lock()
|
||||||
|
blocked := t.waitBlocked
|
||||||
|
t.mutex.Unlock()
|
||||||
|
if blocked {
|
||||||
|
t.reqBox.Set(reqPrompt, nil)
|
||||||
|
t.reqBox.Set(reqInfo, nil)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// No search, wait is a no-op; continue to next action
|
||||||
|
continue
|
||||||
|
}
|
||||||
if !doAction(action) {
|
if !doAction(action) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
// If a nested action list armed the wait (e.g. via
|
||||||
|
// trigger), defer the rest of this list too
|
||||||
|
if !wasBlocked && t.waitBlocked {
|
||||||
|
t.pendingActions = append(t.pendingActions, actions[i+1:]...)
|
||||||
|
return true
|
||||||
|
}
|
||||||
// A terminal action performed. We should stop processing more.
|
// A terminal action performed. We should stop processing more.
|
||||||
if !looping {
|
if !looping {
|
||||||
break
|
break
|
||||||
@@ -6664,6 +6759,15 @@ func (t *Terminal) Loop() error {
|
|||||||
callback(a.a)
|
callback(a.a)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// When wait-blocked, only allow abort/cancel
|
||||||
|
if t.waitBlocked {
|
||||||
|
if a.t == actAbort || a.t == actCancel {
|
||||||
|
t.unblockWait()
|
||||||
|
t.pendingActions = nil
|
||||||
|
req(reqPrompt, reqInfo)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
// When track-blocked, only allow abort/cancel and track-disabling actions
|
// 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 && a.t != actToggleTrack && a.t != actToggleTrackCurrent && a.t != actUntrackCurrent {
|
||||||
if a.t == actAbort || a.t == actCancel {
|
if a.t == actAbort || a.t == actCancel {
|
||||||
@@ -8071,9 +8175,21 @@ func (t *Terminal) Loop() error {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if t.jumping == jumpDisabled || len(actions) > 0 {
|
// Execute pending actions if wait just unblocked. Capture the jump
|
||||||
|
// state first so that a pending 'jump' action isn't cancelled right
|
||||||
|
// away by the wake-up event below.
|
||||||
|
jumpingBefore := t.jumping
|
||||||
|
if len(t.pendingActions) > 0 && !t.waitBlocked {
|
||||||
|
pendingActions := t.pendingActions
|
||||||
|
t.pendingActions = nil
|
||||||
|
if !doActions(pendingActions) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if jumpingBefore == jumpDisabled || len(actions) > 0 {
|
||||||
// Break out of jump mode if any action is submitted to the server
|
// Break out of jump mode if any action is submitted to the server
|
||||||
if t.jumping != jumpDisabled {
|
if jumpingBefore != jumpDisabled {
|
||||||
t.jumping = jumpDisabled
|
t.jumping = jumpDisabled
|
||||||
if acts, prs := t.keymap[tui.JumpCancel.AsEvent()]; prs && !doActions(acts) {
|
if acts, prs := t.keymap[tui.JumpCancel.AsEvent()]; prs && !doActions(acts) {
|
||||||
continue
|
continue
|
||||||
@@ -8132,6 +8248,9 @@ func (t *Terminal) Loop() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
reload := changed || newCommand != nil
|
reload := changed || newCommand != nil
|
||||||
|
if reload {
|
||||||
|
t.searchInProgress = true
|
||||||
|
}
|
||||||
var reloadRequest *searchRequest
|
var reloadRequest *searchRequest
|
||||||
if reload {
|
if reload {
|
||||||
reloadRequest = &searchRequest{sort: t.sort, sync: reloadSync, nth: newNth, withNth: newWithNth, headerLines: newHeaderLines, command: newCommand, environ: t.environ(), changed: changed, denylist: denylist, revision: t.resultMerger.Revision()}
|
reloadRequest = &searchRequest{sort: t.sort, sync: reloadSync, nth: newNth, withNth: newWithNth, headerLines: newHeaderLines, command: newCommand, environ: t.environ(), changed: changed, denylist: denylist, revision: t.resultMerger.Revision()}
|
||||||
|
|||||||
@@ -1162,6 +1162,54 @@ class TestCore < TestInteractive
|
|||||||
tmux.until { |lines| assert_equal 10, lines.match_count }
|
tmux.until { |lines| assert_equal 10, lines.match_count }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_wait_action
|
||||||
|
tmux.send_keys %((seq 100; sleep 60) | #{FZF} --bind 'start:search(1)+wait+best'), :Enter
|
||||||
|
tmux.until { |lines| assert_equal 20, lines.match_count }
|
||||||
|
tmux.until { |lines| assert lines.any_include?('20/100 (..)') }
|
||||||
|
tmux.send_keys 'C-c'
|
||||||
|
tmux.until { |lines| refute lines.any_include?('20/100 (..)') }
|
||||||
|
# Ctrl-C cancels the wait; fzf keeps running and accepts input again
|
||||||
|
tmux.send_keys '99'
|
||||||
|
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_wait_action_start
|
||||||
|
# 'start:wait' blocks on the initial load until reading completes
|
||||||
|
tmux.send_keys %((seq 100; sleep 60) | #{FZF} --bind 'start:wait'), :Enter
|
||||||
|
tmux.until { |lines| assert_equal 100, lines.match_count }
|
||||||
|
tmux.until { |lines| assert lines.any_include?('(..)') }
|
||||||
|
tmux.send_keys 'C-c'
|
||||||
|
tmux.until { |lines| refute lines.any_include?('(..)') }
|
||||||
|
# Ctrl-C cancels the wait; fzf keeps running and accepts input again
|
||||||
|
tmux.send_keys '99'
|
||||||
|
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||||
|
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
|
||||||
|
tmux.send_keys %(seq 100 | #{fzf("--bind 'space:change-query(55)+wait+accept'")}), :Enter
|
||||||
|
tmux.until { |lines| assert_equal 100, lines.match_count }
|
||||||
|
tmux.send_keys :Space
|
||||||
|
assert_equal '55', fzf_output
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_wait_action_trigger_join
|
||||||
|
# A wait armed inside a triggered chord defers the remaining actions of
|
||||||
|
# the outer binding as well
|
||||||
|
tmux.send_keys %((seq 100; sleep 2) | #{FZF} --bind 'start:trigger(x)+change-footer(world)' --bind 'x:search(5)+wait'), :Enter
|
||||||
|
tmux.until { |lines| assert(lines.any_include?('(..)') && !lines.any_include?('world')) }
|
||||||
|
tmux.until { |lines| assert(lines.any_include?('world') && !lines.any_include?('(..)')) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_wait_action_jump
|
||||||
|
# A pending jump action must survive the wake-up event after unblock
|
||||||
|
tmux.send_keys %((seq 100; sleep 2) | #{FZF} --jump-labels abc --bind 'start:search(5)+wait+jump'), :Enter
|
||||||
|
tmux.until { |lines| assert_equal 'a 5', lines[-3] }
|
||||||
|
tmux.send_keys 'a'
|
||||||
|
tmux.until { |lines| assert_equal '> 5', lines[-3] }
|
||||||
|
end
|
||||||
|
|
||||||
def test_clear_selection
|
def test_clear_selection
|
||||||
tmux.send_keys %(seq 100 | #{FZF} --multi --bind space:clear-selection), :Enter
|
tmux.send_keys %(seq 100 | #{FZF} --multi --bind space:clear-selection), :Enter
|
||||||
tmux.until { |lines| assert_equal 100, lines.match_count }
|
tmux.until { |lines| assert_equal 100, lines.match_count }
|
||||||
|
|||||||
Reference in New Issue
Block a user