Add 'search' and 'transform-search'

Close #4202
This commit is contained in:
Junegunn Choi 2025-01-26 01:50:08 +09:00
parent 0237bf09bf
commit 7220d8233e
No known key found for this signature in database
GPG Key ID: 254BC280FEF9C627
7 changed files with 173 additions and 54 deletions

View File

@ -1,8 +1,8 @@
Advanced fzf examples Advanced fzf examples
====================== ======================
* *Last update: 2024/06/24* * *Last update: 2025/01/26*
* *Requires fzf 0.54.0 or later* * *Requires fzf 0.59.0 or later*
--- ---
@ -22,6 +22,7 @@ Advanced fzf examples
* [Switching to fzf-only search mode](#switching-to-fzf-only-search-mode) * [Switching to fzf-only search mode](#switching-to-fzf-only-search-mode)
* [Switching between Ripgrep mode and fzf mode](#switching-between-ripgrep-mode-and-fzf-mode) * [Switching between Ripgrep mode and fzf mode](#switching-between-ripgrep-mode-and-fzf-mode)
* [Switching between Ripgrep mode and fzf mode using a single key binding](#switching-between-ripgrep-mode-and-fzf-mode-using-a-single-key-binding) * [Switching between Ripgrep mode and fzf mode using a single key binding](#switching-between-ripgrep-mode-and-fzf-mode-using-a-single-key-binding)
* [Controlling Ripgrap search and fzf search simultaneously](#controlling-ripgrap-search-and-fzf-search-simultaneously)
* [Log tailing](#log-tailing) * [Log tailing](#log-tailing)
* [Key bindings for git objects](#key-bindings-for-git-objects) * [Key bindings for git objects](#key-bindings-for-git-objects)
* [Files listed in `git status`](#files-listed-in-git-status) * [Files listed in `git status`](#files-listed-in-git-status)
@ -500,6 +501,48 @@ fzf --ansi --disabled --query "$INITIAL_QUERY" \
--bind 'enter:become(vim {1} +{2})' --bind 'enter:become(vim {1} +{2})'
``` ```
### Controlling Ripgrap search and fzf search simultaneously
fzf 0.59.0 added `search` action that allows you to trigger an fzf search
with an arbitrary query string. This means fzf is no longer restricted to the
exact query entered in the prompt.
In the example below, `transform` action is used to conditionally trigger
either `reload` for ripgrep or `search` for fzf. The first word of the query
initiates the Ripgrep process to generate the initial results, while the
remainder of the query is passed to fzf for secondary filtering.
```sh
#!/usr/bin/env bash
# Switch between Ripgrep mode and fzf filtering mode (CTRL-T)
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
INITIAL_QUERY="${*:-}"
TRANSFORMER='
words=($FZF_QUERY)
# If $FZF_QUERY contains multiple words, drop the first word,
# and trigger fzf search with the rest
if [[ ${#words[@]} -gt 1 ]]; then
echo "search:${FZF_QUERY#* }"
# Otherwise, if the query does not end with a space,
# restart ripgrep and reload the list
elif ! [[ $FZF_QUERY =~ \ $ ]]; then
echo "reload:sleep 0.1; $RG_PREFIX \"${words[0]}\" || true"
fi
'
fzf --ansi --disabled --query "$INITIAL_QUERY" \
--with-shell 'bash -c' \
--bind "start:transform:$TRANSFORMER" \
--bind "change:transform:$TRANSFORMER" \
--color "hl:-1:underline,hl+:-1:underline:reverse" \
--delimiter : \
--preview 'bat --color=always {1} --highlight-line {2}' \
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3' \
--bind 'enter:become(vim {1} +{2})'
```
Log tailing Log tailing
----------- -----------

View File

@ -14,6 +14,27 @@ CHANGELOG
--bind 'ctrl-r:reload(ps -ef)' --header 'Press CTRL-R to reload' \ --bind 'ctrl-r:reload(ps -ef)' --header 'Press CTRL-R to reload' \
--header-lines-border bottom --no-list-border --header-lines-border bottom --no-list-border
``` ```
- Added `search(...)` and `transform-search(...)` action to trigger an fzf search with an arbitrary query string. This can be used to extend the search syntax of fzf. In the following example, fzf will use the first word of the query to trigger ripgrep search, and use the rest of the query to perform fzf search within the result.
```sh
TRANSFORMER='
words=($FZF_QUERY)
# If $FZF_QUERY contains multiple words, drop the first word,
# and trigger fzf search with the rest
if [[ ${#words[@]} -gt 1 ]]; then
echo "search:${FZF_QUERY#* }"
# Otherwise, if the query does not end with a space,
# restart ripgrep and reload the list
elif ! [[ $FZF_QUERY =~ \ $ ]]; then
echo "reload:rg --column --color=always --smart-case \"${words[0]}\""
fi
'
fzf --ansi --disabled \
--with-shell 'bash -c' \
--bind "start:transform:$TRANSFORMER" \
--bind "change:transform:$TRANSFORMER"
```
- Added `bell` action to ring the terminal bell - Added `bell` action to ring the terminal bell
```sh ```sh
# Press CTRL-Y to copy the current line to the clipboard and ring the bell # Press CTRL-Y to copy the current line to the clipboard and ring the bell

View File

@ -94,8 +94,8 @@ more weight to the chronological ordering. This also sets
.RS .RS
fzf chooses \fBpath\fR scheme when the input is a TTY device, where fzf would fzf chooses \fBpath\fR scheme when the input is a TTY device, where fzf would
start its built-in walker or run \fB$FZF_DEFAULT_COMMAND\fR, and there is no start its built-in walker or run \fB$FZF_DEFAULT_COMMAND\fR, and there is no
\fBreload\fR action bound to \fBstart\fR event. Otherwise, it chooses \fBreload\fR or \fBtransform\fR action bound to \fBstart\fR event. Otherwise,
\fBdefault\fR scheme. it chooses \fBdefault\fR scheme.
.RE .RE
.TP .TP
@ -1609,6 +1609,7 @@ A key or an event can be bound to one or more of the following actions.
\fBreload(...)\fR (see below for the details) \fBreload(...)\fR (see below for the details)
\fBreload\-sync(...)\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)
\fBsearch(...)\fR (trigger fzf search with the given string)
\fBselect\fR \fBselect\fR
\fBselect\-all\fR (select all matches) \fBselect\-all\fR (select all matches)
\fBshow\-header\fR \fBshow\-header\fR
@ -1639,6 +1640,7 @@ A key or an event can be bound to one or more of the following actions.
\fBtransform\-preview\-label(...)\fR (transform preview label using an external command) \fBtransform\-preview\-label(...)\fR (transform preview label using an external command)
\fBtransform\-prompt(...)\fR (transform prompt string using an external command) \fBtransform\-prompt(...)\fR (transform prompt string using an external command)
\fBtransform\-query(...)\fR (transform query string using an external command) \fBtransform\-query(...)\fR (transform query string using an external command)
\fBtransform\-search(...)\fR (trigger fzf search with the output of an external command)
\fBunbind(...)\fR (unbind bindings) \fBunbind(...)\fR (unbind bindings)
\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

View File

@ -96,45 +96,48 @@ func _() {
_ = x[actTransformPreviewLabel-85] _ = x[actTransformPreviewLabel-85]
_ = x[actTransformPrompt-86] _ = x[actTransformPrompt-86]
_ = x[actTransformQuery-87] _ = x[actTransformQuery-87]
_ = x[actPreview-88] _ = x[actTransformSearch-88]
_ = x[actChangePreview-89] _ = x[actSearch-89]
_ = x[actChangePreviewWindow-90] _ = x[actPreview-90]
_ = x[actPreviewTop-91] _ = x[actChangePreview-91]
_ = x[actPreviewBottom-92] _ = x[actChangePreviewWindow-92]
_ = x[actPreviewUp-93] _ = x[actPreviewTop-93]
_ = x[actPreviewDown-94] _ = x[actPreviewBottom-94]
_ = x[actPreviewPageUp-95] _ = x[actPreviewUp-95]
_ = x[actPreviewPageDown-96] _ = x[actPreviewDown-96]
_ = x[actPreviewHalfPageUp-97] _ = x[actPreviewPageUp-97]
_ = x[actPreviewHalfPageDown-98] _ = x[actPreviewPageDown-98]
_ = x[actPrevHistory-99] _ = x[actPreviewHalfPageUp-99]
_ = x[actPrevSelected-100] _ = x[actPreviewHalfPageDown-100]
_ = x[actPrint-101] _ = x[actPrevHistory-101]
_ = x[actPut-102] _ = x[actPrevSelected-102]
_ = x[actNextHistory-103] _ = x[actPrint-103]
_ = x[actNextSelected-104] _ = x[actPut-104]
_ = x[actExecute-105] _ = x[actNextHistory-105]
_ = x[actExecuteSilent-106] _ = x[actNextSelected-106]
_ = x[actExecuteMulti-107] _ = x[actExecute-107]
_ = x[actSigStop-108] _ = x[actExecuteSilent-108]
_ = x[actFirst-109] _ = x[actExecuteMulti-109]
_ = x[actLast-110] _ = x[actSigStop-110]
_ = x[actReload-111] _ = x[actFirst-111]
_ = x[actReloadSync-112] _ = x[actLast-112]
_ = x[actDisableSearch-113] _ = x[actReload-113]
_ = x[actEnableSearch-114] _ = x[actReloadSync-114]
_ = x[actSelect-115] _ = x[actDisableSearch-115]
_ = x[actDeselect-116] _ = x[actEnableSearch-116]
_ = x[actUnbind-117] _ = x[actSelect-117]
_ = x[actRebind-118] _ = x[actDeselect-118]
_ = x[actBecome-119] _ = x[actUnbind-119]
_ = x[actShowHeader-120] _ = x[actRebind-120]
_ = x[actHideHeader-121] _ = x[actBecome-121]
_ = x[actShowHeader-122]
_ = x[actHideHeader-123]
_ = x[actBell-124]
} }
const _actionType_name = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeListLabelactChangeInputLabelactChangeHeaderactChangeHeaderLabelactChangeMultiactChangePreviewLabelactChangePromptactChangeQueryactChangeNthactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleMultiLineactToggleHscrollactTrackCurrentactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformListLabelactTransformInputLabelactTransformHeaderactTransformHeaderLabelactTransformPreviewLabelactTransformPromptactTransformQueryactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactBecomeactShowHeaderactHideHeader" const _actionType_name = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeListLabelactChangeInputLabelactChangeHeaderactChangeHeaderLabelactChangeMultiactChangePreviewLabelactChangePromptactChangeQueryactChangeNthactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleMultiLineactToggleHscrollactTrackCurrentactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformListLabelactTransformInputLabelactTransformHeaderactTransformHeaderLabelactTransformPreviewLabelactTransformPromptactTransformQueryactTransformSearchactSearchactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactBecomeactShowHeaderactHideHeaderactBell"
var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 42, 50, 68, 76, 85, 102, 123, 138, 159, 183, 198, 207, 227, 245, 264, 279, 299, 313, 334, 349, 363, 375, 389, 402, 419, 427, 440, 456, 468, 476, 490, 504, 515, 526, 544, 561, 568, 587, 599, 613, 622, 637, 649, 662, 673, 684, 696, 710, 731, 746, 759, 777, 793, 808, 825, 832, 837, 846, 857, 868, 881, 896, 907, 920, 935, 942, 955, 968, 985, 1000, 1013, 1027, 1041, 1057, 1077, 1089, 1112, 1133, 1155, 1173, 1196, 1220, 1238, 1255, 1265, 1281, 1303, 1316, 1332, 1344, 1358, 1374, 1392, 1412, 1434, 1448, 1463, 1471, 1477, 1491, 1506, 1516, 1532, 1547, 1557, 1565, 1572, 1581, 1594, 1610, 1625, 1634, 1645, 1654, 1663, 1672, 1685, 1698} var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 42, 50, 68, 76, 85, 102, 123, 138, 159, 183, 198, 207, 227, 245, 264, 279, 299, 313, 334, 349, 363, 375, 389, 402, 419, 427, 440, 456, 468, 476, 490, 504, 515, 526, 544, 561, 568, 587, 599, 613, 622, 637, 649, 662, 673, 684, 696, 710, 731, 746, 759, 777, 793, 808, 825, 832, 837, 846, 857, 868, 881, 896, 907, 920, 935, 942, 955, 968, 985, 1000, 1013, 1027, 1041, 1057, 1077, 1089, 1112, 1133, 1155, 1173, 1196, 1220, 1238, 1255, 1273, 1282, 1292, 1308, 1330, 1343, 1359, 1371, 1385, 1401, 1419, 1439, 1461, 1475, 1490, 1498, 1504, 1518, 1533, 1543, 1559, 1574, 1584, 1592, 1599, 1608, 1621, 1637, 1652, 1661, 1672, 1681, 1690, 1699, 1712, 1725, 1732}
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) {

View File

@ -1332,7 +1332,7 @@ const (
func init() { func init() {
executeRegexp = regexp.MustCompile( executeRegexp = regexp.MustCompile(
`(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:query|prompt|(?:border|list|preview|input|header)-label|header)|transform|change-(?:preview-window|preview|multi|nth)|(?:re|un)bind|pos|put|print)`) `(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:query|prompt|(?:border|list|preview|input|header)-label|header|search)|transform|change-(?:preview-window|preview|multi|nth)|(?:re|un)bind|pos|put|print|search)`)
splitRegexp = regexp.MustCompile("[,:]+") splitRegexp = regexp.MustCompile("[,:]+")
actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+") actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+")
} }
@ -1744,6 +1744,10 @@ func isExecuteAction(str string) actionType {
return actTransformPrompt return actTransformPrompt
case "transform-query": case "transform-query":
return actTransformQuery return actTransformQuery
case "transform-search":
return actTransformSearch
case "search":
return actSearch
} }
return actIgnore return actIgnore
} }
@ -3252,7 +3256,7 @@ func ParseOptions(useDefaults bool, args []string) (*Options, error) {
// 1. explicitly set --scheme=default, // 1. explicitly set --scheme=default,
// 2. or replace $FZF_DEFAULT_COMMAND with an equivalent 'start:reload' // 2. or replace $FZF_DEFAULT_COMMAND with an equivalent 'start:reload'
// binding, which is the new preferred way. // binding, which is the new preferred way.
if !opts.hasReloadOnStart() && util.IsTty(os.Stdin) { if !opts.hasReloadOrTransformOnStart() && util.IsTty(os.Stdin) {
opts.Scheme = "path" opts.Scheme = "path"
} }
_, opts.Criteria, _ = parseScheme(opts.Scheme) _, opts.Criteria, _ = parseScheme(opts.Scheme)
@ -3267,10 +3271,10 @@ func ParseOptions(useDefaults bool, args []string) (*Options, error) {
return opts, nil return opts, nil
} }
func (opts *Options) hasReloadOnStart() bool { func (opts *Options) hasReloadOrTransformOnStart() bool {
if actions, prs := opts.Keymap[tui.Start.AsEvent()]; prs { if actions, prs := opts.Keymap[tui.Start.AsEvent()]; prs {
for _, action := range actions { for _, action := range actions {
if action.t == actReload || action.t == actReloadSync { if action.t == actReload || action.t == actReloadSync || action.t == actTransform {
return true return true
} }
} }

View File

@ -277,6 +277,7 @@ type Terminal struct {
xoffset int xoffset int
yanked []rune yanked []rune
input []rune input []rune
inputOverride *[]rune
multi int multi int
multiLine bool multiLine bool
sort bool sort bool
@ -533,6 +534,8 @@ const (
actTransformPreviewLabel actTransformPreviewLabel
actTransformPrompt actTransformPrompt
actTransformQuery actTransformQuery
actTransformSearch
actSearch
actPreview actPreview
actChangePreview actChangePreview
actChangePreviewWindow actChangePreviewWindow
@ -1354,7 +1357,13 @@ func (t *Terminal) getScrollbar() (int, int) {
func (t *Terminal) Input() (bool, []rune) { func (t *Terminal) Input() (bool, []rune) {
t.mutex.Lock() t.mutex.Lock()
defer t.mutex.Unlock() defer t.mutex.Unlock()
return t.paused, copySlice(t.input) paused := t.paused
src := t.input
if t.inputOverride != nil {
paused = false
src = *t.inputOverride
}
return paused, copySlice(src)
} }
// UpdateCount updates the count information // UpdateCount updates the count information
@ -3837,6 +3846,14 @@ func (t *Terminal) fullRedraw() {
t.printAll() t.printAll()
} }
func (t *Terminal) captureLine(template string) string {
return t.executeCommand(template, false, true, true, true, "")
}
func (t *Terminal) captureLines(template string) string {
return t.executeCommand(template, false, true, true, false, "")
}
func (t *Terminal) executeCommand(template string, forcePlus bool, background bool, capture bool, firstLineOnly bool, info string) string { func (t *Terminal) executeCommand(template string, forcePlus bool, background bool, capture bool, firstLineOnly bool, info string) string {
line := "" line := ""
valid, list := t.buildPlusList(template, forcePlus) valid, list := t.buildPlusList(template, forcePlus)
@ -4751,12 +4768,12 @@ func (t *Terminal) Loop() error {
req(reqPreviewRefresh) req(reqPreviewRefresh)
} }
case actTransformPrompt: case actTransformPrompt:
prompt := t.executeCommand(a.a, false, true, true, true, "") prompt := t.captureLine(a.a)
t.promptString = prompt t.promptString = prompt
t.prompt, t.promptLen = t.parsePrompt(prompt) t.prompt, t.promptLen = t.parsePrompt(prompt)
req(reqPrompt) req(reqPrompt)
case actTransformQuery: case actTransformQuery:
query := t.executeCommand(a.a, false, true, true, true, "") query := t.captureLine(a.a)
t.input = []rune(query) t.input = []rune(query)
t.cx = len(t.input) t.cx = len(t.input)
case actToggleSort: case actToggleSort:
@ -4840,7 +4857,7 @@ func (t *Terminal) Loop() error {
case actChangeHeader, actTransformHeader: case actChangeHeader, actTransformHeader:
header := a.a header := a.a
if a.t == actTransformHeader { if a.t == actTransformHeader {
header = t.executeCommand(a.a, false, true, true, false, "") header = t.captureLines(a.a)
} }
if t.changeHeader(header) { if t.changeHeader(header) {
req(reqHeader, reqList, reqPrompt, reqInfo) req(reqHeader, reqList, reqPrompt, reqInfo)
@ -4878,40 +4895,40 @@ func (t *Terminal) Loop() error {
req(reqRedrawPreviewLabel) req(reqRedrawPreviewLabel)
} }
case actTransform: case actTransform:
body := t.executeCommand(a.a, false, true, true, false, "") body := t.captureLines(a.a)
if actions, err := parseSingleActionList(strings.Trim(body, "\r\n")); err == nil { if actions, err := parseSingleActionList(strings.Trim(body, "\r\n")); err == nil {
return doActions(actions) return doActions(actions)
} }
case actTransformHeaderLabel: case actTransformHeaderLabel:
label := t.executeCommand(a.a, false, true, true, true, "") label := t.captureLine(a.a)
t.headerLabelOpts.label = label t.headerLabelOpts.label = label
if t.headerBorder != nil { if t.headerBorder != nil {
t.headerLabel, t.headerLabelLen = t.ansiLabelPrinter(label, &tui.ColHeaderLabel, false) t.headerLabel, t.headerLabelLen = t.ansiLabelPrinter(label, &tui.ColHeaderLabel, false)
req(reqRedrawHeaderLabel) req(reqRedrawHeaderLabel)
} }
case actTransformInputLabel: case actTransformInputLabel:
label := t.executeCommand(a.a, false, true, true, true, "") label := t.captureLine(a.a)
t.inputLabelOpts.label = label t.inputLabelOpts.label = label
if t.inputBorder != nil { if t.inputBorder != nil {
t.inputLabel, t.inputLabelLen = t.ansiLabelPrinter(label, &tui.ColInputLabel, false) t.inputLabel, t.inputLabelLen = t.ansiLabelPrinter(label, &tui.ColInputLabel, false)
req(reqRedrawInputLabel) req(reqRedrawInputLabel)
} }
case actTransformListLabel: case actTransformListLabel:
label := t.executeCommand(a.a, false, true, true, true, "") label := t.captureLine(a.a)
t.listLabelOpts.label = label t.listLabelOpts.label = label
if t.wborder != nil { if t.wborder != nil {
t.listLabel, t.listLabelLen = t.ansiLabelPrinter(label, &tui.ColListLabel, false) t.listLabel, t.listLabelLen = t.ansiLabelPrinter(label, &tui.ColListLabel, false)
req(reqRedrawListLabel) req(reqRedrawListLabel)
} }
case actTransformBorderLabel: case actTransformBorderLabel:
label := t.executeCommand(a.a, false, true, true, true, "") label := t.captureLine(a.a)
t.borderLabelOpts.label = label t.borderLabelOpts.label = label
if t.border != nil { if t.border != nil {
t.borderLabel, t.borderLabelLen = t.ansiLabelPrinter(label, &tui.ColBorderLabel, false) t.borderLabel, t.borderLabelLen = t.ansiLabelPrinter(label, &tui.ColBorderLabel, false)
req(reqRedrawBorderLabel) req(reqRedrawBorderLabel)
} }
case actTransformPreviewLabel: case actTransformPreviewLabel:
label := t.executeCommand(a.a, false, true, true, true, "") label := t.captureLine(a.a)
t.previewLabelOpts.label = label t.previewLabelOpts.label = label
if t.pborder != nil { if t.pborder != nil {
t.previewLabel, t.previewLabelLen = t.ansiLabelPrinter(label, &tui.ColPreviewLabel, false) t.previewLabel, t.previewLabelLen = t.ansiLabelPrinter(label, &tui.ColPreviewLabel, false)
@ -5309,6 +5326,14 @@ func (t *Terminal) Loop() error {
t.track = trackDisabled t.track = trackDisabled
} }
req(reqInfo) req(reqInfo)
case actSearch:
override := []rune(a.a)
t.inputOverride = &override
changed = true
case actTransformSearch:
override := []rune(t.captureLine(a.a))
t.inputOverride = &override
changed = true
case actEnableSearch: case actEnableSearch:
t.paused = false t.paused = false
changed = true changed = true
@ -5734,6 +5759,9 @@ func (t *Terminal) Loop() error {
} }
t.truncateQuery() t.truncateQuery()
queryChanged = string(previousInput) != string(t.input) queryChanged = string(previousInput) != string(t.input)
if queryChanged {
t.inputOverride = nil
}
changed = changed || queryChanged changed = changed || queryChanged
if onChanges, prs := t.keymap[tui.Change.AsEvent()]; queryChanged && prs && !doActions(onChanges) { if onChanges, prs := t.keymap[tui.Change.AsEvent()]; queryChanged && prs && !doActions(onChanges) {
continue continue

View File

@ -1069,6 +1069,24 @@ class TestCore < TestInteractive
tmux.until { |lines| assert_equal 'up', lines[-1] } tmux.until { |lines| assert_equal 'up', lines[-1] }
end end
def test_search
tmux.send_keys %(seq 100 | #{FZF} --query 0 --bind space:search:1), :Enter
tmux.until { |lines| assert_equal 10, lines.match_count }
tmux.send_keys :Space
tmux.until { |lines| assert_equal 20, lines.match_count }
tmux.send_keys '0'
tmux.until { |lines| assert_equal 1, lines.match_count }
end
def test_transform_search
tmux.send_keys %(seq 1000 | #{FZF} --bind 'change:transform-search:echo {q}{q}'), :Enter
tmux.until { |lines| assert_equal 1000, lines.match_count }
tmux.send_keys '1'
tmux.until { |lines| assert_equal 28, lines.match_count }
tmux.send_keys :BSpace, '0'
tmux.until { |lines| assert_equal 10, lines.match_count }
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 }