summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJunegunn Choi <junegunn.c@gmail.com>2022-08-28 22:16:57 +0900
committerJunegunn Choi <junegunn.c@gmail.com>2022-08-28 22:22:39 +0900
commit6fb41a202a97ad3f2437f6e5aee8890268560412 (patch)
tree5c4d9002481b8314b458bf6ef0812eac04cd40a7
parent4bef330ce15d3ae80959415c24aba4700f5cbe35 (diff)
Add --scheme=[default|path|history] option to choose scoring scheme
Close #2909 Close #2930
-rw-r--r--CHANGELOG.md14
-rw-r--r--man/man1/fzf-tmux.12
-rw-r--r--man/man1/fzf.114
-rw-r--r--shell/key-bindings.bash2
-rw-r--r--shell/key-bindings.fish2
-rw-r--r--shell/key-bindings.zsh2
-rw-r--r--src/algo/algo.go49
-rw-r--r--src/algo/algo_test.go38
-rw-r--r--src/options.go21
9 files changed, 107 insertions, 37 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 48dfb69e..f84735af 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,18 @@
CHANGELOG
=========
-0.32.2
-------
+0.33.0
+------
+- Added `--scheme=[default|path|history]` option to choose scoring scheme
+ - (Experimental)
+ - We updated the scoring algorithm in 0.32.0, however we have learned that
+ this new scheme (`default`) is not always giving the optimal result
+ - `path`: Additional bonus point is only given the the characters after
+ path separator. You might want to choose this scheme if you have many
+ files with spaces in their paths.
+ - `history`: No additional bonus points are given so that we give more
+ weight to the chronological ordering. This is equivalent to the scoring
+ scheme before 0.32.0. This also sets `--tiebreak=index`.
- ANSI color sequences with colon delimiters are now supported.
```sh
printf "\e[38;5;208mOption 1\e[m\nOption 2" | fzf --ansi
diff --git a/man/man1/fzf-tmux.1 b/man/man1/fzf-tmux.1
index 282ada58..47a49c45 100644
--- a/man/man1/fzf-tmux.1
+++ b/man/man1/fzf-tmux.1
@@ -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 "Aug 2022" "fzf 0.32.1" "fzf-tmux - open fzf in tmux split pane"
+.TH fzf-tmux 1 "Aug 2022" "fzf 0.33.0" "fzf-tmux - open fzf in tmux split pane"
.SH NAME
fzf-tmux - open fzf in tmux split pane
diff --git a/man/man1/fzf.1 b/man/man1/fzf.1
index 970ba08e..a6455cc2 100644
--- a/man/man1/fzf.1
+++ b/man/man1/fzf.1
@@ -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 "Aug 2022" "fzf 0.32.2" "fzf - a command-line fuzzy finder"
+.TH fzf 1 "Aug 2022" "fzf 0.33.0" "fzf - a command-line fuzzy finder"
.SH NAME
fzf - a command-line fuzzy finder
@@ -51,6 +51,18 @@ Case-sensitive match
.B "--literal"
Do not normalize latin script letters for matching.
.TP
+.BI "--scheme=" SCHEME
+Choose scoring scheme tailored for different types of input.
+
+.br
+.BR default " Generic scoring scheme designed to work well with any type of input"
+.br
+.BR path " Scoring scheme for paths (additional bonus point only after path separator)
+.br
+.BR history " Scoring scheme for command history (no additional bonus points).
+ Sets \fB--tiebreak=index\fR as well.
+.br
+.TP
.BI "--algo=" TYPE
Fuzzy matching algorithm (default: v2)
diff --git a/shell/key-bindings.bash b/shell/key-bindings.bash
index 701cf5b0..f6797d49 100644
--- a/shell/key-bindings.bash
+++ b/shell/key-bindings.bash
@@ -50,7 +50,7 @@ __fzf_cd__() {
__fzf_history__() {
local output opts script
- opts="--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS +m --read0"
+ opts="--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS -n2..,.. --scheme=history --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS +m --read0"
script='BEGIN { getc; $/ = "\n\t"; $HISTCOUNT = $ENV{last_hist} + 1 } s/^[ *]//; print $HISTCOUNT - $. . "\t$_" if !$seen{$_}++'
output=$(
builtin fc -lnr -2147483648 |
diff --git a/shell/key-bindings.fish b/shell/key-bindings.fish
index 36b3aa34..5fd6f6b2 100644
--- a/shell/key-bindings.fish
+++ b/shell/key-bindings.fish
@@ -53,7 +53,7 @@ function fzf_key_bindings
function fzf-history-widget -d "Show command history"
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
begin
- set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT $FZF_DEFAULT_OPTS --tiebreak=index --bind=ctrl-r:toggle-sort,ctrl-z:ignore $FZF_CTRL_R_OPTS +m"
+ set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT $FZF_DEFAULT_OPTS --scheme=history --bind=ctrl-r:toggle-sort,ctrl-z:ignore $FZF_CTRL_R_OPTS +m"
set -l FISH_MAJOR (echo $version | cut -f1 -d.)
set -l FISH_MINOR (echo $version | cut -f2 -d.)
diff --git a/shell/key-bindings.zsh b/shell/key-bindings.zsh
index b0b81f26..47ef17a7 100644
--- a/shell/key-bindings.zsh
+++ b/shell/key-bindings.zsh
@@ -98,7 +98,7 @@ fzf-history-widget() {
local selected num
setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases 2> /dev/null
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)) )
+ FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS -n2..,.. --scheme=history --bind=ctrl-r:toggle-sort,ctrl-z:ignore $FZF_CTRL_R_OPTS --query=${(qqq)LBUFFER} +m" $(__fzfcmd)) )
local ret=$?
if [ -n "$selected" ]; then
num=$selected[1]
diff --git a/src/algo/algo.go b/src/algo/algo.go
index 15214a68..50dd466c 100644
--- a/src/algo/algo.go
+++ b/src/algo/algo.go
@@ -80,6 +80,7 @@ Scoring criteria
import (
"bytes"
"fmt"
+ "os"
"strings"
"unicode"
"unicode/utf8"
@@ -89,7 +90,8 @@ import (
var DEBUG bool
-const delimiterChars = "/,:;|"
+var delimiterChars = "/,:;|"
+
const whiteChars = " \t\n\v\f\r\x85\xA0"
func indexAt(index int, max int, forward bool) int {
@@ -120,12 +122,6 @@ 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.
@@ -149,6 +145,16 @@ const (
bonusFirstCharMultiplier = 2
)
+var (
+ // Extra bonus for word boundary after whitespace character or beginning of the string
+ bonusBoundaryWhite int16 = bonusBoundary + 2
+
+ // Extra bonus for word boundary after slash, colon, semi-colon, and comma
+ bonusBoundaryDelimiter int16 = bonusBoundary + 1
+
+ initialCharClass charClass = charWhite
+)
+
type charClass int
const (
@@ -161,6 +167,29 @@ const (
charNumber
)
+func Init(scheme string) bool {
+ switch scheme {
+ case "default":
+ bonusBoundaryWhite = bonusBoundary + 2
+ bonusBoundaryDelimiter = bonusBoundary + 1
+ case "path":
+ bonusBoundaryWhite = bonusBoundary
+ bonusBoundaryDelimiter = bonusBoundary + 1
+ if os.PathSeparator == '/' {
+ delimiterChars = "/"
+ } else {
+ delimiterChars = string([]rune{os.PathSeparator, '/'})
+ }
+ initialCharClass = charDelimiter
+ case "history":
+ bonusBoundaryWhite = bonusBoundary
+ bonusBoundaryDelimiter = bonusBoundary
+ default:
+ return false
+ }
+ return true
+}
+
func posArray(withPos bool, len int) *[]int {
if withPos {
pos := make([]int, 0, len)
@@ -407,7 +436,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), charWhite, false
+ pchar0, pchar, prevH0, prevClass, inGap := pattern[0], pattern[0], int16(0), initialCharClass, false
Tsub := T[idx:]
H0sub, C0sub, Bsub := H0[idx:][:len(Tsub)], C0[idx:][:len(Tsub)], B[idx:][:len(Tsub)]
for off, char := range Tsub {
@@ -910,8 +939,8 @@ func EqualMatch(caseSensitive bool, normalize bool, forward bool, text *util.Cha
match = runesStr == string(pattern)
}
if match {
- return Result{trimmedLen, trimmedLen + lenPattern, (scoreMatch+bonusBoundaryWhite)*lenPattern +
- (bonusFirstCharMultiplier-1)*bonusBoundaryWhite}, nil
+ return Result{trimmedLen, trimmedLen + lenPattern, (scoreMatch+int(bonusBoundaryWhite))*lenPattern +
+ (bonusFirstCharMultiplier-1)*int(bonusBoundaryWhite)}, nil
}
return Result{-1, -1, 0}, nil
}
diff --git a/src/algo/algo_test.go b/src/algo/algo_test.go
index 2dbe3833..a7c4e1d3 100644
--- a/src/algo/algo_test.go
+++ b/src/algo/algo_test.go
@@ -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+bonusBoundaryWhite*bonusFirstCharMultiplier+
- bonusBoundaryWhite*2+2*scoreGapStart+4*scoreGapExtension)
+ scoreMatch*3+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+
+ int(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+bonusBoundaryDelimiter*bonusFirstCharMultiplier+bonusBoundaryDelimiter*3)
+ scoreMatch*4+int(bonusBoundaryDelimiter)*bonusFirstCharMultiplier+int(bonusBoundaryDelimiter)*3)
assertMatch(t, fn, false, forward, "/.oh-my-zsh/cache", "zshc", 8, 13,
- scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*2+scoreGapStart+bonusBoundaryDelimiter)
+ scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*2+scoreGapStart+int(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+bonusBoundaryWhite*bonusFirstCharMultiplier+
- bonusBoundaryDelimiter*2+2*scoreGapStart+4*scoreGapExtension)
+ scoreMatch*3+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+
+ int(bonusBoundaryDelimiter)*2+2*scoreGapStart+4*scoreGapExtension)
assertMatch(t, fn, false, forward, "fooBarBaz", "fbb", 0, 7,
- scoreMatch*3+bonusBoundaryWhite*bonusFirstCharMultiplier+
+ scoreMatch*3+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+
bonusCamel123*2+2*scoreGapStart+2*scoreGapExtension)
assertMatch(t, fn, false, forward, "foo barbaz", "fbb", 0, 8,
- scoreMatch*3+bonusBoundaryWhite*bonusFirstCharMultiplier+bonusBoundaryWhite+
+ scoreMatch*3+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+int(bonusBoundaryWhite)+
scoreGapStart*2+scoreGapExtension*3)
assertMatch(t, fn, false, forward, "fooBar Baz", "foob", 0, 4,
- scoreMatch*4+bonusBoundaryWhite*bonusFirstCharMultiplier+bonusBoundaryWhite*3)
+ scoreMatch*4+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+int(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+bonusBoundaryWhite*bonusFirstCharMultiplier+bonusBoundaryDelimiter*2+
+ scoreMatch*3+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+int(bonusBoundaryDelimiter)*2+
scoreGapStart*2+scoreGapExtension*4)
assertMatch(t, fn, true, forward, "FooBarBaz", "FBB", 0, 7,
- scoreMatch*3+bonusBoundaryWhite*bonusFirstCharMultiplier+bonusCamel123*2+
+ scoreMatch*3+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+bonusCamel123*2+
scoreGapStart*2+scoreGapExtension*2)
assertMatch(t, fn, true, forward, "FooBar Baz", "FooB", 0, 4,
- scoreMatch*4+bonusBoundaryWhite*bonusFirstCharMultiplier+bonusBoundaryWhite*2+
- util.Max(bonusCamel123, bonusBoundaryWhite))
+ scoreMatch*4+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+int(bonusBoundaryWhite)*2+
+ util.Max(bonusCamel123, int(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+bonusBoundaryWhite*bonusFirstCharMultiplier+
+ scoreMatch*2+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+
scoreGapStart+scoreGapExtension)
assertMatch(t, FuzzyMatchV1, false, false, "foobar fb", "fb", 7, 9,
- scoreMatch*2+bonusBoundaryWhite*bonusFirstCharMultiplier+bonusBoundaryWhite)
+ scoreMatch*2+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+int(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+bonusBoundaryDelimiter*(bonusFirstCharMultiplier+3))
+ scoreMatch*4+int(bonusBoundaryDelimiter)*(bonusFirstCharMultiplier+3))
assertMatch(t, ExactMatchNaive, false, dir, "/.oh-my-zsh/cache", "zsh/c", 8, 13,
- scoreMatch*5+bonusBoundary*(bonusFirstCharMultiplier+3)+bonusBoundaryDelimiter)
+ scoreMatch*5+bonusBoundary*(bonusFirstCharMultiplier+3)+int(bonusBoundaryDelimiter))
}
}
@@ -128,7 +128,7 @@ func TestExactMatchNaiveBackward(t *testing.T) {
}
func TestPrefixMatch(t *testing.T) {
- score := scoreMatch*3 + bonusBoundaryWhite*bonusFirstCharMultiplier + bonusBoundaryWhite*2
+ score := scoreMatch*3 + int(bonusBoundaryWhite)*bonusFirstCharMultiplier + int(bonusBoundaryWhite)*2
for _, dir := range []bool{true, false} {
assertMatch(t, PrefixMatch, true, dir, "fooBarbaz", "Foo", -1, -1, 0)
@@ -159,7 +159,7 @@ func TestSuffixMatch(t *testing.T) {
// Only when the pattern doesn't end with a space
assertMatch(t, SuffixMatch, false, dir, "fooBarbaz ", "baz ", 6, 10,
- scoreMatch*4+bonusConsecutive*2+bonusBoundaryWhite)
+ scoreMatch*4+bonusConsecutive*2+int(bonusBoundaryWhite))
}
}
diff --git a/src/options.go b/src/options.go
index 46bda82c..41782fa4 100644
--- a/src/options.go
+++ b/src/options.go
@@ -21,9 +21,9 @@ const usage = `usage: fzf [options]
-x, --extended Extended-search mode
(enabled by default; +x or --no-extended to disable)
-e, --exact Enable Exact-match
- --algo=TYPE Fuzzy matching algorithm: [v1|v2] (default: v2)
-i Case-insensitive match (default: smart-case match)
+i Case-sensitive match
+ --scheme=SCHEME Scoring scheme [default|path|history]
--literal Do not normalize latin script letters before matching
-n, --nth=N[,..] Comma-separated list of field index expressions
for limiting search scope. Each can be a non-zero
@@ -194,6 +194,7 @@ func (a previewOpts) sameContentLayout(b previewOpts) bool {
type Options struct {
Fuzzy bool
FuzzyAlgo algo.Algo
+ Scheme string
Extended bool
Phony bool
Case Case
@@ -259,6 +260,7 @@ func defaultOptions() *Options {
return &Options{
Fuzzy: true,
FuzzyAlgo: algo.FuzzyMatchV2,
+ Scheme: "default",
Extended: true,
Phony: false,
Case: CaseSmart,
@@ -441,6 +443,15 @@ func parseAlgo(str string) algo.Algo {
return algo.FuzzyMatchV2
}
+func processScheme(opts *Options) {
+ if !algo.Init(opts.Scheme) {
+ errorExit("invalid scoring scheme (expected: default|path|history)")
+ }
+ if opts.Scheme == "history" {
+ opts.Criteria = []criterion{byScore}
+ }
+}
+
func parseBorder(str string, optional bool) tui.BorderShape {
switch str {
case "rounded":
@@ -1345,6 +1356,8 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Normalize = true
case "--algo":
opts.FuzzyAlgo = parseAlgo(nextString(allArgs, &i, "algorithm required (v1|v2)"))
+ case "--scheme":
+ opts.Scheme = strings.ToLower(nextString(allArgs, &i, "scoring scheme required (default|path|history)"))
case "--expect":
for k, v := range parseKeyChords(nextString(allArgs, &i, "key names required"), "key names required") {
opts.Expect[k] = v
@@ -1551,6 +1564,8 @@ func parseOptions(opts *Options, allArgs []string) {
default:
if match, value := optString(arg, "--algo="); match {
opts.FuzzyAlgo = parseAlgo(value)
+ } else if match, value := optString(arg, "--scheme="); match {
+ opts.Scheme = strings.ToLower(value)
} else if match, value := optString(arg, "-q", "--query="); match {
opts.Query = value
} else if match, value := optString(arg, "-f", "--filter="); match {
@@ -1752,6 +1767,10 @@ func postProcessOptions(opts *Options) {
theme.Cursor = boldify(theme.Cursor)
theme.Spinner = boldify(theme.Spinner)
}
+
+ if opts.Scheme != "default" {
+ processScheme(opts)
+ }
}
func expectsArbitraryString(opt string) bool {