From 9d4a3f62e39473e57854214b669ea6ff3deec8ae Mon Sep 17 00:00:00 2001 From: Jon Gjengset Date: Mon, 17 Sep 2018 13:56:50 -0400 Subject: [PATCH] Add --tiebreak proximity=path This patch adds a new type of `tiebreak`: `proximity=path`. It works by ranking equally-scored candidates by their proximity to the given path argument, where proximity is defined as the number of shared leading path segments when split by the OS path separator. Consider the following simple file hierarchy: test.txt bar/test.txt bar/main.txt misc/test.txt Where a user is currently in the context of `bar/main.txt`. This could be for a number of reasons, such as it being the file that is currently open in their editor (see junegunn/fzf.vim#360 and junegunn/fzf.vim#492). They now want to open `bar/test.txt`. If they invoke `fzf` and type, well, any search, the `by_length` scoring will make the `test.txt` file in the root the "best" candidate, as would `by_start`. `by_end` would propose `misc/test.txt`. `by_score` would also likely suggest `test.txt` in the root. None of these take into account the user's current location. With this patch, the user can invoke `fzf --tiebreak path=bar/main.txt`, which will cause it to rank files that share a prefix segment (`bar/` in this case) to rank higher. Thus, when the user types, say, `t`, `bar/test.txt` will immediately be the top recommendation. In editor context, the user's editor should probably automatically add this flag based on the user's current file. The flag also allows for more natural search in deeper hierarchies. For example, if the user is in `foobar/controller/admin.rb` here: baz/controller/admin.rb baz/views/admin.rb foobar/controller/admin.rb foobar/views/admin.rb And wants to quickly edit the view for that application (i.e., `foobar/views/admin.rb`), then fzf --tiebreak path=foobar/controller/admin.rb will rank `foobar/views/admin.rb` higher than `baz/views/admin.rb` for a search of `admin.rb`, which is what the user most likely expected. Note that contrary to other suggestions like only listing files at or below the user's current directory, this patch does *not* limit the user's ability to search beyond the scope of their current context (e.g., they *can* get to `baz/controller/admin.rb` if they so wish). --- src/core.go | 4 ++-- src/options.go | 42 +++++++++++++++++++++++++++++++++++------- src/result.go | 19 +++++++++++++++++-- 3 files changed, 54 insertions(+), 11 deletions(-) diff --git a/src/core.go b/src/core.go index 023f7be7ed8..bfc20c162b8 100644 --- a/src/core.go +++ b/src/core.go @@ -127,11 +127,11 @@ func Run(opts *Options, revision string) { // Matcher forward := true for _, cri := range opts.Criteria[1:] { - if cri == byEnd { + if cri.by == byEnd { forward = false break } - if cri == byBegin { + if cri.by == byBegin { break } } diff --git a/src/options.go b/src/options.go index fc32344c99f..631149fcb23 100644 --- a/src/options.go +++ b/src/options.go @@ -106,13 +106,19 @@ const ( ) // Sort criteria -type criterion int +type criterion struct { + by by + arg string +} + +type by int const ( - byScore criterion = iota + byScore by = iota byLength byBegin byEnd + byProximity ) type sizeSpec struct { @@ -212,7 +218,7 @@ func defaultOptions() *Options { Delimiter: Delimiter{}, Sort: 1000, Tac: false, - Criteria: []criterion{byScore, byLength}, + Criteria: []criterion{criterion{by: byScore, arg: ""}, criterion{by: byLength, arg: ""}}, Multi: false, Ansi: false, Mouse: true, @@ -488,7 +494,7 @@ func parseKeyChords(str string, message string) map[int]string { } func parseTiebreak(str string) []criterion { - criteria := []criterion{byScore} + criteria := []criterion{criterion{by: byScore, arg: ""}} hasIndex := false hasLength := false hasBegin := false @@ -503,18 +509,40 @@ func parseTiebreak(str string) []criterion { *notExpected = true } for _, str := range strings.Split(strings.ToLower(str), ",") { + if strings.HasPrefix(str, "proximity=") { + path := strings.Join(strings.Split(str, "=")[1:], "=") + if path != "" { + criteria = append(criteria, criterion{ + by: byProximity, + arg: path, + }) + continue + } else { + errorExit("empty proximity path given") + } + } + switch str { case "index": check(&hasIndex, "index") case "length": check(&hasLength, "length") - criteria = append(criteria, byLength) + criteria = append(criteria, criterion{ + by: byLength, + arg: "", + }) case "begin": check(&hasBegin, "begin") - criteria = append(criteria, byBegin) + criteria = append(criteria, criterion{ + by: byBegin, + arg: "", + }) case "end": check(&hasEnd, "end") - criteria = append(criteria, byEnd) + criteria = append(criteria, criterion{ + by: byEnd, + arg: "", + }) default: errorExit("invalid sort criterion: " + str) } diff --git a/src/result.go b/src/result.go index 289d83a0cf7..5bac7e88fe2 100644 --- a/src/result.go +++ b/src/result.go @@ -4,6 +4,7 @@ import ( "math" "sort" "unicode" + "path/filepath" "github.com/junegunn/fzf/src/tui" "github.com/junegunn/fzf/src/util" @@ -47,7 +48,7 @@ func buildResult(item *Item, offsets []Offset, score int) Result { for idx, criterion := range sortCriteria { val := uint16(math.MaxUint16) - switch criterion { + switch criterion.by { case byScore: // Higher is better val = math.MaxUint16 - util.AsUint16(score) @@ -63,12 +64,26 @@ func buildResult(item *Item, offsets []Offset, score int) Result { break } } - if criterion == byBegin { + if criterion.by == byBegin { val = util.AsUint16(minEnd - whitePrefixLen) } else { val = util.AsUint16(math.MaxUint16 - math.MaxUint16*(maxEnd-whitePrefixLen)/int(item.TrimLength())) } } + case byProximity: + // val is number of shared prefixes + path := filepath.SplitList(criterion.arg) + candidate := filepath.SplitList(item.text.ToString()) + end := util.Min(len(path), len(candidate)) + val = 0 + for idx := 0; idx < end; idx++ { + if path[idx] == candidate[idx] { + val++ + } else { + break + } + } + } result.points[3-idx] = val }