Option to prioritize file name matches (#4192)

* 'pathname' is a new tiebreak option for prioritizing matches occurring
  in the file name of the path.

* `--scheme=path` will automatically set `--tiebreak=pathname,length`.

* fzf will automatically choose `path` scheme when the input is a TTY device,
  where fzf would start its built-in walker or run `$FZF_DEFAULT_COMMAND`
  which is usually a command for listing files.

Close #4191
This commit is contained in:
Junegunn Choi
2025-01-24 00:54:53 +09:00
committed by GitHub
parent c71e4ddee4
commit 243a76002c
7 changed files with 127 additions and 28 deletions

View File

@@ -11,6 +11,7 @@ import (
"github.com/junegunn/fzf/src/algo"
"github.com/junegunn/fzf/src/tui"
"github.com/junegunn/fzf/src/util"
"github.com/junegunn/go-shellwords"
"github.com/rivo/uniseg"
@@ -46,8 +47,8 @@ Usage: fzf [options]
--tail=NUM Maximum number of items to keep in memory
--disabled Do not perform search
--tiebreak=CRI[,..] Comma-separated list of sort criteria to apply
when the scores are tied [length|chunk|begin|end|index]
(default: length)
when the scores are tied
[length|chunk|pathname|begin|end|index] (default: length)
INPUT/OUTPUT
--read0 Read input delimited by ASCII NUL characters
@@ -241,6 +242,7 @@ const (
byLength
byBegin
byEnd
byPathname
)
type heightSpec struct {
@@ -653,7 +655,7 @@ func defaultOptions() *Options {
Man: false,
Fuzzy: true,
FuzzyAlgo: algo.FuzzyMatchV2,
Scheme: "default",
Scheme: "", // Unknown
Extended: true,
Phony: false,
Case: CaseSmart,
@@ -664,7 +666,7 @@ func defaultOptions() *Options {
Sort: 1000,
Track: trackDisabled,
Tac: false,
Criteria: []criterion{byScore, byLength},
Criteria: []criterion{}, // Unknown
Multi: 0,
Ansi: false,
Mouse: true,
@@ -804,16 +806,6 @@ func parseAlgo(str string) (algo.Algo, error) {
return nil, errors.New("invalid algorithm (expected: v1 or v2)")
}
func processScheme(opts *Options) error {
if !algo.Init(opts.Scheme) {
return errors.New("invalid scoring scheme (expected: default|path|history)")
}
if opts.Scheme == "history" {
opts.Criteria = []criterion{byScore}
}
return nil
}
func parseBorder(str string, optional bool, allowLine bool) (tui.BorderShape, error) {
switch str {
case "line":
@@ -1037,6 +1029,19 @@ func parseKeyChordsImpl(str string, message string) (map[tui.Event]string, error
return chords, nil
}
func parseScheme(str string) (string, []criterion, error) {
str = strings.ToLower(str)
switch str {
case "history":
return str, []criterion{byScore}, nil
case "path":
return str, []criterion{byScore, byPathname, byLength}, nil
case "default":
return str, []criterion{byScore, byLength}, nil
}
return str, nil, errors.New("invalid scoring scheme: " + str + " (expected: default|path|history)")
}
func parseTiebreak(str string) ([]criterion, error) {
criteria := []criterion{byScore}
hasIndex := false
@@ -1044,6 +1049,7 @@ func parseTiebreak(str string) ([]criterion, error) {
hasLength := false
hasBegin := false
hasEnd := false
hasPathname := false
check := func(notExpected *bool, name string) error {
if *notExpected {
return errors.New("duplicate sort criteria: " + name)
@@ -1065,6 +1071,11 @@ func parseTiebreak(str string) ([]criterion, error) {
return nil, err
}
criteria = append(criteria, byChunk)
case "pathname":
if err := check(&hasPathname, "pathname"); err != nil {
return nil, err
}
criteria = append(criteria, byPathname)
case "length":
if err := check(&hasLength, "length"); err != nil {
return nil, err
@@ -2261,7 +2272,9 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
if err != nil {
return err
}
opts.Scheme = strings.ToLower(str)
if opts.Scheme, opts.Criteria, err = parseScheme(str); err != nil {
return err
}
case "--expect":
str, err := nextString("key names required")
if err != nil {
@@ -3173,7 +3186,9 @@ func postProcessOptions(opts *Options) error {
return errors.New("failed to start pprof profiles: " + err.Error())
}
return processScheme(opts)
algo.Init(opts.Scheme)
return nil
}
func parseShellWords(str string) ([]string, error) {
@@ -3223,7 +3238,26 @@ func ParseOptions(useDefaults bool, args []string) (*Options, error) {
return nil, err
}
// 4. Final validation of merged options
// 4. Change default scheme when built-in walker is used
if len(opts.Scheme) == 0 {
opts.Scheme = "default"
if len(opts.Criteria) == 0 {
// NOTE: Let's assume $FZF_DEFAULT_COMMAND generates a list of file paths.
// But it is possible that it is set to a command that doesn't generate
// file paths.
//
// In that case, you can either
// 1. explicitly set --scheme=default,
// 2. or replace $FZF_DEFAULT_COMMAND with an equivalent 'start:reload'
// binding, which is the new preferred way.
if !opts.hasReloadOnStart() && util.IsTty(os.Stdin) {
opts.Scheme = "path"
}
_, opts.Criteria, _ = parseScheme(opts.Scheme)
}
}
// 5. Final validation of merged options
if err := validateOptions(opts); err != nil {
return nil, err
}
@@ -3231,6 +3265,17 @@ func ParseOptions(useDefaults bool, args []string) (*Options, error) {
return opts, nil
}
func (opts *Options) hasReloadOnStart() bool {
if actions, prs := opts.Keymap[tui.Start.AsEvent()]; prs {
for _, action := range actions {
if action.t == actReload || action.t == actReloadSync {
return true
}
}
}
return false
}
func (opts *Options) extractReloadOnStart() string {
cmd := ""
if actions, prs := opts.Keymap[tui.Start.AsEvent()]; prs {