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 {