From f8521fa90eb1c9b5b2372271bf7561e20b06a941 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 28 Sep 2025 20:59:20 +0900 Subject: [PATCH] Introduce 'raw' mode --- CHANGELOG.md | 106 ++++++++++++++---- man/man1/fzf.1 | 19 +++- src/actiontype_string.go | 197 +++++++++++++++++----------------- src/core.go | 19 ++-- src/matcher.go | 56 ++++++---- src/merger.go | 9 ++ src/options.go | 34 +++++- src/options_test.go | 4 +- src/terminal.go | 225 +++++++++++++++++++++++++++++++++------ src/tui/tui.go | 12 +++ test/test_raw.rb | 60 +++++++++++ 11 files changed, 554 insertions(+), 187 deletions(-) create mode 100644 test/test_raw.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index f96128a2..31d5a61d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,30 +3,98 @@ CHANGELOG 0.66.0 ------ -- Style changes - - Updated `--color base16` (alias: `16`) theme so that it works better with both dark and light themes. - - Narrowed the gutter column by using the left-half block character (`▌`). - - Removed background colors from markers. -- Added `--gutter CHAR` option for customizing the gutter column. Some examples using [box-drawing characters](https://en.wikipedia.org/wiki/Box-drawing_characters): - ```sh - # Right-aligned gutter - fzf --gutter '▐' - # Even thinner gutter - fzf --gutter '▎' +### Introducing "raw" mode - # Checker - fzf --gutter '▚' +In "raw" mode, non-matching items are also displayed in their original position, +but dimmed. This is useful when you want to see the surrounding items of a match +to understand the context. Raw mode can be enabled by using `--raw` option, but +I find it more useful when toggled dynamically using `toggle-raw` action. - # Dotted - fzf --gutter '▖' +```sh +export FZF_CTRL_R_OPTS='--bind ctrl-x:toggle-raw' +``` - # Full-width - fzf --gutter '█' +```sh +tree | fzf --raw --reverse --bind ctrl-x:toggle-raw +``` - # No gutter - fzf --gutter ' ' - ``` +While non-matching items are displayed in dimmed color, they are treated just +like matching items in the list, so you place the cursor on them and do any +action against them. But if you want to navigate through matching items only, +you can use `down-match` and `up-match` actions, which are from now on bound to +`CTRL-N` and `CTRL-P` respectively. Historically, `CTRL-N` and `CTRL-P` are +bound to `next-history` and `prev-history` when `--history` option is used, so +in that case, you'll have to manually bind the actions to the keys of your +choice, or you can use `ALT-DOWN` and `ALT-UP` instead. + +#### Customizing the look + +##### Gutter + +To distinguish the raw mode, the gutter column is rendered in dashed line using +`▖` character. But you can customize it using `--gutter-raw CHAR` option. + +```sh +# If you don't liked the dashed line and you just want a thinner gutter +fzf --bind ctrl-x:toggle-raw --gutter-raw ▎ +``` + +##### Color and style of non-matching items + +Non-matching items are displayed in dimmed color by default, but you can change +it using `--color hidden:...` option. + +```sh +fzf --raw --color hidden:red:strikethrough + +# To unset the default 'dim' attribute, prefix the color spec with 'regular' +fzf --raw --color hidden:regular:red:strikethrough +``` + +### Style changes + +This version introduces some minor changes to the traditional visual style of fzf. + +- Narrowed the gutter column by using the left-half block character (`▌`). +- Removed background colors from markers. +- Updated `--color base16` (alias: `16`) theme so that it works better with both dark and light themes. + +### Added options + +#### `--gutter CHAR` + +Added `--gutter CHAR` option for customizing the gutter column. Some examples using [box-drawing characters](https://en.wikipedia.org/wiki/Box-drawing_characters): + +```sh +# Right-aligned gutter +fzf --gutter '▐' + +# Even thinner gutter +fzf --gutter '▎' + +# Checker +fzf --gutter '▚' + +# Dotted +fzf --gutter '▖' + +# Full-width +fzf --gutter '█' + +# No gutter +fzf --gutter ' ' +``` + +#### `--gutter-raw CHAR` + +As mentioned above, also added `--gutter-raw CHAR` option for customizing the gutter column in raw mode. + +### Compatibility changes + +Starting from this version, fzf is built with Go 1.23. Support for some old OS versions has been dropped. + +See https://go.dev/wiki/MinimumRequirements. 0.65.2 ------ diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 0a6a9ff9..36c322f2 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -299,6 +299,7 @@ color mappings. Each entry is separated by a comma and/or whitespaces. \fBheader (header\-fg) \fRHeader \fBfooter (footer\-fg) \fRFooter \fBnth \fRParts of the line specified by \fB\-\-nth\fR (only supports attributes) + \fBhidden \fRNon-matching items in raw mode (default: \fBdim\fR) .B ANSI COLORS: \fB\-1 \fRDefault terminal foreground/background color @@ -596,6 +597,9 @@ Indicator for wrapped lines. The default is '↳ ' or '> ' depending on .B "\-\-no\-multi\-line" Disable multi-line display of items when using \fB\-\-read0\fR .TP +.B "\-\-raw" +Enable raw mode where non-matching items are also displayed in a dimmed color. +.TP .B "\-\-track" Make fzf track the current selection when the result list is updated. This can be useful when browsing logs using fzf with sorting disabled. It is @@ -1841,9 +1845,11 @@ A key or an event can be bound to one or more of the following actions. \fBdelete\-char\fR \fIdel\fR \fBdelete\-char/eof\fR \fIctrl\-d\fR (same as \fBdelete\-char\fR except aborts fzf if query is empty) \fBdeselect\fR - \fBdeselect\-all\fR (deselect all matches; to also clear non-matched selections, use \fBclear\-multi\fR) + \fBdeselect\-all\fR (deselect all matches; to also clear non-matching selections, use \fBclear\-multi\fR) \fBdisable\-search\fR (disable search functionality) - \fBdown\fR \fIctrl\-j ctrl\-n down\fR + \fBdown\fR \fIctrl\-j down\fR + \fBdown\-match\fR \fIctrl\-n\fR \fIalt\-down\fR (move to the match below the cursor) + \fBdown\-selected\fR (move to the selected item below the cursor) \fBenable\-search\fR (enable search functionality) \fBend\-of\-line\fR \fIctrl\-e end\fR \fBexclude\fR (exclude the current item from the result) @@ -1861,7 +1867,7 @@ A key or an event can be bound to one or more of the following actions. \fBkill\-word\fR \fIalt\-d\fR \fBlast\fR (move to the last match; same as \fBpos(\-1)\fR) \fBnext\-history\fR (\fIctrl\-n\fR on \fB\-\-history\fR) - \fBnext\-selected\fR (move to the next selected item) + \fBnext\-selected\fR (synonym to \fBdown\-selected\fR) \fBpage\-down\fR \fIpgdn\fR \fBpage\-up\fR \fIpgup\fR \fBhalf\-page\-down\fR @@ -1874,7 +1880,7 @@ A key or an event can be bound to one or more of the following actions. \fBoffset\-middle\fR (place the current item is in the middle of the screen) \fBpos(...)\fR (move cursor to the numeric position; negative number to count from the end) \fBprev\-history\fR (\fIctrl\-p\fR on \fB\-\-history\fR) - \fBprev\-selected\fR (move to the previous selected item) + \fBprev\-selected\fR (synonym to \fBup\-selected\fR) \fBpreview(...)\fR (see below for the details) \fBpreview\-down\fR \fIshift\-down\fR \fBpreview\-up\fR \fIshift\-up\fR @@ -1909,6 +1915,7 @@ A key or an event can be bound to one or more of the following actions. \fBtoggle\-multi\-line\fR \fBtoggle\-preview\fR \fBtoggle\-preview\-wrap\fR + \fBtoggle\-raw\fR \fBtoggle\-search\fR (toggle search functionality) \fBtoggle\-sort\fR \fBtoggle\-track\fR (toggle global tracking option (\fB\-\-track\fR)) @@ -1935,7 +1942,9 @@ A key or an event can be bound to one or more of the following actions. \fBunix\-line\-discard\fR \fIctrl\-u\fR \fBunix\-word\-rubout\fR \fIctrl\-w\fR \fBuntrack\-current\fR (stop tracking the current item; no-op if global tracking is enabled) - \fBup\fR \fIctrl\-k ctrl\-p up\fR + \fBup\fR \fIctrl\-k up\fR + \fBup\-match\fR \fIctrl\-p\fR \fIalt\-up\fR (move to the match above the cursor) + \fBup\-selected\fR (move to the selected item above the cursor) \fByank\fR \fIctrl\-y\fR Each \fBtransform*\fR action has a corresponding \fBbg\-transform*\fR diff --git a/src/actiontype_string.go b/src/actiontype_string.go index 2a6873b8..f6316a51 100644 --- a/src/actiontype_string.go +++ b/src/actiontype_string.go @@ -77,106 +77,109 @@ func _() { _ = x[actToggleWrap-66] _ = x[actToggleMultiLine-67] _ = x[actToggleHscroll-68] - _ = x[actTrackCurrent-69] - _ = x[actToggleInput-70] - _ = x[actHideInput-71] - _ = x[actShowInput-72] - _ = x[actUntrackCurrent-73] - _ = x[actDown-74] - _ = x[actUp-75] - _ = x[actPageUp-76] - _ = x[actPageDown-77] - _ = x[actPosition-78] - _ = x[actHalfPageUp-79] - _ = x[actHalfPageDown-80] - _ = x[actOffsetUp-81] - _ = x[actOffsetDown-82] - _ = x[actOffsetMiddle-83] - _ = x[actJump-84] - _ = x[actJumpAccept-85] - _ = x[actPrintQuery-86] - _ = x[actRefreshPreview-87] - _ = x[actReplaceQuery-88] - _ = x[actToggleSort-89] - _ = x[actShowPreview-90] - _ = x[actHidePreview-91] - _ = x[actTogglePreview-92] - _ = x[actTogglePreviewWrap-93] - _ = x[actTransform-94] - _ = x[actTransformBorderLabel-95] - _ = x[actTransformGhost-96] - _ = x[actTransformHeader-97] - _ = x[actTransformFooter-98] - _ = x[actTransformHeaderLabel-99] - _ = x[actTransformFooterLabel-100] - _ = x[actTransformInputLabel-101] - _ = x[actTransformListLabel-102] - _ = x[actTransformNth-103] - _ = x[actTransformPointer-104] - _ = x[actTransformPreviewLabel-105] - _ = x[actTransformPrompt-106] - _ = x[actTransformQuery-107] - _ = x[actTransformSearch-108] - _ = x[actTrigger-109] - _ = x[actBgTransform-110] - _ = x[actBgTransformBorderLabel-111] - _ = x[actBgTransformGhost-112] - _ = x[actBgTransformHeader-113] - _ = x[actBgTransformFooter-114] - _ = x[actBgTransformHeaderLabel-115] - _ = x[actBgTransformFooterLabel-116] - _ = x[actBgTransformInputLabel-117] - _ = x[actBgTransformListLabel-118] - _ = x[actBgTransformNth-119] - _ = x[actBgTransformPointer-120] - _ = x[actBgTransformPreviewLabel-121] - _ = x[actBgTransformPrompt-122] - _ = x[actBgTransformQuery-123] - _ = x[actBgTransformSearch-124] - _ = x[actBgCancel-125] - _ = x[actSearch-126] - _ = x[actPreview-127] - _ = x[actPreviewTop-128] - _ = x[actPreviewBottom-129] - _ = x[actPreviewUp-130] - _ = x[actPreviewDown-131] - _ = x[actPreviewPageUp-132] - _ = x[actPreviewPageDown-133] - _ = x[actPreviewHalfPageUp-134] - _ = x[actPreviewHalfPageDown-135] - _ = x[actPrevHistory-136] - _ = x[actPrevSelected-137] - _ = x[actPrint-138] - _ = x[actPut-139] - _ = x[actNextHistory-140] - _ = x[actNextSelected-141] - _ = x[actExecute-142] - _ = x[actExecuteSilent-143] - _ = x[actExecuteMulti-144] - _ = x[actSigStop-145] - _ = x[actFirst-146] - _ = x[actLast-147] - _ = x[actReload-148] - _ = x[actReloadSync-149] - _ = x[actDisableSearch-150] - _ = x[actEnableSearch-151] - _ = x[actSelect-152] - _ = x[actDeselect-153] - _ = x[actUnbind-154] - _ = x[actRebind-155] - _ = x[actToggleBind-156] - _ = x[actBecome-157] - _ = x[actShowHeader-158] - _ = x[actHideHeader-159] - _ = x[actBell-160] - _ = x[actExclude-161] - _ = x[actExcludeMulti-162] - _ = x[actAsync-163] + _ = x[actToggleRaw-69] + _ = x[actTrackCurrent-70] + _ = x[actToggleInput-71] + _ = x[actHideInput-72] + _ = x[actShowInput-73] + _ = x[actUntrackCurrent-74] + _ = x[actDown-75] + _ = x[actDownMatch-76] + _ = x[actUp-77] + _ = x[actUpMatch-78] + _ = x[actPageUp-79] + _ = x[actPageDown-80] + _ = x[actPosition-81] + _ = x[actHalfPageUp-82] + _ = x[actHalfPageDown-83] + _ = x[actOffsetUp-84] + _ = x[actOffsetDown-85] + _ = x[actOffsetMiddle-86] + _ = x[actJump-87] + _ = x[actJumpAccept-88] + _ = x[actPrintQuery-89] + _ = x[actRefreshPreview-90] + _ = x[actReplaceQuery-91] + _ = x[actToggleSort-92] + _ = x[actShowPreview-93] + _ = x[actHidePreview-94] + _ = x[actTogglePreview-95] + _ = x[actTogglePreviewWrap-96] + _ = x[actTransform-97] + _ = x[actTransformBorderLabel-98] + _ = x[actTransformGhost-99] + _ = x[actTransformHeader-100] + _ = x[actTransformFooter-101] + _ = x[actTransformHeaderLabel-102] + _ = x[actTransformFooterLabel-103] + _ = x[actTransformInputLabel-104] + _ = x[actTransformListLabel-105] + _ = x[actTransformNth-106] + _ = x[actTransformPointer-107] + _ = x[actTransformPreviewLabel-108] + _ = x[actTransformPrompt-109] + _ = x[actTransformQuery-110] + _ = x[actTransformSearch-111] + _ = x[actTrigger-112] + _ = x[actBgTransform-113] + _ = x[actBgTransformBorderLabel-114] + _ = x[actBgTransformGhost-115] + _ = x[actBgTransformHeader-116] + _ = x[actBgTransformFooter-117] + _ = x[actBgTransformHeaderLabel-118] + _ = x[actBgTransformFooterLabel-119] + _ = x[actBgTransformInputLabel-120] + _ = x[actBgTransformListLabel-121] + _ = x[actBgTransformNth-122] + _ = x[actBgTransformPointer-123] + _ = x[actBgTransformPreviewLabel-124] + _ = x[actBgTransformPrompt-125] + _ = x[actBgTransformQuery-126] + _ = x[actBgTransformSearch-127] + _ = x[actBgCancel-128] + _ = x[actSearch-129] + _ = x[actPreview-130] + _ = x[actPreviewTop-131] + _ = x[actPreviewBottom-132] + _ = x[actPreviewUp-133] + _ = x[actPreviewDown-134] + _ = x[actPreviewPageUp-135] + _ = x[actPreviewPageDown-136] + _ = x[actPreviewHalfPageUp-137] + _ = x[actPreviewHalfPageDown-138] + _ = x[actPrevHistory-139] + _ = x[actPrevSelected-140] + _ = x[actPrint-141] + _ = x[actPut-142] + _ = x[actNextHistory-143] + _ = x[actNextSelected-144] + _ = x[actExecute-145] + _ = x[actExecuteSilent-146] + _ = x[actExecuteMulti-147] + _ = x[actSigStop-148] + _ = x[actFirst-149] + _ = x[actLast-150] + _ = x[actReload-151] + _ = x[actReloadSync-152] + _ = x[actDisableSearch-153] + _ = x[actEnableSearch-154] + _ = x[actSelect-155] + _ = x[actDeselect-156] + _ = x[actUnbind-157] + _ = x[actRebind-158] + _ = x[actToggleBind-159] + _ = x[actBecome-160] + _ = x[actShowHeader-161] + _ = x[actHideHeader-162] + _ = x[actBell-163] + _ = x[actExclude-164] + _ = x[actExcludeMulti-165] + _ = x[actAsync-166] } -const _actionType_name = "actIgnoreactStartactClickactInvalidactBracketedPasteBeginactBracketedPasteEndactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactBackwardSubWordactCancelactChangeBorderLabelactChangeGhostactChangeHeaderactChangeFooteractChangeHeaderLabelactChangeFooterLabelactChangeInputLabelactChangeListLabelactChangeMultiactChangeNthactChangePointeractChangePreviewactChangePreviewLabelactChangePreviewWindowactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactForwardSubWordactKillLineactKillWordactKillSubWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactBackwardKillSubWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleMultiLineactToggleHscrollactTrackCurrentactToggleInputactHideInputactShowInputactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformGhostactTransformHeaderactTransformFooteractTransformHeaderLabelactTransformFooterLabelactTransformInputLabelactTransformListLabelactTransformNthactTransformPointeractTransformPreviewLabelactTransformPromptactTransformQueryactTransformSearchactTriggeractBgTransformactBgTransformBorderLabelactBgTransformGhostactBgTransformHeaderactBgTransformFooteractBgTransformHeaderLabelactBgTransformFooterLabelactBgTransformInputLabelactBgTransformListLabelactBgTransformNthactBgTransformPointeractBgTransformPreviewLabelactBgTransformPromptactBgTransformQueryactBgTransformSearchactBgCancelactSearchactPreviewactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactToggleBindactBecomeactShowHeaderactHideHeaderactBellactExcludeactExcludeMultiactAsync" +const _actionType_name = "actIgnoreactStartactClickactInvalidactBracketedPasteBeginactBracketedPasteEndactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactBackwardSubWordactCancelactChangeBorderLabelactChangeGhostactChangeHeaderactChangeFooteractChangeHeaderLabelactChangeFooterLabelactChangeInputLabelactChangeListLabelactChangeMultiactChangeNthactChangePointeractChangePreviewactChangePreviewLabelactChangePreviewWindowactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactForwardSubWordactKillLineactKillWordactKillSubWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactBackwardKillSubWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleMultiLineactToggleHscrollactToggleRawactTrackCurrentactToggleInputactHideInputactShowInputactUntrackCurrentactDownactDownMatchactUpactUpMatchactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformGhostactTransformHeaderactTransformFooteractTransformHeaderLabelactTransformFooterLabelactTransformInputLabelactTransformListLabelactTransformNthactTransformPointeractTransformPreviewLabelactTransformPromptactTransformQueryactTransformSearchactTriggeractBgTransformactBgTransformBorderLabelactBgTransformGhostactBgTransformHeaderactBgTransformFooteractBgTransformHeaderLabelactBgTransformFooterLabelactBgTransformInputLabelactBgTransformListLabelactBgTransformNthactBgTransformPointeractBgTransformPreviewLabelactBgTransformPromptactBgTransformQueryactBgTransformSearchactBgCancelactSearchactPreviewactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactToggleBindactBecomeactShowHeaderactHideHeaderactBellactExcludeactExcludeMultiactAsync" -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, 993, 1009, 1024, 1038, 1050, 1062, 1079, 1086, 1091, 1100, 1111, 1122, 1135, 1150, 1161, 1174, 1189, 1196, 1209, 1222, 1239, 1254, 1267, 1281, 1295, 1311, 1331, 1343, 1366, 1383, 1401, 1419, 1442, 1465, 1487, 1508, 1523, 1542, 1566, 1584, 1601, 1619, 1629, 1643, 1668, 1687, 1707, 1727, 1752, 1777, 1801, 1824, 1841, 1862, 1888, 1908, 1927, 1947, 1958, 1967, 1977, 1990, 2006, 2018, 2032, 2048, 2066, 2086, 2108, 2122, 2137, 2145, 2151, 2165, 2180, 2190, 2206, 2221, 2231, 2239, 2246, 2255, 2268, 2284, 2299, 2308, 2319, 2328, 2337, 2350, 2359, 2372, 2385, 2392, 2402, 2417, 2425} +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, 993, 1009, 1021, 1036, 1050, 1062, 1074, 1091, 1098, 1110, 1115, 1125, 1134, 1145, 1156, 1169, 1184, 1195, 1208, 1223, 1230, 1243, 1256, 1273, 1288, 1301, 1315, 1329, 1345, 1365, 1377, 1400, 1417, 1435, 1453, 1476, 1499, 1521, 1542, 1557, 1576, 1600, 1618, 1635, 1653, 1663, 1677, 1702, 1721, 1741, 1761, 1786, 1811, 1835, 1858, 1875, 1896, 1922, 1942, 1961, 1981, 1992, 2001, 2011, 2024, 2040, 2052, 2066, 2082, 2100, 2120, 2142, 2156, 2171, 2179, 2185, 2199, 2214, 2224, 2240, 2255, 2265, 2273, 2280, 2289, 2302, 2318, 2333, 2342, 2353, 2362, 2371, 2384, 2393, 2406, 2419, 2426, 2436, 2451, 2459} 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 a4fbfe04..286d574a 100644 --- a/src/core.go +++ b/src/core.go @@ -267,11 +267,11 @@ func Run(opts *Options) (int, error) { // NOTE: Streaming filter is inherently not compatible with --tail snapshot, _, _ := chunkList.Snapshot(opts.Tail) - merger, _ := matcher.scan(MatchRequest{ + result := matcher.scan(MatchRequest{ chunks: snapshot, pattern: pattern}) - for i := 0; i < merger.Length(); i++ { - opts.Printer(merger.Get(i).item.AsString(opts.Ansi)) + for i := 0; i < result.merger.Length(); i++ { + opts.Printer(result.merger.Get(i).item.AsString(opts.Ansi)) found = true } } @@ -479,12 +479,13 @@ func Run(opts *Options) (int, error) { case EvtSearchFin: switch val := value.(type) { - case *Merger: + case MatchResult: + merger := val.merger if deferred { - count := val.Length() + count := merger.Length() if opts.Select1 && count > 1 || opts.Exit0 && !opts.Select1 && count > 0 { - determine(val.final) - } else if val.final { + determine(merger.final) + } else if merger.final { if opts.Exit0 && count == 0 || opts.Select1 && count == 1 { if opts.PrintQuery { opts.Printer(opts.Query) @@ -502,7 +503,7 @@ func Run(opts *Options) (int, error) { } } for i := 0; i < count; i++ { - opts.Printer(transformer(val.Get(i).item)) + opts.Printer(transformer(merger.Get(i).item)) } if count == 0 { exitCode = ExitNoMatch @@ -510,7 +511,7 @@ func Run(opts *Options) (int, error) { stop = true return } - determine(val.final) + determine(merger.final) } } terminal.UpdateList(val) diff --git a/src/matcher.go b/src/matcher.go index 95926fe4..7fd01353 100644 --- a/src/matcher.go +++ b/src/matcher.go @@ -19,6 +19,20 @@ type MatchRequest struct { revision revision } +type MatchResult struct { + merger *Merger + passMerger *Merger + cancelled bool +} + +func (mr MatchResult) cacheable() bool { + return mr.merger != nil && mr.merger.cacheable() +} + +func (mr MatchResult) final() bool { + return mr.merger != nil && mr.merger.final +} + // Matcher is responsible for performing search type Matcher struct { cache *ChunkCache @@ -29,7 +43,7 @@ type Matcher struct { reqBox *util.EventBox partitions int slab []*util.Slab - mergerCache map[string]*Merger + mergerCache map[string]MatchResult revision revision } @@ -51,7 +65,7 @@ func NewMatcher(cache *ChunkCache, patternBuilder func([]rune) *Pattern, reqBox: util.NewEventBox(), partitions: partitions, slab: make([]*util.Slab, partitions), - mergerCache: make(map[string]*Merger), + mergerCache: make(map[string]MatchResult), revision: revision} } @@ -85,7 +99,7 @@ func (m *Matcher) Loop() { cacheCleared := false if request.sort != m.sort || request.revision != m.revision { m.sort = request.sort - m.mergerCache = make(map[string]*Merger) + m.mergerCache = make(map[string]MatchResult) if !request.revision.compatible(m.revision) { m.cache.Clear() } @@ -95,33 +109,32 @@ func (m *Matcher) Loop() { // Restart search patternString := request.pattern.AsString() - var merger *Merger - cancelled := false + var result MatchResult count := CountItems(request.chunks) if !cacheCleared { if count == prevCount { // Look up mergerCache - if cached, found := m.mergerCache[patternString]; found && cached.final == request.final { - merger = cached + if cached, found := m.mergerCache[patternString]; found && cached.final() == request.final { + result = cached } } else { // Invalidate mergerCache prevCount = count - m.mergerCache = make(map[string]*Merger) + m.mergerCache = make(map[string]MatchResult) } } - if merger == nil { - merger, cancelled = m.scan(request) + if result.merger == nil { + result = m.scan(request) } - if !cancelled { - if merger.cacheable() { - m.mergerCache[patternString] = merger + if !result.cancelled { + if result.cacheable() { + m.mergerCache[patternString] = result } - merger.final = request.final - m.eventBox.Set(EvtSearchFin, merger) + result.merger.final = request.final + m.eventBox.Set(EvtSearchFin, result) } } } @@ -152,16 +165,18 @@ type partialResult struct { matches []Result } -func (m *Matcher) scan(request MatchRequest) (*Merger, bool) { +func (m *Matcher) scan(request MatchRequest) MatchResult { startedAt := time.Now() numChunks := len(request.chunks) if numChunks == 0 { - return EmptyMerger(request.revision), false + m := EmptyMerger(request.revision) + return MatchResult{m, m, false} } pattern := request.pattern + passMerger := PassMerger(&request.chunks, m.tac, request.revision) if pattern.IsEmpty() { - return PassMerger(&request.chunks, m.tac, request.revision), false + return MatchResult{passMerger, passMerger, false} } minIndex := request.chunks[0].items[0].Index() @@ -224,7 +239,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) { } if m.reqBox.Peek(reqReset) { - return nil, wait() + return MatchResult{nil, nil, wait()} } if time.Since(startedAt) > progressMinDuration { @@ -237,7 +252,8 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) { partialResult := <-resultChan partialResults[partialResult.index] = partialResult.matches } - return NewMerger(pattern, partialResults, m.sort && request.pattern.sortable, m.tac, request.revision, minIndex, maxIndex), false + merger := NewMerger(pattern, partialResults, m.sort && request.pattern.sortable, m.tac, request.revision, minIndex, maxIndex) + return MatchResult{merger, passMerger, false} } // Reset is called to interrupt/signal the ongoing search diff --git a/src/merger.go b/src/merger.go index 688f3571..b9bdabb8 100644 --- a/src/merger.go +++ b/src/merger.go @@ -141,6 +141,15 @@ func (mg *Merger) Get(idx int) Result { panic(fmt.Sprintf("Index out of bounds (unsorted, %d/%d)", idx, mg.count)) } +func (mg *Merger) ToMap() map[int32]Result { + ret := make(map[int32]Result, mg.count) + for i := 0; i < mg.count; i++ { + result := mg.Get(i) + ret[result.Index()] = result + } + return ret +} + func (mg *Merger) cacheable() bool { return mg.count < mergerCacheMax } diff --git a/src/options.go b/src/options.go index f3ba8518..e7f693b6 100644 --- a/src/options.go +++ b/src/options.go @@ -98,6 +98,7 @@ Usage: fzf [options] --wrap Enable line wrap --wrap-sign=STR Indicator for wrapped lines --no-multi-line Disable multi-line display of items when using --read0 + --raw Enable raw mode (show non-matching items) --track Track the current selection when the result is updated --tac Reverse the order of the input --gap[=N] Render empty lines between each item @@ -111,6 +112,7 @@ Usage: fzf [options] highlighted substring (default: 10) --jump-labels=CHARS Label characters for jump mode --gutter=CHAR Character used for the gutter column (default: '▌') + --gutter-raw=CHAR Character used for the gutter column in raw mode (default: '▖') --pointer=STR Pointer to the current line (default: '▌' or '>') --marker=STR Multi-select marker (default: '┃' or '>') --marker-multi-line=STR Multi-select marker for multi-line entries; @@ -562,6 +564,7 @@ type Options struct { AcceptNth func(Delimiter) func([]Token, int32) string Delimiter Delimiter Sort int + Raw bool Track trackOption Tac bool Tail int @@ -593,6 +596,7 @@ type Options struct { JumpLabels string Prompt string Gutter *string + GutterRaw *string Pointer *string Marker *string MarkerMulti *[3]string @@ -714,6 +718,7 @@ func defaultOptions() *Options { JumpLabels: defaultJumpLabels, Prompt: "> ", Gutter: nil, + GutterRaw: nil, Pointer: nil, Marker: nil, MarkerMulti: nil, @@ -1442,6 +1447,8 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) (*tui.ColorTheme, erro mergeAttr(&theme.SelectedBg) case "nth": mergeAttr(&theme.Nth) + case "hidden": + mergeAttr(&theme.Hidden) case "gutter": mergeAttr(&theme.Gutter) case "hl": @@ -1741,6 +1748,8 @@ func parseActionList(masked string, original string, prevActions []*action, putA appendAction(actToggleMultiLine) case "toggle-hscroll": appendAction(actToggleHscroll) + case "toggle-raw": + appendAction(actToggleRaw) case "show-header": appendAction(actShowHeader) case "hide-header": @@ -1761,8 +1770,12 @@ func parseActionList(masked string, original string, prevActions []*action, putA appendAction(actToggle) case "down": appendAction(actDown) + case "down-match": + appendAction(actDownMatch) case "up": appendAction(actUp) + case "up-match": + appendAction(actUpMatch) case "first", "top": appendAction(actFirst) case "last": @@ -1779,9 +1792,9 @@ func parseActionList(masked string, original string, prevActions []*action, putA appendAction(actPrevHistory) case "next-history": appendAction(actNextHistory) - case "prev-selected": + case "up-selected", "prev-selected": appendAction(actPrevSelected) - case "next-selected": + case "down-selected", "next-selected": appendAction(actNextSelected) case "show-preview": appendAction(actShowPreview) @@ -2682,6 +2695,10 @@ func parseOptions(index *int, opts *Options, allArgs []string) error { } case "+s", "--no-sort": opts.Sort = 0 + case "--raw": + opts.Raw = true + case "--no-raw": + opts.Raw = false case "--track": opts.Track = trackEnabled case "--no-track": @@ -2866,6 +2883,13 @@ func parseOptions(index *int, opts *Options, allArgs []string) error { } str = firstLine(str) opts.Gutter = &str + case "--gutter-raw": + str, err := nextString("gutter character for raw mode required") + if err != nil { + return err + } + str = firstLine(str) + opts.GutterRaw = &str case "--pointer": str, err := nextString("pointer sign required") if err != nil { @@ -3390,6 +3414,12 @@ func validateOptions(opts *Options) error { } } + if opts.GutterRaw != nil { + if err := validateSign(*opts.GutterRaw, "gutter", 1); err != nil { + return err + } + } + if opts.Scrollbar != nil { runes := []rune(*opts.Scrollbar) if len(runes) > 2 { diff --git a/src/options_test.go b/src/options_test.go index a14dece7..7b152d75 100644 --- a/src/options_test.go +++ b/src/options_test.go @@ -350,8 +350,8 @@ func TestDefaultCtrlNP(t *testing.T) { t.Error() } } - check([]string{}, tui.CtrlN, actDown) - check([]string{}, tui.CtrlP, actUp) + check([]string{}, tui.CtrlN, actDownMatch) + check([]string{}, tui.CtrlP, actUpMatch) check([]string{"--bind=ctrl-n:accept"}, tui.CtrlN, actAccept) check([]string{"--bind=ctrl-p:accept"}, tui.CtrlP, actAccept) diff --git a/src/terminal.go b/src/terminal.go index d18a0ba7..88b7a62e 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -274,9 +274,11 @@ type Terminal struct { footerLabelLen int footerLabelOpts labelOpts gutterReverse bool + gutterRawReverse bool pointer string pointerLen int pointerEmpty string + pointerEmptyRaw string marker string markerLen int markerEmpty string @@ -297,6 +299,7 @@ type Terminal struct { subWordNext string cx int cy int + lastMatchingIndex int32 offset int xoffset int yanked []rune @@ -383,6 +386,9 @@ type Terminal struct { printer func(string) printsep string merger *Merger + passMerger *Merger + resultMerger *Merger + matchMap map[int32]Result selected map[int32]selectedItem version int64 revision revision @@ -429,6 +435,7 @@ type Terminal struct { clickFooterColumn int proxyScript string numLinesCache map[int32]numLinesCacheValue + raw bool } type numLinesCacheValue struct { @@ -569,13 +576,16 @@ const ( actToggleWrap actToggleMultiLine actToggleHscroll + actToggleRaw actTrackCurrent actToggleInput actHideInput actShowInput actUntrackCurrent actDown + actDownMatch actUp + actUpMatch actPageUp actPageDown actPosition @@ -796,8 +806,10 @@ func defaultKeymap() map[tui.Event][]*action { add(tui.CtrlK, actUp) add(tui.CtrlL, actClearScreen) add(tui.Enter, actAccept) - add(tui.CtrlN, actDown) - add(tui.CtrlP, actUp) + add(tui.CtrlN, actDownMatch) + add(tui.CtrlP, actUpMatch) + add(tui.AltDown, actDownMatch) + add(tui.AltUp, actUpMatch) add(tui.CtrlU, actUnixLineDiscard) add(tui.CtrlW, actUnixWordRubout) add(tui.CtrlY, actYank) @@ -953,6 +965,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor } keymapCopy := maps.Clone(opts.Keymap) + em := EmptyMerger(revision{}) t := Terminal{ initDelay: delay, infoCommand: opts.InfoCommand, @@ -980,6 +993,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor subWordNext: subWordNext, cx: len(input), cy: 0, + lastMatchingIndex: minItem.Index(), offset: 0, xoffset: 0, yanked: []rune{}, @@ -1039,6 +1053,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor nth: opts.Nth, nthCurrent: opts.Nth, tabstop: opts.Tabstop, + raw: opts.Raw, hasStartActions: false, hasResultActions: false, hasFocusActions: false, @@ -1052,7 +1067,10 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor printer: opts.Printer, printsep: opts.PrintSep, proxyScript: opts.ProxyScript, - merger: EmptyMerger(revision{}), + merger: em, + passMerger: em, + resultMerger: em, + matchMap: make(map[int32]Result), selected: make(map[int32]selectedItem), runningCmds: util.NewConcurrentSet[*runningCmd](), reqBox: util.NewEventBox(), @@ -1093,7 +1111,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor tui.InitTheme(opts.Theme, renderer.DefaultTheme(), opts.Black, opts.InputBorderShape.Visible(), opts.HeaderBorderShape.Visible()) // Gutter character - var gutterChar string + var gutterChar, gutterRawChar string if opts.Gutter != nil { gutterChar = *opts.Gutter } else if t.unicode && !t.theme.Gutter.Color.IsDefault() { @@ -1103,12 +1121,24 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor t.gutterReverse = true } + if opts.GutterRaw != nil { + gutterRawChar = *opts.GutterRaw + } else if t.unicode && !t.theme.Gutter.Color.IsDefault() { + // TODO: Doesn't look too good. Maybe use a different color instead, or both? + gutterRawChar = "▖" + } else { + gutterRawChar = ":" + t.gutterRawReverse = false + } + t.prompt, t.promptLen = t.parsePrompt(opts.Prompt) // Pre-calculated empty pointer and marker signs if t.pointerLen == 0 { t.pointerEmpty = "" + t.pointerEmptyRaw = "" } else { t.pointerEmpty = gutterChar + strings.Repeat(" ", util.Max(0, t.pointerLen-1)) + t.pointerEmptyRaw = gutterRawChar + strings.Repeat(" ", util.Max(0, t.pointerLen-1)) } t.markerEmpty = strings.Repeat(" ", t.markerLen) @@ -1282,7 +1312,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_MATCH_COUNT=%d", t.merger.Length())) + 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)) env = append(env, fmt.Sprintf("FZF_COLUMNS=%d", t.areaColumns)) @@ -1693,7 +1723,8 @@ func (t *Terminal) UpdateProgress(progress float32) { } // UpdateList updates Merger to display the list -func (t *Terminal) UpdateList(merger *Merger) { +func (t *Terminal) UpdateList(result MatchResult) { + merger := result.merger t.mutex.Lock() prevIndex := minItem.Index() newRevision := merger.Revision() @@ -1706,6 +1737,15 @@ func (t *Terminal) UpdateList(merger *Merger) { } t.progress = 100 t.merger = merger + t.resultMerger = merger + t.passMerger = result.passMerger + if t.raw { + t.merger = result.passMerger + t.matchMap = t.resultMerger.ToMap() + } else { + t.merger = result.merger + t.matchMap = make(map[int32]Result) + } if t.revision != newRevision { if !t.revision.compatible(newRevision) { // Reloaded: clear selection @@ -1754,7 +1794,7 @@ func (t *Terminal) UpdateList(merger *Merger) { } needActivation := false if !t.reading { - switch t.merger.Length() { + switch t.resultMerger.Length() { case 0: zero := tui.Zero.AsEvent() if _, prs := t.keymap[zero]; prs { @@ -2794,7 +2834,7 @@ func (t *Terminal) printInfoImpl() { return } - found := t.merger.Length() + found := t.resultMerger.Length() total := util.Max(found, t.count) output := fmt.Sprintf("%d/%d", found, total) if t.toggleSort { @@ -3115,12 +3155,16 @@ func (t *Terminal) gutter(current bool) { var color tui.ColorPair if current { color = tui.ColCurrentCursorEmpty - } else if t.gutterReverse { + } else if !t.raw && t.gutterReverse || t.raw && t.gutterRawReverse { color = tui.ColCursorEmpty } else { color = tui.ColCursorEmptyChar } - t.window.CPrint(color, t.pointerEmpty) + gutter := t.pointerEmpty + if t.raw { + gutter = t.pointerEmptyRaw + } + t.window.CPrint(color, gutter) } func (t *Terminal) renderGapLine(line int, barRange [2]int, drawLine bool) { @@ -3155,7 +3199,11 @@ func (t *Terminal) printList() { for line, itemCount := startLine, 0; line <= maxy; line, itemCount = line+1, itemCount+1 { if itemCount < count { item := t.merger.Get(itemCount + t.offset) - line = t.printItem(item, line, maxy, itemCount, itemCount == t.cy-t.offset, barRange) + current := itemCount == t.cy-t.offset + if current && (!t.raw || t.isItemMatch(item.item)) { + t.lastMatchingIndex = item.Index() + } + line = t.printItem(item, line, maxy, itemCount, current, barRange) } else if !t.prevLines[line].empty { t.renderEmptyLine(line, barRange) } @@ -3177,6 +3225,14 @@ func (t *Terminal) printBar(lineNum int, forceRedraw bool, barRange [2]int) bool func (t *Terminal) printItem(result Result, line int, maxLine int, index int, current bool, barRange [2]int) int { item := result.item + matched := true + var matchResult Result + if t.raw { + if matchResult, matched = t.matchMap[item.Index()]; matched { + result = matchResult + } + } + _, selected := t.selected[item.Index()] label := "" extraWidth := 0 @@ -3303,7 +3359,13 @@ func (t *Terminal) printItem(result Result, line int, maxLine int, index int, cu } return indentSize } - finalLineNum = t.printHighlighted(result, tui.ColCurrent, tui.ColCurrentMatch, true, true, line, maxLine, forceRedraw, preTask, postTask) + base := tui.ColCurrent + match := tui.ColCurrentMatch + if !matched { + base = base.WithFg(t.theme.Hidden) + match = match.WithFg(t.theme.Hidden) + } + finalLineNum = t.printHighlighted(result, base, match, true, true, line, maxLine, forceRedraw, preTask, postTask) } else { preTask := func(marker markerClass) int { w := t.window.Width() - t.pointerLen @@ -3337,6 +3399,10 @@ func (t *Terminal) printItem(result Result, line int, maxLine int, index int, cu base = base.WithBg(altBg) match = match.WithBg(altBg) } + if !matched { + base = base.WithFg(t.theme.Hidden) + match = match.WithFg(t.theme.Hidden) + } finalLineNum = t.printHighlighted(result, base, match, false, true, line, maxLine, forceRedraw, preTask, postTask) } for i := 0; i < t.gap && finalLineNum < maxLine; i++ { @@ -3389,8 +3455,8 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat item := result.item matchOffsets := []Offset{} var pos *[]int - if match && t.merger.pattern != nil { - _, matchOffsets, pos = t.merger.pattern.MatchItem(item, true, t.slab) + if match && t.resultMerger.pattern != nil { + _, matchOffsets, pos = t.resultMerger.pattern.MatchItem(item, true, t.slab) } charOffsets := matchOffsets if pos != nil { @@ -3422,7 +3488,7 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat } if !wholeCovered && t.nthAttr > 0 { var tokens []Token - if item.transformed != nil && item.transformed.revision == t.merger.revision { + if item.transformed != nil && item.transformed.revision == t.resultMerger.revision { tokens = item.transformed.tokens } else { tokens = Transform(Tokenize(item.text.ToString(), t.delimiter), t.nthCurrent) @@ -4670,6 +4736,33 @@ func (t *Terminal) currentItem() *Item { return nil } +func (t *Terminal) isCurrentItemMatch() bool { + cnt := t.merger.Length() + if t.cy >= 0 && cnt > 0 && cnt > t.cy { + if !t.raw { + return true + } + item := t.merger.Get(t.cy).item + return t.isItemMatch(item) + } + return false +} + +func (t *Terminal) isItemMatch(item *Item) bool { + _, matched := t.matchMap[item.Index()] + return matched +} + +func (t *Terminal) filterSelected() { + filtered := make(map[int32]selectedItem) + for k, v := range t.selected { + if t.isItemMatch(v.item) { + filtered[k] = v + } + } + t.selected = filtered +} + func (t *Terminal) buildPlusList(template string, forcePlus bool) (bool, [3][]*Item) { current := t.currentItem() slot, plus, asterisk, forceUpdate := hasPreviewFlags(template) @@ -4714,6 +4807,10 @@ func (t *Terminal) selectItem(item *Item) bool { if len(t.selected) >= t.multi { return false } + // TODO: Should we allow selecting non-matching items? + // if t.raw && !t.isItemMatch(item) { + // return false + // } if _, found := t.selected[item.Index()]; found { return true } @@ -5910,8 +6007,9 @@ func (t *Terminal) Loop() error { } case actSelectAll: if t.multi > 0 { - for i := 0; i < t.merger.Length(); i++ { - if !t.selectItem(t.merger.Get(i).item) { + // Limit the scope only to the matching items + for i := 0; i < t.resultMerger.Length(); i++ { + if !t.selectItem(t.resultMerger.Get(i).item) { break } } @@ -5919,8 +6017,10 @@ func (t *Terminal) Loop() error { } case actDeselectAll: if t.multi > 0 { - for i := 0; i < t.merger.Length() && len(t.selected) > 0; i++ { - t.deselectItem(t.merger.Get(i).item) + // Also limit the scope only to the matching items, while this may + // not be straightforward in raw mode. + for i := 0; i < t.resultMerger.Length() && len(t.selected) > 0; i++ { + t.deselectItem(t.resultMerger.Get(i).item) } req(reqList, reqInfo) } @@ -5948,17 +6048,17 @@ func (t *Terminal) Loop() error { case actToggleAll: if t.multi > 0 { prevIndexes := make(map[int]struct{}) - for i := 0; i < t.merger.Length() && len(t.selected) > 0; i++ { - item := t.merger.Get(i).item + for i := 0; i < t.resultMerger.Length() && len(t.selected) > 0; i++ { + item := t.resultMerger.Get(i).item if _, found := t.selected[item.Index()]; found { prevIndexes[i] = struct{}{} t.deselectItem(item) } } - for i := 0; i < t.merger.Length(); i++ { + for i := 0; i < t.resultMerger.Length(); i++ { if _, found := prevIndexes[i]; !found { - item := t.merger.Get(i).item + item := t.resultMerger.Get(i).item if !t.selectItem(item) { break } @@ -5986,19 +6086,77 @@ func (t *Terminal) Loop() error { t.vmove(1, true) req(reqList) } - case actDown: - t.vmove(-1, true) + case actDown, actDownMatch: + if t.raw && a.t == actDownMatch { + if t.resultMerger.Length() > 0 { + prevCy := t.cy + for t.vmove(-1, true) && !t.isCurrentItemMatch() { + } + if !t.isCurrentItemMatch() { + t.vset(prevCy) + } + } + } else { + t.vmove(-1, true) + } req(reqList) - case actUp: - t.vmove(1, true) + case actUp, actUpMatch: + if t.raw && a.t == actUpMatch { + if t.resultMerger.Length() > 0 { + prevCy := t.cy + for t.vmove(1, true) && !t.isCurrentItemMatch() { + } + if !t.isCurrentItemMatch() { + t.vset(prevCy) + } + } + } else { + t.vmove(1, true) + } + req(reqList) + case actToggleRaw: + t.raw = !t.raw + prevPos := t.cy - t.offset + prevIndex := t.currentIndex() + if t.raw { + // Build matchMap if not available + if len(t.matchMap) == 0 { + t.matchMap = t.resultMerger.ToMap() + } + t.merger = t.passMerger + } else { + t.merger = t.resultMerger + + // Need to remove non-matching items from the selection + if t.multi > 0 && len(t.selected) > 0 { + t.filterSelected() + req(reqInfo) + } + } + + // Try to retain position + if prevIndex != minItem.Index() { + i := t.merger.FindIndex(prevIndex) + if i >= 0 { + t.cy = i + } else { + t.cy = t.merger.FindIndex(t.lastMatchingIndex) + } + t.offset = t.cy - prevPos + } + + // List needs to be rerendered + t.forceRerenderList() req(reqList) case actAccept: req(reqClose) case actAcceptNonEmpty: + // TODO: Allow accepting unmatched items in raw mode? if len(t.selected) > 0 || t.merger.Length() > 0 || !t.reading && t.count == 0 { req(reqClose) } case actAcceptOrPrintQuery: + // TODO: Allow accepting unmatched items in raw mode? if len(t.selected) > 0 || t.merger.Length() > 0 { req(reqClose) } else { @@ -6834,7 +6992,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.merger.Revision()} + reloadRequest = &searchRequest{sort: t.sort, sync: reloadSync, nth: newNth, command: newCommand, environ: t.environ(), changed: changed, denylist: denylist, revision: t.resultMerger.Revision()} } // Dispatch queued background requests @@ -6954,7 +7112,8 @@ func (t *Terminal) constrain() { } } -func (t *Terminal) vmove(o int, allowCycle bool) { +// Returns true if the cursor position is successfully updated +func (t *Terminal) vmove(o int, allowCycle bool) bool { if t.layout != layoutDefault { o *= -1 } @@ -6971,7 +7130,7 @@ func (t *Terminal) vmove(o int, allowCycle bool) { } } } - t.vset(dest) + return t.vset(dest) } func (t *Terminal) vset(o int) bool { @@ -7038,9 +7197,9 @@ func (t *Terminal) dumpStatus(params getParams) string { selected[i] = t.dumpItem(selectedItems[i+params.offset].item) } - matches := make([]StatusItem, util.Max(0, util.Min(params.limit, t.merger.Length()-params.offset))) + matches := make([]StatusItem, util.Max(0, util.Min(params.limit, t.resultMerger.Length()-params.offset))) for i := range matches { - matches[i] = t.dumpItem(t.merger.Get(i + params.offset).item) + matches[i] = t.dumpItem(t.resultMerger.Get(i + params.offset).item) } var current *StatusItem @@ -7057,7 +7216,7 @@ func (t *Terminal) dumpStatus(params getParams) string { Position: t.cy, Sort: t.sort, TotalCount: t.count, - MatchCount: t.merger.Length(), + MatchCount: t.resultMerger.Length(), Current: current, Matches: matches, Selected: selected, diff --git a/src/tui/tui.go b/src/tui/tui.go index 106d6c86..ae9e8c49 100644 --- a/src/tui/tui.go +++ b/src/tui/tui.go @@ -381,6 +381,12 @@ func (p ColorPair) WithAttr(attr Attr) ColorPair { return dup } +func (p ColorPair) WithFg(fg ColorAttr) ColorPair { + dup := p + fgPair := ColorPair{fg.Color, colUndefined, fg.Attr} + return dup.Merge(fgPair) +} + func (p ColorPair) WithBg(bg ColorAttr) ColorPair { dup := p bgPair := ColorPair{colUndefined, bg.Color, bg.Attr} @@ -410,6 +416,7 @@ type ColorTheme struct { ListBg ColorAttr AltBg ColorAttr Nth ColorAttr + Hidden ColorAttr SelectedFg ColorAttr SelectedBg ColorAttr SelectedMatch ColorAttr @@ -866,6 +873,7 @@ func EmptyTheme() *ColorTheme { FooterLabel: ColorAttr{colUndefined, AttrUndefined}, GapLine: ColorAttr{colUndefined, AttrUndefined}, Nth: ColorAttr{colUndefined, AttrUndefined}, + Hidden: ColorAttr{colUndefined, Dim}, } } @@ -916,6 +924,7 @@ func NoColorTheme() *ColorTheme { FooterLabel: ColorAttr{colDefault, AttrUndefined}, GapLine: ColorAttr{colDefault, AttrUndefined}, Nth: ColorAttr{colUndefined, AttrUndefined}, + Hidden: ColorAttr{colUndefined, Dim}, } } @@ -967,6 +976,7 @@ func init() { FooterLabel: ColorAttr{colUndefined, AttrUndefined}, GapLine: ColorAttr{colUndefined, AttrUndefined}, Nth: ColorAttr{colUndefined, AttrUndefined}, + Hidden: ColorAttr{colUndefined, Dim}, } Dark256 = &ColorTheme{ Colored: true, @@ -1015,6 +1025,7 @@ func init() { FooterLabel: ColorAttr{colUndefined, AttrUndefined}, GapLine: ColorAttr{colUndefined, AttrUndefined}, Nth: ColorAttr{colUndefined, AttrUndefined}, + Hidden: ColorAttr{colUndefined, Dim}, } Light256 = &ColorTheme{ Colored: true, @@ -1063,6 +1074,7 @@ func init() { FooterLabel: ColorAttr{colUndefined, AttrUndefined}, GapLine: ColorAttr{colUndefined, AttrUndefined}, Nth: ColorAttr{colUndefined, AttrUndefined}, + Hidden: ColorAttr{colUndefined, Dim}, } } diff --git a/test/test_raw.rb b/test/test_raw.rb new file mode 100644 index 00000000..4e3c36ea --- /dev/null +++ b/test/test_raw.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require_relative 'lib/common' + +# Testing raw mode +class TestRaw < TestInteractive + def test_raw_mode + tmux.send_keys %(seq 1000 | #{FZF} --raw --bind ctrl-x:toggle-raw --gutter '▌' --multi), :Enter + tmux.until { assert_equal 1000, it.match_count } + + tmux.send_keys 1 + tmux.until { assert_equal 272, it.match_count } + + tmux.send_keys :Up + tmux.until { assert_includes it, '> 2' } + + tmux.send_keys 'C-p' + tmux.until do + assert_includes it, '> 10' + assert_includes it, '▖ 9' + end + + tmux.send_keys 'C-x' + tmux.until do + assert_includes it, '> 10' + assert_includes it, '▌ 1' + end + + tmux.send_keys :Up, 'C-x' + tmux.until do + assert_includes it, '> 11' + assert_includes it, '▖ 10' + end + + tmux.send_keys 1 + tmux.until { assert_equal 28, it.match_count } + + tmux.send_keys 'C-p' + tmux.until do + assert_includes it, '> 101' + assert_includes it, '▖ 100' + end + + tmux.send_keys 'C-n' + tmux.until do + assert_includes it, '> 11' + assert_includes it, '▖ 10' + end + + tmux.send_keys :Tab, :Tab, :Tab + tmux.until { assert_equal 3, it.select_count } + + tmux.send_keys 'C-x' + tmux.until do + assert_equal 1, it.select_count + assert_includes it, '▌ 110' + assert_includes it, '>>11' + end + end +end