Compare commits

..

21 Commits

Author SHA1 Message Date
Junegunn Choi
4993d19466 0.32.1 2022-08-08 23:57:32 +09:00
Junegunn Choi
19f9bbca0d Allow specifying fzf options in $FZF_TMUX_OPTS without '--' 2022-08-08 23:50:47 +09:00
Junegunn Choi
779d8e1627 Use go 1.19 2022-08-08 13:39:00 +09:00
Junegunn Choi
bb07410448 Add --strip-cwd-prefix to fd examples 2022-08-08 13:38:21 +09:00
Yang Tian
d826f9e72f [fzf-tmux] Use fzf border instead of tmux popup border (#2908)
Co-authored-by: Yang Tian <yang.tian@getcruise.com>
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2022-08-07 10:28:45 +09:00
Junegunn Choi
6a6130615d [fzf-tmux] Remove code for tmux 3.2 beta compatibility 2022-08-06 19:08:13 +09:00
lbesnard
a8e7021be2 [completion] ssh: Remove values with '%' (#2548)
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2022-08-03 22:39:42 +09:00
Junegunn Choi
38259d0382 Fix incorrect ordering of --tiebreak=chunk 2022-08-03 22:18:26 +09:00
Junegunn Choi
f7e7259910 0.32.0 2022-08-02 21:56:14 +09:00
Junegunn Choi
f0bfeba733 Add new tiebreak: 'chunk'
Favors the line with shorter matched chunk. A chunk is a set of
consecutive non-whitespace characters.

Unlike the default `length`, this new scheme works well with tabular input.

  # length prefers item #1, because the whole line is shorter,
  # chunk prefers item #2, because the matched chunk ("foo") is shorter
  fzf --height=6 --header-lines=2 --tiebreak=chunk --reverse --query=fo << "EOF"
  N | Field1 | Field2 | Field3
  - | ------ | ------ | ------
  1 | hello  | foobar | baz
  2 | world  | foo    | bazbaz
  EOF

If the input does not contain any spaces, `chunk` is equivalent to
`length`. But we're not going to set it as the default because it is
computationally more expensive.

Close #2285
Close #2537
- Not the exact solution to --tiebreak=length not taking --nth into account,
  but this should work. And the added benefit is that it works well even
  when --nth is not provided.
- Adding a bonus point to the last character of a word didn't turn out great.
  The order of the result suddenly changes when you type in the last
  character in the word producing a jarring effect.
2022-08-02 21:48:19 +09:00
Junegunn Choi
c3a7a24eea Tweak bonus points to word boundaries
Close https://github.com/junegunn/fzf.vim/issues/1004

  # jobs/latency.js is favored over job_latency.js
  printf 'job_latency.js\njobs/latency.js' | fzf -qlatency
2022-08-02 20:57:13 +09:00
Junegunn Choi
bbbcd780c9 Allow "--version" to be used as the argument to --query, --header, et al.
But why?
2022-08-02 20:57:12 +09:00
kissge
475469a2e7 [zsh] Make awk regex compatible with both GNU and macOS (#2906) 2022-08-02 20:56:25 +09:00
Junegunn Choi
3a7447dcb6 Update FUNDING.yml 2022-07-30 19:35:41 +09:00
Junegunn Choi
e5d8cbd383 [vim] Fix version check on windows/powershell (addendum)
Should handle powershell.exe as well

Fix https://github.com/junegunn/fzf.vim/issues/1411
2022-07-30 19:08:04 +09:00
Junegunn Choi
3c08dca7e7 Fix README examples so that they work both on bash and zsh
Close #2887
2022-07-29 22:03:02 +09:00
Carl Kamholtz
d083f01d22 [vim] Add option to force 24 bit colors on Windows (#2889) 2022-07-29 21:51:35 +09:00
Bob Matcuk
68cf393644 [bash] Fix 'possible retry loop' problem of bash-completion (#2891)
Close #2474
Close #2583
2022-07-29 21:50:59 +09:00
Junegunn Choi
18f7230662 Fix mouse location in --height mode
Fix #2900
2022-07-29 15:42:44 +09:00
Carl Kamholtz
728f735281 [vim] Fix version check on windows/powershell (#2894)
- Replace fzf#shellescape with shellescape
- Prepend command with '&' in powershell to deal with quoted exe
2022-07-29 08:18:06 +09:00
Junegunn Choi
ecc418ba77 0.31.0 2022-07-21 23:04:57 +09:00
25 changed files with 263 additions and 104 deletions

2
.github/FUNDING.yml vendored
View File

@@ -1 +1 @@
custom: ["https://paypal.me/junegunn", "https://www.buymeacoffee.com/junegunn"]
github: junegunn

View File

@@ -18,7 +18,7 @@ builds:
post: |
sh -c '
cat > /tmp/fzf-gon-amd64.hcl << EOF
source = ["./dist/fzf-macos_darwin_amd64/fzf"]
source = ["./dist/fzf-macos_darwin_amd64_v1/fzf"]
bundle_id = "kr.junegunn.fzf"
apple_id {
username = "junegunn.c@gmail.com"

View File

@@ -1 +1 @@
golang 1.18
golang 1.19

View File

@@ -1,6 +1,54 @@
CHANGELOG
=========
0.32.1
------
- Fixed incorrect ordering of `--tiebreak=chunk`
- fzf-tmux will show fzf border instead of tmux popup border (requires tmux 3.3)
```sh
fzf-tmux -p70%
fzf-tmux -p70% --color=border:bright-red
fzf-tmux -p100%,60% --color=border:bright-yellow --border=horizontal --padding 1,5 --margin 1,0
fzf-tmux -p70%,100% --color=border:bright-green --border=vertical
# Key bindings (CTRL-T, CTRL-R, ALT-C) will use these options
export FZF_TMUX_OPTS='-p100%,60% --color=border:green --border=horizontal --padding 1,5 --margin 1,0'
```
0.32.0
------
- Updated the scoring algorithm
- Different bonus points to different categories of word boundaries
(listed higher to lower bonus point)
- Word after whitespace characters or beginning of the string
- Word after common delimiter characters (`/,:;|`)
- Word after other non-word characters
```sh
# foo/bar.sh` is preferred over `foo-bar.sh` on `bar`
fzf --query=bar --height=4 << EOF
foo-bar.sh
foo/bar.sh
EOF
```
- Added a new tiebreak `chunk`
- Favors the line with shorter matched chunk. A chunk is a set of
consecutive non-whitespace characters.
- Unlike the default `length`, this scheme works well with tabular input
```sh
# length prefers item #1, because the whole line is shorter,
# chunk prefers item #2, because the matched chunk ("foo") is shorter
fzf --height=6 --header-lines=2 --tiebreak=chunk --reverse --query=fo << "EOF"
N | Field1 | Field2 | Field3
- | ------ | ------ | ------
1 | hello | foobar | baz
2 | world | foo | bazbaz
EOF
```
- If the input does not contain any spaces, `chunk` is equivalent to
`length`. But we're not going to set it as the default because it is
computationally more expensive.
- Bug fixes and improvements
0.31.0
------
- Added support for an alternative preview window layout that is activated

View File

@@ -557,7 +557,7 @@ more details.
```sh
FZF_DEFAULT_COMMAND='ps -ef' \
fzf --bind 'ctrl-r:reload($FZF_DEFAULT_COMMAND)' \
fzf --bind 'ctrl-r:reload(eval "$FZF_DEFAULT_COMMAND")' \
--header 'Press CTRL-R to reload' --header-lines=1 \
--height=50% --layout=reverse
```
@@ -566,7 +566,7 @@ FZF_DEFAULT_COMMAND='ps -ef' \
```sh
FZF_DEFAULT_COMMAND='find . -type f' \
fzf --bind 'ctrl-d:reload(find . -type d),ctrl-f:reload($FZF_DEFAULT_COMMAND)' \
fzf --bind 'ctrl-d:reload(find . -type d),ctrl-f:reload(eval "$FZF_DEFAULT_COMMAND")' \
--height=50% --layout=reverse
```
@@ -662,10 +662,10 @@ default find command to traverse the file system while respecting
```sh
# Feed the output of fd into fzf
fd --type f | fzf
fd --type f --strip-cwd-prefix | fzf
# Setting fd as the default source for fzf
export FZF_DEFAULT_COMMAND='fd --type f'
export FZF_DEFAULT_COMMAND='fd --type f --strip-cwd-prefix'
# Now fzf (w/o pipe) will use fd instead of find
fzf
@@ -678,7 +678,7 @@ If you want the command to follow symbolic links and don't want it to exclude
hidden files, use the following command:
```sh
export FZF_DEFAULT_COMMAND='fd --type f --hidden --follow --exclude .git'
export FZF_DEFAULT_COMMAND='fd --type f --strip-cwd-prefix --hidden --follow --exclude .git'
```
#### Fish shell

View File

@@ -10,7 +10,6 @@ fail() {
fzf="$(command -v fzf 2> /dev/null)" || fzf="$(dirname "$0")/fzf"
[[ -x "$fzf" ]] || fail 'fzf executable not found'
tmux_args=()
args=()
opt=""
skip=""
@@ -58,7 +57,7 @@ while [[ $# -gt 0 ]]; do
;;
-p*|-w*|-h*|-x*|-y*|-d*|-u*|-r*|-l*)
if [[ "$arg" =~ ^-[pwhxy] ]]; then
[[ "$opt" =~ "-K -E" ]] || opt="-K -E"
[[ "$opt" =~ "-E" ]] || opt="-E"
elif [[ "$arg" =~ ^.[lr] ]]; then
opt="-h"
if [[ "$arg" =~ ^.l ]]; then
@@ -119,8 +118,6 @@ while [[ $# -gt 0 ]]; do
# "--" can be used to separate fzf-tmux options from fzf options to
# avoid conflicts
skip=1
tmux_args=("${args[@]}")
args=()
continue
;;
*)
@@ -139,7 +136,7 @@ fi
args=("${args[@]}" "--no-height" "--bind=ctrl-z:ignore")
# Handle zoomed tmux pane without popup options by moving it to a temp window
if [[ ! "$opt" =~ "-K -E" ]] && tmux list-panes -F '#F' | grep -q Z; then
if [[ ! "$opt" =~ "-E" ]] && tmux list-panes -F '#F' | grep -q Z; then
zoomed_without_popup=1
original_window=$(tmux display-message -p "#{window_id}")
tmp_window=$(tmux new-window -d -P -F "#{window_id}" "bash -c 'while :; do for c in \\| / - '\\;' do sleep 0.2; printf \"\\r\$c fzf-tmux is running\\r\"; done; done'")
@@ -181,7 +178,14 @@ trap 'cleanup 1' SIGUSR1
trap 'cleanup' EXIT
envs="export TERM=$TERM "
[[ "$opt" =~ "-K -E" ]] && FZF_DEFAULT_OPTS="--margin 0,1 $FZF_DEFAULT_OPTS"
if [[ "$opt" =~ "-E" ]]; then
FZF_DEFAULT_OPTS="--margin 0,1 $FZF_DEFAULT_OPTS"
tmux_verson=$(tmux -V)
if [[ ! $tmux_verson =~ 3\.2 ]]; then
FZF_DEFAULT_OPTS="--border $FZF_DEFAULT_OPTS"
opt="-B $opt"
fi
fi
[[ -n "$FZF_DEFAULT_OPTS" ]] && envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS")"
[[ -n "$FZF_DEFAULT_COMMAND" ]] && envs="$envs FZF_DEFAULT_COMMAND=$(printf %q "$FZF_DEFAULT_COMMAND")"
echo "$envs;" > "$argsf"
@@ -195,7 +199,7 @@ close="; trap - EXIT SIGINT SIGTERM $close"
export TMUX=$(cut -d , -f 1,2 <<< "$TMUX")
mkfifo -m o+w $fifo2
if [[ "$opt" =~ "-K -E" ]]; then
if [[ "$opt" =~ "-E" ]]; then
cat $fifo2 &
if [[ -n "$term" ]] || [[ -t 0 ]]; then
cat <<< "\"$fzf\" $opts > $fifo2; out=\$? $close; exit \$out" >> $argsf
@@ -205,15 +209,7 @@ if [[ "$opt" =~ "-K -E" ]]; then
cat <&0 > $fifo1 &
fi
# tmux dropped the support for `-K`, `-R` to popup command
# TODO: We can remove this once tmux 3.2 is released
if [[ ! "$(tmux popup --help 2>&1)" =~ '-R shell-command' ]]; then
opt="${opt/-K/}"
else
opt="${opt} -R"
fi
tmux popup -d "$PWD" "${tmux_args[@]}" $opt "bash $argsf" > /dev/null 2>&1
tmux popup -d "$PWD" $opt "bash $argsf" > /dev/null 2>&1
exit $?
fi
@@ -227,7 +223,7 @@ else
fi
tmux set-window-option synchronize-panes off \;\
set-window-option remain-on-exit off \;\
split-window -c "$PWD" $opt "${tmux_args[@]}" "bash -c 'exec -a fzf bash $argsf'" $swap \
split-window -c "$PWD" $opt "bash -c 'exec -a fzf bash $argsf'" $swap \
> /dev/null 2>&1 || { "$fzf" "${args[@]}"; exit $?; }
cat $fifo2
exit "$(cat $fifo3)"

View File

@@ -2,7 +2,7 @@
set -u
version=0.31.0
version=0.32.1
auto_completion=
key_bindings=
update_config=2

View File

@@ -1,4 +1,4 @@
$version="0.31.0"
$version="0.32.1"
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition

View File

@@ -5,7 +5,7 @@ import (
"github.com/junegunn/fzf/src/protector"
)
var version string = "0.31"
var version string = "0.32"
var revision string = "devel"
func main() {

View File

@@ -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 "Jul 2022" "fzf 0.31.0" "fzf-tmux - open fzf in tmux split pane"
.TH fzf-tmux 1 "Aug 2022" "fzf 0.32.1" "fzf-tmux - open fzf in tmux split pane"
.SH NAME
fzf-tmux - open fzf in tmux split pane

View File

@@ -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 "Jul 2022" "fzf 0.31.0" "fzf - a command-line fuzzy finder"
.TH fzf 1 "Aug 2022" "fzf 0.32.1" "fzf - a command-line fuzzy finder"
.SH NAME
fzf - a command-line fuzzy finder
@@ -95,6 +95,8 @@ Comma-separated list of sort criteria to apply when the scores are tied.
.br
.BR length " Prefers line with shorter length"
.br
.BR chunk " Prefers line with shorter matched chunk (delimited by whitespaces)"
.br
.BR begin " Prefers line with matched substring closer to the beginning"
.br
.BR end " Prefers line with matched substring closer to the end"

View File

@@ -164,7 +164,7 @@ function s:get_version(bin)
if has_key(s:versions, a:bin)
return s:versions[a:bin]
end
let command = fzf#shellescape(a:bin) . ' --version --no-height'
let command = (&shell =~ 'powershell' ? '&' : '') . shellescape(a:bin) . ' --version --no-height'
let output = systemlist(command)
if v:shell_error || empty(output)
return ''
@@ -342,7 +342,8 @@ function! s:common_sink(action, lines) abort
endfunction
function! s:get_color(attr, ...)
let gui = !s:is_win && !has('win32unix') && has('termguicolors') && &termguicolors
" Force 24 bit colors: g:fzf_force_termguicolors (temporary workaround for https://github.com/junegunn/fzf.vim/issues/1152)
let gui = get(g:, 'fzf_force_termguicolors', 0) || (!s:is_win && !has('win32unix') && has('termguicolors') && &termguicolors)
let fam = gui ? 'gui' : 'cterm'
let pat = gui ? '^#[a-f0-9]\+' : '^[0-9]\+$'
for group in a:000

View File

@@ -161,7 +161,11 @@ _fzf_handle_dynamic_completion() {
__fzf_generic_path_completion() {
local cur base dir leftover matches trigger cmd
cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}"
cmd="${COMP_WORDS[0]}"
if [[ $cmd == \\* ]]; then
cmd="${cmd:1}"
fi
cmd="${cmd//[^A-Za-z0-9_=]/_}"
COMPREPLY=()
trigger=${FZF_COMPLETION_TRIGGER-'**'}
cur="${COMP_WORDS[COMP_CWORD]}"
@@ -275,7 +279,7 @@ _fzf_proc_completion_post() {
_fzf_host_completion() {
_fzf_complete +m -- "$@" < <(
command cat <(command tail -n +1 ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null | command grep -i '^\s*host\(name\)\? ' | awk '{for (i = 2; i <= NF; i++) print $1 " " $i}' | command grep -v '[*?]') \
command cat <(command tail -n +1 ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null | command grep -i '^\s*host\(name\)\? ' | awk '{for (i = 2; i <= NF; i++) print $1 " " $i}' | command grep -v '[*?%]') \
<(command grep -oE '^[[a-z0-9.,:-]+' ~/.ssh/known_hosts | tr ',' '\n' | tr -d '[' | awk '{ print $1 " " $1 }') \
<(command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') |
awk '{if (length($2) > 0) {print $2}}' | sort -u

View File

@@ -224,7 +224,7 @@ _fzf_complete_telnet() {
_fzf_complete_ssh() {
_fzf_complete +m -- "$@" < <(
setopt localoptions nonomatch
command cat <(command tail -n +1 ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null | command grep -i '^\s*host\(name\)\? ' | awk '{for (i = 2; i <= NF; i++) print $1 " " $i}' | command grep -v '[*?]') \
command cat <(command tail -n +1 ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null | command grep -i '^\s*host\(name\)\? ' | awk '{for (i = 2; i <= NF; i++) print $1 " " $i}' | command grep -v '[*?%]') \
<(command grep -oE '^[[a-z0-9.,:-]+' ~/.ssh/known_hosts | tr ',' '\n' | tr -d '[' | awk '{ print $1 " " $1 }') \
<(command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') |
awk '{if (length($2) > 0) {print $2}}' | sort -u

View File

@@ -97,7 +97,7 @@ bindkey -M viins '\ec' fzf-cd-widget
fzf-history-widget() {
local selected num
setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases 2> /dev/null
selected=( $(fc -rl 1 | awk '{ cmd=$0; sub(/^\s*[0-9]+\**\s+/, "", cmd); if (!seen[cmd]++) print $0 }' |
selected=( $(fc -rl 1 | awk '{ cmd=$0; sub(/^[ \t]*[0-9]+\**[ \t]+/, "", cmd); if (!seen[cmd]++) print $0 }' |
FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort,ctrl-z:ignore $FZF_CTRL_R_OPTS --query=${(qqq)LBUFFER} +m" $(__fzfcmd)) )
local ret=$?
if [ -n "$selected" ]; then

View File

@@ -89,6 +89,9 @@ import (
var DEBUG bool
const delimiterChars = "/,:;|"
const whiteChars = " \t\n\v\f\r\x85\xA0"
func indexAt(index int, max int, forward bool) int {
if forward {
return index
@@ -117,6 +120,12 @@ const (
// in web2 dictionary and my file system.
bonusBoundary = scoreMatch / 2
// Extra bonus for word boundary after whitespace character or beginning of the string
bonusBoundaryWhite = bonusBoundary + 2
// Extra bonus for word boundary after slash, colon, semi-colon, and comma
bonusBoundaryDelimiter = bonusBoundary + 1
// Although bonus point for non-word characters is non-contextual, we need it
// for computing bonus points for consecutive chunks starting with a non-word
// character.
@@ -143,7 +152,9 @@ const (
type charClass int
const (
charNonWord charClass = iota
charWhite charClass = iota
charNonWord
charDelimiter
charLower
charUpper
charLetter
@@ -181,6 +192,10 @@ func charClassOfAscii(char rune) charClass {
return charUpper
} else if char >= '0' && char <= '9' {
return charNumber
} else if strings.IndexRune(whiteChars, char) >= 0 {
return charWhite
} else if strings.IndexRune(delimiterChars, char) >= 0 {
return charDelimiter
}
return charNonWord
}
@@ -194,6 +209,10 @@ func charClassOfNonAscii(char rune) charClass {
return charNumber
} else if unicode.IsLetter(char) {
return charLetter
} else if unicode.IsSpace(char) {
return charWhite
} else if strings.IndexRune(delimiterChars, char) >= 0 {
return charDelimiter
}
return charNonWord
}
@@ -206,22 +225,33 @@ func charClassOf(char rune) charClass {
}
func bonusFor(prevClass charClass, class charClass) int16 {
if prevClass == charNonWord && class != charNonWord {
// Word boundary
return bonusBoundary
} else if prevClass == charLower && class == charUpper ||
if class > charNonWord {
if prevClass == charWhite {
// Word boundary after whitespace
return bonusBoundaryWhite
} else if prevClass == charDelimiter {
// Word boundary after a delimiter character
return bonusBoundaryDelimiter
} else if prevClass == charNonWord {
// Word boundary
return bonusBoundary
}
}
if prevClass == charLower && class == charUpper ||
prevClass != charNumber && class == charNumber {
// camelCase letter123
return bonusCamel123
} else if class == charNonWord {
return bonusNonWord
} else if class == charWhite {
return bonusBoundaryWhite
}
return 0
}
func bonusAt(input *util.Chars, idx int) int16 {
if idx == 0 {
return bonusBoundary
return bonusBoundaryWhite
}
return bonusFor(charClassOf(input.Get(idx-1)), charClassOf(input.Get(idx)))
}
@@ -377,7 +407,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
// Phase 2. Calculate bonus for each point
maxScore, maxScorePos := int16(0), 0
pidx, lastIdx := 0, 0
pchar0, pchar, prevH0, prevClass, inGap := pattern[0], pattern[0], int16(0), charNonWord, false
pchar0, pchar, prevH0, prevClass, inGap := pattern[0], pattern[0], int16(0), charWhite, false
Tsub := T[idx:]
H0sub, C0sub, Bsub := H0[idx:][:len(Tsub)], C0[idx:][:len(Tsub)], B[idx:][:len(Tsub)]
for off, char := range Tsub {
@@ -417,7 +447,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
C0sub[off] = 1
if M == 1 && (forward && score > maxScore || !forward && score >= maxScore) {
maxScore, maxScorePos = score, idx+off
if forward && bonus == bonusBoundary {
if forward && bonus >= bonusBoundary {
break
}
}
@@ -486,11 +516,14 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
s1 = Hdiag[off] + scoreMatch
b := Bsub[off]
consecutive = Cdiag[off] + 1
// Break consecutive chunk
if b == bonusBoundary {
consecutive = 1
} else if consecutive > 1 {
b = util.Max16(b, util.Max16(bonusConsecutive, B[col-int(consecutive)+1]))
if consecutive > 1 {
fb := B[col-int(consecutive)+1]
// Break consecutive chunk
if b >= bonusBoundary && b > fb {
consecutive = 1
} else {
b = util.Max16(b, util.Max16(bonusConsecutive, fb))
}
}
if s1+b < s2 {
s1 += Bsub[off]
@@ -555,7 +588,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
func calculateScore(caseSensitive bool, normalize bool, text *util.Chars, pattern []rune, sidx int, eidx int, withPos bool) (int, *[]int) {
pidx, score, inGap, consecutive, firstBonus := 0, 0, false, 0, int16(0)
pos := posArray(withPos, len(pattern))
prevClass := charNonWord
prevClass := charWhite
if sidx > 0 {
prevClass = charClassOf(text.Get(sidx - 1))
}
@@ -583,7 +616,7 @@ func calculateScore(caseSensitive bool, normalize bool, text *util.Chars, patter
firstBonus = bonus
} else {
// Break consecutive chunk
if bonus == bonusBoundary {
if bonus >= bonusBoundary && bonus > firstBonus {
firstBonus = bonus
}
bonus = util.Max16(util.Max16(bonus, firstBonus), bonusConsecutive)
@@ -741,7 +774,7 @@ func ExactMatchNaive(caseSensitive bool, normalize bool, forward bool, text *uti
if bonus > bestBonus {
bestPos, bestBonus = index, bonus
}
if bonus == bonusBoundary {
if bonus >= bonusBoundary {
break
}
index -= pidx - 1
@@ -877,8 +910,8 @@ func EqualMatch(caseSensitive bool, normalize bool, forward bool, text *util.Cha
match = runesStr == string(pattern)
}
if match {
return Result{trimmedLen, trimmedLen + lenPattern, (scoreMatch+bonusBoundary)*lenPattern +
(bonusFirstCharMultiplier-1)*bonusBoundary}, nil
return Result{trimmedLen, trimmedLen + lenPattern, (scoreMatch+bonusBoundaryWhite)*lenPattern +
(bonusFirstCharMultiplier-1)*bonusBoundaryWhite}, nil
}
return Result{-1, -1, 0}, nil
}

View File

@@ -45,29 +45,29 @@ func TestFuzzyMatch(t *testing.T) {
assertMatch(t, fn, false, forward, "fooBarbaz1", "oBZ", 2, 9,
scoreMatch*3+bonusCamel123+scoreGapStart+scoreGapExtension*3)
assertMatch(t, fn, false, forward, "foo bar baz", "fbb", 0, 9,
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+
bonusBoundary*2+2*scoreGapStart+4*scoreGapExtension)
scoreMatch*3+bonusBoundaryWhite*bonusFirstCharMultiplier+
bonusBoundaryWhite*2+2*scoreGapStart+4*scoreGapExtension)
assertMatch(t, fn, false, forward, "/AutomatorDocument.icns", "rdoc", 9, 13,
scoreMatch*4+bonusCamel123+bonusConsecutive*2)
assertMatch(t, fn, false, forward, "/man1/zshcompctl.1", "zshc", 6, 10,
scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*3)
scoreMatch*4+bonusBoundaryDelimiter*bonusFirstCharMultiplier+bonusBoundaryDelimiter*3)
assertMatch(t, fn, false, forward, "/.oh-my-zsh/cache", "zshc", 8, 13,
scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*3+scoreGapStart)
scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*2+scoreGapStart+bonusBoundaryDelimiter)
assertMatch(t, fn, false, forward, "ab0123 456", "12356", 3, 10,
scoreMatch*5+bonusConsecutive*3+scoreGapStart+scoreGapExtension)
assertMatch(t, fn, false, forward, "abc123 456", "12356", 3, 10,
scoreMatch*5+bonusCamel123*bonusFirstCharMultiplier+bonusCamel123*2+bonusConsecutive+scoreGapStart+scoreGapExtension)
assertMatch(t, fn, false, forward, "foo/bar/baz", "fbb", 0, 9,
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+
bonusBoundary*2+2*scoreGapStart+4*scoreGapExtension)
scoreMatch*3+bonusBoundaryWhite*bonusFirstCharMultiplier+
bonusBoundaryDelimiter*2+2*scoreGapStart+4*scoreGapExtension)
assertMatch(t, fn, false, forward, "fooBarBaz", "fbb", 0, 7,
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+
scoreMatch*3+bonusBoundaryWhite*bonusFirstCharMultiplier+
bonusCamel123*2+2*scoreGapStart+2*scoreGapExtension)
assertMatch(t, fn, false, forward, "foo barbaz", "fbb", 0, 8,
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary+
scoreMatch*3+bonusBoundaryWhite*bonusFirstCharMultiplier+bonusBoundaryWhite+
scoreGapStart*2+scoreGapExtension*3)
assertMatch(t, fn, false, forward, "fooBar Baz", "foob", 0, 4,
scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*3)
scoreMatch*4+bonusBoundaryWhite*bonusFirstCharMultiplier+bonusBoundaryWhite*3)
assertMatch(t, fn, false, forward, "xFoo-Bar Baz", "foo-b", 1, 6,
scoreMatch*5+bonusCamel123*bonusFirstCharMultiplier+bonusCamel123*2+
bonusNonWord+bonusBoundary)
@@ -75,14 +75,14 @@ func TestFuzzyMatch(t *testing.T) {
assertMatch(t, fn, true, forward, "fooBarbaz", "oBz", 2, 9,
scoreMatch*3+bonusCamel123+scoreGapStart+scoreGapExtension*3)
assertMatch(t, fn, true, forward, "Foo/Bar/Baz", "FBB", 0, 9,
scoreMatch*3+bonusBoundary*(bonusFirstCharMultiplier+2)+
scoreMatch*3+bonusBoundaryWhite*bonusFirstCharMultiplier+bonusBoundaryDelimiter*2+
scoreGapStart*2+scoreGapExtension*4)
assertMatch(t, fn, true, forward, "FooBarBaz", "FBB", 0, 7,
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+bonusCamel123*2+
scoreMatch*3+bonusBoundaryWhite*bonusFirstCharMultiplier+bonusCamel123*2+
scoreGapStart*2+scoreGapExtension*2)
assertMatch(t, fn, true, forward, "FooBar Baz", "FooB", 0, 4,
scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*2+
util.Max(bonusCamel123, bonusBoundary))
scoreMatch*4+bonusBoundaryWhite*bonusFirstCharMultiplier+bonusBoundaryWhite*2+
util.Max(bonusCamel123, bonusBoundaryWhite))
// Consecutive bonus updated
assertMatch(t, fn, true, forward, "foo-bar", "o-ba", 2, 6,
@@ -98,10 +98,10 @@ func TestFuzzyMatch(t *testing.T) {
func TestFuzzyMatchBackward(t *testing.T) {
assertMatch(t, FuzzyMatchV1, false, true, "foobar fb", "fb", 0, 4,
scoreMatch*2+bonusBoundary*bonusFirstCharMultiplier+
scoreMatch*2+bonusBoundaryWhite*bonusFirstCharMultiplier+
scoreGapStart+scoreGapExtension)
assertMatch(t, FuzzyMatchV1, false, false, "foobar fb", "fb", 7, 9,
scoreMatch*2+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary)
scoreMatch*2+bonusBoundaryWhite*bonusFirstCharMultiplier+bonusBoundaryWhite)
}
func TestExactMatchNaive(t *testing.T) {
@@ -114,9 +114,9 @@ func TestExactMatchNaive(t *testing.T) {
assertMatch(t, ExactMatchNaive, false, dir, "/AutomatorDocument.icns", "rdoc", 9, 13,
scoreMatch*4+bonusCamel123+bonusConsecutive*2)
assertMatch(t, ExactMatchNaive, false, dir, "/man1/zshcompctl.1", "zshc", 6, 10,
scoreMatch*4+bonusBoundary*(bonusFirstCharMultiplier+3))
scoreMatch*4+bonusBoundaryDelimiter*(bonusFirstCharMultiplier+3))
assertMatch(t, ExactMatchNaive, false, dir, "/.oh-my-zsh/cache", "zsh/c", 8, 13,
scoreMatch*5+bonusBoundary*(bonusFirstCharMultiplier+4))
scoreMatch*5+bonusBoundary*(bonusFirstCharMultiplier+3)+bonusBoundaryDelimiter)
}
}
@@ -128,7 +128,7 @@ func TestExactMatchNaiveBackward(t *testing.T) {
}
func TestPrefixMatch(t *testing.T) {
score := (scoreMatch+bonusBoundary)*3 + bonusBoundary*(bonusFirstCharMultiplier-1)
score := scoreMatch*3 + bonusBoundaryWhite*bonusFirstCharMultiplier + bonusBoundaryWhite*2
for _, dir := range []bool{true, false} {
assertMatch(t, PrefixMatch, true, dir, "fooBarbaz", "Foo", -1, -1, 0)
@@ -156,9 +156,10 @@ func TestSuffixMatch(t *testing.T) {
// Strip trailing white space from the string
assertMatch(t, SuffixMatch, false, dir, "fooBarbaz ", "baz", 6, 9,
scoreMatch*3+bonusConsecutive*2)
// Only when the pattern doesn't end with a space
assertMatch(t, SuffixMatch, false, dir, "fooBarbaz ", "baz ", 6, 10,
scoreMatch*4+bonusConsecutive*2+bonusNonWord)
scoreMatch*4+bonusConsecutive*2+bonusBoundaryWhite)
}
}
@@ -182,9 +183,9 @@ func TestNormalize(t *testing.T) {
input, pattern, sidx, eidx, score)
}
}
test("Só Danço Samba", "So", 0, 2, 56, FuzzyMatchV1, FuzzyMatchV2, PrefixMatch, ExactMatchNaive)
test("Só Danço Samba", "sodc", 0, 7, 89, FuzzyMatchV1, FuzzyMatchV2)
test("Danço", "danco", 0, 5, 128, FuzzyMatchV1, FuzzyMatchV2, PrefixMatch, SuffixMatch, ExactMatchNaive, EqualMatch)
test("Só Danço Samba", "So", 0, 2, 62, FuzzyMatchV1, FuzzyMatchV2, PrefixMatch, ExactMatchNaive)
test("Só Danço Samba", "sodc", 0, 7, 97, FuzzyMatchV1, FuzzyMatchV2)
test("Danço", "danco", 0, 5, 140, FuzzyMatchV1, FuzzyMatchV2, PrefixMatch, SuffixMatch, ExactMatchNaive, EqualMatch)
}
func TestLongString(t *testing.T) {

View File

@@ -146,18 +146,20 @@ func Run(opts *Options, version string, revision string) {
// Matcher
forward := true
for _, cri := range opts.Criteria[1:] {
if cri == byEnd {
withPos := false
for idx := len(opts.Criteria) - 1; idx > 0; idx-- {
switch opts.Criteria[idx] {
case byChunk:
withPos = true
case byEnd:
forward = false
break
}
if cri == byBegin {
break
case byBegin:
forward = true
}
}
patternBuilder := func(runes []rune) *Pattern {
return BuildPattern(
opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward,
opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, withPos,
opts.Filter == nil, opts.Nth, opts.Delimiter, runes)
}
matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox)

View File

@@ -35,7 +35,7 @@ const usage = `usage: fzf [options]
--tac Reverse the order of the input
--disabled Do not perform search
--tiebreak=CRI[,..] Comma-separated list of sort criteria to apply
when the scores are tied [length|begin|end|index]
when the scores are tied [length|chunk|begin|end|index]
(default: length)
Interface
@@ -125,6 +125,7 @@ type criterion int
const (
byScore criterion = iota
byChunk
byLength
byBegin
byEnd
@@ -611,6 +612,7 @@ func parseKeyChords(str string, message string) map[tui.Event]string {
func parseTiebreak(str string) []criterion {
criteria := []criterion{byScore}
hasIndex := false
hasChunk := false
hasLength := false
hasBegin := false
hasEnd := false
@@ -627,6 +629,9 @@ func parseTiebreak(str string) []criterion {
switch str {
case "index":
check(&hasIndex, "index")
case "chunk":
check(&hasChunk, "chunk")
criteria = append(criteria, byChunk)
case "length":
check(&hasLength, "length")
criteria = append(criteria, byLength)
@@ -640,6 +645,9 @@ func parseTiebreak(str string) []criterion {
errorExit("invalid sort criterion: " + str)
}
}
if len(criteria) > 4 {
errorExit("at most 3 tiebreaks are allowed: " + str)
}
return criteria
}
@@ -1536,6 +1544,8 @@ func parseOptions(opts *Options, allArgs []string) {
opts.ClearOnExit = false
case "--version":
opts.Version = true
case "--":
// Ignored
default:
if match, value := optString(arg, "--algo="); match {
opts.FuzzyAlgo = parseAlgo(value)
@@ -1742,12 +1752,20 @@ func postProcessOptions(opts *Options) {
}
}
func expectsArbitraryString(opt string) bool {
switch opt {
case "-q", "--query", "-f", "--filter", "--header", "--prompt":
return true
}
return false
}
// ParseOptions parses command-line options
func ParseOptions() *Options {
opts := defaultOptions()
for _, arg := range os.Args[1:] {
if arg == "--version" {
for idx, arg := range os.Args[1:] {
if arg == "--version" && (idx == 0 || idx > 0 && !expectsArbitraryString(os.Args[idx])) {
opts.Version = true
return opts
}

View File

@@ -51,6 +51,7 @@ type Pattern struct {
caseSensitive bool
normalize bool
forward bool
withPos bool
text []rune
termSets []termSet
sortable bool
@@ -85,7 +86,7 @@ func clearChunkCache() {
// BuildPattern builds Pattern object from the given arguments
func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, normalize bool, forward bool,
cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern {
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern {
var asString string
if extended {
@@ -145,6 +146,7 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
caseSensitive: caseSensitive,
normalize: normalize,
forward: forward,
withPos: withPos,
text: []rune(asString),
termSets: termSets,
sortable: sortable,
@@ -302,13 +304,13 @@ func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Re
if space == nil {
for idx := 0; idx < chunk.count; idx++ {
if match, _, _ := p.MatchItem(&chunk.items[idx], false, slab); match != nil {
if match, _, _ := p.MatchItem(&chunk.items[idx], p.withPos, slab); match != nil {
matches = append(matches, *match)
}
}
} else {
for _, result := range space {
if match, _, _ := p.MatchItem(result.item, false, slab); match != nil {
if match, _, _ := p.MatchItem(result.item, p.withPos, slab); match != nil {
matches = append(matches, *match)
}
}

View File

@@ -67,7 +67,7 @@ func TestParseTermsEmpty(t *testing.T) {
func TestExact(t *testing.T) {
defer clearPatternCache()
clearPatternCache()
pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true,
pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true,
[]Range{}, Delimiter{}, []rune("'abc"))
chars := util.ToChars([]byte("aabbcc abc"))
res, pos := algo.ExactMatchNaive(
@@ -83,7 +83,7 @@ func TestExact(t *testing.T) {
func TestEqual(t *testing.T) {
defer clearPatternCache()
clearPatternCache()
pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("^AbC$"))
pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("^AbC$"))
match := func(str string, sidxExpected int, eidxExpected int) {
chars := util.ToChars([]byte(str))
@@ -106,17 +106,17 @@ func TestEqual(t *testing.T) {
func TestCaseSensitivity(t *testing.T) {
defer clearPatternCache()
clearPatternCache()
pat1 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("abc"))
pat1 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache()
pat2 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("Abc"))
pat2 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc"))
clearPatternCache()
pat3 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, true, []Range{}, Delimiter{}, []rune("abc"))
pat3 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, false, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache()
pat4 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, true, []Range{}, Delimiter{}, []rune("Abc"))
pat4 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc"))
clearPatternCache()
pat5 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, true, []Range{}, Delimiter{}, []rune("abc"))
pat5 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, false, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache()
pat6 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, true, []Range{}, Delimiter{}, []rune("Abc"))
pat6 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc"))
if string(pat1.text) != "abc" || pat1.caseSensitive != false ||
string(pat2.text) != "Abc" || pat2.caseSensitive != true ||
@@ -129,7 +129,7 @@ func TestCaseSensitivity(t *testing.T) {
}
func TestOrigTextAndTransformed(t *testing.T) {
pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("jg"))
pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("jg"))
tokens := Tokenize("junegunn", Delimiter{})
trans := Transform(tokens, []Range{{1, 1}})
@@ -164,7 +164,7 @@ func TestOrigTextAndTransformed(t *testing.T) {
func TestCacheKey(t *testing.T) {
test := func(extended bool, patStr string, expected string, cacheable bool) {
clearPatternCache()
pat := BuildPattern(true, algo.FuzzyMatchV2, extended, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune(patStr))
pat := BuildPattern(true, algo.FuzzyMatchV2, extended, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune(patStr))
if pat.CacheKey() != expected {
t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey())
}
@@ -188,7 +188,7 @@ func TestCacheKey(t *testing.T) {
func TestCacheable(t *testing.T) {
test := func(fuzzy bool, str string, expected string, cacheable bool) {
clearPatternCache()
pat := BuildPattern(fuzzy, algo.FuzzyMatchV2, true, CaseSmart, true, true, true, []Range{}, Delimiter{}, []rune(str))
pat := BuildPattern(fuzzy, algo.FuzzyMatchV2, true, CaseSmart, true, true, false, true, []Range{}, Delimiter{}, []rune(str))
if pat.CacheKey() != expected {
t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey())
}

View File

@@ -49,6 +49,21 @@ func buildResult(item *Item, offsets []Offset, score int) Result {
case byScore:
// Higher is better
val = math.MaxUint16 - util.AsUint16(score)
case byChunk:
b := minBegin
e := maxEnd
l := item.text.Length()
for ; b >= 1; b-- {
if unicode.IsSpace(item.text.Get(b - 1)) {
break
}
}
for ; e < l; e++ {
if unicode.IsSpace(item.text.Get(e)) {
break
}
}
val = util.AsUint16(e - b)
case byLength:
val = item.TrimLength()
case byBegin, byEnd:

View File

@@ -54,9 +54,9 @@ func TestResultRank(t *testing.T) {
// FIXME global
sortCriteria = []criterion{byScore, byLength}
strs := [][]rune{[]rune("foo"), []rune("foobar"), []rune("bar"), []rune("baz")}
str := []rune("foo")
item1 := buildResult(
withIndex(&Item{text: util.RunesToChars(strs[0])}, 1), []Offset{}, 2)
withIndex(&Item{text: util.RunesToChars(str)}, 1), []Offset{}, 2)
if item1.points[3] != math.MaxUint16-2 || // Bonus
item1.points[2] != 3 || // Length
item1.points[1] != 0 || // Unused
@@ -65,7 +65,7 @@ func TestResultRank(t *testing.T) {
t.Error(item1)
}
// Only differ in index
item2 := buildResult(&Item{text: util.RunesToChars(strs[0])}, []Offset{}, 2)
item2 := buildResult(&Item{text: util.RunesToChars(str)}, []Offset{}, 2)
items := []Result{item1, item2}
sort.Sort(ByRelevance(items))
@@ -98,6 +98,23 @@ func TestResultRank(t *testing.T) {
}
}
func TestChunkTiebreak(t *testing.T) {
// FIXME global
sortCriteria = []criterion{byScore, byChunk}
score := 100
test := func(input string, offset Offset, chunk string) {
item := buildResult(withIndex(&Item{text: util.RunesToChars([]rune(input))}, 1), []Offset{offset}, score)
if !(item.points[3] == math.MaxUint16-uint16(score) && item.points[2] == uint16(len(chunk))) {
t.Error(item.points)
}
}
test("hello foobar goodbye", Offset{8, 9}, "foobar")
test("hello foobar goodbye", Offset{7, 18}, "foobar goodbye")
test("hello foobar goodbye", Offset{0, 1}, "hello")
test("hello foobar goodbye", Offset{5, 7}, "hello foobar") // TBD
}
func TestColorOffset(t *testing.T) {
// ------------ 20 ---- -- ----
// ++++++++ ++++++++++

View File

@@ -540,7 +540,7 @@ func (r *LightRenderer) mouseSequence(sz *int) Event {
t := atoi(elems[0], -1)
x := atoi(elems[1], -1) - 1
y := atoi(elems[2], -1) - 1
y := atoi(elems[2], -1) - 1 - r.yoffset
if t < 0 || x < 0 || y < 0 {
return Event{Invalid, 0, nil}
}

View File

@@ -754,6 +754,26 @@ class TestGoFZF < TestBase
assert_equal output, `#{FZF} -fh -n2 -d: < #{tempname}`.lines(chomp: true)
end
def test_tiebreak_chunk
writelines(tempname, [
'1 foobarbaz ba',
'2 foobar baz',
'3 foo barbaz'
])
assert_equal [
'3 foo barbaz',
'2 foobar baz',
'1 foobarbaz ba'
], `#{FZF} -fo --tiebreak=chunk < #{tempname}`.lines(chomp: true)
assert_equal [
'1 foobarbaz ba',
'2 foobar baz',
'3 foo barbaz'
], `#{FZF} -fba --tiebreak=chunk < #{tempname}`.lines(chomp: true)
end
def test_invalid_cache
tmux.send_keys "(echo d; echo D; echo x) | #{fzf('-q d')}", :Enter
tmux.until { |lines| assert_equal ' 2/3', lines[-2] }