diff --git a/CHANGELOG.md b/CHANGELOG.md index fec32d97..bb986dee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,22 @@ CHANGELOG 0.45.0 ------ +- Added `transform` action to conditionally perform a series of actions + ```sh + # Disallow selecting an empty line + echo -e "1. Hello\n2. Goodbye\n\n3. Exit" | + fzf --reverse --header 'Select one' \ + --bind 'enter:transform:[[ -n {} ]] && echo accept || echo "change-header:Invalid selection"' + + # Move cursor past the empty line + echo -e "1. Hello\n2. Goodbye\n\n3. Exit" | + fzf --reverse --header 'Select one' \ + --bind 'enter:transform:[[ -n {} ]] && echo accept || echo "change-header:Invalid selection"' \ + --bind 'focus:transform:[[ -n {} ]] && exit; [[ {fzf:action} =~ up$ ]] && echo up || echo down' + ``` +- Added placeholder expressions + - `{fzf:action}` - the name of the last action performed + - `{fzf:query}` - synonym for `{q}` - Added support for negative height ```sh # Terminal height minus 1, so you can still see the command line diff --git a/Makefile b/Makefile index a55c6bdb..32164a6e 100644 --- a/Makefile +++ b/Makefile @@ -89,6 +89,9 @@ bench: install: bin/fzf +generate: + PATH=$(PATH):$(GOPATH)/bin $(GO) generate ./... + build: goreleaser build --rm-dist --snapshot --skip-post-hooks @@ -181,4 +184,4 @@ update: $(GO) get -u $(GO) mod tidy -.PHONY: all build release test bench install clean docker docker-test update +.PHONY: all generate build release test bench install clean docker docker-test update diff --git a/go.mod b/go.mod index 31f9b0b0..ca3ebb98 100644 --- a/go.mod +++ b/go.mod @@ -14,8 +14,10 @@ require ( require ( github.com/gdamore/encoding v1.0.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect - golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect + golang.org/x/mod v0.14.0 // indirect + golang.org/x/sync v0.5.0 // indirect golang.org/x/text v0.5.0 // indirect + golang.org/x/tools v0.16.1 // indirect ) go 1.17 diff --git a/go.sum b/go.sum index 3270c091..d9bb109f 100644 --- a/go.sum +++ b/go.sum @@ -19,6 +19,8 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -26,6 +28,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= +golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -46,4 +50,6 @@ golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA= +golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index c4fe788c..20e7e17d 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -574,10 +574,6 @@ e.g. When using a field index expression, leading and trailing whitespace is stripped from the replacement string. To preserve the whitespace, use the \fBs\fR flag. -Also, \fB{q}\fR is replaced to the current query string, and \fB{n}\fR is -replaced to zero-based ordinal index of the line. Use \fB{+n}\fR if you want -all index numbers when multiple lines are selected. - A placeholder expression with \fBf\fR flag is replaced to the path of a temporary file that holds the evaluated list. This is useful when you multi-select a large number of items and the length of the evaluated string may @@ -589,6 +585,15 @@ e.g. seq 100000 | fzf --multi --bind ctrl-a:select-all \\ --preview "awk '{sum+=\\$1} END {print sum}' {+f}"\fR +Also, + +* \fB{q}\fR (or \fB{fzf:query}\fR) is replaced to the current query string +.br +* \fB{n}\fR is replaced to the zero-based ordinal index of the current item. + Use \fB{+n}\fR if you want all index numbers when multiple lines are selected. +.br +* \fB{fzf:action}\fR is replaced to to the name of the last action performed + Note that you can escape a placeholder pattern by prepending a backslash. Preview window will be updated even when there is no match for the current diff --git a/src/actiontype_string.go b/src/actiontype_string.go new file mode 100644 index 00000000..90fa909a --- /dev/null +++ b/src/actiontype_string.go @@ -0,0 +1,127 @@ +// Code generated by "stringer -type=actionType"; DO NOT EDIT. + +package fzf + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[actIgnore-0] + _ = x[actStart-1] + _ = x[actClick-2] + _ = x[actInvalid-3] + _ = x[actChar-4] + _ = x[actMouse-5] + _ = x[actBeginningOfLine-6] + _ = x[actAbort-7] + _ = x[actAccept-8] + _ = x[actAcceptNonEmpty-9] + _ = x[actAcceptOrPrintQuery-10] + _ = x[actBackwardChar-11] + _ = x[actBackwardDeleteChar-12] + _ = x[actBackwardDeleteCharEof-13] + _ = x[actBackwardWord-14] + _ = x[actCancel-15] + _ = x[actChangeBorderLabel-16] + _ = x[actChangeHeader-17] + _ = x[actChangePreviewLabel-18] + _ = x[actChangePrompt-19] + _ = x[actChangeQuery-20] + _ = x[actClearScreen-21] + _ = x[actClearQuery-22] + _ = x[actClearSelection-23] + _ = x[actClose-24] + _ = x[actDeleteChar-25] + _ = x[actDeleteCharEof-26] + _ = x[actEndOfLine-27] + _ = x[actForwardChar-28] + _ = x[actForwardWord-29] + _ = x[actKillLine-30] + _ = x[actKillWord-31] + _ = x[actUnixLineDiscard-32] + _ = x[actUnixWordRubout-33] + _ = x[actYank-34] + _ = x[actBackwardKillWord-35] + _ = x[actSelectAll-36] + _ = x[actDeselectAll-37] + _ = x[actToggle-38] + _ = x[actToggleSearch-39] + _ = x[actToggleAll-40] + _ = x[actToggleDown-41] + _ = x[actToggleUp-42] + _ = x[actToggleIn-43] + _ = x[actToggleOut-44] + _ = x[actToggleTrack-45] + _ = x[actToggleHeader-46] + _ = x[actTrack-47] + _ = x[actDown-48] + _ = x[actUp-49] + _ = x[actPageUp-50] + _ = x[actPageDown-51] + _ = x[actPosition-52] + _ = x[actHalfPageUp-53] + _ = x[actHalfPageDown-54] + _ = x[actOffsetUp-55] + _ = x[actOffsetDown-56] + _ = x[actJump-57] + _ = x[actJumpAccept-58] + _ = x[actPrintQuery-59] + _ = x[actRefreshPreview-60] + _ = x[actReplaceQuery-61] + _ = x[actToggleSort-62] + _ = x[actShowPreview-63] + _ = x[actHidePreview-64] + _ = x[actTogglePreview-65] + _ = x[actTogglePreviewWrap-66] + _ = x[actTransform-67] + _ = x[actTransformBorderLabel-68] + _ = x[actTransformHeader-69] + _ = x[actTransformPreviewLabel-70] + _ = x[actTransformPrompt-71] + _ = x[actTransformQuery-72] + _ = x[actPreview-73] + _ = x[actChangePreview-74] + _ = x[actChangePreviewWindow-75] + _ = x[actPreviewTop-76] + _ = x[actPreviewBottom-77] + _ = x[actPreviewUp-78] + _ = x[actPreviewDown-79] + _ = x[actPreviewPageUp-80] + _ = x[actPreviewPageDown-81] + _ = x[actPreviewHalfPageUp-82] + _ = x[actPreviewHalfPageDown-83] + _ = x[actPrevHistory-84] + _ = x[actPrevSelected-85] + _ = x[actPut-86] + _ = x[actNextHistory-87] + _ = x[actNextSelected-88] + _ = x[actExecute-89] + _ = x[actExecuteSilent-90] + _ = x[actExecuteMulti-91] + _ = x[actSigStop-92] + _ = x[actFirst-93] + _ = x[actLast-94] + _ = x[actReload-95] + _ = x[actReloadSync-96] + _ = x[actDisableSearch-97] + _ = x[actEnableSearch-98] + _ = x[actSelect-99] + _ = x[actDeselect-100] + _ = x[actUnbind-101] + _ = x[actRebind-102] + _ = x[actBecome-103] + _ = x[actResponse-104] +} + +const _actionType_name = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeHeaderactChangePreviewLabelactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleHeaderactTrackactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformHeaderactTransformPreviewLabelactTransformPromptactTransformQueryactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactBecomeactResponse" + +var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 42, 50, 68, 76, 85, 102, 123, 138, 159, 183, 198, 207, 227, 242, 263, 278, 292, 306, 319, 336, 344, 357, 373, 385, 399, 413, 424, 435, 453, 470, 477, 496, 508, 522, 531, 546, 558, 571, 582, 593, 605, 619, 634, 642, 649, 654, 663, 674, 685, 698, 713, 724, 737, 744, 757, 770, 787, 802, 815, 829, 843, 859, 879, 891, 914, 932, 956, 974, 991, 1001, 1017, 1039, 1052, 1068, 1080, 1094, 1110, 1128, 1148, 1170, 1184, 1199, 1205, 1219, 1234, 1244, 1260, 1275, 1285, 1293, 1300, 1309, 1322, 1338, 1353, 1362, 1373, 1382, 1391, 1400, 1411} + +func (i actionType) String() string { + if i < 0 || i >= actionType(len(_actionType_index)-1) { + return "actionType(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _actionType_name[_actionType_index[i]:_actionType_index[i+1]] +} diff --git a/src/options.go b/src/options.go index ca8b2476..0822817a 100644 --- a/src/options.go +++ b/src/options.go @@ -979,7 +979,7 @@ const ( func init() { executeRegexp = regexp.MustCompile( - `(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:header|query|prompt|border-label|preview-label)|change-preview-window|change-preview|(?:re|un)bind|pos|put)`) + `(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:header|query|prompt|border-label|preview-label)|transform|change-preview-window|change-preview|(?:re|un)bind|pos|put)`) splitRegexp = regexp.MustCompile("[,:]+") actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+") } @@ -1086,7 +1086,7 @@ func parseActionList(masked string, original string, prevActions []*action, putA case "backward-delete-char": appendAction(actBackwardDeleteChar) case "backward-delete-char/eof": - appendAction(actBackwardDeleteCharEOF) + appendAction(actBackwardDeleteCharEof) case "backward-word": appendAction(actBackwardWord) case "clear-screen": @@ -1094,7 +1094,7 @@ func parseActionList(masked string, original string, prevActions []*action, putA case "delete-char": appendAction(actDeleteChar) case "delete-char/eof": - appendAction(actDeleteCharEOF) + appendAction(actDeleteCharEof) case "deselect": appendAction(actDeselect) case "end-of-line": @@ -1213,7 +1213,7 @@ func parseActionList(masked string, original string, prevActions []*action, putA appendAction(actDisableSearch) case "put": if putAllowed { - appendAction(actRune) + appendAction(actChar) } else { exit("unable to put non-printable character") } @@ -1333,6 +1333,8 @@ func isExecuteAction(str string) actionType { return actExecuteMulti case "put": return actPut + case "transform": + return actTransform case "transform-border-label": return actTransformBorderLabel case "transform-preview-label": diff --git a/src/terminal.go b/src/terminal.go index 394be4fc..31339d3e 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -52,11 +52,12 @@ var offsetComponentRegex *regexp.Regexp var offsetTrimCharsRegex *regexp.Regexp var activeTempFiles []string var passThroughRegex *regexp.Regexp +var actionTypeRegex *regexp.Regexp const clearCode string = "\x1b[2J" func init() { - placeholder = regexp.MustCompile(`\\?(?:{[+sf]*[0-9,-.]*}|{q}|{\+?f?nf?})`) + placeholder = regexp.MustCompile(`\\?(?:{[+sf]*[0-9,-.]*}|{q}|{fzf:(?:query|action)}|{\+?f?nf?})`) whiteSuffix = regexp.MustCompile(`\s*$`) offsetComponentRegex = regexp.MustCompile(`([+-][0-9]+)|(-?/[1-9][0-9]*)`) offsetTrimCharsRegex = regexp.MustCompile(`[^0-9/+-]`) @@ -285,6 +286,7 @@ type Terminal struct { tui tui.Renderer executing *util.AtomicBool termSize tui.TermSize + lastAction actionType } type selectedItem struct { @@ -332,12 +334,15 @@ type action struct { a string } +//go:generate stringer -type=actionType type actionType int const ( actIgnore actionType = iota + actStart + actClick actInvalid - actRune + actChar actMouse actBeginningOfLine actAbort @@ -346,7 +351,7 @@ const ( actAcceptOrPrintQuery actBackwardChar actBackwardDeleteChar - actBackwardDeleteCharEOF + actBackwardDeleteCharEof actBackwardWord actCancel actChangeBorderLabel @@ -359,7 +364,7 @@ const ( actClearSelection actClose actDeleteChar - actDeleteCharEOF + actDeleteCharEof actEndOfLine actForwardChar actForwardWord @@ -400,6 +405,7 @@ const ( actHidePreview actTogglePreview actTogglePreviewWrap + actTransform actTransformBorderLabel actTransformHeader actTransformPreviewLabel @@ -441,13 +447,15 @@ const ( func processExecution(action actionType) bool { switch action { - case actTransformBorderLabel, + case actTransform, + actTransformBorderLabel, actTransformHeader, actTransformPreviewLabel, actTransformPrompt, actTransformQuery, actPreview, actChangePreview, + actRefreshPreview, actExecute, actExecuteSilent, actExecuteMulti, @@ -514,7 +522,7 @@ func defaultKeymap() map[tui.Event][]*action { add(tui.CtrlG, actAbort) add(tui.CtrlQ, actAbort) add(tui.ESC, actAbort) - add(tui.CtrlD, actDeleteCharEOF) + add(tui.CtrlD, actDeleteCharEof) add(tui.CtrlE, actEndOfLine) add(tui.CtrlF, actForwardChar) add(tui.CtrlH, actBackwardDeleteChar) @@ -556,7 +564,7 @@ func defaultKeymap() map[tui.Event][]*action { add(tui.SDown, actPreviewDown) add(tui.Mouse, actMouse) - add(tui.LeftClick, actIgnore) + add(tui.LeftClick, actClick) add(tui.RightClick, actToggle) add(tui.SLeftClick, actToggle) add(tui.SRightClick, actToggle) @@ -740,7 +748,8 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { eventChan: make(chan tui.Event, 3), // load / zero|one | GetChar tui: renderer, initFunc: func() { renderer.Init() }, - executing: util.NewAtomicBool(false)} + executing: util.NewAtomicBool(false), + lastAction: actStart} t.prompt, t.promptLen = t.parsePrompt(opts.Prompt) t.pointer, t.pointerLen = t.processTabs([]rune(opts.Pointer), 0) t.marker, t.markerLen = t.processTabs([]rune(opts.Marker), 0) @@ -2344,6 +2353,10 @@ func parsePlaceholder(match string) (bool, string, placeholderFlags) { return true, match[1:], flags } + if strings.HasPrefix(match, "{fzf:") { + return false, match, flags + } + skipChars := 1 for _, char := range match[1:] { switch char { @@ -2408,7 +2421,7 @@ func cleanTemporaryFiles() { func (t *Terminal) replacePlaceholder(template string, forcePlus bool, input string, list []*Item) string { return replacePlaceholder( - template, t.ansi, t.delimiter, t.printsep, forcePlus, input, list) + template, t.ansi, t.delimiter, t.printsep, forcePlus, input, list, t.lastAction) } func (t *Terminal) evaluateScrollOffset() int { @@ -2446,7 +2459,7 @@ func (t *Terminal) evaluateScrollOffset() int { return util.Max(0, base) } -func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, printsep string, forcePlus bool, query string, allItems []*Item) string { +func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, printsep string, forcePlus bool, query string, allItems []*Item, lastAction actionType) string { current := allItems[:1] selected := allItems[1:] if current[0] == nil { @@ -2467,7 +2480,16 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, pr switch { case escaped: return match - case match == "{q}": + case match == "{fzf:action}": + name := "" + for i, r := range lastAction.String()[3:] { + if i > 0 && r >= 'A' && r <= 'Z' { + name += "-" + } + name += string(r) + } + return strings.ToLower(name) + case match == "{q}" || match == "{fzf:query}": return quoteEntry(query) case match == "{}": replace = func(item *Item) string { @@ -3207,7 +3229,7 @@ func (t *Terminal) Loop() { } doAction = func(a *action) bool { switch a.t { - case actIgnore: + case actIgnore, actStart, actClick: case actResponse: t.serverOutputChan <- t.dumpStatus(parseGetParams(a.a)) case actBecome: @@ -3354,6 +3376,10 @@ func (t *Terminal) Loop() { t.previewLabel, t.previewLabelLen = t.ansiLabelPrinter(a.a, &tui.ColPreviewLabel, false) req(reqRedrawPreviewLabel) } + case actTransform: + body := t.executeCommand(a.a, false, true, true, false) + actions := parseSingleActionList(strings.Trim(body, "\r\n"), func(message string) {}) + t.serverInputChan <- actions case actTransformBorderLabel: if t.border != nil { label := t.executeCommand(a.a, false, true, true, true) @@ -3384,7 +3410,7 @@ func (t *Terminal) Loop() { req(reqQuit) case actDeleteChar: t.delChar() - case actDeleteCharEOF: + case actDeleteCharEof: if !t.delChar() && t.cx == 0 { req(reqQuit) } @@ -3398,7 +3424,7 @@ func (t *Terminal) Loop() { t.input = []rune{} t.cx = 0 } - case actBackwardDeleteCharEOF: + case actBackwardDeleteCharEof: if len(t.input) == 0 { req(reqQuit) } else if t.cx > 0 { @@ -3617,7 +3643,7 @@ func (t *Terminal) Loop() { t.yanked = copySlice(t.input[t.cx:]) t.input = t.input[:t.cx] } - case actRune: + case actChar: prefix := copySlice(t.input[:t.cx]) t.input = append(append(prefix, event.Char), t.input[t.cx:]...) t.cx++ @@ -3895,6 +3921,10 @@ func (t *Terminal) Loop() { } } } + + if !processExecution(a.t) { + t.lastAction = a.t + } return true } @@ -3908,7 +3938,7 @@ func (t *Terminal) Loop() { actions = t.keymap[event.Comparable()] } if len(actions) == 0 && event.Type == tui.Rune { - doAction(&action{t: actRune}) + doAction(&action{t: actChar}) } else if !doActions(actions) { continue } diff --git a/src/terminal_test.go b/src/terminal_test.go index ebb6bda5..271c64a3 100644 --- a/src/terminal_test.go +++ b/src/terminal_test.go @@ -52,90 +52,90 @@ func TestReplacePlaceholder(t *testing.T) { */ // {}, preserve ansi - result = replacePlaceholder("echo {}", false, Delimiter{}, printsep, false, "query", items1) + result = replacePlaceholder("echo {}", false, Delimiter{}, printsep, false, "query", items1, actIgnore) checkFormat("echo {{.O}} foo{{.I}}bar \x1b[31mbaz\x1b[m{{.O}}") // {}, strip ansi - result = replacePlaceholder("echo {}", true, Delimiter{}, printsep, false, "query", items1) + result = replacePlaceholder("echo {}", true, Delimiter{}, printsep, false, "query", items1, actIgnore) checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}") // {}, with multiple items - result = replacePlaceholder("echo {}", true, Delimiter{}, printsep, false, "query", items2) + result = replacePlaceholder("echo {}", true, Delimiter{}, printsep, false, "query", items2, actIgnore) checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}") // {..}, strip leading whitespaces, preserve ansi - result = replacePlaceholder("echo {..}", false, Delimiter{}, printsep, false, "query", items1) + result = replacePlaceholder("echo {..}", false, Delimiter{}, printsep, false, "query", items1, actIgnore) checkFormat("echo {{.O}}foo{{.I}}bar \x1b[31mbaz\x1b[m{{.O}}") // {..}, strip leading whitespaces, strip ansi - result = replacePlaceholder("echo {..}", true, Delimiter{}, printsep, false, "query", items1) + result = replacePlaceholder("echo {..}", true, Delimiter{}, printsep, false, "query", items1, actIgnore) checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}") // {q} - result = replacePlaceholder("echo {} {q}", true, Delimiter{}, printsep, false, "query", items1) + result = replacePlaceholder("echo {} {q}", true, Delimiter{}, printsep, false, "query", items1, actIgnore) checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}} {{.O}}query{{.O}}") // {q}, multiple items - result = replacePlaceholder("echo {+}{q}{+}", true, Delimiter{}, printsep, false, "query 'string'", items2) + result = replacePlaceholder("echo {+}{q}{+}", true, Delimiter{}, printsep, false, "query 'string'", items2, actIgnore) checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}{{.O}}query {{.I}}string{{.I}}{{.O}}{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}") - result = replacePlaceholder("echo {}{q}{}", true, Delimiter{}, printsep, false, "query 'string'", items2) + result = replacePlaceholder("echo {}{q}{}", true, Delimiter{}, printsep, false, "query 'string'", items2, actIgnore) checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}{{.O}}query {{.I}}string{{.I}}{{.O}}{{.O}}foo{{.I}}bar baz{{.O}}") - result = replacePlaceholder("echo {1}/{2}/{2,1}/{-1}/{-2}/{}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items1) + result = replacePlaceholder("echo {1}/{2}/{2,1}/{-1}/{-2}/{}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items1, actIgnore) checkFormat("echo {{.O}}foo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}bazfoo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}foo{{.I}}bar{{.O}}/{{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}}") - result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items2) + result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items2, actIgnore) checkFormat("echo {{.O}}foo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}baz{{.O}}/{{.O}}foo{{.I}}bar{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}}") - result = replacePlaceholder("echo {+1}/{+2}/{+-1}/{+-2}/{+..}/{n.t}/\\{}/\\{1}/\\{q}/{+3}", true, Delimiter{}, printsep, false, "query", items2) + result = replacePlaceholder("echo {+1}/{+2}/{+-1}/{+-2}/{+..}/{n.t}/\\{}/\\{1}/\\{q}/{+3}", true, Delimiter{}, printsep, false, "query", items2, actIgnore) checkFormat("echo {{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}} {{.O}}{{.O}}") // forcePlus - result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, true, "query", items2) + result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, true, "query", items2, actIgnore) checkFormat("echo {{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}} {{.O}}{{.O}}") // Whitespace preserving flag with "'" delimiter - result = replacePlaceholder("echo {s1}", true, Delimiter{str: &delim}, printsep, false, "query", items1) + result = replacePlaceholder("echo {s1}", true, Delimiter{str: &delim}, printsep, false, "query", items1, actIgnore) checkFormat("echo {{.O}} foo{{.O}}") - result = replacePlaceholder("echo {s2}", true, Delimiter{str: &delim}, printsep, false, "query", items1) + result = replacePlaceholder("echo {s2}", true, Delimiter{str: &delim}, printsep, false, "query", items1, actIgnore) checkFormat("echo {{.O}}bar baz{{.O}}") - result = replacePlaceholder("echo {s}", true, Delimiter{str: &delim}, printsep, false, "query", items1) + result = replacePlaceholder("echo {s}", true, Delimiter{str: &delim}, printsep, false, "query", items1, actIgnore) checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}") - result = replacePlaceholder("echo {s..}", true, Delimiter{str: &delim}, printsep, false, "query", items1) + result = replacePlaceholder("echo {s..}", true, Delimiter{str: &delim}, printsep, false, "query", items1, actIgnore) checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}") // Whitespace preserving flag with regex delimiter regex = regexp.MustCompile(`\w+`) - result = replacePlaceholder("echo {s1}", true, Delimiter{regex: regex}, printsep, false, "query", items1) + result = replacePlaceholder("echo {s1}", true, Delimiter{regex: regex}, printsep, false, "query", items1, actIgnore) checkFormat("echo {{.O}} {{.O}}") - result = replacePlaceholder("echo {s2}", true, Delimiter{regex: regex}, printsep, false, "query", items1) + result = replacePlaceholder("echo {s2}", true, Delimiter{regex: regex}, printsep, false, "query", items1, actIgnore) checkFormat("echo {{.O}}{{.I}}{{.O}}") - result = replacePlaceholder("echo {s3}", true, Delimiter{regex: regex}, printsep, false, "query", items1) + result = replacePlaceholder("echo {s3}", true, Delimiter{regex: regex}, printsep, false, "query", items1, actIgnore) checkFormat("echo {{.O}} {{.O}}") // No match - result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, nil}) + result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, nil}, actIgnore) check("echo /") // No match, but with selections - result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, item1}) + result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, item1}, actIgnore) checkFormat("echo /{{.O}} foo{{.I}}bar baz{{.O}}") // String delimiter - result = replacePlaceholder("echo {}/{1}/{2}", true, Delimiter{str: &delim}, printsep, false, "query", items1) + result = replacePlaceholder("echo {}/{1}/{2}", true, Delimiter{str: &delim}, printsep, false, "query", items1, actIgnore) checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}foo{{.O}}/{{.O}}bar baz{{.O}}") // Regex delimiter regex = regexp.MustCompile("[oa]+") // foo'bar baz - result = replacePlaceholder("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, printsep, false, "query", items1) + result = replacePlaceholder("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, printsep, false, "query", items1, actIgnore) checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}f{{.O}}/{{.O}}r b{{.O}}/{{.O}}{{.I}}bar b{{.O}}") /* @@ -198,18 +198,23 @@ func TestReplacePlaceholder(t *testing.T) { // query flag is not removed after parsing, so it gets doubled // while the double q is invalid, it is useful here for testing purposes templateToOutput[`{q}`] = "{{.O}}" + query + "{{.O}}" + templateToOutput[`{fzf:query}`] = "{{.O}}" + query + "{{.O}}" + templateToOutput[`{fzf:action}`] = "backward-delete-char-eof" // IV. escaping placeholder templateToOutput[`\{}`] = `{}` + templateToOutput[`\{q}`] = `{q}` + templateToOutput[`\{fzf:query}`] = `{fzf:query}` + templateToOutput[`\{fzf:action}`] = `{fzf:action}` templateToOutput[`\{++}`] = `{++}` templateToOutput[`{++}`] = templateToOutput[`{+}`] for giveTemplate, wantOutput := range templateToOutput { - result = replacePlaceholder(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3) + result = replacePlaceholder(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3, actBackwardDeleteCharEof) checkFormat(wantOutput) } for giveTemplate, wantOutput := range templateToFile { - path := replacePlaceholder(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3) + path := replacePlaceholder(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3, actIgnore) data, err := readFile(path) if err != nil { @@ -566,7 +571,7 @@ func testCommands(t *testing.T, tests []testCase) { gotOutput := replacePlaceholder( test.give.template, stripAnsi, delimiter, printsep, forcePlus, test.give.query, - test.give.allItems) + test.give.allItems, actIgnore) switch { case test.want.output != "": if gotOutput != test.want.output { diff --git a/test/test_go.rb b/test/test_go.rb index 5a9c48b7..771064ee 100755 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -2016,6 +2016,13 @@ class TestGoFZF < TestBase tmux.until { |lines| assert_equal '> RAB', lines[-1] } end + def test_transform + tmux.send_keys %{#{FZF} --bind 'focus:transform:echo "change-prompt({fzf:action})"'}, :Enter + tmux.until { |lines| assert_equal 'start', lines[-1] } + tmux.send_keys :Up + tmux.until { |lines| assert_equal 'up', lines[-1] } + 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 }