summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJunegunn Choi <junegunn.c@gmail.com>2025-01-24 00:54:53 +0900
committerGitHub <noreply@github.com>2025-01-24 00:54:53 +0900
commit243a76002c93b474cf8401b37670a43803a0a2d2 (patch)
treee0972774a3e45905b140c6d4affe370cbda04991 /src
parentc71e4ddee41d3381fb9ec25cc7078bdb25ac4b27 (diff)
downloadfzf-243a76002c93b474cf8401b37670a43803a0a2d2.tar.gz
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
Diffstat (limited to 'src')
-rw-r--r--src/core.go3
-rw-r--r--src/options.go79
-rw-r--r--src/result.go15
3 files changed, 80 insertions, 17 deletions
diff --git a/src/core.go b/src/core.go
index b8851d79..71cf04da 100644
--- a/src/core.go
+++ b/src/core.go
@@ -188,6 +188,9 @@ func Run(opts *Options) (int, error) {
forward = false
case byBegin:
forward = true
+ case byPathname:
+ withPos = true
+ forward = false
}
}
diff --git a/src/options.go b/src/options.go
index 4b5b25fa..d31b1001 100644
--- a/src/options.go
+++ b/src/options.go
@@ -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 {
diff --git a/src/result.go b/src/result.go
index 10e0c6d6..28d42e7d 100644
--- a/src/result.go
+++ b/src/result.go
@@ -69,6 +69,21 @@ func buildResult(item *Item, offsets []Offset, score int) Result {
}
case byLength:
val = item.TrimLength()
+ case byPathname:
+ if validOffsetFound {
+ // lastDelim := strings.LastIndexByte(item.text.ToString(), '/')
+ lastDelim := -1
+ s := item.text.ToString()
+ for i := len(s) - 1; i >= 0; i-- {
+ if s[i] == '/' || s[i] == '\\' {
+ lastDelim = i
+ break
+ }
+ }
+ if lastDelim <= minBegin {
+ val = util.AsUint16(minBegin - lastDelim)
+ }
+ }
case byBegin, byEnd:
if validOffsetFound {
whitePrefixLen := 0