Skip to content

Commit

Permalink
Add --tiebreak proximity=path
Browse files Browse the repository at this point in the history
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).
  • Loading branch information
jonhoo committed Sep 17, 2018
1 parent 8540902 commit 9d4a3f6
Show file tree
Hide file tree
Showing 3 changed files with 54 additions and 11 deletions.
4 changes: 2 additions & 2 deletions src/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down
42 changes: 35 additions & 7 deletions src/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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)
}
Expand Down
19 changes: 17 additions & 2 deletions src/result.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"math"
"sort"
"unicode"
"path/filepath"

"github.com/junegunn/fzf/src/tui"
"github.com/junegunn/fzf/src/util"
Expand Down Expand Up @@ -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)
Expand All @@ -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
}
Expand Down

0 comments on commit 9d4a3f6

Please sign in to comment.