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
This commit is contained in:
Junegunn Choi
2026-02-23 01:45:47 +09:00
parent 98a3b1fff8
commit b9804f5873
13 changed files with 420 additions and 227 deletions

View File

@@ -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) {

View File

@@ -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 {

View File

@@ -65,7 +65,6 @@ const (
EvtSearchNew
EvtSearchProgress
EvtSearchFin
EvtHeader
EvtReady
EvtQuit
)

View File

@@ -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:

View File

@@ -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}
}

View File

@@ -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

View File

@@ -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":

View File

@@ -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
}

View File

@@ -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) {

View File

@@ -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