From d83eb2800a09d86e17c0339d86bd1f22f68164a8 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 13 Jan 2025 00:13:31 +0900 Subject: [PATCH] Add change-nth action Example: # Start with --nth 1, then 2, then 3, then back to the default, 1 echo 'foo foobar foobarbaz' | fzf --bind 'space:change-nth(2|3|)' --nth 1 -q foo Close #4172 Close #3109 --- CHANGELOG.md | 5 + man/man1/fzf.1 | 3 +- src/actiontype_string.go | 197 ++++++++++++++++++++------------------- src/core.go | 10 +- src/options.go | 4 +- src/terminal.go | 23 ++++- test/test_go.rb | 23 +++++ 7 files changed, 163 insertions(+), 102 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e6e7cc4..3e30749b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,6 +67,11 @@ Also, fzf now offers "style presets" for quick customization, which can be activ ``` - Added `toggle-multi-line` action - Added `toggle-hscroll` action +- Added `change-nth` action for dynamically changing the value of the `--nth` option + ```sh + # Start with --nth 1, then 2, then 3, then back to the default, 1 + echo 'foo foobar foobarbaz' | fzf --bind 'space:change-nth(2|3|)' --nth 1 -q foo + ``` - A single-character delimiter is now treated as a plain string delimiter rather than a regular expression delimiter, even if it's a regular expression meta-character. - This means you can just write `--delimiter '|'` instead of escaping it as `--delimiter '\|'` - Bug fixes diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 8a5b3638..85cc28f9 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -496,7 +496,7 @@ the label. Label is printed on the top border line by default, add .SS LIST SECTION .TP -.B "\-m, \-\-multi" +.BI "\-m, \-\-multi" "[=MAX]" Enable multi-select with tab/shift\-tab. It optionally takes an integer argument which denotes the maximum number of items that can be selected. .TP @@ -1525,6 +1525,7 @@ A key or an event can be bound to one or more of the following actions. \fBchange\-list\-label(...)\fR (change \fB\-\-list\-label\fR to the given string) \fBchange\-multi\fR (enable multi-select mode with no limit) \fBchange\-multi(...)\fR (enable multi-select mode with a limit or disable it with 0) + \fBchange\-nth(...)\fR (change \fB\-\-nth\fR option; rotate through the multiple options separated by '|') \fBchange\-preview(...)\fR (change \fB\-\-preview\fR option) \fBchange\-preview\-label(...)\fR (change \fB\-\-preview\-label\fR to the given string) \fBchange\-preview\-window(...)\fR (change \fB\-\-preview\-window\fR option; rotate through the multiple option sets separated by '|') diff --git a/src/actiontype_string.go b/src/actiontype_string.go index 12162080..4459e251 100644 --- a/src/actiontype_string.go +++ b/src/actiontype_string.go @@ -33,107 +33,108 @@ func _() { _ = x[actChangePreviewLabel-22] _ = x[actChangePrompt-23] _ = x[actChangeQuery-24] - _ = x[actClearScreen-25] - _ = x[actClearQuery-26] - _ = x[actClearSelection-27] - _ = x[actClose-28] - _ = x[actDeleteChar-29] - _ = x[actDeleteCharEof-30] - _ = x[actEndOfLine-31] - _ = x[actFatal-32] - _ = x[actForwardChar-33] - _ = x[actForwardWord-34] - _ = x[actKillLine-35] - _ = x[actKillWord-36] - _ = x[actUnixLineDiscard-37] - _ = x[actUnixWordRubout-38] - _ = x[actYank-39] - _ = x[actBackwardKillWord-40] - _ = x[actSelectAll-41] - _ = x[actDeselectAll-42] - _ = x[actToggle-43] - _ = x[actToggleSearch-44] - _ = x[actToggleAll-45] - _ = x[actToggleDown-46] - _ = x[actToggleUp-47] - _ = x[actToggleIn-48] - _ = x[actToggleOut-49] - _ = x[actToggleTrack-50] - _ = x[actToggleTrackCurrent-51] - _ = x[actToggleHeader-52] - _ = x[actToggleWrap-53] - _ = x[actToggleMultiLine-54] - _ = x[actToggleHscroll-55] - _ = x[actTrackCurrent-56] - _ = x[actUntrackCurrent-57] - _ = x[actDown-58] - _ = x[actUp-59] - _ = x[actPageUp-60] - _ = x[actPageDown-61] - _ = x[actPosition-62] - _ = x[actHalfPageUp-63] - _ = x[actHalfPageDown-64] - _ = x[actOffsetUp-65] - _ = x[actOffsetDown-66] - _ = x[actOffsetMiddle-67] - _ = x[actJump-68] - _ = x[actJumpAccept-69] - _ = x[actPrintQuery-70] - _ = x[actRefreshPreview-71] - _ = x[actReplaceQuery-72] - _ = x[actToggleSort-73] - _ = x[actShowPreview-74] - _ = x[actHidePreview-75] - _ = x[actTogglePreview-76] - _ = x[actTogglePreviewWrap-77] - _ = x[actTransform-78] - _ = x[actTransformBorderLabel-79] - _ = x[actTransformListLabel-80] - _ = x[actTransformInputLabel-81] - _ = x[actTransformHeader-82] - _ = x[actTransformHeaderLabel-83] - _ = x[actTransformPreviewLabel-84] - _ = x[actTransformPrompt-85] - _ = x[actTransformQuery-86] - _ = x[actPreview-87] - _ = x[actChangePreview-88] - _ = x[actChangePreviewWindow-89] - _ = x[actPreviewTop-90] - _ = x[actPreviewBottom-91] - _ = x[actPreviewUp-92] - _ = x[actPreviewDown-93] - _ = x[actPreviewPageUp-94] - _ = x[actPreviewPageDown-95] - _ = x[actPreviewHalfPageUp-96] - _ = x[actPreviewHalfPageDown-97] - _ = x[actPrevHistory-98] - _ = x[actPrevSelected-99] - _ = x[actPrint-100] - _ = x[actPut-101] - _ = x[actNextHistory-102] - _ = x[actNextSelected-103] - _ = x[actExecute-104] - _ = x[actExecuteSilent-105] - _ = x[actExecuteMulti-106] - _ = x[actSigStop-107] - _ = x[actFirst-108] - _ = x[actLast-109] - _ = x[actReload-110] - _ = x[actReloadSync-111] - _ = x[actDisableSearch-112] - _ = x[actEnableSearch-113] - _ = x[actSelect-114] - _ = x[actDeselect-115] - _ = x[actUnbind-116] - _ = x[actRebind-117] - _ = x[actBecome-118] - _ = x[actShowHeader-119] - _ = x[actHideHeader-120] + _ = x[actChangeNth-25] + _ = x[actClearScreen-26] + _ = x[actClearQuery-27] + _ = x[actClearSelection-28] + _ = x[actClose-29] + _ = x[actDeleteChar-30] + _ = x[actDeleteCharEof-31] + _ = x[actEndOfLine-32] + _ = x[actFatal-33] + _ = x[actForwardChar-34] + _ = x[actForwardWord-35] + _ = x[actKillLine-36] + _ = x[actKillWord-37] + _ = x[actUnixLineDiscard-38] + _ = x[actUnixWordRubout-39] + _ = x[actYank-40] + _ = x[actBackwardKillWord-41] + _ = x[actSelectAll-42] + _ = x[actDeselectAll-43] + _ = x[actToggle-44] + _ = x[actToggleSearch-45] + _ = x[actToggleAll-46] + _ = x[actToggleDown-47] + _ = x[actToggleUp-48] + _ = x[actToggleIn-49] + _ = x[actToggleOut-50] + _ = x[actToggleTrack-51] + _ = x[actToggleTrackCurrent-52] + _ = x[actToggleHeader-53] + _ = x[actToggleWrap-54] + _ = x[actToggleMultiLine-55] + _ = x[actToggleHscroll-56] + _ = x[actTrackCurrent-57] + _ = x[actUntrackCurrent-58] + _ = x[actDown-59] + _ = x[actUp-60] + _ = x[actPageUp-61] + _ = x[actPageDown-62] + _ = x[actPosition-63] + _ = x[actHalfPageUp-64] + _ = x[actHalfPageDown-65] + _ = x[actOffsetUp-66] + _ = x[actOffsetDown-67] + _ = x[actOffsetMiddle-68] + _ = x[actJump-69] + _ = x[actJumpAccept-70] + _ = x[actPrintQuery-71] + _ = x[actRefreshPreview-72] + _ = x[actReplaceQuery-73] + _ = x[actToggleSort-74] + _ = x[actShowPreview-75] + _ = x[actHidePreview-76] + _ = x[actTogglePreview-77] + _ = x[actTogglePreviewWrap-78] + _ = x[actTransform-79] + _ = x[actTransformBorderLabel-80] + _ = x[actTransformListLabel-81] + _ = x[actTransformInputLabel-82] + _ = x[actTransformHeader-83] + _ = x[actTransformHeaderLabel-84] + _ = x[actTransformPreviewLabel-85] + _ = x[actTransformPrompt-86] + _ = x[actTransformQuery-87] + _ = x[actPreview-88] + _ = x[actChangePreview-89] + _ = x[actChangePreviewWindow-90] + _ = x[actPreviewTop-91] + _ = x[actPreviewBottom-92] + _ = x[actPreviewUp-93] + _ = x[actPreviewDown-94] + _ = x[actPreviewPageUp-95] + _ = x[actPreviewPageDown-96] + _ = x[actPreviewHalfPageUp-97] + _ = x[actPreviewHalfPageDown-98] + _ = x[actPrevHistory-99] + _ = x[actPrevSelected-100] + _ = x[actPrint-101] + _ = x[actPut-102] + _ = x[actNextHistory-103] + _ = x[actNextSelected-104] + _ = x[actExecute-105] + _ = x[actExecuteSilent-106] + _ = x[actExecuteMulti-107] + _ = x[actSigStop-108] + _ = x[actFirst-109] + _ = x[actLast-110] + _ = x[actReload-111] + _ = x[actReloadSync-112] + _ = x[actDisableSearch-113] + _ = x[actEnableSearch-114] + _ = x[actSelect-115] + _ = x[actDeselect-116] + _ = x[actUnbind-117] + _ = x[actRebind-118] + _ = x[actBecome-119] + _ = x[actShowHeader-120] + _ = x[actHideHeader-121] } -const _actionType_name = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeListLabelactChangeInputLabelactChangeHeaderactChangeHeaderLabelactChangeMultiactChangePreviewLabelactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleMultiLineactToggleHscrollactTrackCurrentactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformListLabelactTransformInputLabelactTransformHeaderactTransformHeaderLabelactTransformPreviewLabelactTransformPromptactTransformQueryactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactBecomeactShowHeaderactHideHeader" +const _actionType_name = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeListLabelactChangeInputLabelactChangeHeaderactChangeHeaderLabelactChangeMultiactChangePreviewLabelactChangePromptactChangeQueryactChangeNthactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleMultiLineactToggleHscrollactTrackCurrentactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformListLabelactTransformInputLabelactTransformHeaderactTransformHeaderLabelactTransformPreviewLabelactTransformPromptactTransformQueryactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactBecomeactShowHeaderactHideHeader" -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, 377, 390, 407, 415, 428, 444, 456, 464, 478, 492, 503, 514, 532, 549, 556, 575, 587, 601, 610, 625, 637, 650, 661, 672, 684, 698, 719, 734, 747, 765, 781, 796, 813, 820, 825, 834, 845, 856, 869, 884, 895, 908, 923, 930, 943, 956, 973, 988, 1001, 1015, 1029, 1045, 1065, 1077, 1100, 1121, 1143, 1161, 1184, 1208, 1226, 1243, 1253, 1269, 1291, 1304, 1320, 1332, 1346, 1362, 1380, 1400, 1422, 1436, 1451, 1459, 1465, 1479, 1494, 1504, 1520, 1535, 1545, 1553, 1560, 1569, 1582, 1598, 1613, 1622, 1633, 1642, 1651, 1660, 1673, 1686} +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} func (i actionType) String() string { if i < 0 || i >= actionType(len(_actionType_index)-1) { diff --git a/src/core.go b/src/core.go index 89f40ceb..35a143fe 100644 --- a/src/core.go +++ b/src/core.go @@ -190,11 +190,13 @@ func Run(opts *Options) (int, error) { forward = true } } + + nth := opts.Nth patternCache := make(map[string]*Pattern) patternBuilder := func(runes []rune) *Pattern { return BuildPattern(cache, patternCache, opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, withPos, - opts.Filter == nil, opts.Nth, opts.Delimiter, runes) + opts.Filter == nil, nth, opts.Delimiter, runes) } inputRevision := revision{} snapshotRevision := revision{} @@ -373,6 +375,12 @@ func Run(opts *Options) (int, error) { command = val.command environ = val.environ changed = val.changed + if val.nth != nil { + // Change nth and clear caches + nth = *val.nth + patternCache = make(map[string]*Pattern) + inputRevision.bumpMajor() + } if command != nil { useSnapshot = val.sync } diff --git a/src/options.go b/src/options.go index ac4a1aca..3932cb56 100644 --- a/src/options.go +++ b/src/options.go @@ -1306,7 +1306,7 @@ const ( func init() { 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)|(?: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)|transform|change-(?:preview-window|preview|multi|nth)|(?:re|un)bind|pos|put|print)`) splitRegexp = regexp.MustCompile("[,:]+") actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+") } @@ -1684,6 +1684,8 @@ func isExecuteAction(str string) actionType { return actChangeQuery case "change-multi": return actChangeMulti + case "change-nth": + return actChangeNth case "pos": return actPosition case "execute": diff --git a/src/terminal.go b/src/terminal.go index 1ca1c804..88c2b4a0 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -299,6 +299,7 @@ type Terminal struct { scrollbar string previewScrollbar string ansi bool + nth []Range tabstop int margin [4]sizeSpec padding [4]sizeSpec @@ -462,6 +463,7 @@ const ( actChangePreviewLabel actChangePrompt actChangeQuery + actChangeNth actClearScreen actClearQuery actClearSelection @@ -597,6 +599,7 @@ type placeholderFlags struct { type searchRequest struct { sort bool sync bool + nth *[]Range command *commandSpec environ []string changed bool @@ -880,6 +883,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor header: []string{}, header0: opts.Header, ansi: opts.Ansi, + nth: opts.Nth, tabstop: opts.Tabstop, hasStartActions: false, hasResultActions: false, @@ -4359,6 +4363,7 @@ func (t *Terminal) Loop() error { } for loopIndex := int64(0); looping; loopIndex++ { var newCommand *commandSpec + var newNth *[]Range var reloadSync bool changed := false beof := false @@ -4618,6 +4623,22 @@ func (t *Terminal) Loop() error { } t.multi = multi req(reqList, reqInfo) + case actChangeNth: + changed = true + + // Split nth expression + tokens := strings.Split(a.a, "|") + if nth, err := splitNth(tokens[0]); err == nil { + // Changed + newNth = &nth + } else { + // The default + newNth = &t.nth + } + // Cycle + if len(tokens) > 1 { + a.a = strings.Join(append(tokens[1:], tokens[0]), "|") + } case actChangeQuery: t.input = []rune(a.a) t.cx = len(t.input) @@ -5537,7 +5558,7 @@ func (t *Terminal) Loop() error { reload := changed || newCommand != nil var reloadRequest *searchRequest if reload { - reloadRequest = &searchRequest{sort: t.sort, sync: reloadSync, command: newCommand, environ: t.environ(), changed: changed} + reloadRequest = &searchRequest{sort: t.sort, sync: reloadSync, nth: newNth, command: newCommand, environ: t.environ(), changed: changed} } t.mutex.Unlock() // Must be unlocked before touching reqBox diff --git a/test/test_go.rb b/test/test_go.rb index db2d8f1d..b40bfb42 100755 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -3718,6 +3718,29 @@ class TestGoFZF < TestBase BLOCK tmux.until { assert_block(block, _1) } end + + def test_change_nth + input = [ + 'foo bar bar bar bar', + 'foo foo bar bar bar', + 'foo foo foo bar bar', + 'foo foo foo foo bar' + ] + writelines(input) + tmux.send_keys %(#{FZF} -qfoo -n1 --bind 'space:change-nth:2|3|4|5|' < #{tempname}), :Enter + + tmux.until { |lines| assert_equal 4, lines.match_count } + tmux.send_keys :Space + tmux.until { |lines| assert_equal 3, lines.match_count } + tmux.send_keys :Space + tmux.until { |lines| assert_equal 2, lines.match_count } + tmux.send_keys :Space + tmux.until { |lines| assert_equal 1, lines.match_count } + tmux.send_keys :Space + tmux.until { |lines| assert_equal 0, lines.match_count } + tmux.send_keys :Space + tmux.until { |lines| assert_equal 4, lines.match_count } + end end module TestShell