mirror of
https://github.com/junegunn/fzf.git
synced 2025-11-14 14:23:47 -05:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3347d61591 | ||
|
|
9abf2c8c9c | ||
|
|
84e2262ad6 | ||
|
|
378137d34a | ||
|
|
66ca16f836 | ||
|
|
282884ad83 | ||
|
|
7877ac42f0 | ||
|
|
19ef8891e3 | ||
|
|
bfea9e53a6 | ||
|
|
a2420026ab | ||
|
|
1be1991299 | ||
|
|
67dd7e1923 | ||
|
|
2b584586ed | ||
|
|
a1994ff0ab | ||
|
|
ca0e858871 | ||
|
|
06c6615507 | ||
|
|
818d0be436 | ||
|
|
fcd2baa945 | ||
|
|
62e0a2824a |
35
CHANGELOG.md
35
CHANGELOG.md
@@ -1,6 +1,41 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
0.60.0
|
||||
------
|
||||
_Release highlights: https://junegunn.github.io/fzf/releases/0.60.0/_
|
||||
|
||||
- Added `--accept-nth` for choosing output fields
|
||||
```sh
|
||||
ps -ef | fzf --multi --header-lines 1 | awk '{print $2}'
|
||||
# Becomes
|
||||
ps -ef | fzf --multi --header-lines 1 --accept-nth 2
|
||||
|
||||
git branch | fzf | cut -c3-
|
||||
# Can be rewritten as
|
||||
git branch | fzf --accept-nth -1
|
||||
```
|
||||
- `--accept-nth` and `--with-nth` now support a template that includes multiple field index expressions in curly braces
|
||||
```sh
|
||||
echo foo,bar,baz | fzf --delimiter , --accept-nth '{1}, {3}, {2}'
|
||||
# foo, baz, bar
|
||||
|
||||
echo foo,bar,baz | fzf --delimiter , --with-nth '{1},{3},{2},{1..2}'
|
||||
# foo,baz,bar,foo,bar
|
||||
```
|
||||
- Added `exclude` and `exclude-multi` actions for dynamically excluding items
|
||||
```sh
|
||||
seq 100 | fzf --bind 'ctrl-x:exclude'
|
||||
|
||||
# 'exclude-multi' will exclude the selected items or the current item
|
||||
seq 100 | fzf --multi --bind 'ctrl-x:exclude-multi'
|
||||
```
|
||||
- Preview window now prints wrap indicator when wrapping is enabled
|
||||
```sh
|
||||
seq 100 | xargs | fzf --wrap --preview 'echo {}' --preview-window wrap
|
||||
```
|
||||
- Bug fixes and improvements
|
||||
|
||||
0.59.0
|
||||
------
|
||||
_Release highlights: https://junegunn.github.io/fzf/releases/0.59.0/_
|
||||
|
||||
@@ -57,15 +57,15 @@ elif ! [[ $KITTY_WINDOW_ID ]] && (( FZF_PREVIEW_TOP + FZF_PREVIEW_LINES == $(stt
|
||||
dim=${FZF_PREVIEW_COLUMNS}x$((FZF_PREVIEW_LINES - 1))
|
||||
fi
|
||||
|
||||
# 1. Use kitty icat on kitty terminal
|
||||
if [[ $KITTY_WINDOW_ID ]]; then
|
||||
# 1. Use icat (from Kitty) if kitten is installed
|
||||
if [[ $KITTY_WINDOW_ID ]] || [[ $GHOSTTY_RESOURCES_DIR ]] && command -v kitten > /dev/null; then
|
||||
# 1. 'memory' is the fastest option but if you want the image to be scrollable,
|
||||
# you have to use 'stream'.
|
||||
#
|
||||
# 2. The last line of the output is the ANSI reset code without newline.
|
||||
# This confuses fzf and makes it render scroll offset indicator.
|
||||
# So we remove the last line and append the reset code to its previous line.
|
||||
kitty icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed '$d' | sed $'$s/$/\e[m/'
|
||||
kitten icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed '$d' | sed $'$s/$/\e[m/'
|
||||
|
||||
# 2. Use chafa with Sixel output
|
||||
elif command -v chafa > /dev/null; then
|
||||
|
||||
4
go.mod
4
go.mod
@@ -6,8 +6,8 @@ require (
|
||||
github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741
|
||||
github.com/mattn/go-isatty v0.0.20
|
||||
github.com/rivo/uniseg v0.4.7
|
||||
golang.org/x/sys v0.29.0
|
||||
golang.org/x/term v0.28.0
|
||||
golang.org/x/sys v0.30.0
|
||||
golang.org/x/term v0.29.0
|
||||
)
|
||||
|
||||
require (
|
||||
|
||||
6
go.sum
6
go.sum
@@ -54,8 +54,9 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
@@ -64,8 +65,9 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
|
||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
|
||||
2
install
2
install
@@ -2,7 +2,7 @@
|
||||
|
||||
set -u
|
||||
|
||||
version=0.59.0
|
||||
version=0.60.0
|
||||
auto_completion=
|
||||
key_bindings=
|
||||
update_config=2
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
$version="0.59.0"
|
||||
$version="0.60.0"
|
||||
|
||||
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||
|
||||
|
||||
2
main.go
2
main.go
@@ -11,7 +11,7 @@ import (
|
||||
"github.com/junegunn/fzf/src/protector"
|
||||
)
|
||||
|
||||
var version = "0.59"
|
||||
var version = "0.60"
|
||||
var revision = "devel"
|
||||
|
||||
//go:embed shell/key-bindings.bash
|
||||
|
||||
@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
..
|
||||
.TH fzf\-tmux 1 "Feb 2025" "fzf 0.59.0" "fzf\-tmux - open fzf in tmux split pane"
|
||||
.TH fzf\-tmux 1 "Feb 2025" "fzf 0.60.0" "fzf\-tmux - open fzf in tmux split pane"
|
||||
|
||||
.SH NAME
|
||||
fzf\-tmux - open fzf in tmux split pane
|
||||
|
||||
@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
..
|
||||
.TH fzf 1 "Feb 2025" "fzf 0.59.0" "fzf - a command-line fuzzy finder"
|
||||
.TH fzf 1 "Feb 2025" "fzf 0.60.0" "fzf - a command-line fuzzy finder"
|
||||
|
||||
.SH NAME
|
||||
fzf - a command-line fuzzy finder
|
||||
@@ -117,8 +117,33 @@ transformed lines (unlike in \fB\-\-preview\fR where fields are extracted from
|
||||
the original lines) because fzf doesn't allow searching against the hidden
|
||||
fields.
|
||||
.TP
|
||||
.BI "\-\-with\-nth=" "N[,..]"
|
||||
Transform the presentation of each line using field index expressions
|
||||
.BI "\-\-with\-nth=" "N[,..] or TEMPLATE"
|
||||
Transform the presentation of each line using the field index expressions.
|
||||
For advanced transformation, you can provide a template containing field index
|
||||
expressions in curly braces.
|
||||
|
||||
.RS
|
||||
e.g.
|
||||
# Single expression: drop the first field
|
||||
echo foo bar baz | fzf --with-nth 2..
|
||||
|
||||
# Use template to rearrange fields
|
||||
echo foo,bar,baz | fzf --delimiter , --with-nth '{1},{3},{2},{1..2}'
|
||||
.RE
|
||||
.TP
|
||||
.BI "\-\-accept\-nth=" "N[,..] or TEMPLATE"
|
||||
Define which fields to print on accept. The last delimiter is stripped from the
|
||||
output. For advanced transformation, you can provide a template containing
|
||||
field index expressions in curly braces.
|
||||
|
||||
.RS
|
||||
e.g.
|
||||
# Single expression
|
||||
echo foo bar baz | fzf --accept-nth 2
|
||||
|
||||
# Template
|
||||
echo foo bar baz | fzf --accept-nth '1st: {1}, 2nd: {2}, 3rd: {3}'
|
||||
.RE
|
||||
.TP
|
||||
.B "+s, \-\-no\-sort"
|
||||
Do not sort the result
|
||||
@@ -1597,6 +1622,8 @@ A key or an event can be bound to one or more of the following actions.
|
||||
\fBdown\fR \fIctrl\-j ctrl\-n down\fR
|
||||
\fBenable\-search\fR (enable search functionality)
|
||||
\fBend\-of\-line\fR \fIctrl\-e end\fR
|
||||
\fBexclude\fR (exclude the current item from the result)
|
||||
\fBexclude\-multi\fR (exclude the selected items or the current item from the result)
|
||||
\fBexecute(...)\fR (see below for the details)
|
||||
\fBexecute\-silent(...)\fR (see below for the details)
|
||||
\fBfirst\fR (move to the first match; same as \fBpos(1)\fR)
|
||||
|
||||
@@ -154,8 +154,8 @@ function fzf_key_bindings
|
||||
# eval is used to do shell expansion on paths
|
||||
eval set commandline $commandline
|
||||
|
||||
# Combine multiple consecutive slashes into one
|
||||
set commandline (string replace -r -a -- '/+' '/' $commandline)
|
||||
# Combine multiple consecutive slashes into one, and unescape.
|
||||
set commandline (string replace -r -a -- '/+' '/' $commandline | string unescape -n)
|
||||
|
||||
if test -z "$commandline"
|
||||
# Default to current directory with no --query
|
||||
@@ -176,9 +176,9 @@ function fzf_key_bindings
|
||||
end
|
||||
end
|
||||
|
||||
echo (string escape -- $dir)
|
||||
echo (string escape -- $fzf_query)
|
||||
echo $prefix
|
||||
echo -- $dir
|
||||
string escape -- $fzf_query
|
||||
echo -- $prefix
|
||||
end
|
||||
|
||||
function __fzf_get_dir -d 'Find the longest existing filepath from input string'
|
||||
@@ -194,7 +194,7 @@ function fzf_key_bindings
|
||||
set dir (dirname -- "$dir")
|
||||
end
|
||||
|
||||
echo $dir
|
||||
string escape -n -- $dir
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -138,11 +138,13 @@ func _() {
|
||||
_ = x[actShowHeader-127]
|
||||
_ = x[actHideHeader-128]
|
||||
_ = x[actBell-129]
|
||||
_ = x[actExclude-130]
|
||||
_ = x[actExcludeMulti-131]
|
||||
}
|
||||
|
||||
const _actionType_name = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeListLabelactChangeInputLabelactChangeHeaderactChangeHeaderLabelactChangeMultiactChangePreviewLabelactChangePromptactChangeQueryactChangeNthactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleMultiLineactToggleHscrollactTrackCurrentactToggleInputactHideInputactShowInputactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformListLabelactTransformInputLabelactTransformHeaderactTransformHeaderLabelactTransformNthactTransformPreviewLabelactTransformPromptactTransformQueryactTransformSearchactSearchactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactToggleBindactBecomeactShowHeaderactHideHeaderactBell"
|
||||
const _actionType_name = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeListLabelactChangeInputLabelactChangeHeaderactChangeHeaderLabelactChangeMultiactChangePreviewLabelactChangePromptactChangeQueryactChangeNthactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleMultiLineactToggleHscrollactTrackCurrentactToggleInputactHideInputactShowInputactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformListLabelactTransformInputLabelactTransformHeaderactTransformHeaderLabelactTransformNthactTransformPreviewLabelactTransformPromptactTransformQueryactTransformSearchactSearchactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactToggleBindactBecomeactShowHeaderactHideHeaderactBellactExcludeactExcludeMulti"
|
||||
|
||||
var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 42, 50, 68, 76, 85, 102, 123, 138, 159, 183, 198, 207, 227, 245, 264, 279, 299, 313, 334, 349, 363, 375, 389, 402, 419, 427, 440, 456, 468, 476, 490, 504, 515, 526, 544, 561, 568, 587, 599, 613, 622, 637, 649, 662, 673, 684, 696, 710, 731, 746, 759, 777, 793, 808, 822, 834, 846, 863, 870, 875, 884, 895, 906, 919, 934, 945, 958, 973, 980, 993, 1006, 1023, 1038, 1051, 1065, 1079, 1095, 1115, 1127, 1150, 1171, 1193, 1211, 1234, 1249, 1273, 1291, 1308, 1326, 1335, 1345, 1361, 1383, 1396, 1412, 1424, 1438, 1454, 1472, 1492, 1514, 1528, 1543, 1551, 1557, 1571, 1586, 1596, 1612, 1627, 1637, 1645, 1652, 1661, 1674, 1690, 1705, 1714, 1725, 1734, 1743, 1756, 1765, 1778, 1791, 1798}
|
||||
var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 42, 50, 68, 76, 85, 102, 123, 138, 159, 183, 198, 207, 227, 245, 264, 279, 299, 313, 334, 349, 363, 375, 389, 402, 419, 427, 440, 456, 468, 476, 490, 504, 515, 526, 544, 561, 568, 587, 599, 613, 622, 637, 649, 662, 673, 684, 696, 710, 731, 746, 759, 777, 793, 808, 822, 834, 846, 863, 870, 875, 884, 895, 906, 919, 934, 945, 958, 973, 980, 993, 1006, 1023, 1038, 1051, 1065, 1079, 1095, 1115, 1127, 1150, 1171, 1193, 1211, 1234, 1249, 1273, 1291, 1308, 1326, 1335, 1345, 1361, 1383, 1396, 1412, 1424, 1438, 1454, 1472, 1492, 1514, 1528, 1543, 1551, 1557, 1571, 1586, 1596, 1612, 1627, 1637, 1645, 1652, 1661, 1674, 1690, 1705, 1714, 1725, 1734, 1743, 1756, 1765, 1778, 1791, 1798, 1808, 1823}
|
||||
|
||||
func (i actionType) String() string {
|
||||
if i < 0 || i >= actionType(len(_actionType_index)-1) {
|
||||
|
||||
55
src/core.go
55
src/core.go
@@ -96,7 +96,7 @@ func Run(opts *Options) (int, error) {
|
||||
var chunkList *ChunkList
|
||||
var itemIndex int32
|
||||
header := make([]string, 0, opts.HeaderLines)
|
||||
if len(opts.WithNth) == 0 {
|
||||
if opts.WithNth == nil {
|
||||
chunkList = NewChunkList(cache, func(item *Item, data []byte) bool {
|
||||
if len(header) < opts.HeaderLines {
|
||||
header = append(header, byteString(data))
|
||||
@@ -109,6 +109,7 @@ func Run(opts *Options) (int, error) {
|
||||
return true
|
||||
})
|
||||
} else {
|
||||
nthTransformer := opts.WithNth(opts.Delimiter)
|
||||
chunkList = NewChunkList(cache, func(item *Item, data []byte) bool {
|
||||
tokens := Tokenize(byteString(data), opts.Delimiter)
|
||||
if opts.Ansi && opts.Theme.Colored && len(tokens) > 1 {
|
||||
@@ -127,15 +128,13 @@ func Run(opts *Options) (int, error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
trans := Transform(tokens, opts.WithNth)
|
||||
transformed := joinTokens(trans)
|
||||
transformed := nthTransformer(tokens)
|
||||
if len(header) < opts.HeaderLines {
|
||||
header = append(header, transformed)
|
||||
eventBox.Set(EvtHeader, header)
|
||||
return false
|
||||
}
|
||||
item.text, item.colors = ansiProcessor(stringBytes(transformed))
|
||||
item.text.TrimTrailingWhitespaces()
|
||||
item.text.Index = itemIndex
|
||||
item.origText = &data
|
||||
itemIndex++
|
||||
@@ -195,15 +194,30 @@ func Run(opts *Options) (int, error) {
|
||||
}
|
||||
|
||||
nth := opts.Nth
|
||||
nthRevision := 0
|
||||
patternCache := make(map[string]*Pattern)
|
||||
patternBuilder := func(runes []rune) *Pattern {
|
||||
return BuildPattern(cache, patternCache,
|
||||
opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, withPos,
|
||||
opts.Filter == nil, nth, opts.Delimiter, nthRevision, runes)
|
||||
}
|
||||
inputRevision := revision{}
|
||||
snapshotRevision := revision{}
|
||||
patternCache := make(map[string]*Pattern)
|
||||
denyMutex := sync.Mutex{}
|
||||
denylist := make(map[int32]struct{})
|
||||
clearDenylist := func() {
|
||||
denyMutex.Lock()
|
||||
if len(denylist) > 0 {
|
||||
patternCache = make(map[string]*Pattern)
|
||||
}
|
||||
denylist = make(map[int32]struct{})
|
||||
denyMutex.Unlock()
|
||||
}
|
||||
patternBuilder := func(runes []rune) *Pattern {
|
||||
denyMutex.Lock()
|
||||
denylistCopy := make(map[int32]struct{})
|
||||
for k, v := range denylist {
|
||||
denylistCopy[k] = v
|
||||
}
|
||||
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)
|
||||
}
|
||||
matcher := NewMatcher(cache, patternBuilder, sort, opts.Tac, eventBox, inputRevision)
|
||||
|
||||
// Filtering mode
|
||||
@@ -302,6 +316,9 @@ func Run(opts *Options) (int, error) {
|
||||
var snapshot []*Chunk
|
||||
var count int
|
||||
restart := func(command commandSpec, environ []string) {
|
||||
if !useSnapshot {
|
||||
clearDenylist()
|
||||
}
|
||||
reading = true
|
||||
chunkList.Clear()
|
||||
itemIndex = 0
|
||||
@@ -348,7 +365,8 @@ func Run(opts *Options) (int, error) {
|
||||
} else {
|
||||
reading = reading && evt == EvtReadNew
|
||||
}
|
||||
if useSnapshot && evt == EvtReadFin {
|
||||
if useSnapshot && evt == EvtReadFin { // reload-sync
|
||||
clearDenylist()
|
||||
useSnapshot = false
|
||||
}
|
||||
if !useSnapshot {
|
||||
@@ -379,10 +397,21 @@ func Run(opts *Options) (int, error) {
|
||||
command = val.command
|
||||
environ = val.environ
|
||||
changed = val.changed
|
||||
bump := false
|
||||
if len(val.denylist) > 0 && val.revision.compatible(inputRevision) {
|
||||
denyMutex.Lock()
|
||||
for _, itemIndex := range val.denylist {
|
||||
denylist[itemIndex] = struct{}{}
|
||||
}
|
||||
denyMutex.Unlock()
|
||||
bump = true
|
||||
}
|
||||
if val.nth != nil {
|
||||
// Change nth and clear caches
|
||||
nth = *val.nth
|
||||
nthRevision++
|
||||
bump = true
|
||||
}
|
||||
if bump {
|
||||
patternCache = make(map[string]*Pattern)
|
||||
cache.Clear()
|
||||
inputRevision.bumpMinor()
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
type transformed struct {
|
||||
// Because nth can be changed dynamically by change-nth action, we need to
|
||||
// keep the revision number at the time of transformation.
|
||||
revision int
|
||||
revision revision
|
||||
tokens []Token
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ Usage: fzf [options]
|
||||
integer or a range expression ([BEGIN]..[END]).
|
||||
--with-nth=N[,..] Transform the presentation of each line using
|
||||
field index expressions
|
||||
--accept-nth=N[,..] Define which fields to print on accept
|
||||
-d, --delimiter=STR Field delimiter regex (default: AWK-style)
|
||||
+s, --no-sort Do not sort the result
|
||||
--literal Do not normalize latin script letters
|
||||
@@ -543,7 +544,8 @@ type Options struct {
|
||||
Case Case
|
||||
Normalize bool
|
||||
Nth []Range
|
||||
WithNth []Range
|
||||
WithNth func(Delimiter) func([]Token) string
|
||||
AcceptNth func(Delimiter) func([]Token) string
|
||||
Delimiter Delimiter
|
||||
Sort int
|
||||
Track trackOption
|
||||
@@ -665,7 +667,6 @@ func defaultOptions() *Options {
|
||||
Case: CaseSmart,
|
||||
Normalize: true,
|
||||
Nth: make([]Range, 0),
|
||||
WithNth: make([]Range, 0),
|
||||
Delimiter: Delimiter{},
|
||||
Sort: 1000,
|
||||
Track: trackDisabled,
|
||||
@@ -768,6 +769,62 @@ func splitNth(str string) ([]Range, error) {
|
||||
return ranges, nil
|
||||
}
|
||||
|
||||
func nthTransformer(str string) (func(Delimiter) func([]Token) string, error) {
|
||||
// ^[0-9,-.]+$"
|
||||
if match, _ := regexp.MatchString("^[0-9,-.]+$", str); match {
|
||||
nth, err := splitNth(str)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return func(Delimiter) func([]Token) string {
|
||||
return func(tokens []Token) string {
|
||||
return JoinTokens(Transform(tokens, nth))
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
// {...} {...} ...
|
||||
placeholder := regexp.MustCompile("{[0-9,-.]+}")
|
||||
indexes := placeholder.FindAllStringIndex(str, -1)
|
||||
if indexes == nil {
|
||||
return nil, errors.New("template should include at least 1 placeholder: " + str)
|
||||
}
|
||||
|
||||
type NthParts struct {
|
||||
str string
|
||||
nth []Range
|
||||
}
|
||||
|
||||
parts := make([]NthParts, len(indexes))
|
||||
idx := 0
|
||||
for _, index := range indexes {
|
||||
if idx < index[0] {
|
||||
parts = append(parts, NthParts{str: str[idx:index[0]]})
|
||||
}
|
||||
if nth, err := splitNth(str[index[0]+1 : index[1]-1]); err == nil {
|
||||
parts = append(parts, NthParts{nth: nth})
|
||||
}
|
||||
idx = index[1]
|
||||
}
|
||||
if idx < len(str) {
|
||||
parts = append(parts, NthParts{str: str[idx:]})
|
||||
}
|
||||
|
||||
return func(delimiter Delimiter) func([]Token) string {
|
||||
return func(tokens []Token) string {
|
||||
str := ""
|
||||
for _, holder := range parts {
|
||||
if holder.nth != nil {
|
||||
str += StripLastDelimiter(JoinTokens(Transform(tokens, holder.nth)), delimiter)
|
||||
} else {
|
||||
str += holder.str
|
||||
}
|
||||
}
|
||||
return str
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func delimiterRegexp(str string) Delimiter {
|
||||
// Special handling of \t
|
||||
str = strings.ReplaceAll(str, "\\t", "\t")
|
||||
@@ -1600,6 +1657,10 @@ func parseActionList(masked string, original string, prevActions []*action, putA
|
||||
}
|
||||
case "bell":
|
||||
appendAction(actBell)
|
||||
case "exclude":
|
||||
appendAction(actExclude)
|
||||
case "exclude-multi":
|
||||
appendAction(actExcludeMulti)
|
||||
default:
|
||||
t := isExecuteAction(specLower)
|
||||
if t == actIgnore {
|
||||
@@ -2380,7 +2441,15 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if opts.WithNth, err = splitNth(str); err != nil {
|
||||
if opts.WithNth, err = nthTransformer(str); err != nil {
|
||||
return err
|
||||
}
|
||||
case "--accept-nth":
|
||||
str, err := nextString("nth expression required")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if opts.AcceptNth, err = nthTransformer(str); err != nil {
|
||||
return err
|
||||
}
|
||||
case "-s", "--sort":
|
||||
|
||||
@@ -60,9 +60,10 @@ type Pattern struct {
|
||||
cacheKey string
|
||||
delimiter Delimiter
|
||||
nth []Range
|
||||
revision int
|
||||
revision revision
|
||||
procFun map[termType]algo.Algo
|
||||
cache *ChunkCache
|
||||
denylist map[int32]struct{}
|
||||
}
|
||||
|
||||
var _splitRegex *regexp.Regexp
|
||||
@@ -73,7 +74,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 int, runes []rune) *Pattern {
|
||||
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, revision revision, runes []rune, denylist map[int32]struct{}) *Pattern {
|
||||
|
||||
var asString string
|
||||
if extended {
|
||||
@@ -144,6 +145,7 @@ func BuildPattern(cache *ChunkCache, patternCache map[string]*Pattern, fuzzy boo
|
||||
revision: revision,
|
||||
delimiter: delimiter,
|
||||
cache: cache,
|
||||
denylist: denylist,
|
||||
procFun: make(map[termType]algo.Algo)}
|
||||
|
||||
ptr.cacheKey = ptr.buildCacheKey()
|
||||
@@ -243,6 +245,9 @@ func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet
|
||||
|
||||
// IsEmpty returns true if the pattern is effectively empty
|
||||
func (p *Pattern) IsEmpty() bool {
|
||||
if len(p.denylist) > 0 {
|
||||
return false
|
||||
}
|
||||
if !p.extended {
|
||||
return len(p.text) == 0
|
||||
}
|
||||
@@ -296,6 +301,8 @@ func (p *Pattern) Match(chunk *Chunk, slab *util.Slab) []Result {
|
||||
func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Result {
|
||||
matches := []Result{}
|
||||
|
||||
if len(p.denylist) == 0 {
|
||||
// Huge code duplication for minimizing unnecessary map lookups
|
||||
if space == nil {
|
||||
for idx := 0; idx < chunk.count; idx++ {
|
||||
if match, _, _ := p.MatchItem(&chunk.items[idx], p.withPos, slab); match != nil {
|
||||
@@ -310,6 +317,28 @@ func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Re
|
||||
}
|
||||
}
|
||||
return matches
|
||||
}
|
||||
|
||||
if space == nil {
|
||||
for idx := 0; idx < chunk.count; idx++ {
|
||||
if _, prs := p.denylist[chunk.items[idx].Index()]; prs {
|
||||
continue
|
||||
}
|
||||
if match, _, _ := p.MatchItem(&chunk.items[idx], p.withPos, slab); match != nil {
|
||||
matches = append(matches, *match)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, result := range space {
|
||||
if _, prs := p.denylist[result.item.Index()]; prs {
|
||||
continue
|
||||
}
|
||||
if match, _, _ := p.MatchItem(result.item, p.withPos, slab); match != nil {
|
||||
matches = append(matches, *match)
|
||||
}
|
||||
}
|
||||
}
|
||||
return matches
|
||||
}
|
||||
|
||||
// MatchItem returns true if the Item is a match
|
||||
@@ -403,6 +432,13 @@ func (p *Pattern) transformInput(item *Item) []Token {
|
||||
|
||||
tokens := Tokenize(item.text.ToString(), p.delimiter)
|
||||
ret := Transform(tokens, p.nth)
|
||||
// Strip the last delimiter to allow suffix match
|
||||
if len(ret) > 0 && !p.delimiter.IsAwk() {
|
||||
chars := ret[len(ret)-1].text
|
||||
stripped := StripLastDelimiter(chars.ToString(), p.delimiter)
|
||||
newChars := util.ToChars(stringBytes(stripped))
|
||||
ret[len(ret)-1].text = &newChars
|
||||
}
|
||||
item.transformed = &transformed{p.revision, ret}
|
||||
return ret
|
||||
}
|
||||
|
||||
@@ -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, 0, runes)
|
||||
withPos, cacheable, nth, delimiter, revision{}, runes, nil)
|
||||
}
|
||||
|
||||
func TestExact(t *testing.T) {
|
||||
|
||||
166
src/terminal.go
166
src/terminal.go
@@ -305,6 +305,7 @@ type Terminal struct {
|
||||
nthAttr tui.Attr
|
||||
nth []Range
|
||||
nthCurrent []Range
|
||||
acceptNth func([]Token) string
|
||||
tabstop int
|
||||
margin [4]sizeSpec
|
||||
padding [4]sizeSpec
|
||||
@@ -390,6 +391,12 @@ type Terminal struct {
|
||||
clickHeaderLine int
|
||||
clickHeaderColumn int
|
||||
proxyScript string
|
||||
numLinesCache map[int32]numLinesCacheValue
|
||||
}
|
||||
|
||||
type numLinesCacheValue struct {
|
||||
atMost int
|
||||
numLines int
|
||||
}
|
||||
|
||||
type selectedItem struct {
|
||||
@@ -577,6 +584,8 @@ const (
|
||||
actShowHeader
|
||||
actHideHeader
|
||||
actBell
|
||||
actExclude
|
||||
actExcludeMulti
|
||||
)
|
||||
|
||||
func (a actionType) Name() string {
|
||||
@@ -620,6 +629,8 @@ type searchRequest struct {
|
||||
command *commandSpec
|
||||
environ []string
|
||||
changed bool
|
||||
denylist []int32
|
||||
revision revision
|
||||
}
|
||||
|
||||
type previewRequest struct {
|
||||
@@ -947,7 +958,11 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
|
||||
initFunc: func() error { return renderer.Init() },
|
||||
executing: util.NewAtomicBool(false),
|
||||
lastAction: actStart,
|
||||
lastFocus: minItem.Index()}
|
||||
lastFocus: minItem.Index(),
|
||||
numLinesCache: make(map[int32]numLinesCacheValue)}
|
||||
if opts.AcceptNth != nil {
|
||||
t.acceptNth = opts.AcceptNth(t.delimiter)
|
||||
}
|
||||
|
||||
// This should be called before accessing tui.Color*
|
||||
tui.InitTheme(opts.Theme, renderer.DefaultTheme(), opts.Black, opts.InputBorderShape.Visible(), opts.HeaderBorderShape.Visible())
|
||||
@@ -1318,6 +1333,10 @@ func (t *Terminal) wrapCols() int {
|
||||
return util.Max(t.window.Width()-(t.pointerLen+t.markerLen+1), 1)
|
||||
}
|
||||
|
||||
func (t *Terminal) clearNumLinesCache() {
|
||||
t.numLinesCache = make(map[int32]numLinesCacheValue)
|
||||
}
|
||||
|
||||
// Number of lines the item takes including the gap
|
||||
func (t *Terminal) numItemLines(item *Item, atMost int) (int, bool) {
|
||||
var numLines int
|
||||
@@ -1325,6 +1344,12 @@ func (t *Terminal) numItemLines(item *Item, atMost int) (int, bool) {
|
||||
numLines = 1 + t.gap
|
||||
return numLines, numLines > atMost
|
||||
}
|
||||
if cached, prs := t.numLinesCache[item.Index()]; prs {
|
||||
// Can we use this cache? Let's be conservative.
|
||||
if cached.atMost >= atMost {
|
||||
return cached.numLines, false
|
||||
}
|
||||
}
|
||||
var overflow bool
|
||||
if !t.wrap && t.multiLine {
|
||||
numLines, overflow = item.text.NumLines(atMost)
|
||||
@@ -1334,6 +1359,9 @@ func (t *Terminal) numItemLines(item *Item, atMost int) (int, bool) {
|
||||
numLines = len(lines)
|
||||
}
|
||||
numLines += t.gap
|
||||
if !overflow {
|
||||
t.numLinesCache[item.Index()] = numLinesCacheValue{atMost, numLines}
|
||||
}
|
||||
return numLines, overflow || numLines > atMost
|
||||
}
|
||||
|
||||
@@ -1461,6 +1489,7 @@ func (t *Terminal) UpdateList(merger *Merger) {
|
||||
if !t.revision.compatible(newRevision) {
|
||||
// Reloaded: clear selection
|
||||
t.selected = make(map[int32]selectedItem)
|
||||
t.clearNumLinesCache()
|
||||
} else {
|
||||
// Trimmed by --tail: filter selection by index
|
||||
filtered := make(map[int32]selectedItem)
|
||||
@@ -1540,16 +1569,26 @@ func (t *Terminal) output() bool {
|
||||
for _, s := range t.printQueue {
|
||||
t.printer(s)
|
||||
}
|
||||
transform := func(item *Item) string {
|
||||
return item.AsString(t.ansi)
|
||||
}
|
||||
if t.acceptNth != nil {
|
||||
transform = func(item *Item) string {
|
||||
tokens := Tokenize(item.AsString(t.ansi), t.delimiter)
|
||||
transformed := t.acceptNth(tokens)
|
||||
return StripLastDelimiter(transformed, t.delimiter)
|
||||
}
|
||||
}
|
||||
found := len(t.selected) > 0
|
||||
if !found {
|
||||
current := t.currentItem()
|
||||
if current != nil {
|
||||
t.printer(current.AsString(t.ansi))
|
||||
t.printer(transform(current))
|
||||
found = true
|
||||
}
|
||||
} else {
|
||||
for _, sel := range t.sortSelected() {
|
||||
t.printer(sel.item.AsString(t.ansi))
|
||||
t.printer(transform(sel.item))
|
||||
}
|
||||
}
|
||||
return found
|
||||
@@ -1712,6 +1751,7 @@ func (t *Terminal) hasHeaderLinesWindow() bool {
|
||||
}
|
||||
|
||||
func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
|
||||
t.clearNumLinesCache()
|
||||
t.forcePreview = forcePreview
|
||||
screenWidth, screenHeight, marginInt, paddingInt := t.adjustMarginAndPadding()
|
||||
width := screenWidth - marginInt[1] - marginInt[3]
|
||||
@@ -1900,6 +1940,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
|
||||
pwidth -= 1
|
||||
}
|
||||
t.pwindow = t.tui.NewWindow(y, x, pwidth, pheight, tui.WindowPreview, noBorder, true)
|
||||
t.pwindow.SetWrapSign(t.wrapSign, t.wrapSignWidth)
|
||||
if !hadPreviewWindow {
|
||||
t.pwindow.Erase()
|
||||
}
|
||||
@@ -2932,7 +2973,7 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
|
||||
}
|
||||
if !wholeCovered && t.nthAttr > 0 {
|
||||
var tokens []Token
|
||||
if item.transformed != nil {
|
||||
if item.transformed != nil && item.transformed.revision == t.merger.revision {
|
||||
tokens = item.transformed.tokens
|
||||
} else {
|
||||
tokens = Transform(Tokenize(item.text.ToString(), t.delimiter), t.nthCurrent)
|
||||
@@ -3825,7 +3866,7 @@ func replacePlaceholder(params replacePlaceholderParams) (string, []string) {
|
||||
elems, prefixLength := awkTokenizer(params.query)
|
||||
tokens := withPrefixLengths(elems, prefixLength)
|
||||
trans := Transform(tokens, nth)
|
||||
result := joinTokens(trans)
|
||||
result := JoinTokens(trans)
|
||||
if !flags.preserveSpace {
|
||||
result = strings.TrimSpace(result)
|
||||
}
|
||||
@@ -3875,7 +3916,7 @@ func replacePlaceholder(params replacePlaceholderParams) (string, []string) {
|
||||
replace = func(item *Item) string {
|
||||
tokens := Tokenize(item.AsString(params.stripAnsi), params.delimiter)
|
||||
trans := Transform(tokens, ranges)
|
||||
str := joinTokens(trans)
|
||||
str := JoinTokens(trans)
|
||||
|
||||
// trim the last delimiter
|
||||
if params.delimiter.str != nil {
|
||||
@@ -4719,6 +4760,7 @@ func (t *Terminal) Loop() error {
|
||||
changed := false
|
||||
beof := false
|
||||
queryChanged := false
|
||||
denylist := []int32{}
|
||||
|
||||
// Special handling of --sync. Activate the interface on the second tick.
|
||||
if loopIndex == 1 && t.deferActivation() {
|
||||
@@ -4875,6 +4917,27 @@ func (t *Terminal) Loop() error {
|
||||
}
|
||||
case actBell:
|
||||
t.tui.Bell()
|
||||
case actExcludeMulti:
|
||||
if len(t.selected) > 0 {
|
||||
for _, item := range t.sortSelected() {
|
||||
denylist = append(denylist, item.item.Index())
|
||||
}
|
||||
// Clear selected items
|
||||
t.selected = make(map[int32]selectedItem)
|
||||
t.version++
|
||||
} else {
|
||||
item := t.currentItem()
|
||||
if item != nil {
|
||||
denylist = append(denylist, item.Index())
|
||||
}
|
||||
}
|
||||
changed = true
|
||||
case actExclude:
|
||||
if item := t.currentItem(); item != nil {
|
||||
denylist = append(denylist, item.Index())
|
||||
t.deselectItem(item)
|
||||
changed = true
|
||||
}
|
||||
case actExecute, actExecuteSilent:
|
||||
t.executeCommand(a.a, false, a.t == actExecuteSilent, false, false, "")
|
||||
case actExecuteMulti:
|
||||
@@ -5021,34 +5084,52 @@ func (t *Terminal) Loop() error {
|
||||
} else {
|
||||
req(reqHeader)
|
||||
}
|
||||
case actChangeHeaderLabel:
|
||||
t.headerLabelOpts.label = a.a
|
||||
if t.headerBorder != nil {
|
||||
t.headerLabel, t.headerLabelLen = t.ansiLabelPrinter(a.a, &tui.ColHeaderLabel, false)
|
||||
req(reqRedrawHeaderLabel)
|
||||
case actChangeHeaderLabel, actTransformHeaderLabel:
|
||||
label := a.a
|
||||
if a.t == actTransformHeaderLabel {
|
||||
label = t.captureLine(a.a)
|
||||
}
|
||||
case actChangeInputLabel:
|
||||
t.inputLabelOpts.label = a.a
|
||||
t.headerLabelOpts.label = label
|
||||
t.headerLabel, t.headerLabelLen = t.ansiLabelPrinter(label, &tui.ColHeaderLabel, false)
|
||||
req(reqRedrawHeaderLabel)
|
||||
case actChangeInputLabel, actTransformInputLabel:
|
||||
label := a.a
|
||||
if a.t == actTransformInputLabel {
|
||||
label = t.captureLine(a.a)
|
||||
}
|
||||
t.inputLabelOpts.label = label
|
||||
if t.inputBorder != nil {
|
||||
t.inputLabel, t.inputLabelLen = t.ansiLabelPrinter(a.a, &tui.ColInputLabel, false)
|
||||
t.inputLabel, t.inputLabelLen = t.ansiLabelPrinter(label, &tui.ColInputLabel, false)
|
||||
req(reqRedrawInputLabel)
|
||||
}
|
||||
case actChangeListLabel:
|
||||
t.listLabelOpts.label = a.a
|
||||
case actChangeListLabel, actTransformListLabel:
|
||||
label := a.a
|
||||
if a.t == actTransformListLabel {
|
||||
label = t.captureLine(a.a)
|
||||
}
|
||||
t.listLabelOpts.label = label
|
||||
if t.wborder != nil {
|
||||
t.listLabel, t.listLabelLen = t.ansiLabelPrinter(a.a, &tui.ColListLabel, false)
|
||||
t.listLabel, t.listLabelLen = t.ansiLabelPrinter(label, &tui.ColListLabel, false)
|
||||
req(reqRedrawListLabel)
|
||||
}
|
||||
case actChangeBorderLabel:
|
||||
t.borderLabelOpts.label = a.a
|
||||
case actChangeBorderLabel, actTransformBorderLabel:
|
||||
label := a.a
|
||||
if a.t == actTransformBorderLabel {
|
||||
label = t.captureLine(a.a)
|
||||
}
|
||||
t.borderLabelOpts.label = label
|
||||
if t.border != nil {
|
||||
t.borderLabel, t.borderLabelLen = t.ansiLabelPrinter(a.a, &tui.ColBorderLabel, false)
|
||||
t.borderLabel, t.borderLabelLen = t.ansiLabelPrinter(label, &tui.ColBorderLabel, false)
|
||||
req(reqRedrawBorderLabel)
|
||||
}
|
||||
case actChangePreviewLabel:
|
||||
t.previewLabelOpts.label = a.a
|
||||
case actChangePreviewLabel, actTransformPreviewLabel:
|
||||
label := a.a
|
||||
if a.t == actTransformPreviewLabel {
|
||||
label = t.captureLine(a.a)
|
||||
}
|
||||
t.previewLabelOpts.label = label
|
||||
if t.pborder != nil {
|
||||
t.previewLabel, t.previewLabelLen = t.ansiLabelPrinter(a.a, &tui.ColPreviewLabel, false)
|
||||
t.previewLabel, t.previewLabelLen = t.ansiLabelPrinter(label, &tui.ColPreviewLabel, false)
|
||||
req(reqRedrawPreviewLabel)
|
||||
}
|
||||
case actTransform:
|
||||
@@ -5056,41 +5137,6 @@ func (t *Terminal) Loop() error {
|
||||
if actions, err := parseSingleActionList(strings.Trim(body, "\r\n")); err == nil {
|
||||
return doActions(actions)
|
||||
}
|
||||
case actTransformHeaderLabel:
|
||||
label := t.captureLine(a.a)
|
||||
t.headerLabelOpts.label = label
|
||||
if t.headerBorder != nil {
|
||||
t.headerLabel, t.headerLabelLen = t.ansiLabelPrinter(label, &tui.ColHeaderLabel, false)
|
||||
req(reqRedrawHeaderLabel)
|
||||
}
|
||||
case actTransformInputLabel:
|
||||
label := t.captureLine(a.a)
|
||||
t.inputLabelOpts.label = label
|
||||
if t.inputBorder != nil {
|
||||
t.inputLabel, t.inputLabelLen = t.ansiLabelPrinter(label, &tui.ColInputLabel, false)
|
||||
req(reqRedrawInputLabel)
|
||||
}
|
||||
case actTransformListLabel:
|
||||
label := t.captureLine(a.a)
|
||||
t.listLabelOpts.label = label
|
||||
if t.wborder != nil {
|
||||
t.listLabel, t.listLabelLen = t.ansiLabelPrinter(label, &tui.ColListLabel, false)
|
||||
req(reqRedrawListLabel)
|
||||
}
|
||||
case actTransformBorderLabel:
|
||||
label := t.captureLine(a.a)
|
||||
t.borderLabelOpts.label = label
|
||||
if t.border != nil {
|
||||
t.borderLabel, t.borderLabelLen = t.ansiLabelPrinter(label, &tui.ColBorderLabel, false)
|
||||
req(reqRedrawBorderLabel)
|
||||
}
|
||||
case actTransformPreviewLabel:
|
||||
label := t.captureLine(a.a)
|
||||
t.previewLabelOpts.label = label
|
||||
if t.pborder != nil {
|
||||
t.previewLabel, t.previewLabelLen = t.ansiLabelPrinter(label, &tui.ColPreviewLabel, false)
|
||||
req(reqRedrawPreviewLabel)
|
||||
}
|
||||
case actChangePrompt:
|
||||
t.promptString = a.a
|
||||
t.prompt, t.promptLen = t.parsePrompt(a.a)
|
||||
@@ -5464,9 +5510,11 @@ func (t *Terminal) Loop() error {
|
||||
req(reqList, reqInfo, reqPrompt, reqHeader)
|
||||
case actToggleWrap:
|
||||
t.wrap = !t.wrap
|
||||
t.clearNumLinesCache()
|
||||
req(reqList, reqHeader)
|
||||
case actToggleMultiLine:
|
||||
t.multiLine = !t.multiLine
|
||||
t.clearNumLinesCache()
|
||||
req(reqList)
|
||||
case actToggleHscroll:
|
||||
// Force re-rendering of the list
|
||||
@@ -5999,7 +6047,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}
|
||||
reloadRequest = &searchRequest{sort: t.sort, sync: reloadSync, nth: newNth, command: newCommand, environ: t.environ(), changed: changed, denylist: denylist, revision: t.merger.Revision()}
|
||||
}
|
||||
t.mutex.Unlock() // Must be unlocked before touching reqBox
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
@@ -77,6 +78,11 @@ type Delimiter struct {
|
||||
str *string
|
||||
}
|
||||
|
||||
// IsAwk returns true if the delimiter is an AWK-style delimiter
|
||||
func (d Delimiter) IsAwk() bool {
|
||||
return d.regex == nil && d.str == nil
|
||||
}
|
||||
|
||||
// String returns the string representation of a Delimiter.
|
||||
func (d Delimiter) String() string {
|
||||
return fmt.Sprintf("Delimiter{regex: %v, str: &%q}", d.regex, *d.str)
|
||||
@@ -211,7 +217,22 @@ func Tokenize(text string, delimiter Delimiter) []Token {
|
||||
return withPrefixLengths(tokens, 0)
|
||||
}
|
||||
|
||||
func joinTokens(tokens []Token) string {
|
||||
// StripLastDelimiter removes the trailing delimiter and whitespaces
|
||||
func StripLastDelimiter(str string, delimiter Delimiter) string {
|
||||
if delimiter.str != nil {
|
||||
str = strings.TrimSuffix(str, *delimiter.str)
|
||||
} else if delimiter.regex != nil {
|
||||
locs := delimiter.regex.FindAllStringIndex(str, -1)
|
||||
if len(locs) > 0 {
|
||||
lastLoc := locs[len(locs)-1]
|
||||
str = str[:lastLoc[0]]
|
||||
}
|
||||
}
|
||||
return strings.TrimRightFunc(str, unicode.IsSpace)
|
||||
}
|
||||
|
||||
// JoinTokens concatenates the tokens into a single string
|
||||
func JoinTokens(tokens []Token) string {
|
||||
var output bytes.Buffer
|
||||
for _, token := range tokens {
|
||||
output.WriteString(token.text.ToString())
|
||||
@@ -229,7 +250,7 @@ func Transform(tokens []Token, withNth []Range) []Token {
|
||||
if r.begin == r.end {
|
||||
idx := r.begin
|
||||
if idx == rangeEllipsis {
|
||||
chars := util.ToChars(stringBytes(joinTokens(tokens)))
|
||||
chars := util.ToChars(stringBytes(JoinTokens(tokens)))
|
||||
parts = append(parts, &chars)
|
||||
} else {
|
||||
if idx < 0 {
|
||||
|
||||
@@ -85,14 +85,14 @@ func TestTransform(t *testing.T) {
|
||||
{
|
||||
ranges, _ := splitNth("1,2,3")
|
||||
tx := Transform(tokens, ranges)
|
||||
if joinTokens(tx) != "abc: def: ghi: " {
|
||||
if JoinTokens(tx) != "abc: def: ghi: " {
|
||||
t.Errorf("%s", tx)
|
||||
}
|
||||
}
|
||||
{
|
||||
ranges, _ := splitNth("1..2,3,2..,1")
|
||||
tx := Transform(tokens, ranges)
|
||||
if string(joinTokens(tx)) != "abc: def: ghi: def: ghi: jklabc: " ||
|
||||
if string(JoinTokens(tx)) != "abc: def: ghi: def: ghi: jklabc: " ||
|
||||
len(tx) != 4 ||
|
||||
tx[0].text.ToString() != "abc: def: " || tx[0].prefixLength != 2 ||
|
||||
tx[1].text.ToString() != "ghi: " || tx[1].prefixLength != 14 ||
|
||||
@@ -107,7 +107,7 @@ func TestTransform(t *testing.T) {
|
||||
{
|
||||
ranges, _ := splitNth("1..2,3,2..,1")
|
||||
tx := Transform(tokens, ranges)
|
||||
if joinTokens(tx) != " abc: def: ghi: def: ghi: jkl abc:" ||
|
||||
if JoinTokens(tx) != " abc: def: ghi: def: ghi: jkl abc:" ||
|
||||
len(tx) != 4 ||
|
||||
tx[0].text.ToString() != " abc: def:" || tx[0].prefixLength != 0 ||
|
||||
tx[1].text.ToString() != " ghi:" || tx[1].prefixLength != 12 ||
|
||||
|
||||
@@ -44,8 +44,9 @@ func (r *LightRenderer) stderr(str string) {
|
||||
r.stderrInternal(str, true, "")
|
||||
}
|
||||
|
||||
const CR string = "\x1b[2m␍"
|
||||
const LF string = "\x1b[2m␊"
|
||||
const DIM string = "\x1b[2m"
|
||||
const CR string = DIM + "␍"
|
||||
const LF string = DIM + "␊"
|
||||
|
||||
func (r *LightRenderer) stderrInternal(str string, allowNLCR bool, resetCode string) {
|
||||
bytes := []byte(str)
|
||||
@@ -140,6 +141,8 @@ type LightWindow struct {
|
||||
tabstop int
|
||||
fg Color
|
||||
bg Color
|
||||
wrapSign string
|
||||
wrapSignWidth int
|
||||
}
|
||||
|
||||
func NewLightRenderer(ttyin *os.File, theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, clearOnExit bool, fullscreen bool, maxHeightFunc func(int) int) (Renderer, error) {
|
||||
@@ -1105,11 +1108,12 @@ type wrappedLine struct {
|
||||
displayWidth int
|
||||
}
|
||||
|
||||
func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLine {
|
||||
func wrapLine(input string, prefixLength int, initialMax int, tabstop int, wrapSignWidth int) []wrappedLine {
|
||||
lines := []wrappedLine{}
|
||||
width := 0
|
||||
line := ""
|
||||
gr := uniseg.NewGraphemes(input)
|
||||
max := initialMax
|
||||
for gr.Next() {
|
||||
rs := gr.Runes()
|
||||
str := string(rs)
|
||||
@@ -1131,6 +1135,7 @@ func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLin
|
||||
line = str
|
||||
prefixLength = 0
|
||||
width = w
|
||||
max = initialMax - wrapSignWidth
|
||||
}
|
||||
}
|
||||
lines = append(lines, wrappedLine{string(line), width})
|
||||
@@ -1140,7 +1145,7 @@ func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLin
|
||||
func (w *LightWindow) fill(str string, resetCode string) FillReturn {
|
||||
allLines := strings.Split(str, "\n")
|
||||
for i, line := range allLines {
|
||||
lines := wrapLine(line, w.posx, w.width, w.tabstop)
|
||||
lines := wrapLine(line, w.posx, w.width, w.tabstop, w.wrapSignWidth)
|
||||
for j, wl := range lines {
|
||||
w.stderrInternal(wl.text, false, resetCode)
|
||||
w.posx += wl.displayWidth
|
||||
@@ -1153,6 +1158,18 @@ func (w *LightWindow) fill(str string, resetCode string) FillReturn {
|
||||
w.MoveAndClear(w.posy, w.posx)
|
||||
w.Move(w.posy+1, 0)
|
||||
w.renderer.stderr(resetCode)
|
||||
if len(lines) > 1 {
|
||||
sign := w.wrapSign
|
||||
width := w.wrapSignWidth
|
||||
if width > w.width {
|
||||
runes, truncatedWidth := util.Truncate(w.wrapSign, w.width)
|
||||
sign = string(runes)
|
||||
width = truncatedWidth
|
||||
}
|
||||
w.stderrInternal(DIM+sign, false, resetCode)
|
||||
w.renderer.stderr(resetCode)
|
||||
w.Move(w.posy, width)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1226,6 +1243,11 @@ func (w *LightWindow) EraseMaybe() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (w *LightWindow) SetWrapSign(sign string, width int) {
|
||||
w.wrapSign = sign
|
||||
w.wrapSignWidth = width
|
||||
}
|
||||
|
||||
func (r *LightRenderer) HideCursor() {
|
||||
r.showCursor = false
|
||||
r.csi("?25l")
|
||||
|
||||
@@ -53,6 +53,8 @@ type TcellWindow struct {
|
||||
uri *string
|
||||
params *string
|
||||
showCursor bool
|
||||
wrapSign string
|
||||
wrapSignWidth int
|
||||
}
|
||||
|
||||
func (w *TcellWindow) Top() int {
|
||||
@@ -629,6 +631,11 @@ func (w *TcellWindow) EraseMaybe() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (w *TcellWindow) SetWrapSign(sign string, width int) {
|
||||
w.wrapSign = sign
|
||||
w.wrapSignWidth = width
|
||||
}
|
||||
|
||||
func (w *TcellWindow) EncloseX(x int) bool {
|
||||
return x >= w.left && x < (w.left+w.width)
|
||||
}
|
||||
@@ -757,11 +764,26 @@ Loop:
|
||||
|
||||
// word wrap:
|
||||
xPos := w.left + w.lastX + lx
|
||||
if xPos >= (w.left + w.width) {
|
||||
if xPos >= w.left+w.width {
|
||||
w.lastY++
|
||||
if w.lastY >= w.height {
|
||||
return FillSuspend
|
||||
}
|
||||
w.lastX = 0
|
||||
lx = 0
|
||||
xPos = w.left
|
||||
sign := w.wrapSign
|
||||
if w.wrapSignWidth > w.width {
|
||||
runes, _ := util.Truncate(sign, w.width)
|
||||
sign = string(runes)
|
||||
}
|
||||
wgr := uniseg.NewGraphemes(sign)
|
||||
for wgr.Next() {
|
||||
rs := wgr.Runes()
|
||||
_screen.SetContent(w.left+lx, w.top+w.lastY, rs[0], rs[1:], style.Dim(true))
|
||||
lx += uniseg.StringWidth(string(rs))
|
||||
}
|
||||
xPos = w.left + lx
|
||||
}
|
||||
|
||||
yPos := w.top + w.lastY
|
||||
|
||||
@@ -659,6 +659,8 @@ type Window interface {
|
||||
LinkEnd()
|
||||
Erase()
|
||||
EraseMaybe() bool
|
||||
|
||||
SetWrapSign(string, int)
|
||||
}
|
||||
|
||||
type FullscreenRenderer struct {
|
||||
|
||||
@@ -184,9 +184,25 @@ func (chars *Chars) TrailingWhitespaces() int {
|
||||
return whitespaces
|
||||
}
|
||||
|
||||
func (chars *Chars) TrimTrailingWhitespaces() {
|
||||
whitespaces := chars.TrailingWhitespaces()
|
||||
chars.slice = chars.slice[0 : len(chars.slice)-whitespaces]
|
||||
func (chars *Chars) TrimSuffix(runes []rune) {
|
||||
lastIdx := len(chars.slice)
|
||||
firstIdx := lastIdx - len(runes)
|
||||
if firstIdx < 0 {
|
||||
return
|
||||
}
|
||||
|
||||
for i := firstIdx; i < lastIdx; i++ {
|
||||
char := chars.Get(i)
|
||||
if char != runes[i-firstIdx] {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
chars.slice = chars.slice[0:firstIdx]
|
||||
}
|
||||
|
||||
func (chars *Chars) SliceRight(last int) {
|
||||
chars.slice = chars.slice[:last]
|
||||
}
|
||||
|
||||
func (chars *Chars) ToString() string {
|
||||
|
||||
@@ -1665,4 +1665,120 @@ class TestCore < TestInteractive
|
||||
assert_equal '', File.read(tempname).chomp
|
||||
end
|
||||
end
|
||||
|
||||
def test_exclude_multi
|
||||
tmux.send_keys %(seq 1000 | #{FZF} --multi --bind 'a:exclude-multi,b:reload(seq 1000),c:reload-sync(seq 1000)'), :Enter
|
||||
|
||||
tmux.until do |lines|
|
||||
assert_equal 1000, lines.match_count
|
||||
assert_includes lines, '> 1'
|
||||
end
|
||||
tmux.send_keys :a
|
||||
tmux.until do |lines|
|
||||
assert_includes lines, '> 2'
|
||||
assert_equal 999, lines.match_count
|
||||
end
|
||||
tmux.send_keys :Up, :BTab, :BTab, :BTab, :a
|
||||
tmux.until do |lines|
|
||||
assert_equal 996, lines.match_count
|
||||
assert_includes lines, '> 9'
|
||||
end
|
||||
tmux.send_keys :b
|
||||
tmux.until do |lines|
|
||||
assert_equal 1000, lines.match_count
|
||||
assert_includes lines, '> 5'
|
||||
end
|
||||
tmux.send_keys :Tab, :Tab, :Tab, :a
|
||||
tmux.until do |lines|
|
||||
assert_equal 997, lines.match_count
|
||||
assert_includes lines, '> 2'
|
||||
end
|
||||
tmux.send_keys :c
|
||||
tmux.until do |lines|
|
||||
assert_equal 1000, lines.match_count
|
||||
assert_includes lines, '> 2'
|
||||
end
|
||||
|
||||
# TODO: We should also check the behavior of 'exclude' during reloads
|
||||
end
|
||||
|
||||
def test_exclude
|
||||
tmux.send_keys %(seq 1000 | #{FZF} --multi --bind 'a:exclude,b:reload(seq 1000),c:reload-sync(seq 1000)'), :Enter
|
||||
|
||||
tmux.until do |lines|
|
||||
assert_equal 1000, lines.match_count
|
||||
assert_includes lines, '> 1'
|
||||
end
|
||||
tmux.send_keys :a
|
||||
tmux.until do |lines|
|
||||
assert_includes lines, '> 2'
|
||||
assert_equal 999, lines.match_count
|
||||
end
|
||||
tmux.send_keys :Up, :BTab, :BTab, :BTab, :a
|
||||
tmux.until do |lines|
|
||||
assert_equal 998, lines.match_count
|
||||
assert_equal 3, lines.select_count
|
||||
assert_includes lines, '> 7'
|
||||
end
|
||||
tmux.send_keys :b
|
||||
tmux.until do |lines|
|
||||
assert_equal 1000, lines.match_count
|
||||
assert_equal 0, lines.select_count
|
||||
assert_includes lines, '> 5'
|
||||
end
|
||||
tmux.send_keys :Tab, :Tab, :Tab, :a
|
||||
tmux.until do |lines|
|
||||
assert_equal 999, lines.match_count
|
||||
assert_equal 3, lines.select_count
|
||||
assert_includes lines, '>>3'
|
||||
end
|
||||
tmux.send_keys :a
|
||||
tmux.until do |lines|
|
||||
assert_equal 998, lines.match_count
|
||||
assert_equal 2, lines.select_count
|
||||
assert_includes lines, '>>4'
|
||||
end
|
||||
tmux.send_keys :c
|
||||
tmux.until do |lines|
|
||||
assert_equal 1000, lines.match_count
|
||||
assert_includes lines, '> 2'
|
||||
end
|
||||
|
||||
# TODO: We should also check the behavior of 'exclude' during reloads
|
||||
end
|
||||
|
||||
def test_accept_nth
|
||||
tmux.send_keys %((echo "foo bar baz"; echo "bar baz foo") | #{FZF} --multi --accept-nth 2,2 --sync --bind start:select-all+accept > #{tempname}), :Enter
|
||||
wait do
|
||||
assert_path_exists tempname
|
||||
assert_equal ['bar bar', 'baz baz'], File.readlines(tempname, chomp: true)
|
||||
end
|
||||
end
|
||||
|
||||
def test_accept_nth_string_delimiter
|
||||
tmux.send_keys %(echo "foo ,bar,baz" | #{FZF} -d, --accept-nth 2,2,1,3,1 --sync --bind start:accept > #{tempname}), :Enter
|
||||
wait do
|
||||
assert_path_exists tempname
|
||||
# Last delimiter and the whitespaces are removed
|
||||
assert_equal ['bar,bar,foo ,bazfoo'], File.readlines(tempname, chomp: true)
|
||||
end
|
||||
end
|
||||
|
||||
def test_accept_nth_regex_delimiter
|
||||
tmux.send_keys %(echo "foo :,:bar,baz" | #{FZF} --delimiter='[:,]+' --accept-nth 2,2,1,3,1 --sync --bind start:accept > #{tempname}), :Enter
|
||||
wait do
|
||||
assert_path_exists tempname
|
||||
# Last delimiter and the whitespaces are removed
|
||||
assert_equal ['bar,bar,foo :,:bazfoo'], File.readlines(tempname, chomp: true)
|
||||
end
|
||||
end
|
||||
|
||||
def test_accept_nth_template
|
||||
tmux.send_keys %(echo "foo ,bar,baz" | #{FZF} -d, --accept-nth '1st: {1}, 3rd: {3}, 2nd: {2}' --sync --bind start:accept > #{tempname}), :Enter
|
||||
wait do
|
||||
assert_path_exists tempname
|
||||
# Last delimiter and the whitespaces are removed
|
||||
assert_equal ['1st: foo, 3rd: baz, 2nd: bar'], File.readlines(tempname, chomp: true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -52,6 +52,12 @@ class TestFilter < TestBase
|
||||
`find . -print0 | #{FZF} --read0 -e -f "^#{lines.last}$"`.chomp
|
||||
end
|
||||
|
||||
def test_nth_suffix_match
|
||||
assert_equal \
|
||||
'foo,bar,baz',
|
||||
`echo foo,bar,baz | #{FZF} -d, -f'bar$' -n2`.chomp
|
||||
end
|
||||
|
||||
def test_with_nth_basic
|
||||
writelines(['hello world ', 'byebye'])
|
||||
assert_equal \
|
||||
@@ -59,6 +65,13 @@ class TestFilter < TestBase
|
||||
`#{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1 < #{tempname}`.chomp
|
||||
end
|
||||
|
||||
def test_with_nth_template
|
||||
writelines(['hello world ', 'byebye'])
|
||||
assert_equal \
|
||||
'hello world ',
|
||||
`#{FZF} -f"^he he.he." -x -n 2.. --with-nth '{2} {1}. {1}.' < #{tempname}`.chomp
|
||||
end
|
||||
|
||||
def test_with_nth_ansi
|
||||
writelines(["\x1b[33mhello \x1b[34;1mworld\x1b[m ", 'byebye'])
|
||||
assert_equal \
|
||||
|
||||
@@ -978,4 +978,17 @@ class TestLayout < TestInteractive
|
||||
setup
|
||||
end
|
||||
end
|
||||
|
||||
def test_change_header_and_label_at_once
|
||||
tmux.send_keys %(seq 10 | #{FZF} --border sharp --header-border sharp --header-label-pos 3 --bind 'focus:change-header(header)+change-header-label(label)'), :Enter
|
||||
block = <<~BLOCK
|
||||
│ ┌─label──
|
||||
│ │ header
|
||||
│ └────────
|
||||
│ 10/10 ─
|
||||
│ >
|
||||
└──────────
|
||||
BLOCK
|
||||
tmux.until { assert_block(block, it) }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -453,7 +453,7 @@ class TestPreview < TestInteractive
|
||||
tmux.send_keys 'f'
|
||||
tmux.until do |lines|
|
||||
assert_equal '::', lines[0]
|
||||
assert_equal ' 3', lines[1]
|
||||
assert_equal '↳ 3', lines[1]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -527,7 +527,7 @@ class TestPreview < TestInteractive
|
||||
tmux.send_keys "seq 10 | #{FZF} --preview-border rounded --preview-window '~5,2,+0,<100000(~0,+100,wrap,noinfo)' --preview 'seq 1000'", :Enter
|
||||
tmux.until { |lines| assert_equal 10, lines.match_count }
|
||||
tmux.until do |lines|
|
||||
assert_equal ['╭────╮', '│ 10 │', '│ 0 │', '│ 10 │', '│ 1 │'], lines.take(5).map(&:strip)
|
||||
assert_equal ['╭────╮', '│ 10 │', '│ ↳ 0│', '│ 10 │', '│ ↳ 1│'], lines.take(5).map(&:strip)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
Reference in New Issue
Block a user