From b9804f58730df540e76bb77027b4760576916fb2 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 23 Feb 2026 01:45:47 +0900 Subject: [PATCH] Add change-header-lines action to dynamically change --header-lines All input lines now enter the chunklist with sequential indices, and header lines are excluded from matching via Pattern.startIndex and PassMerger offset. This allows the number of header lines to be changed at runtime with change-header-lines(N), transform-header-lines, and bg-transform-header-lines actions. - Remove EvtHeader event; header items are read directly from chunks - Add startIndex to Pattern and PassMerger for skipping header items - Add targetIndex field to Terminal for cursor repositioning across header-lines changes Close #4659 --- man/man1/fzf.1 | 2 + src/actiontype_string.go | 307 ++++++++++++++++++++------------------- src/chunklist.go | 14 ++ src/constants.go | 1 - src/core.go | 47 +++--- src/matcher.go | 2 +- src/merger.go | 51 ++++--- src/options.go | 8 +- src/pattern.go | 17 ++- src/pattern_test.go | 2 +- src/terminal.go | 99 +++++++++---- test/test_core.rb | 74 ++++++++++ test/test_filter.rb | 23 +++ 13 files changed, 420 insertions(+), 227 deletions(-) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index a008c60a..056df678 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -1881,6 +1881,7 @@ A key or an event can be bound to one or more of the following actions. \fBchange\-border\-label(...)\fR (change \fB\-\-border\-label\fR to the given string) \fBchange\-ghost(...)\fR (change ghost text to the given string) \fBchange\-header(...)\fR (change header to the given string; doesn't affect \fB\-\-header\-lines\fR) + \fBchange\-header\-lines(N)\fR (change the number of \fB\-\-header\-lines\fR) \fBchange\-header\-label(...)\fR (change \fB\-\-header\-label\fR to the given string) \fBchange\-input\-label(...)\fR (change \fB\-\-input\-label\fR to the given string) \fBchange\-list\-label(...)\fR (change \fB\-\-list\-label\fR to the given string) @@ -1987,6 +1988,7 @@ A key or an event can be bound to one or more of the following actions. \fBtransform\-border\-label(...)\fR (transform border label using an external command) \fBtransform\-ghost(...)\fR (transform ghost text using an external command) \fBtransform\-header(...)\fR (transform header using an external command) + \fBtransform\-header\-lines(...)\fR (transform the number of \fB\-\-header\-lines\fR using an external command) \fBtransform\-header\-label(...)\fR (transform header label using an external command) \fBtransform\-input\-label(...)\fR (transform input label using an external command) \fBtransform\-list\-label(...)\fR (transform list label using an external command) diff --git a/src/actiontype_string.go b/src/actiontype_string.go index f85f73bc..ec7fd954 100644 --- a/src/actiontype_string.go +++ b/src/actiontype_string.go @@ -30,161 +30,164 @@ func _() { _ = x[actChangeBorderLabel-19] _ = x[actChangeGhost-20] _ = x[actChangeHeader-21] - _ = x[actChangeFooter-22] - _ = x[actChangeHeaderLabel-23] - _ = x[actChangeFooterLabel-24] - _ = x[actChangeInputLabel-25] - _ = x[actChangeListLabel-26] - _ = x[actChangeMulti-27] - _ = x[actChangeNth-28] - _ = x[actChangePointer-29] - _ = x[actChangePreview-30] - _ = x[actChangePreviewLabel-31] - _ = x[actChangePreviewWindow-32] - _ = x[actChangePrompt-33] - _ = x[actChangeQuery-34] - _ = x[actClearScreen-35] - _ = x[actClearQuery-36] - _ = x[actClearSelection-37] - _ = x[actClose-38] - _ = x[actDeleteChar-39] - _ = x[actDeleteCharEof-40] - _ = x[actEndOfLine-41] - _ = x[actFatal-42] - _ = x[actForwardChar-43] - _ = x[actForwardWord-44] - _ = x[actForwardSubWord-45] - _ = x[actKillLine-46] - _ = x[actKillWord-47] - _ = x[actKillSubWord-48] - _ = x[actUnixLineDiscard-49] - _ = x[actUnixWordRubout-50] - _ = x[actYank-51] - _ = x[actBackwardKillWord-52] - _ = x[actBackwardKillSubWord-53] - _ = x[actSelectAll-54] - _ = x[actDeselectAll-55] - _ = x[actToggle-56] - _ = x[actToggleSearch-57] - _ = x[actToggleAll-58] - _ = x[actToggleDown-59] - _ = x[actToggleUp-60] - _ = x[actToggleIn-61] - _ = x[actToggleOut-62] - _ = x[actToggleTrack-63] - _ = x[actToggleTrackCurrent-64] - _ = x[actToggleHeader-65] - _ = x[actToggleWrap-66] - _ = x[actToggleWrapWord-67] - _ = x[actToggleMultiLine-68] - _ = x[actToggleHscroll-69] - _ = x[actToggleRaw-70] - _ = x[actEnableRaw-71] - _ = x[actDisableRaw-72] - _ = x[actTrackCurrent-73] - _ = x[actToggleInput-74] - _ = x[actHideInput-75] - _ = x[actShowInput-76] - _ = x[actUntrackCurrent-77] - _ = x[actDown-78] - _ = x[actDownMatch-79] - _ = x[actUp-80] - _ = x[actUpMatch-81] - _ = x[actPageUp-82] - _ = x[actPageDown-83] - _ = x[actPosition-84] - _ = x[actHalfPageUp-85] - _ = x[actHalfPageDown-86] - _ = x[actOffsetUp-87] - _ = x[actOffsetDown-88] - _ = x[actOffsetMiddle-89] - _ = x[actJump-90] - _ = x[actJumpAccept-91] - _ = x[actPrintQuery-92] - _ = x[actRefreshPreview-93] - _ = x[actReplaceQuery-94] - _ = x[actToggleSort-95] - _ = x[actShowPreview-96] - _ = x[actHidePreview-97] - _ = x[actTogglePreview-98] - _ = x[actTogglePreviewWrap-99] - _ = x[actTogglePreviewWrapWord-100] - _ = x[actTransform-101] - _ = x[actTransformBorderLabel-102] - _ = x[actTransformGhost-103] - _ = x[actTransformHeader-104] - _ = x[actTransformFooter-105] - _ = x[actTransformHeaderLabel-106] - _ = x[actTransformFooterLabel-107] - _ = x[actTransformInputLabel-108] - _ = x[actTransformListLabel-109] - _ = x[actTransformNth-110] - _ = x[actTransformPointer-111] - _ = x[actTransformPreviewLabel-112] - _ = x[actTransformPrompt-113] - _ = x[actTransformQuery-114] - _ = x[actTransformSearch-115] - _ = x[actTrigger-116] - _ = x[actBgTransform-117] - _ = x[actBgTransformBorderLabel-118] - _ = x[actBgTransformGhost-119] - _ = x[actBgTransformHeader-120] - _ = x[actBgTransformFooter-121] - _ = x[actBgTransformHeaderLabel-122] - _ = x[actBgTransformFooterLabel-123] - _ = x[actBgTransformInputLabel-124] - _ = x[actBgTransformListLabel-125] - _ = x[actBgTransformNth-126] - _ = x[actBgTransformPointer-127] - _ = x[actBgTransformPreviewLabel-128] - _ = x[actBgTransformPrompt-129] - _ = x[actBgTransformQuery-130] - _ = x[actBgTransformSearch-131] - _ = x[actBgCancel-132] - _ = x[actSearch-133] - _ = x[actPreview-134] - _ = x[actPreviewTop-135] - _ = x[actPreviewBottom-136] - _ = x[actPreviewUp-137] - _ = x[actPreviewDown-138] - _ = x[actPreviewPageUp-139] - _ = x[actPreviewPageDown-140] - _ = x[actPreviewHalfPageUp-141] - _ = x[actPreviewHalfPageDown-142] - _ = x[actPrevHistory-143] - _ = x[actPrevSelected-144] - _ = x[actPrint-145] - _ = x[actPut-146] - _ = x[actNextHistory-147] - _ = x[actNextSelected-148] - _ = x[actExecute-149] - _ = x[actExecuteSilent-150] - _ = x[actExecuteMulti-151] - _ = x[actSigStop-152] - _ = x[actBest-153] - _ = x[actFirst-154] - _ = x[actLast-155] - _ = x[actReload-156] - _ = x[actReloadSync-157] - _ = x[actDisableSearch-158] - _ = x[actEnableSearch-159] - _ = x[actSelect-160] - _ = x[actDeselect-161] - _ = x[actUnbind-162] - _ = x[actRebind-163] - _ = x[actToggleBind-164] - _ = x[actBecome-165] - _ = x[actShowHeader-166] - _ = x[actHideHeader-167] - _ = x[actBell-168] - _ = x[actExclude-169] - _ = x[actExcludeMulti-170] - _ = x[actAsync-171] + _ = x[actChangeHeaderLines-22] + _ = x[actChangeFooter-23] + _ = x[actChangeHeaderLabel-24] + _ = x[actChangeFooterLabel-25] + _ = x[actChangeInputLabel-26] + _ = x[actChangeListLabel-27] + _ = x[actChangeMulti-28] + _ = x[actChangeNth-29] + _ = x[actChangePointer-30] + _ = x[actChangePreview-31] + _ = x[actChangePreviewLabel-32] + _ = x[actChangePreviewWindow-33] + _ = x[actChangePrompt-34] + _ = x[actChangeQuery-35] + _ = x[actClearScreen-36] + _ = x[actClearQuery-37] + _ = x[actClearSelection-38] + _ = x[actClose-39] + _ = x[actDeleteChar-40] + _ = x[actDeleteCharEof-41] + _ = x[actEndOfLine-42] + _ = x[actFatal-43] + _ = x[actForwardChar-44] + _ = x[actForwardWord-45] + _ = x[actForwardSubWord-46] + _ = x[actKillLine-47] + _ = x[actKillWord-48] + _ = x[actKillSubWord-49] + _ = x[actUnixLineDiscard-50] + _ = x[actUnixWordRubout-51] + _ = x[actYank-52] + _ = x[actBackwardKillWord-53] + _ = x[actBackwardKillSubWord-54] + _ = x[actSelectAll-55] + _ = x[actDeselectAll-56] + _ = x[actToggle-57] + _ = x[actToggleSearch-58] + _ = x[actToggleAll-59] + _ = x[actToggleDown-60] + _ = x[actToggleUp-61] + _ = x[actToggleIn-62] + _ = x[actToggleOut-63] + _ = x[actToggleTrack-64] + _ = x[actToggleTrackCurrent-65] + _ = x[actToggleHeader-66] + _ = x[actToggleWrap-67] + _ = x[actToggleWrapWord-68] + _ = x[actToggleMultiLine-69] + _ = x[actToggleHscroll-70] + _ = x[actToggleRaw-71] + _ = x[actEnableRaw-72] + _ = x[actDisableRaw-73] + _ = x[actTrackCurrent-74] + _ = x[actToggleInput-75] + _ = x[actHideInput-76] + _ = x[actShowInput-77] + _ = x[actUntrackCurrent-78] + _ = x[actDown-79] + _ = x[actDownMatch-80] + _ = x[actUp-81] + _ = x[actUpMatch-82] + _ = x[actPageUp-83] + _ = x[actPageDown-84] + _ = x[actPosition-85] + _ = x[actHalfPageUp-86] + _ = x[actHalfPageDown-87] + _ = x[actOffsetUp-88] + _ = x[actOffsetDown-89] + _ = x[actOffsetMiddle-90] + _ = x[actJump-91] + _ = x[actJumpAccept-92] + _ = x[actPrintQuery-93] + _ = x[actRefreshPreview-94] + _ = x[actReplaceQuery-95] + _ = x[actToggleSort-96] + _ = x[actShowPreview-97] + _ = x[actHidePreview-98] + _ = x[actTogglePreview-99] + _ = x[actTogglePreviewWrap-100] + _ = x[actTogglePreviewWrapWord-101] + _ = x[actTransform-102] + _ = x[actTransformBorderLabel-103] + _ = x[actTransformGhost-104] + _ = x[actTransformHeader-105] + _ = x[actTransformHeaderLines-106] + _ = x[actTransformFooter-107] + _ = x[actTransformHeaderLabel-108] + _ = x[actTransformFooterLabel-109] + _ = x[actTransformInputLabel-110] + _ = x[actTransformListLabel-111] + _ = x[actTransformNth-112] + _ = x[actTransformPointer-113] + _ = x[actTransformPreviewLabel-114] + _ = x[actTransformPrompt-115] + _ = x[actTransformQuery-116] + _ = x[actTransformSearch-117] + _ = x[actTrigger-118] + _ = x[actBgTransform-119] + _ = x[actBgTransformBorderLabel-120] + _ = x[actBgTransformGhost-121] + _ = x[actBgTransformHeader-122] + _ = x[actBgTransformHeaderLines-123] + _ = x[actBgTransformFooter-124] + _ = x[actBgTransformHeaderLabel-125] + _ = x[actBgTransformFooterLabel-126] + _ = x[actBgTransformInputLabel-127] + _ = x[actBgTransformListLabel-128] + _ = x[actBgTransformNth-129] + _ = x[actBgTransformPointer-130] + _ = x[actBgTransformPreviewLabel-131] + _ = x[actBgTransformPrompt-132] + _ = x[actBgTransformQuery-133] + _ = x[actBgTransformSearch-134] + _ = x[actBgCancel-135] + _ = x[actSearch-136] + _ = x[actPreview-137] + _ = x[actPreviewTop-138] + _ = x[actPreviewBottom-139] + _ = x[actPreviewUp-140] + _ = x[actPreviewDown-141] + _ = x[actPreviewPageUp-142] + _ = x[actPreviewPageDown-143] + _ = x[actPreviewHalfPageUp-144] + _ = x[actPreviewHalfPageDown-145] + _ = x[actPrevHistory-146] + _ = x[actPrevSelected-147] + _ = x[actPrint-148] + _ = x[actPut-149] + _ = x[actNextHistory-150] + _ = x[actNextSelected-151] + _ = x[actExecute-152] + _ = x[actExecuteSilent-153] + _ = x[actExecuteMulti-154] + _ = x[actSigStop-155] + _ = x[actBest-156] + _ = x[actFirst-157] + _ = x[actLast-158] + _ = x[actReload-159] + _ = x[actReloadSync-160] + _ = x[actDisableSearch-161] + _ = x[actEnableSearch-162] + _ = x[actSelect-163] + _ = x[actDeselect-164] + _ = x[actUnbind-165] + _ = x[actRebind-166] + _ = x[actToggleBind-167] + _ = x[actBecome-168] + _ = x[actShowHeader-169] + _ = x[actHideHeader-170] + _ = x[actBell-171] + _ = x[actExclude-172] + _ = x[actExcludeMulti-173] + _ = x[actAsync-174] } -const _actionType_name = "actIgnoreactStartactClickactInvalidactBracketedPasteBeginactBracketedPasteEndactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactBackwardSubWordactCancelactChangeBorderLabelactChangeGhostactChangeHeaderactChangeFooteractChangeHeaderLabelactChangeFooterLabelactChangeInputLabelactChangeListLabelactChangeMultiactChangeNthactChangePointeractChangePreviewactChangePreviewLabelactChangePreviewWindowactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactForwardSubWordactKillLineactKillWordactKillSubWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactBackwardKillSubWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleWrapWordactToggleMultiLineactToggleHscrollactToggleRawactEnableRawactDisableRawactTrackCurrentactToggleInputactHideInputactShowInputactUntrackCurrentactDownactDownMatchactUpactUpMatchactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTogglePreviewWrapWordactTransformactTransformBorderLabelactTransformGhostactTransformHeaderactTransformFooteractTransformHeaderLabelactTransformFooterLabelactTransformInputLabelactTransformListLabelactTransformNthactTransformPointeractTransformPreviewLabelactTransformPromptactTransformQueryactTransformSearchactTriggeractBgTransformactBgTransformBorderLabelactBgTransformGhostactBgTransformHeaderactBgTransformFooteractBgTransformHeaderLabelactBgTransformFooterLabelactBgTransformInputLabelactBgTransformListLabelactBgTransformNthactBgTransformPointeractBgTransformPreviewLabelactBgTransformPromptactBgTransformQueryactBgTransformSearchactBgCancelactSearchactPreviewactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactBestactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactToggleBindactBecomeactShowHeaderactHideHeaderactBellactExcludeactExcludeMultiactAsync" +const _actionType_name = "actIgnoreactStartactClickactInvalidactBracketedPasteBeginactBracketedPasteEndactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactBackwardSubWordactCancelactChangeBorderLabelactChangeGhostactChangeHeaderactChangeHeaderLinesactChangeFooteractChangeHeaderLabelactChangeFooterLabelactChangeInputLabelactChangeListLabelactChangeMultiactChangeNthactChangePointeractChangePreviewactChangePreviewLabelactChangePreviewWindowactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactForwardSubWordactKillLineactKillWordactKillSubWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactBackwardKillSubWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleWrapWordactToggleMultiLineactToggleHscrollactToggleRawactEnableRawactDisableRawactTrackCurrentactToggleInputactHideInputactShowInputactUntrackCurrentactDownactDownMatchactUpactUpMatchactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTogglePreviewWrapWordactTransformactTransformBorderLabelactTransformGhostactTransformHeaderactTransformHeaderLinesactTransformFooteractTransformHeaderLabelactTransformFooterLabelactTransformInputLabelactTransformListLabelactTransformNthactTransformPointeractTransformPreviewLabelactTransformPromptactTransformQueryactTransformSearchactTriggeractBgTransformactBgTransformBorderLabelactBgTransformGhostactBgTransformHeaderactBgTransformHeaderLinesactBgTransformFooteractBgTransformHeaderLabelactBgTransformFooterLabelactBgTransformInputLabelactBgTransformListLabelactBgTransformNthactBgTransformPointeractBgTransformPreviewLabelactBgTransformPromptactBgTransformQueryactBgTransformSearchactBgCancelactSearchactPreviewactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactBestactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactToggleBindactBecomeactShowHeaderactHideHeaderactBellactExcludeactExcludeMultiactAsync" -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, 331, 351, 371, 390, 408, 422, 434, 450, 466, 487, 509, 524, 538, 552, 565, 582, 590, 603, 619, 631, 639, 653, 667, 684, 695, 706, 720, 738, 755, 762, 781, 803, 815, 829, 838, 853, 865, 878, 889, 900, 912, 926, 947, 962, 975, 992, 1010, 1026, 1038, 1050, 1063, 1078, 1092, 1104, 1116, 1133, 1140, 1152, 1157, 1167, 1176, 1187, 1198, 1211, 1226, 1237, 1250, 1265, 1272, 1285, 1298, 1315, 1330, 1343, 1357, 1371, 1387, 1407, 1431, 1443, 1466, 1483, 1501, 1519, 1542, 1565, 1587, 1608, 1623, 1642, 1666, 1684, 1701, 1719, 1729, 1743, 1768, 1787, 1807, 1827, 1852, 1877, 1901, 1924, 1941, 1962, 1988, 2008, 2027, 2047, 2058, 2067, 2077, 2090, 2106, 2118, 2132, 2148, 2166, 2186, 2208, 2222, 2237, 2245, 2251, 2265, 2280, 2290, 2306, 2321, 2331, 2338, 2346, 2353, 2362, 2375, 2391, 2406, 2415, 2426, 2435, 2444, 2457, 2466, 2479, 2492, 2499, 2509, 2524, 2532} +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, 507, 529, 544, 558, 572, 585, 602, 610, 623, 639, 651, 659, 673, 687, 704, 715, 726, 740, 758, 775, 782, 801, 823, 835, 849, 858, 873, 885, 898, 909, 920, 932, 946, 967, 982, 995, 1012, 1030, 1046, 1058, 1070, 1083, 1098, 1112, 1124, 1136, 1153, 1160, 1172, 1177, 1187, 1196, 1207, 1218, 1231, 1246, 1257, 1270, 1285, 1292, 1305, 1318, 1335, 1350, 1363, 1377, 1391, 1407, 1427, 1451, 1463, 1486, 1503, 1521, 1544, 1562, 1585, 1608, 1630, 1651, 1666, 1685, 1709, 1727, 1744, 1762, 1772, 1786, 1811, 1830, 1850, 1875, 1895, 1920, 1945, 1969, 1992, 2009, 2030, 2056, 2076, 2095, 2115, 2126, 2135, 2145, 2158, 2174, 2186, 2200, 2216, 2234, 2254, 2276, 2290, 2305, 2313, 2319, 2333, 2348, 2358, 2374, 2389, 2399, 2406, 2414, 2421, 2430, 2443, 2459, 2474, 2483, 2494, 2503, 2512, 2525, 2534, 2547, 2560, 2567, 2577, 2592, 2600} func (i actionType) String() string { if i < 0 || i >= actionType(len(_actionType_index)-1) { diff --git a/src/chunklist.go b/src/chunklist.go index ce4a56a0..63b6188c 100644 --- a/src/chunklist.go +++ b/src/chunklist.go @@ -52,6 +52,20 @@ func (cl *ChunkList) lastChunk() *Chunk { return cl.chunks[len(cl.chunks)-1] } +// GetItems returns the first n items from the given chunks +func GetItems(chunks []*Chunk, n int) []Item { + items := make([]Item, 0, n) + for _, chunk := range chunks { + for i := 0; i < chunk.count && len(items) < n; i++ { + items = append(items, chunk.items[i]) + } + if len(items) >= n { + break + } + } + return items +} + // CountItems returns the total number of Items func CountItems(cs []*Chunk) int { if len(cs) == 0 { diff --git a/src/constants.go b/src/constants.go index 5f5c8ca1..9f964c48 100644 --- a/src/constants.go +++ b/src/constants.go @@ -65,7 +65,6 @@ const ( EvtSearchNew EvtSearchProgress EvtSearchFin - EvtHeader EvtReady EvtQuit ) diff --git a/src/core.go b/src/core.go index debcc3f1..414d7e8d 100644 --- a/src/core.go +++ b/src/core.go @@ -17,7 +17,6 @@ Reader -> EvtReadNew -> Matcher (restart) Terminal -> EvtSearchNew:bool -> Matcher (restart) Matcher -> EvtSearchProgress -> Terminal (update info) Matcher -> EvtSearchFin -> Terminal (update list) -Matcher -> EvtHeader -> Terminal (update header) */ type revision struct { @@ -113,14 +112,8 @@ func Run(opts *Options) (int, error) { cache := NewChunkCache() var chunkList *ChunkList var itemIndex int32 - header := make([]string, 0, opts.HeaderLines) if opts.WithNth == nil { chunkList = NewChunkList(cache, func(item *Item, data []byte) bool { - if len(header) < opts.HeaderLines { - header = append(header, byteString(data)) - eventBox.Set(EvtHeader, header) - return false - } item.text, item.colors = ansiProcessor(data) item.text.Index = itemIndex itemIndex++ @@ -147,11 +140,6 @@ func Run(opts *Options) (int, error) { } } transformed := nthTransformer(tokens, itemIndex) - if len(header) < opts.HeaderLines { - header = append(header, transformed) - eventBox.Set(EvtHeader, header) - return false - } item.text, item.colors = ansiProcessor(stringBytes(transformed)) // We should not trim trailing whitespaces with background colors @@ -236,13 +224,15 @@ func Run(opts *Options) (int, error) { denylist = make(map[int32]struct{}) denyMutex.Unlock() } + headerLines := int32(opts.HeaderLines) + headerUpdated := false patternBuilder := func(runes []rune) *Pattern { denyMutex.Lock() denylistCopy := maps.Clone(denylist) denyMutex.Unlock() return BuildPattern(cache, patternCache, opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, withPos, - opts.Filter == nil, nth, opts.Delimiter, inputRevision, runes, denylistCopy) + opts.Filter == nil, nth, opts.Delimiter, inputRevision, runes, denylistCopy, headerLines) } matcher := NewMatcher(cache, patternBuilder, sort, opts.Tac, eventBox, inputRevision) @@ -265,6 +255,9 @@ func Run(opts *Options) (int, error) { func(runes []byte) bool { item := Item{} if chunkList.trans(&item, runes) { + if item.Index() < headerLines { + return false + } mutex.Lock() if result, _, _ := pattern.MatchItem(&item, false, slab); result != nil { opts.Printer(transformer(&item)) @@ -349,11 +342,11 @@ func Run(opts *Options) (int, error) { clearDenylist() } reading = true + headerUpdated = false startTick = ticks chunkList.Clear() itemIndex = 0 inputRevision.bumpMajor() - header = make([]string, 0, opts.HeaderLines) readyChan := make(chan bool) go reader.restart(command, environ, readyChan) <-readyChan @@ -411,7 +404,11 @@ func Run(opts *Options) (int, error) { snapshotRevision = inputRevision } total = count - terminal.UpdateCount(total, !reading, value.(*string)) + terminal.UpdateCount(max(0, total-int(headerLines)), !reading, value.(*string)) + if headerLines > 0 && !headerUpdated { + terminal.UpdateHeader(GetItems(snapshot, int(headerLines))) + headerUpdated = int32(total) >= headerLines + } if heightUnknown && !deferred { determine(!reading) } @@ -421,6 +418,7 @@ func Run(opts *Options) (int, error) { var command *commandSpec var environ []string var changed bool + headerLinesChanged := false switch val := value.(type) { case searchRequest: sort = val.sort @@ -441,6 +439,12 @@ func Run(opts *Options) (int, error) { nth = *val.nth bump = true } + if val.headerLines != nil { + headerLines = int32(*val.headerLines) + headerUpdated = false + headerLinesChanged = true + bump = true + } if bump { patternCache = make(map[string]*Pattern) cache.Clear() @@ -477,6 +481,14 @@ func Run(opts *Options) (int, error) { snapshotRevision = inputRevision } } + if headerLinesChanged { + terminal.UpdateCount(max(0, total-int(headerLines)), !reading, nil) + if headerLines > 0 { + terminal.UpdateHeader(GetItems(snapshot, int(headerLines))) + } else { + terminal.UpdateHeader(nil) + } + } matcher.Reset(snapshot, input(), true, !reading, sort, snapshotRevision) delay = false @@ -486,11 +498,6 @@ func Run(opts *Options) (int, error) { terminal.UpdateProgress(val) } - case EvtHeader: - headerPadded := make([]string, opts.HeaderLines) - copy(headerPadded, value.([]string)) - terminal.UpdateHeader(headerPadded) - case EvtSearchFin: switch val := value.(type) { case MatchResult: diff --git a/src/matcher.go b/src/matcher.go index bc02c77b..eb22abd8 100644 --- a/src/matcher.go +++ b/src/matcher.go @@ -174,7 +174,7 @@ func (m *Matcher) scan(request MatchRequest) MatchResult { return MatchResult{m, m, false} } pattern := request.pattern - passMerger := PassMerger(&request.chunks, m.tac, request.revision) + passMerger := PassMerger(&request.chunks, m.tac, request.revision, pattern.startIndex) if pattern.IsEmpty() { return MatchResult{passMerger, passMerger, false} } diff --git a/src/merger.go b/src/merger.go index b9bdabb8..d4deaf4f 100644 --- a/src/merger.go +++ b/src/merger.go @@ -10,42 +10,46 @@ func EmptyMerger(revision revision) *Merger { // Merger holds a set of locally sorted lists of items and provides the view of // a single, globally-sorted list type Merger struct { - pattern *Pattern - lists [][]Result - merged []Result - chunks *[]*Chunk - cursors []int - sorted bool - tac bool - final bool - count int - pass bool - revision revision - minIndex int32 - maxIndex int32 + pattern *Pattern + lists [][]Result + merged []Result + chunks *[]*Chunk + cursors []int + sorted bool + tac bool + final bool + count int + pass bool + startIndex int + revision revision + minIndex int32 + maxIndex int32 } // PassMerger returns a new Merger that simply returns the items in the -// original order -func PassMerger(chunks *[]*Chunk, tac bool, revision revision) *Merger { +// original order. startIndex items are skipped from the beginning. +func PassMerger(chunks *[]*Chunk, tac bool, revision revision, startIndex int32) *Merger { var minIndex, maxIndex int32 if len(*chunks) > 0 { minIndex = (*chunks)[0].items[0].Index() maxIndex = (*chunks)[len(*chunks)-1].lastIndex(minIndex) } + si := int(startIndex) mg := Merger{ - pattern: nil, - chunks: chunks, - tac: tac, - count: 0, - pass: true, - revision: revision, - minIndex: minIndex, - maxIndex: maxIndex} + pattern: nil, + chunks: chunks, + tac: tac, + count: 0, + pass: true, + startIndex: si, + revision: revision, + minIndex: minIndex + startIndex, + maxIndex: maxIndex} for _, chunk := range *mg.chunks { mg.count += chunk.count } + mg.count = max(0, mg.count-si) return &mg } @@ -113,6 +117,7 @@ func (mg *Merger) Get(idx int) Result { if mg.tac { idx = mg.count - idx - 1 } + idx += mg.startIndex firstChunk := (*mg.chunks)[0] if firstChunk.count < chunkSize && idx >= firstChunk.count { idx -= firstChunk.count diff --git a/src/options.go b/src/options.go index 84c9a525..47b0095a 100644 --- a/src/options.go +++ b/src/options.go @@ -1626,7 +1626,7 @@ const ( func init() { executeRegexp = regexp.MustCompile( - `(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|bg-transform|transform)-(?:query|prompt|(?:border|list|preview|input|header|footer)-label|header|footer|search|nth|pointer|ghost)|bg-transform|transform|change-(?:preview-window|preview|multi)|(?:re|un|toggle-)bind|pos|put|print|search|trigger)`) + `(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|bg-transform|transform)-(?:query|prompt|(?:border|list|preview|input|header|footer)-label|header-lines|header|footer|search|nth|pointer|ghost)|bg-transform|transform|change-(?:preview-window|preview|multi)|(?:re|un|toggle-)bind|pos|put|print|search|trigger)`) splitRegexp = regexp.MustCompile("[,:]+") actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+") } @@ -2037,6 +2037,8 @@ func isExecuteAction(str string) actionType { return actPreview case "change-header": return actChangeHeader + case "change-header-lines": + return actChangeHeaderLines case "change-footer": return actChangeFooter case "change-list-label": @@ -2097,6 +2099,8 @@ func isExecuteAction(str string) actionType { return actTransformFooter case "transform-header": return actTransformHeader + case "transform-header-lines": + return actTransformHeaderLines case "transform-ghost": return actTransformGhost case "transform-nth": @@ -2127,6 +2131,8 @@ func isExecuteAction(str string) actionType { return actBgTransformFooter case "bg-transform-header": return actBgTransformHeader + case "bg-transform-header-lines": + return actBgTransformHeaderLines case "bg-transform-ghost": return actBgTransformGhost case "bg-transform-nth": diff --git a/src/pattern.go b/src/pattern.go index 8e6966c3..4e34c62f 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -64,6 +64,7 @@ type Pattern struct { procFun map[termType]algo.Algo cache *ChunkCache denylist map[int32]struct{} + startIndex int32 } var _splitRegex *regexp.Regexp @@ -74,7 +75,7 @@ func init() { // BuildPattern builds Pattern object from the given arguments func BuildPattern(cache *ChunkCache, patternCache map[string]*Pattern, fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, normalize bool, forward bool, - withPos bool, cacheable bool, nth []Range, delimiter Delimiter, revision revision, runes []rune, denylist map[int32]struct{}) *Pattern { + withPos bool, cacheable bool, nth []Range, delimiter Delimiter, revision revision, runes []rune, denylist map[int32]struct{}, startIndex int32) *Pattern { var asString string if extended { @@ -146,6 +147,7 @@ func BuildPattern(cache *ChunkCache, patternCache map[string]*Pattern, fuzzy boo delimiter: delimiter, cache: cache, denylist: denylist, + startIndex: startIndex, procFun: make(map[termType]algo.Algo)} ptr.cacheKey = ptr.buildCacheKey() @@ -301,10 +303,19 @@ func (p *Pattern) Match(chunk *Chunk, slab *util.Slab) []Result { func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Result { matches := []Result{} + // Skip header items in chunks that contain them + startIdx := 0 + if p.startIndex > 0 && chunk.count > 0 && chunk.items[0].Index() < p.startIndex { + startIdx = int(p.startIndex - chunk.items[0].Index()) + if startIdx >= chunk.count { + return matches + } + } + if len(p.denylist) == 0 { // Huge code duplication for minimizing unnecessary map lookups if space == nil { - for idx := 0; idx < chunk.count; idx++ { + for idx := startIdx; idx < chunk.count; idx++ { if match, _, _ := p.MatchItem(&chunk.items[idx], p.withPos, slab); match != nil { matches = append(matches, *match) } @@ -320,7 +331,7 @@ func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Re } if space == nil { - for idx := 0; idx < chunk.count; idx++ { + for idx := startIdx; idx < chunk.count; idx++ { if _, prs := p.denylist[chunk.items[idx].Index()]; prs { continue } diff --git a/src/pattern_test.go b/src/pattern_test.go index 8e566263..c0c6e962 100644 --- a/src/pattern_test.go +++ b/src/pattern_test.go @@ -68,7 +68,7 @@ func buildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, withPos bool, cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern { return BuildPattern(NewChunkCache(), make(map[string]*Pattern), fuzzy, fuzzyAlgo, extended, caseMode, normalize, forward, - withPos, cacheable, nth, delimiter, revision{}, runes, nil) + withPos, cacheable, nth, delimiter, revision{}, runes, nil, 0) } func TestExact(t *testing.T) { diff --git a/src/terminal.go b/src/terminal.go index d761dd77..5187fa04 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -314,6 +314,7 @@ type Terminal struct { sort bool toggleSort bool track trackOption + targetIndex int32 delimiter Delimiter expect map[tui.Event]string keymap map[tui.Event][]*action @@ -327,7 +328,7 @@ type Terminal struct { headerVisible bool headerFirst bool headerLines int - header []string + header []Item header0 []string footer []string ellipsis string @@ -542,6 +543,7 @@ const ( actChangeBorderLabel actChangeGhost actChangeHeader + actChangeHeaderLines actChangeFooter actChangeHeaderLabel actChangeFooterLabel @@ -627,6 +629,7 @@ const ( actTransformBorderLabel actTransformGhost actTransformHeader + actTransformHeaderLines actTransformFooter actTransformHeaderLabel actTransformFooterLabel @@ -645,6 +648,7 @@ const ( actBgTransformBorderLabel actBgTransformGhost actBgTransformHeader + actBgTransformHeaderLines actBgTransformFooter actBgTransformHeaderLabel actBgTransformFooterLabel @@ -710,6 +714,7 @@ func processExecution(action actionType) bool { actTransformBorderLabel, actTransformGhost, actTransformHeader, + actTransformHeaderLines, actTransformFooter, actTransformHeaderLabel, actTransformFooterLabel, @@ -725,6 +730,7 @@ func processExecution(action actionType) bool { actBgTransformBorderLabel, actBgTransformGhost, actBgTransformHeader, + actBgTransformHeaderLines, actBgTransformFooter, actBgTransformHeaderLabel, actBgTransformFooterLabel, @@ -761,14 +767,15 @@ type placeholderFlags struct { } type searchRequest struct { - sort bool - sync bool - nth *[]Range - command *commandSpec - environ []string - changed bool - denylist []int32 - revision revision + sort bool + sync bool + nth *[]Range + headerLines *int + command *commandSpec + environ []string + changed bool + denylist []int32 + revision revision } type previewRequest struct { @@ -1022,6 +1029,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor sort: opts.Sort > 0, toggleSort: opts.ToggleSort, track: opts.Track, + targetIndex: minItem.Index(), delimiter: opts.Delimiter, expect: opts.Expect, keymap: opts.Keymap, @@ -1063,7 +1071,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor headerFirst: opts.HeaderFirst, headerLines: opts.HeaderLines, gap: opts.Gap, - header: []string{}, + header: []Item{}, footer: opts.Footer, header0: opts.Header, ansi: opts.Ansi, @@ -1364,7 +1372,7 @@ func (t *Terminal) environImpl(forPreview bool) []string { } } env = append(env, "FZF_INPUT_STATE="+inputState) - env = append(env, fmt.Sprintf("FZF_TOTAL_COUNT=%d", t.count)) + env = append(env, fmt.Sprintf("FZF_TOTAL_COUNT=%d", max(0, t.count-t.headerLines))) env = append(env, fmt.Sprintf("FZF_MATCH_COUNT=%d", t.resultMerger.Length())) env = append(env, fmt.Sprintf("FZF_SELECT_COUNT=%d", len(t.selected))) env = append(env, fmt.Sprintf("FZF_LINES=%d", t.areaLines)) @@ -1755,8 +1763,14 @@ func (t *Terminal) changeFooter(footer string) { } // UpdateHeader updates the header -func (t *Terminal) UpdateHeader(header []string) { +func (t *Terminal) UpdateHeader(header []Item) { t.mutex.Lock() + // Pad to t.headerLines so that click coordinate mapping works correctly + if len(header) < t.headerLines { + padded := make([]Item, t.headerLines) + copy(padded, header) + header = padded + } t.header = header t.mutex.Unlock() t.reqBox.Set(reqHeader, nil) @@ -1788,6 +1802,10 @@ func (t *Terminal) UpdateList(result MatchResult) { prevIndex = merger.First().item.Index() } } + if t.targetIndex != minItem.Index() { + prevIndex = t.targetIndex + t.targetIndex = minItem.Index() + } t.progress = 100 t.merger = merger t.resultMerger = merger @@ -3079,11 +3097,11 @@ func (t *Terminal) printHeader() { } t.withWindow(t.headerWindow, func() { - var lines []string + var headerItems []Item if !t.hasHeaderLinesWindow() { - lines = t.header + headerItems = t.header } - t.printHeaderImpl(t.headerWindow, t.headerBorderShape, t.header0, lines) + t.printHeaderImpl(t.headerWindow, t.headerBorderShape, t.header0, headerItems) }) if w, shape := t.determineHeaderLinesShape(); w { t.withWindow(t.headerLinesWindow, func() { @@ -3145,7 +3163,7 @@ func (t *Terminal) headerIndentImpl(base int, borderShape tui.BorderShape) int { return indentSize } -func (t *Terminal) printHeaderImpl(window tui.Window, borderShape tui.BorderShape, lines1 []string, lines2 []string) { +func (t *Terminal) printHeaderImpl(window tui.Window, borderShape tui.BorderShape, lines1 []string, lines2 []Item) { max := t.window.Height() if !t.inputless && t.inputWindow == nil && window == nil && t.headerFirst { max-- @@ -3172,7 +3190,8 @@ func (t *Terminal) printHeaderImpl(window tui.Window, borderShape tui.BorderShap } indent := strings.Repeat(" ", indentSize) t.wrap = false - for idx, lineStr := range append(append([]string{}, lines1...), lines2...) { + totalLines := len(lines1) + len(lines2) + for idx := 0; idx < totalLines; idx++ { line := idx if needReverse && idx < len(lines1) { line = len(lines1) - idx - 1 @@ -3186,11 +3205,18 @@ func (t *Terminal) printHeaderImpl(window tui.Window, borderShape tui.BorderShap if line >= max { continue } - trimmed, colors, newState := extractColor(lineStr, state, nil) - state = newState - item := &Item{ - text: util.ToChars([]byte(trimmed)), - colors: colors} + + var item *Item + if idx < len(lines1) { + trimmed, colors, newState := extractColor(lines1[idx], state, nil) + state = newState + item = &Item{ + text: util.ToChars([]byte(trimmed)), + colors: colors} + } else { + headerItem := lines2[idx-len(lines1)] + item = &headerItem + } t.printHighlighted(Result{item: item}, tui.ColHeader, tui.ColHeader, false, false, false, line, line, true, @@ -5288,9 +5314,13 @@ func (t *Terminal) addClickHeaderWord(env []string) []string { return env } - // NOTE: t.header is padded with empty strings so that its size is equal to t.headerLines nthBase := 0 - headers := [2][]string{t.header, t.header0} + // Convert header items to strings for click handling + headerStrs := make([]string, len(t.header)) + for i, item := range t.header { + headerStrs[i] = item.text.ToString() + } + headers := [2][]string{headerStrs, t.header0} if t.layout == layoutReverse { headers[0], headers[1] = headers[1], headers[0] } @@ -5892,6 +5922,7 @@ func (t *Terminal) Loop() error { events := []util.EventType{} changed := false var newNth *[]Range + var newHeaderLines *int req := func(evts ...util.EventType) { for _, event := range evts { events = append(events, event) @@ -5908,6 +5939,7 @@ func (t *Terminal) Loop() error { events = []util.EventType{} changed = false newNth = nil + newHeaderLines = nil beof := false queryChanged := false denylist := []int32{} @@ -6247,6 +6279,23 @@ func (t *Terminal) Loop() error { } case actPrintQuery: req(reqPrintQuery) + case actChangeHeaderLines, actTransformHeaderLines, actBgTransformHeaderLines: + capture(true, func(expr string) { + if n, err := strconv.Atoi(expr); err == nil && n >= 0 && n != t.headerLines { + t.headerLines = n + newHeaderLines = &n + changed = true + // Deselect items that are now part of the header + for idx := range t.selected { + if idx < int32(n) { + delete(t.selected, idx) + } + } + // Tell UpdateList to reposition cursor to the current item + t.targetIndex = t.currentIndex() + req(reqList, reqPrompt, reqInfo, reqHeader) + } + }) case actChangeMulti: multi := t.multi if a.a == "" { @@ -7428,7 +7477,7 @@ func (t *Terminal) Loop() error { reload := changed || newCommand != nil var reloadRequest *searchRequest if reload { - reloadRequest = &searchRequest{sort: t.sort, sync: reloadSync, nth: newNth, command: newCommand, environ: t.environ(), changed: changed, denylist: denylist, revision: t.resultMerger.Revision()} + reloadRequest = &searchRequest{sort: t.sort, sync: reloadSync, nth: newNth, headerLines: newHeaderLines, command: newCommand, environ: t.environ(), changed: changed, denylist: denylist, revision: t.resultMerger.Revision()} } // Dispatch queued background requests diff --git a/test/test_core.rb b/test/test_core.rb index 3f932eed..2ca9e26e 100644 --- a/test/test_core.rb +++ b/test/test_core.rb @@ -2176,6 +2176,80 @@ class TestCore < TestInteractive end end + def test_change_header_lines + tmux.send_keys %(seq 10 | #{FZF} --header-lines 3 --bind 'space:change-header-lines(5),enter:transform-header-lines(echo 1)'), :Enter + tmux.until do |lines| + assert_equal 7, lines.item_count + assert lines.any_include?('> 4') + end + tmux.send_keys :Space + tmux.until do |lines| + assert_equal 5, lines.item_count + assert lines.any_include?('> 6') + end + tmux.send_keys :Enter + tmux.until do |lines| + assert_equal 9, lines.item_count + assert lines.any_include?('> 6') + end + end + + def test_change_header_lines_to_zero + tmux.send_keys %(seq 5 | #{FZF} --header-lines 3 --bind 'space:bg-transform-header-lines(echo 0)'), :Enter + tmux.until do |lines| + assert_equal 2, lines.item_count + assert lines.any_include?('> 4') + end + tmux.send_keys :Space + tmux.until do |lines| + assert_equal 5, lines.item_count + # All items are now in the list, cursor stays on item 4 + assert lines.any_include?('> 4') + end + end + + def test_change_header_lines_deselect + # Selected items that become part of the header should be deselected + tmux.send_keys %(seq 10 | #{FZF} --multi --header-lines 0 --bind 'space:change-header-lines(3),enter:change-header-lines(1)'), :Enter + tmux.until do |lines| + assert_equal 10, lines.item_count + assert lines.any_include?('> 1') + end + # Select items 1, 2, 3 (these will become header lines) + tmux.send_keys :BTab, :BTab, :BTab + tmux.until { |lines| assert_equal 3, lines.select_count } + # Also select item 4 (this should remain selected) + tmux.send_keys :BTab + tmux.until { |lines| assert_equal 4, lines.select_count } + # Change header-lines to 3: items 1, 2, 3 become headers and should be deselected + tmux.send_keys :Space + tmux.until do |lines| + assert_equal 7, lines.item_count + assert_equal 1, lines.select_count + assert lines.any_include?('> 5') + end + # Change header-lines to 1 + tmux.send_keys :Enter + tmux.until do |lines| + assert_equal 9, lines.item_count + assert_equal 1, lines.select_count + assert lines.any_include?('> 5') + end + end + + def test_change_header_lines_reverse + tmux.send_keys %(seq 10 | #{FZF} --header-lines 2 --reverse --bind 'space:change-header-lines(4)'), :Enter + tmux.until do |lines| + assert_equal 8, lines.item_count + assert lines.any_include?('> 3') + end + tmux.send_keys :Space + tmux.until do |lines| + assert_equal 6, lines.item_count + assert lines.any_include?('> 5') + end + end + def test_zero_width_characters tmux.send_keys %(for i in {1..1000}; do string+="a̱$i"; printf '\\e[43m%s\\e[0m\\n' "$string"; done | #{FZF} --ansi --query a500 --ellipsis XX), :Enter tmux.until do |lines| diff --git a/test/test_filter.rb b/test/test_filter.rb index e9ca0302..35b00854 100644 --- a/test/test_filter.rb +++ b/test/test_filter.rb @@ -326,4 +326,27 @@ class TestFilter < TestBase writelines(['emp001 Alice Engineering', 'emp002 Bob Marketing']) assert_equal 'emp001', `#{FZF} -d' ' --with-nth 2 --accept-nth 1 -f Alice < #{tempname}`.chomp end + + def test_header_lines_filter + assert_equal %w[4 5 6 7 8 9 10], + `seq 10 | #{FZF} --header-lines 3 -f ""`.lines(chomp: true) + assert_equal %w[5], + `seq 10 | #{FZF} --header-lines 3 -f 5`.lines(chomp: true) + # Header items should not be matched + assert_empty `seq 10 | #{FZF} --header-lines 3 -f "^1$"`.lines(chomp: true) + end + + def test_header_lines_filter_with_nth + writelines(%w[a:1 b:2 c:3 d:4 e:5]) + assert_equal %w[c:3 d:4 e:5], + `#{FZF} --header-lines 2 -d: --with-nth 2 -f "" < #{tempname}`.lines(chomp: true) + assert_equal %w[d:4], + `#{FZF} --header-lines 2 -d: --with-nth 2 -f 4 < #{tempname}`.lines(chomp: true) + end + + def test_header_lines_all_headers + # When all lines are header lines, no results + assert_empty `seq 3 | #{FZF} --header-lines 10 -f ""`.chomp + assert_equal 1, $CHILD_STATUS.exitstatus + end end