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
|
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
|
0.59.0
|
||||||
------
|
------
|
||||||
_Release highlights: https://junegunn.github.io/fzf/releases/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))
|
dim=${FZF_PREVIEW_COLUMNS}x$((FZF_PREVIEW_LINES - 1))
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 1. Use kitty icat on kitty terminal
|
# 1. Use icat (from Kitty) if kitten is installed
|
||||||
if [[ $KITTY_WINDOW_ID ]]; then
|
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,
|
# 1. 'memory' is the fastest option but if you want the image to be scrollable,
|
||||||
# you have to use 'stream'.
|
# you have to use 'stream'.
|
||||||
#
|
#
|
||||||
# 2. The last line of the output is the ANSI reset code without newline.
|
# 2. The last line of the output is the ANSI reset code without newline.
|
||||||
# This confuses fzf and makes it render scroll offset indicator.
|
# 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.
|
# 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
|
# 2. Use chafa with Sixel output
|
||||||
elif command -v chafa > /dev/null; then
|
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/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741
|
||||||
github.com/mattn/go-isatty v0.0.20
|
github.com/mattn/go-isatty v0.0.20
|
||||||
github.com/rivo/uniseg v0.4.7
|
github.com/rivo/uniseg v0.4.7
|
||||||
golang.org/x/sys v0.29.0
|
golang.org/x/sys v0.30.0
|
||||||
golang.org/x/term v0.28.0
|
golang.org/x/term v0.29.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
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.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.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.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.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/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-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
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.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
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.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.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.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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
|||||||
2
install
2
install
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
set -u
|
set -u
|
||||||
|
|
||||||
version=0.59.0
|
version=0.60.0
|
||||||
auto_completion=
|
auto_completion=
|
||||||
key_bindings=
|
key_bindings=
|
||||||
update_config=2
|
update_config=2
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
$version="0.59.0"
|
$version="0.60.0"
|
||||||
|
|
||||||
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition
|
$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"
|
"github.com/junegunn/fzf/src/protector"
|
||||||
)
|
)
|
||||||
|
|
||||||
var version = "0.59"
|
var version = "0.60"
|
||||||
var revision = "devel"
|
var revision = "devel"
|
||||||
|
|
||||||
//go:embed shell/key-bindings.bash
|
//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
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
THE SOFTWARE.
|
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
|
.SH NAME
|
||||||
fzf\-tmux - open fzf in tmux split pane
|
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
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
THE SOFTWARE.
|
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
|
.SH NAME
|
||||||
fzf - a command-line fuzzy finder
|
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
|
the original lines) because fzf doesn't allow searching against the hidden
|
||||||
fields.
|
fields.
|
||||||
.TP
|
.TP
|
||||||
.BI "\-\-with\-nth=" "N[,..]"
|
.BI "\-\-with\-nth=" "N[,..] or TEMPLATE"
|
||||||
Transform the presentation of each line using field index expressions
|
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
|
.TP
|
||||||
.B "+s, \-\-no\-sort"
|
.B "+s, \-\-no\-sort"
|
||||||
Do not sort the result
|
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
|
\fBdown\fR \fIctrl\-j ctrl\-n down\fR
|
||||||
\fBenable\-search\fR (enable search functionality)
|
\fBenable\-search\fR (enable search functionality)
|
||||||
\fBend\-of\-line\fR \fIctrl\-e end\fR
|
\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(...)\fR (see below for the details)
|
||||||
\fBexecute\-silent(...)\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)
|
\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 is used to do shell expansion on paths
|
||||||
eval set commandline $commandline
|
eval set commandline $commandline
|
||||||
|
|
||||||
# Combine multiple consecutive slashes into one
|
# Combine multiple consecutive slashes into one, and unescape.
|
||||||
set commandline (string replace -r -a -- '/+' '/' $commandline)
|
set commandline (string replace -r -a -- '/+' '/' $commandline | string unescape -n)
|
||||||
|
|
||||||
if test -z "$commandline"
|
if test -z "$commandline"
|
||||||
# Default to current directory with no --query
|
# Default to current directory with no --query
|
||||||
@@ -176,9 +176,9 @@ function fzf_key_bindings
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
echo (string escape -- $dir)
|
echo -- $dir
|
||||||
echo (string escape -- $fzf_query)
|
string escape -- $fzf_query
|
||||||
echo $prefix
|
echo -- $prefix
|
||||||
end
|
end
|
||||||
|
|
||||||
function __fzf_get_dir -d 'Find the longest existing filepath from input string'
|
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")
|
set dir (dirname -- "$dir")
|
||||||
end
|
end
|
||||||
|
|
||||||
echo $dir
|
string escape -n -- $dir
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -138,11 +138,13 @@ func _() {
|
|||||||
_ = x[actShowHeader-127]
|
_ = x[actShowHeader-127]
|
||||||
_ = x[actHideHeader-128]
|
_ = x[actHideHeader-128]
|
||||||
_ = x[actBell-129]
|
_ = 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 {
|
func (i actionType) String() string {
|
||||||
if i < 0 || i >= actionType(len(_actionType_index)-1) {
|
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 chunkList *ChunkList
|
||||||
var itemIndex int32
|
var itemIndex int32
|
||||||
header := make([]string, 0, opts.HeaderLines)
|
header := make([]string, 0, opts.HeaderLines)
|
||||||
if len(opts.WithNth) == 0 {
|
if opts.WithNth == nil {
|
||||||
chunkList = NewChunkList(cache, func(item *Item, data []byte) bool {
|
chunkList = NewChunkList(cache, func(item *Item, data []byte) bool {
|
||||||
if len(header) < opts.HeaderLines {
|
if len(header) < opts.HeaderLines {
|
||||||
header = append(header, byteString(data))
|
header = append(header, byteString(data))
|
||||||
@@ -109,6 +109,7 @@ func Run(opts *Options) (int, error) {
|
|||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
nthTransformer := opts.WithNth(opts.Delimiter)
|
||||||
chunkList = NewChunkList(cache, func(item *Item, data []byte) bool {
|
chunkList = NewChunkList(cache, func(item *Item, data []byte) bool {
|
||||||
tokens := Tokenize(byteString(data), opts.Delimiter)
|
tokens := Tokenize(byteString(data), opts.Delimiter)
|
||||||
if opts.Ansi && opts.Theme.Colored && len(tokens) > 1 {
|
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 := nthTransformer(tokens)
|
||||||
transformed := joinTokens(trans)
|
|
||||||
if len(header) < opts.HeaderLines {
|
if len(header) < opts.HeaderLines {
|
||||||
header = append(header, transformed)
|
header = append(header, transformed)
|
||||||
eventBox.Set(EvtHeader, header)
|
eventBox.Set(EvtHeader, header)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
item.text, item.colors = ansiProcessor(stringBytes(transformed))
|
item.text, item.colors = ansiProcessor(stringBytes(transformed))
|
||||||
item.text.TrimTrailingWhitespaces()
|
|
||||||
item.text.Index = itemIndex
|
item.text.Index = itemIndex
|
||||||
item.origText = &data
|
item.origText = &data
|
||||||
itemIndex++
|
itemIndex++
|
||||||
@@ -195,15 +194,30 @@ func Run(opts *Options) (int, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
nth := opts.Nth
|
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{}
|
inputRevision := revision{}
|
||||||
snapshotRevision := 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)
|
matcher := NewMatcher(cache, patternBuilder, sort, opts.Tac, eventBox, inputRevision)
|
||||||
|
|
||||||
// Filtering mode
|
// Filtering mode
|
||||||
@@ -302,6 +316,9 @@ func Run(opts *Options) (int, error) {
|
|||||||
var snapshot []*Chunk
|
var snapshot []*Chunk
|
||||||
var count int
|
var count int
|
||||||
restart := func(command commandSpec, environ []string) {
|
restart := func(command commandSpec, environ []string) {
|
||||||
|
if !useSnapshot {
|
||||||
|
clearDenylist()
|
||||||
|
}
|
||||||
reading = true
|
reading = true
|
||||||
chunkList.Clear()
|
chunkList.Clear()
|
||||||
itemIndex = 0
|
itemIndex = 0
|
||||||
@@ -348,7 +365,8 @@ func Run(opts *Options) (int, error) {
|
|||||||
} else {
|
} else {
|
||||||
reading = reading && evt == EvtReadNew
|
reading = reading && evt == EvtReadNew
|
||||||
}
|
}
|
||||||
if useSnapshot && evt == EvtReadFin {
|
if useSnapshot && evt == EvtReadFin { // reload-sync
|
||||||
|
clearDenylist()
|
||||||
useSnapshot = false
|
useSnapshot = false
|
||||||
}
|
}
|
||||||
if !useSnapshot {
|
if !useSnapshot {
|
||||||
@@ -379,10 +397,21 @@ func Run(opts *Options) (int, error) {
|
|||||||
command = val.command
|
command = val.command
|
||||||
environ = val.environ
|
environ = val.environ
|
||||||
changed = val.changed
|
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 {
|
if val.nth != nil {
|
||||||
// Change nth and clear caches
|
// Change nth and clear caches
|
||||||
nth = *val.nth
|
nth = *val.nth
|
||||||
nthRevision++
|
bump = true
|
||||||
|
}
|
||||||
|
if bump {
|
||||||
patternCache = make(map[string]*Pattern)
|
patternCache = make(map[string]*Pattern)
|
||||||
cache.Clear()
|
cache.Clear()
|
||||||
inputRevision.bumpMinor()
|
inputRevision.bumpMinor()
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
type transformed struct {
|
type transformed struct {
|
||||||
// Because nth can be changed dynamically by change-nth action, we need to
|
// Because nth can be changed dynamically by change-nth action, we need to
|
||||||
// keep the revision number at the time of transformation.
|
// keep the revision number at the time of transformation.
|
||||||
revision int
|
revision revision
|
||||||
tokens []Token
|
tokens []Token
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ Usage: fzf [options]
|
|||||||
integer or a range expression ([BEGIN]..[END]).
|
integer or a range expression ([BEGIN]..[END]).
|
||||||
--with-nth=N[,..] Transform the presentation of each line using
|
--with-nth=N[,..] Transform the presentation of each line using
|
||||||
field index expressions
|
field index expressions
|
||||||
|
--accept-nth=N[,..] Define which fields to print on accept
|
||||||
-d, --delimiter=STR Field delimiter regex (default: AWK-style)
|
-d, --delimiter=STR Field delimiter regex (default: AWK-style)
|
||||||
+s, --no-sort Do not sort the result
|
+s, --no-sort Do not sort the result
|
||||||
--literal Do not normalize latin script letters
|
--literal Do not normalize latin script letters
|
||||||
@@ -543,7 +544,8 @@ type Options struct {
|
|||||||
Case Case
|
Case Case
|
||||||
Normalize bool
|
Normalize bool
|
||||||
Nth []Range
|
Nth []Range
|
||||||
WithNth []Range
|
WithNth func(Delimiter) func([]Token) string
|
||||||
|
AcceptNth func(Delimiter) func([]Token) string
|
||||||
Delimiter Delimiter
|
Delimiter Delimiter
|
||||||
Sort int
|
Sort int
|
||||||
Track trackOption
|
Track trackOption
|
||||||
@@ -665,7 +667,6 @@ func defaultOptions() *Options {
|
|||||||
Case: CaseSmart,
|
Case: CaseSmart,
|
||||||
Normalize: true,
|
Normalize: true,
|
||||||
Nth: make([]Range, 0),
|
Nth: make([]Range, 0),
|
||||||
WithNth: make([]Range, 0),
|
|
||||||
Delimiter: Delimiter{},
|
Delimiter: Delimiter{},
|
||||||
Sort: 1000,
|
Sort: 1000,
|
||||||
Track: trackDisabled,
|
Track: trackDisabled,
|
||||||
@@ -768,6 +769,62 @@ func splitNth(str string) ([]Range, error) {
|
|||||||
return ranges, nil
|
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 {
|
func delimiterRegexp(str string) Delimiter {
|
||||||
// Special handling of \t
|
// Special handling of \t
|
||||||
str = strings.ReplaceAll(str, "\\t", "\t")
|
str = strings.ReplaceAll(str, "\\t", "\t")
|
||||||
@@ -1600,6 +1657,10 @@ func parseActionList(masked string, original string, prevActions []*action, putA
|
|||||||
}
|
}
|
||||||
case "bell":
|
case "bell":
|
||||||
appendAction(actBell)
|
appendAction(actBell)
|
||||||
|
case "exclude":
|
||||||
|
appendAction(actExclude)
|
||||||
|
case "exclude-multi":
|
||||||
|
appendAction(actExcludeMulti)
|
||||||
default:
|
default:
|
||||||
t := isExecuteAction(specLower)
|
t := isExecuteAction(specLower)
|
||||||
if t == actIgnore {
|
if t == actIgnore {
|
||||||
@@ -2380,7 +2441,15 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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
|
return err
|
||||||
}
|
}
|
||||||
case "-s", "--sort":
|
case "-s", "--sort":
|
||||||
|
|||||||
@@ -60,9 +60,10 @@ type Pattern struct {
|
|||||||
cacheKey string
|
cacheKey string
|
||||||
delimiter Delimiter
|
delimiter Delimiter
|
||||||
nth []Range
|
nth []Range
|
||||||
revision int
|
revision revision
|
||||||
procFun map[termType]algo.Algo
|
procFun map[termType]algo.Algo
|
||||||
cache *ChunkCache
|
cache *ChunkCache
|
||||||
|
denylist map[int32]struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
var _splitRegex *regexp.Regexp
|
var _splitRegex *regexp.Regexp
|
||||||
@@ -73,7 +74,7 @@ func init() {
|
|||||||
|
|
||||||
// BuildPattern builds Pattern object from the given arguments
|
// 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,
|
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
|
var asString string
|
||||||
if extended {
|
if extended {
|
||||||
@@ -144,6 +145,7 @@ func BuildPattern(cache *ChunkCache, patternCache map[string]*Pattern, fuzzy boo
|
|||||||
revision: revision,
|
revision: revision,
|
||||||
delimiter: delimiter,
|
delimiter: delimiter,
|
||||||
cache: cache,
|
cache: cache,
|
||||||
|
denylist: denylist,
|
||||||
procFun: make(map[termType]algo.Algo)}
|
procFun: make(map[termType]algo.Algo)}
|
||||||
|
|
||||||
ptr.cacheKey = ptr.buildCacheKey()
|
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
|
// IsEmpty returns true if the pattern is effectively empty
|
||||||
func (p *Pattern) IsEmpty() bool {
|
func (p *Pattern) IsEmpty() bool {
|
||||||
|
if len(p.denylist) > 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if !p.extended {
|
if !p.extended {
|
||||||
return len(p.text) == 0
|
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 {
|
func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Result {
|
||||||
matches := []Result{}
|
matches := []Result{}
|
||||||
|
|
||||||
|
if len(p.denylist) == 0 {
|
||||||
|
// Huge code duplication for minimizing unnecessary map lookups
|
||||||
if space == nil {
|
if space == nil {
|
||||||
for idx := 0; idx < chunk.count; idx++ {
|
for idx := 0; idx < chunk.count; idx++ {
|
||||||
if match, _, _ := p.MatchItem(&chunk.items[idx], p.withPos, slab); match != nil {
|
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
|
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
|
// 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)
|
tokens := Tokenize(item.text.ToString(), p.delimiter)
|
||||||
ret := Transform(tokens, p.nth)
|
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}
|
item.transformed = &transformed{p.revision, ret}
|
||||||
return 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 {
|
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern {
|
||||||
return BuildPattern(NewChunkCache(), make(map[string]*Pattern),
|
return BuildPattern(NewChunkCache(), make(map[string]*Pattern),
|
||||||
fuzzy, fuzzyAlgo, extended, caseMode, normalize, forward,
|
fuzzy, fuzzyAlgo, extended, caseMode, normalize, forward,
|
||||||
withPos, cacheable, nth, delimiter, 0, runes)
|
withPos, cacheable, nth, delimiter, revision{}, runes, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestExact(t *testing.T) {
|
func TestExact(t *testing.T) {
|
||||||
|
|||||||
166
src/terminal.go
166
src/terminal.go
@@ -305,6 +305,7 @@ type Terminal struct {
|
|||||||
nthAttr tui.Attr
|
nthAttr tui.Attr
|
||||||
nth []Range
|
nth []Range
|
||||||
nthCurrent []Range
|
nthCurrent []Range
|
||||||
|
acceptNth func([]Token) string
|
||||||
tabstop int
|
tabstop int
|
||||||
margin [4]sizeSpec
|
margin [4]sizeSpec
|
||||||
padding [4]sizeSpec
|
padding [4]sizeSpec
|
||||||
@@ -390,6 +391,12 @@ type Terminal struct {
|
|||||||
clickHeaderLine int
|
clickHeaderLine int
|
||||||
clickHeaderColumn int
|
clickHeaderColumn int
|
||||||
proxyScript string
|
proxyScript string
|
||||||
|
numLinesCache map[int32]numLinesCacheValue
|
||||||
|
}
|
||||||
|
|
||||||
|
type numLinesCacheValue struct {
|
||||||
|
atMost int
|
||||||
|
numLines int
|
||||||
}
|
}
|
||||||
|
|
||||||
type selectedItem struct {
|
type selectedItem struct {
|
||||||
@@ -577,6 +584,8 @@ const (
|
|||||||
actShowHeader
|
actShowHeader
|
||||||
actHideHeader
|
actHideHeader
|
||||||
actBell
|
actBell
|
||||||
|
actExclude
|
||||||
|
actExcludeMulti
|
||||||
)
|
)
|
||||||
|
|
||||||
func (a actionType) Name() string {
|
func (a actionType) Name() string {
|
||||||
@@ -620,6 +629,8 @@ type searchRequest struct {
|
|||||||
command *commandSpec
|
command *commandSpec
|
||||||
environ []string
|
environ []string
|
||||||
changed bool
|
changed bool
|
||||||
|
denylist []int32
|
||||||
|
revision revision
|
||||||
}
|
}
|
||||||
|
|
||||||
type previewRequest struct {
|
type previewRequest struct {
|
||||||
@@ -947,7 +958,11 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
|
|||||||
initFunc: func() error { return renderer.Init() },
|
initFunc: func() error { return renderer.Init() },
|
||||||
executing: util.NewAtomicBool(false),
|
executing: util.NewAtomicBool(false),
|
||||||
lastAction: actStart,
|
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*
|
// This should be called before accessing tui.Color*
|
||||||
tui.InitTheme(opts.Theme, renderer.DefaultTheme(), opts.Black, opts.InputBorderShape.Visible(), opts.HeaderBorderShape.Visible())
|
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)
|
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
|
// Number of lines the item takes including the gap
|
||||||
func (t *Terminal) numItemLines(item *Item, atMost int) (int, bool) {
|
func (t *Terminal) numItemLines(item *Item, atMost int) (int, bool) {
|
||||||
var numLines int
|
var numLines int
|
||||||
@@ -1325,6 +1344,12 @@ func (t *Terminal) numItemLines(item *Item, atMost int) (int, bool) {
|
|||||||
numLines = 1 + t.gap
|
numLines = 1 + t.gap
|
||||||
return numLines, numLines > atMost
|
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
|
var overflow bool
|
||||||
if !t.wrap && t.multiLine {
|
if !t.wrap && t.multiLine {
|
||||||
numLines, overflow = item.text.NumLines(atMost)
|
numLines, overflow = item.text.NumLines(atMost)
|
||||||
@@ -1334,6 +1359,9 @@ func (t *Terminal) numItemLines(item *Item, atMost int) (int, bool) {
|
|||||||
numLines = len(lines)
|
numLines = len(lines)
|
||||||
}
|
}
|
||||||
numLines += t.gap
|
numLines += t.gap
|
||||||
|
if !overflow {
|
||||||
|
t.numLinesCache[item.Index()] = numLinesCacheValue{atMost, numLines}
|
||||||
|
}
|
||||||
return numLines, overflow || numLines > atMost
|
return numLines, overflow || numLines > atMost
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1461,6 +1489,7 @@ func (t *Terminal) UpdateList(merger *Merger) {
|
|||||||
if !t.revision.compatible(newRevision) {
|
if !t.revision.compatible(newRevision) {
|
||||||
// Reloaded: clear selection
|
// Reloaded: clear selection
|
||||||
t.selected = make(map[int32]selectedItem)
|
t.selected = make(map[int32]selectedItem)
|
||||||
|
t.clearNumLinesCache()
|
||||||
} else {
|
} else {
|
||||||
// Trimmed by --tail: filter selection by index
|
// Trimmed by --tail: filter selection by index
|
||||||
filtered := make(map[int32]selectedItem)
|
filtered := make(map[int32]selectedItem)
|
||||||
@@ -1540,16 +1569,26 @@ func (t *Terminal) output() bool {
|
|||||||
for _, s := range t.printQueue {
|
for _, s := range t.printQueue {
|
||||||
t.printer(s)
|
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
|
found := len(t.selected) > 0
|
||||||
if !found {
|
if !found {
|
||||||
current := t.currentItem()
|
current := t.currentItem()
|
||||||
if current != nil {
|
if current != nil {
|
||||||
t.printer(current.AsString(t.ansi))
|
t.printer(transform(current))
|
||||||
found = true
|
found = true
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for _, sel := range t.sortSelected() {
|
for _, sel := range t.sortSelected() {
|
||||||
t.printer(sel.item.AsString(t.ansi))
|
t.printer(transform(sel.item))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return found
|
return found
|
||||||
@@ -1712,6 +1751,7 @@ func (t *Terminal) hasHeaderLinesWindow() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
|
func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
|
||||||
|
t.clearNumLinesCache()
|
||||||
t.forcePreview = forcePreview
|
t.forcePreview = forcePreview
|
||||||
screenWidth, screenHeight, marginInt, paddingInt := t.adjustMarginAndPadding()
|
screenWidth, screenHeight, marginInt, paddingInt := t.adjustMarginAndPadding()
|
||||||
width := screenWidth - marginInt[1] - marginInt[3]
|
width := screenWidth - marginInt[1] - marginInt[3]
|
||||||
@@ -1900,6 +1940,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
|
|||||||
pwidth -= 1
|
pwidth -= 1
|
||||||
}
|
}
|
||||||
t.pwindow = t.tui.NewWindow(y, x, pwidth, pheight, tui.WindowPreview, noBorder, true)
|
t.pwindow = t.tui.NewWindow(y, x, pwidth, pheight, tui.WindowPreview, noBorder, true)
|
||||||
|
t.pwindow.SetWrapSign(t.wrapSign, t.wrapSignWidth)
|
||||||
if !hadPreviewWindow {
|
if !hadPreviewWindow {
|
||||||
t.pwindow.Erase()
|
t.pwindow.Erase()
|
||||||
}
|
}
|
||||||
@@ -2932,7 +2973,7 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
|
|||||||
}
|
}
|
||||||
if !wholeCovered && t.nthAttr > 0 {
|
if !wholeCovered && t.nthAttr > 0 {
|
||||||
var tokens []Token
|
var tokens []Token
|
||||||
if item.transformed != nil {
|
if item.transformed != nil && item.transformed.revision == t.merger.revision {
|
||||||
tokens = item.transformed.tokens
|
tokens = item.transformed.tokens
|
||||||
} else {
|
} else {
|
||||||
tokens = Transform(Tokenize(item.text.ToString(), t.delimiter), t.nthCurrent)
|
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)
|
elems, prefixLength := awkTokenizer(params.query)
|
||||||
tokens := withPrefixLengths(elems, prefixLength)
|
tokens := withPrefixLengths(elems, prefixLength)
|
||||||
trans := Transform(tokens, nth)
|
trans := Transform(tokens, nth)
|
||||||
result := joinTokens(trans)
|
result := JoinTokens(trans)
|
||||||
if !flags.preserveSpace {
|
if !flags.preserveSpace {
|
||||||
result = strings.TrimSpace(result)
|
result = strings.TrimSpace(result)
|
||||||
}
|
}
|
||||||
@@ -3875,7 +3916,7 @@ func replacePlaceholder(params replacePlaceholderParams) (string, []string) {
|
|||||||
replace = func(item *Item) string {
|
replace = func(item *Item) string {
|
||||||
tokens := Tokenize(item.AsString(params.stripAnsi), params.delimiter)
|
tokens := Tokenize(item.AsString(params.stripAnsi), params.delimiter)
|
||||||
trans := Transform(tokens, ranges)
|
trans := Transform(tokens, ranges)
|
||||||
str := joinTokens(trans)
|
str := JoinTokens(trans)
|
||||||
|
|
||||||
// trim the last delimiter
|
// trim the last delimiter
|
||||||
if params.delimiter.str != nil {
|
if params.delimiter.str != nil {
|
||||||
@@ -4719,6 +4760,7 @@ func (t *Terminal) Loop() error {
|
|||||||
changed := false
|
changed := false
|
||||||
beof := false
|
beof := false
|
||||||
queryChanged := false
|
queryChanged := false
|
||||||
|
denylist := []int32{}
|
||||||
|
|
||||||
// Special handling of --sync. Activate the interface on the second tick.
|
// Special handling of --sync. Activate the interface on the second tick.
|
||||||
if loopIndex == 1 && t.deferActivation() {
|
if loopIndex == 1 && t.deferActivation() {
|
||||||
@@ -4875,6 +4917,27 @@ func (t *Terminal) Loop() error {
|
|||||||
}
|
}
|
||||||
case actBell:
|
case actBell:
|
||||||
t.tui.Bell()
|
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:
|
case actExecute, actExecuteSilent:
|
||||||
t.executeCommand(a.a, false, a.t == actExecuteSilent, false, false, "")
|
t.executeCommand(a.a, false, a.t == actExecuteSilent, false, false, "")
|
||||||
case actExecuteMulti:
|
case actExecuteMulti:
|
||||||
@@ -5021,34 +5084,52 @@ func (t *Terminal) Loop() error {
|
|||||||
} else {
|
} else {
|
||||||
req(reqHeader)
|
req(reqHeader)
|
||||||
}
|
}
|
||||||
case actChangeHeaderLabel:
|
case actChangeHeaderLabel, actTransformHeaderLabel:
|
||||||
t.headerLabelOpts.label = a.a
|
label := a.a
|
||||||
if t.headerBorder != nil {
|
if a.t == actTransformHeaderLabel {
|
||||||
t.headerLabel, t.headerLabelLen = t.ansiLabelPrinter(a.a, &tui.ColHeaderLabel, false)
|
label = t.captureLine(a.a)
|
||||||
req(reqRedrawHeaderLabel)
|
|
||||||
}
|
}
|
||||||
case actChangeInputLabel:
|
t.headerLabelOpts.label = label
|
||||||
t.inputLabelOpts.label = a.a
|
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 {
|
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)
|
req(reqRedrawInputLabel)
|
||||||
}
|
}
|
||||||
case actChangeListLabel:
|
case actChangeListLabel, actTransformListLabel:
|
||||||
t.listLabelOpts.label = a.a
|
label := a.a
|
||||||
|
if a.t == actTransformListLabel {
|
||||||
|
label = t.captureLine(a.a)
|
||||||
|
}
|
||||||
|
t.listLabelOpts.label = label
|
||||||
if t.wborder != nil {
|
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)
|
req(reqRedrawListLabel)
|
||||||
}
|
}
|
||||||
case actChangeBorderLabel:
|
case actChangeBorderLabel, actTransformBorderLabel:
|
||||||
t.borderLabelOpts.label = a.a
|
label := a.a
|
||||||
|
if a.t == actTransformBorderLabel {
|
||||||
|
label = t.captureLine(a.a)
|
||||||
|
}
|
||||||
|
t.borderLabelOpts.label = label
|
||||||
if t.border != nil {
|
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)
|
req(reqRedrawBorderLabel)
|
||||||
}
|
}
|
||||||
case actChangePreviewLabel:
|
case actChangePreviewLabel, actTransformPreviewLabel:
|
||||||
t.previewLabelOpts.label = a.a
|
label := a.a
|
||||||
|
if a.t == actTransformPreviewLabel {
|
||||||
|
label = t.captureLine(a.a)
|
||||||
|
}
|
||||||
|
t.previewLabelOpts.label = label
|
||||||
if t.pborder != nil {
|
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)
|
req(reqRedrawPreviewLabel)
|
||||||
}
|
}
|
||||||
case actTransform:
|
case actTransform:
|
||||||
@@ -5056,41 +5137,6 @@ func (t *Terminal) Loop() error {
|
|||||||
if actions, err := parseSingleActionList(strings.Trim(body, "\r\n")); err == nil {
|
if actions, err := parseSingleActionList(strings.Trim(body, "\r\n")); err == nil {
|
||||||
return doActions(actions)
|
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:
|
case actChangePrompt:
|
||||||
t.promptString = a.a
|
t.promptString = a.a
|
||||||
t.prompt, t.promptLen = t.parsePrompt(a.a)
|
t.prompt, t.promptLen = t.parsePrompt(a.a)
|
||||||
@@ -5464,9 +5510,11 @@ func (t *Terminal) Loop() error {
|
|||||||
req(reqList, reqInfo, reqPrompt, reqHeader)
|
req(reqList, reqInfo, reqPrompt, reqHeader)
|
||||||
case actToggleWrap:
|
case actToggleWrap:
|
||||||
t.wrap = !t.wrap
|
t.wrap = !t.wrap
|
||||||
|
t.clearNumLinesCache()
|
||||||
req(reqList, reqHeader)
|
req(reqList, reqHeader)
|
||||||
case actToggleMultiLine:
|
case actToggleMultiLine:
|
||||||
t.multiLine = !t.multiLine
|
t.multiLine = !t.multiLine
|
||||||
|
t.clearNumLinesCache()
|
||||||
req(reqList)
|
req(reqList)
|
||||||
case actToggleHscroll:
|
case actToggleHscroll:
|
||||||
// Force re-rendering of the list
|
// Force re-rendering of the list
|
||||||
@@ -5999,7 +6047,7 @@ func (t *Terminal) Loop() error {
|
|||||||
reload := changed || newCommand != nil
|
reload := changed || newCommand != nil
|
||||||
var reloadRequest *searchRequest
|
var reloadRequest *searchRequest
|
||||||
if reload {
|
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
|
t.mutex.Unlock() // Must be unlocked before touching reqBox
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
"github.com/junegunn/fzf/src/util"
|
"github.com/junegunn/fzf/src/util"
|
||||||
)
|
)
|
||||||
@@ -77,6 +78,11 @@ type Delimiter struct {
|
|||||||
str *string
|
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.
|
// String returns the string representation of a Delimiter.
|
||||||
func (d Delimiter) String() string {
|
func (d Delimiter) String() string {
|
||||||
return fmt.Sprintf("Delimiter{regex: %v, str: &%q}", d.regex, *d.str)
|
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)
|
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
|
var output bytes.Buffer
|
||||||
for _, token := range tokens {
|
for _, token := range tokens {
|
||||||
output.WriteString(token.text.ToString())
|
output.WriteString(token.text.ToString())
|
||||||
@@ -229,7 +250,7 @@ func Transform(tokens []Token, withNth []Range) []Token {
|
|||||||
if r.begin == r.end {
|
if r.begin == r.end {
|
||||||
idx := r.begin
|
idx := r.begin
|
||||||
if idx == rangeEllipsis {
|
if idx == rangeEllipsis {
|
||||||
chars := util.ToChars(stringBytes(joinTokens(tokens)))
|
chars := util.ToChars(stringBytes(JoinTokens(tokens)))
|
||||||
parts = append(parts, &chars)
|
parts = append(parts, &chars)
|
||||||
} else {
|
} else {
|
||||||
if idx < 0 {
|
if idx < 0 {
|
||||||
|
|||||||
@@ -85,14 +85,14 @@ func TestTransform(t *testing.T) {
|
|||||||
{
|
{
|
||||||
ranges, _ := splitNth("1,2,3")
|
ranges, _ := splitNth("1,2,3")
|
||||||
tx := Transform(tokens, ranges)
|
tx := Transform(tokens, ranges)
|
||||||
if joinTokens(tx) != "abc: def: ghi: " {
|
if JoinTokens(tx) != "abc: def: ghi: " {
|
||||||
t.Errorf("%s", tx)
|
t.Errorf("%s", tx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
ranges, _ := splitNth("1..2,3,2..,1")
|
ranges, _ := splitNth("1..2,3,2..,1")
|
||||||
tx := Transform(tokens, ranges)
|
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 ||
|
len(tx) != 4 ||
|
||||||
tx[0].text.ToString() != "abc: def: " || tx[0].prefixLength != 2 ||
|
tx[0].text.ToString() != "abc: def: " || tx[0].prefixLength != 2 ||
|
||||||
tx[1].text.ToString() != "ghi: " || tx[1].prefixLength != 14 ||
|
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")
|
ranges, _ := splitNth("1..2,3,2..,1")
|
||||||
tx := Transform(tokens, ranges)
|
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 ||
|
len(tx) != 4 ||
|
||||||
tx[0].text.ToString() != " abc: def:" || tx[0].prefixLength != 0 ||
|
tx[0].text.ToString() != " abc: def:" || tx[0].prefixLength != 0 ||
|
||||||
tx[1].text.ToString() != " ghi:" || tx[1].prefixLength != 12 ||
|
tx[1].text.ToString() != " ghi:" || tx[1].prefixLength != 12 ||
|
||||||
|
|||||||
@@ -44,8 +44,9 @@ func (r *LightRenderer) stderr(str string) {
|
|||||||
r.stderrInternal(str, true, "")
|
r.stderrInternal(str, true, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
const CR string = "\x1b[2m␍"
|
const DIM string = "\x1b[2m"
|
||||||
const LF string = "\x1b[2m␊"
|
const CR string = DIM + "␍"
|
||||||
|
const LF string = DIM + "␊"
|
||||||
|
|
||||||
func (r *LightRenderer) stderrInternal(str string, allowNLCR bool, resetCode string) {
|
func (r *LightRenderer) stderrInternal(str string, allowNLCR bool, resetCode string) {
|
||||||
bytes := []byte(str)
|
bytes := []byte(str)
|
||||||
@@ -140,6 +141,8 @@ type LightWindow struct {
|
|||||||
tabstop int
|
tabstop int
|
||||||
fg Color
|
fg Color
|
||||||
bg 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) {
|
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
|
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{}
|
lines := []wrappedLine{}
|
||||||
width := 0
|
width := 0
|
||||||
line := ""
|
line := ""
|
||||||
gr := uniseg.NewGraphemes(input)
|
gr := uniseg.NewGraphemes(input)
|
||||||
|
max := initialMax
|
||||||
for gr.Next() {
|
for gr.Next() {
|
||||||
rs := gr.Runes()
|
rs := gr.Runes()
|
||||||
str := string(rs)
|
str := string(rs)
|
||||||
@@ -1131,6 +1135,7 @@ func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLin
|
|||||||
line = str
|
line = str
|
||||||
prefixLength = 0
|
prefixLength = 0
|
||||||
width = w
|
width = w
|
||||||
|
max = initialMax - wrapSignWidth
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lines = append(lines, wrappedLine{string(line), width})
|
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 {
|
func (w *LightWindow) fill(str string, resetCode string) FillReturn {
|
||||||
allLines := strings.Split(str, "\n")
|
allLines := strings.Split(str, "\n")
|
||||||
for i, line := range allLines {
|
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 {
|
for j, wl := range lines {
|
||||||
w.stderrInternal(wl.text, false, resetCode)
|
w.stderrInternal(wl.text, false, resetCode)
|
||||||
w.posx += wl.displayWidth
|
w.posx += wl.displayWidth
|
||||||
@@ -1153,6 +1158,18 @@ func (w *LightWindow) fill(str string, resetCode string) FillReturn {
|
|||||||
w.MoveAndClear(w.posy, w.posx)
|
w.MoveAndClear(w.posy, w.posx)
|
||||||
w.Move(w.posy+1, 0)
|
w.Move(w.posy+1, 0)
|
||||||
w.renderer.stderr(resetCode)
|
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
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *LightWindow) SetWrapSign(sign string, width int) {
|
||||||
|
w.wrapSign = sign
|
||||||
|
w.wrapSignWidth = width
|
||||||
|
}
|
||||||
|
|
||||||
func (r *LightRenderer) HideCursor() {
|
func (r *LightRenderer) HideCursor() {
|
||||||
r.showCursor = false
|
r.showCursor = false
|
||||||
r.csi("?25l")
|
r.csi("?25l")
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ type TcellWindow struct {
|
|||||||
uri *string
|
uri *string
|
||||||
params *string
|
params *string
|
||||||
showCursor bool
|
showCursor bool
|
||||||
|
wrapSign string
|
||||||
|
wrapSignWidth int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *TcellWindow) Top() int {
|
func (w *TcellWindow) Top() int {
|
||||||
@@ -629,6 +631,11 @@ func (w *TcellWindow) EraseMaybe() bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *TcellWindow) SetWrapSign(sign string, width int) {
|
||||||
|
w.wrapSign = sign
|
||||||
|
w.wrapSignWidth = width
|
||||||
|
}
|
||||||
|
|
||||||
func (w *TcellWindow) EncloseX(x int) bool {
|
func (w *TcellWindow) EncloseX(x int) bool {
|
||||||
return x >= w.left && x < (w.left+w.width)
|
return x >= w.left && x < (w.left+w.width)
|
||||||
}
|
}
|
||||||
@@ -757,11 +764,26 @@ Loop:
|
|||||||
|
|
||||||
// word wrap:
|
// word wrap:
|
||||||
xPos := w.left + w.lastX + lx
|
xPos := w.left + w.lastX + lx
|
||||||
if xPos >= (w.left + w.width) {
|
if xPos >= w.left+w.width {
|
||||||
w.lastY++
|
w.lastY++
|
||||||
|
if w.lastY >= w.height {
|
||||||
|
return FillSuspend
|
||||||
|
}
|
||||||
w.lastX = 0
|
w.lastX = 0
|
||||||
lx = 0
|
lx = 0
|
||||||
xPos = w.left
|
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
|
yPos := w.top + w.lastY
|
||||||
|
|||||||
@@ -659,6 +659,8 @@ type Window interface {
|
|||||||
LinkEnd()
|
LinkEnd()
|
||||||
Erase()
|
Erase()
|
||||||
EraseMaybe() bool
|
EraseMaybe() bool
|
||||||
|
|
||||||
|
SetWrapSign(string, int)
|
||||||
}
|
}
|
||||||
|
|
||||||
type FullscreenRenderer struct {
|
type FullscreenRenderer struct {
|
||||||
|
|||||||
@@ -184,9 +184,25 @@ func (chars *Chars) TrailingWhitespaces() int {
|
|||||||
return whitespaces
|
return whitespaces
|
||||||
}
|
}
|
||||||
|
|
||||||
func (chars *Chars) TrimTrailingWhitespaces() {
|
func (chars *Chars) TrimSuffix(runes []rune) {
|
||||||
whitespaces := chars.TrailingWhitespaces()
|
lastIdx := len(chars.slice)
|
||||||
chars.slice = chars.slice[0 : len(chars.slice)-whitespaces]
|
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 {
|
func (chars *Chars) ToString() string {
|
||||||
|
|||||||
@@ -1665,4 +1665,120 @@ class TestCore < TestInteractive
|
|||||||
assert_equal '', File.read(tempname).chomp
|
assert_equal '', File.read(tempname).chomp
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|||||||
@@ -52,6 +52,12 @@ class TestFilter < TestBase
|
|||||||
`find . -print0 | #{FZF} --read0 -e -f "^#{lines.last}$"`.chomp
|
`find . -print0 | #{FZF} --read0 -e -f "^#{lines.last}$"`.chomp
|
||||||
end
|
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
|
def test_with_nth_basic
|
||||||
writelines(['hello world ', 'byebye'])
|
writelines(['hello world ', 'byebye'])
|
||||||
assert_equal \
|
assert_equal \
|
||||||
@@ -59,6 +65,13 @@ class TestFilter < TestBase
|
|||||||
`#{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1 < #{tempname}`.chomp
|
`#{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1 < #{tempname}`.chomp
|
||||||
end
|
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
|
def test_with_nth_ansi
|
||||||
writelines(["\x1b[33mhello \x1b[34;1mworld\x1b[m ", 'byebye'])
|
writelines(["\x1b[33mhello \x1b[34;1mworld\x1b[m ", 'byebye'])
|
||||||
assert_equal \
|
assert_equal \
|
||||||
|
|||||||
@@ -978,4 +978,17 @@ class TestLayout < TestInteractive
|
|||||||
setup
|
setup
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|||||||
@@ -453,7 +453,7 @@ class TestPreview < TestInteractive
|
|||||||
tmux.send_keys 'f'
|
tmux.send_keys 'f'
|
||||||
tmux.until do |lines|
|
tmux.until do |lines|
|
||||||
assert_equal '::', lines[0]
|
assert_equal '::', lines[0]
|
||||||
assert_equal ' 3', lines[1]
|
assert_equal '↳ 3', lines[1]
|
||||||
end
|
end
|
||||||
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.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 { |lines| assert_equal 10, lines.match_count }
|
||||||
tmux.until do |lines|
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user