mirror of
https://github.com/junegunn/fzf.git
synced 2025-05-19 04:40:22 -07:00
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:
parent
c71e4ddee4
commit
243a76002c
@ -1,6 +1,13 @@
|
|||||||
CHANGELOG
|
CHANGELOG
|
||||||
=========
|
=========
|
||||||
|
|
||||||
|
0.59.0
|
||||||
|
------
|
||||||
|
- Prioritizing file name matches (#4192)
|
||||||
|
- Added a new tiebreak option `pathname` for prioritizing file name matches
|
||||||
|
- `--scheme=path` now sets `--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.
|
||||||
|
|
||||||
0.58.0
|
0.58.0
|
||||||
------
|
------
|
||||||
_Release highlights: https://junegunn.github.io/fzf/releases/0.58.0/_
|
_Release highlights: https://junegunn.github.io/fzf/releases/0.58.0/_
|
||||||
|
19
README.md
19
README.md
@ -83,6 +83,7 @@ Table of Contents
|
|||||||
* [Custom fuzzy completion](#custom-fuzzy-completion)
|
* [Custom fuzzy completion](#custom-fuzzy-completion)
|
||||||
* [Vim plugin](#vim-plugin)
|
* [Vim plugin](#vim-plugin)
|
||||||
* [Advanced topics](#advanced-topics)
|
* [Advanced topics](#advanced-topics)
|
||||||
|
* [Customizing for different types of input](#customizing-for-different-types-of-input)
|
||||||
* [Performance](#performance)
|
* [Performance](#performance)
|
||||||
* [Executing external programs](#executing-external-programs)
|
* [Executing external programs](#executing-external-programs)
|
||||||
* [Turning into a different process](#turning-into-a-different-process)
|
* [Turning into a different process](#turning-into-a-different-process)
|
||||||
@ -718,6 +719,22 @@ See [README-VIM.md](README-VIM.md).
|
|||||||
Advanced topics
|
Advanced topics
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
|
### Customizing for different types of input
|
||||||
|
|
||||||
|
Since fzf is a general-purpose text filter, its algorithm was designed to
|
||||||
|
"generally" work well with any kind of input. However, admittedly, there is no
|
||||||
|
true one-size-fits-all solution, and you may want to tweak the algorithm and
|
||||||
|
some of the settings depending on the type of the input. To make this process
|
||||||
|
easier, fzf provides a set of "scheme"s for some common input types.
|
||||||
|
|
||||||
|
| Scheme | Description |
|
||||||
|
| :--- | :--- |
|
||||||
|
| `--scheme=default` | Generic scheme designed to work well with any kind of input |
|
||||||
|
| `--scheme=path` | Suitable for file paths |
|
||||||
|
| `--scheme=history` | Suitable for command history or any input where chronological ordering is important |
|
||||||
|
|
||||||
|
(See `fzf --man` for the details)
|
||||||
|
|
||||||
### Performance
|
### Performance
|
||||||
|
|
||||||
fzf is fast. Performance should not be a problem in most use cases. However,
|
fzf is fast. Performance should not be a problem in most use cases. However,
|
||||||
@ -727,6 +744,8 @@ you might want to be aware of the options that can affect performance.
|
|||||||
makes the initial scanning slower. So it's not recommended that you add it
|
makes the initial scanning slower. So it's not recommended that you add it
|
||||||
to your `$FZF_DEFAULT_OPTS`.
|
to your `$FZF_DEFAULT_OPTS`.
|
||||||
- `--nth` makes fzf slower because it has to tokenize each line.
|
- `--nth` makes fzf slower because it has to tokenize each line.
|
||||||
|
- A plain string `--delimiter` should be preferred over a regular expression
|
||||||
|
delimiter.
|
||||||
- `--with-nth` makes fzf slower as fzf has to tokenize and reassemble each
|
- `--with-nth` makes fzf slower as fzf has to tokenize and reassemble each
|
||||||
line.
|
line.
|
||||||
|
|
||||||
|
@ -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
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
THE SOFTWARE.
|
THE SOFTWARE.
|
||||||
..
|
..
|
||||||
.TH fzf 1 "Jan 2025" "fzf 0.58.0" "fzf - a command-line fuzzy finder"
|
.TH fzf 1 "Jan 2025" "fzf 0.59.0" "fzf - a command-line fuzzy finder"
|
||||||
|
|
||||||
.SH NAME
|
.SH NAME
|
||||||
fzf - a command-line fuzzy finder
|
fzf - a command-line fuzzy finder
|
||||||
@ -76,7 +76,8 @@ Generic scoring scheme designed to work well with any type of input.
|
|||||||
.RS
|
.RS
|
||||||
Additional bonus point is only given to the characters after path separator.
|
Additional bonus point is only given to the characters after path separator.
|
||||||
You might want to choose this scheme over \fBdefault\fR if you have many files
|
You might want to choose this scheme over \fBdefault\fR if you have many files
|
||||||
with spaces in their paths.
|
with spaces in their paths. This also sets \fB\-\-tiebreak=pathname,length\fR,
|
||||||
|
to prioritize matches occurring in the tail element of a file path.
|
||||||
.RE
|
.RE
|
||||||
.RE
|
.RE
|
||||||
|
|
||||||
@ -90,6 +91,13 @@ more weight to the chronological ordering. This also sets
|
|||||||
.RE
|
.RE
|
||||||
.RE
|
.RE
|
||||||
|
|
||||||
|
.RS
|
||||||
|
fzf chooses \fBpath\fR scheme when the input is a TTY device, where fzf would
|
||||||
|
start its built-in walker or run \fB$FZF_DEFAULT_COMMAND\fR, and there is no
|
||||||
|
\fBreload\fR action bound to \fBstart\fR event. Otherwise, it chooses
|
||||||
|
\fBdefault\fR scheme.
|
||||||
|
.RE
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.BI "\-\-algo=" TYPE
|
.BI "\-\-algo=" TYPE
|
||||||
Fuzzy matching algorithm (default: v2)
|
Fuzzy matching algorithm (default: v2)
|
||||||
@ -140,15 +148,17 @@ Comma-separated list of sort criteria to apply when the scores are tied.
|
|||||||
.br
|
.br
|
||||||
|
|
||||||
.br
|
.br
|
||||||
.BR length " Prefers line with shorter length"
|
.BR length " Prefers line with shorter length"
|
||||||
.br
|
.br
|
||||||
.BR chunk " Prefers line with shorter matched chunk (delimited by whitespaces)"
|
.BR chunk " Prefers line with shorter matched chunk (delimited by whitespaces)"
|
||||||
.br
|
.br
|
||||||
.BR begin " Prefers line with matched substring closer to the beginning"
|
.BR pathname " Prefers line with matched substring in the file name of the path"
|
||||||
.br
|
.br
|
||||||
.BR end " Prefers line with matched substring closer to the end"
|
.BR begin " Prefers line with matched substring closer to the beginning"
|
||||||
.br
|
.br
|
||||||
.BR index " Prefers line that appeared earlier in the input stream"
|
.BR end " Prefers line with matched substring closer to the end"
|
||||||
|
.br
|
||||||
|
.BR index " Prefers line that appeared earlier in the input stream"
|
||||||
.br
|
.br
|
||||||
|
|
||||||
.br
|
.br
|
||||||
@ -1128,9 +1138,9 @@ Show man page
|
|||||||
.SH ENVIRONMENT VARIABLES
|
.SH ENVIRONMENT VARIABLES
|
||||||
.TP
|
.TP
|
||||||
.B FZF_DEFAULT_COMMAND
|
.B FZF_DEFAULT_COMMAND
|
||||||
Default command to use when input is tty. On *nix systems, fzf runs the command
|
Default command to use when input is a TTY device. On *nix systems, fzf runs
|
||||||
with \fB$SHELL \-c\fR if \fBSHELL\fR is set, otherwise with \fBsh \-c\fR, so in
|
the command with \fB$SHELL \-c\fR if \fBSHELL\fR is set, otherwise with \fBsh
|
||||||
this case make sure that the command is POSIX-compliant.
|
\-c\fR, so in this case make sure that the command is POSIX-compliant.
|
||||||
.TP
|
.TP
|
||||||
.B FZF_DEFAULT_OPTS
|
.B FZF_DEFAULT_OPTS
|
||||||
Default options.
|
Default options.
|
||||||
|
@ -1081,7 +1081,7 @@ endfunction
|
|||||||
|
|
||||||
function! s:cmd(bang, ...) abort
|
function! s:cmd(bang, ...) abort
|
||||||
let args = copy(a:000)
|
let args = copy(a:000)
|
||||||
let opts = { 'options': ['--multi'] }
|
let opts = { 'options': ['--multi', '--scheme', 'path'] }
|
||||||
if len(args) && isdirectory(expand(args[-1]))
|
if len(args) && isdirectory(expand(args[-1]))
|
||||||
let opts.dir = substitute(substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g'), '[/\\]*$', '/', '')
|
let opts.dir = substitute(substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g'), '[/\\]*$', '/', '')
|
||||||
if s:is_win && !&shellslash
|
if s:is_win && !&shellslash
|
||||||
|
@ -188,6 +188,9 @@ func Run(opts *Options) (int, error) {
|
|||||||
forward = false
|
forward = false
|
||||||
case byBegin:
|
case byBegin:
|
||||||
forward = true
|
forward = true
|
||||||
|
case byPathname:
|
||||||
|
withPos = true
|
||||||
|
forward = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
|
|
||||||
"github.com/junegunn/fzf/src/algo"
|
"github.com/junegunn/fzf/src/algo"
|
||||||
"github.com/junegunn/fzf/src/tui"
|
"github.com/junegunn/fzf/src/tui"
|
||||||
|
"github.com/junegunn/fzf/src/util"
|
||||||
|
|
||||||
"github.com/junegunn/go-shellwords"
|
"github.com/junegunn/go-shellwords"
|
||||||
"github.com/rivo/uniseg"
|
"github.com/rivo/uniseg"
|
||||||
@ -46,8 +47,8 @@ Usage: fzf [options]
|
|||||||
--tail=NUM Maximum number of items to keep in memory
|
--tail=NUM Maximum number of items to keep in memory
|
||||||
--disabled Do not perform search
|
--disabled Do not perform search
|
||||||
--tiebreak=CRI[,..] Comma-separated list of sort criteria to apply
|
--tiebreak=CRI[,..] Comma-separated list of sort criteria to apply
|
||||||
when the scores are tied [length|chunk|begin|end|index]
|
when the scores are tied
|
||||||
(default: length)
|
[length|chunk|pathname|begin|end|index] (default: length)
|
||||||
|
|
||||||
INPUT/OUTPUT
|
INPUT/OUTPUT
|
||||||
--read0 Read input delimited by ASCII NUL characters
|
--read0 Read input delimited by ASCII NUL characters
|
||||||
@ -241,6 +242,7 @@ const (
|
|||||||
byLength
|
byLength
|
||||||
byBegin
|
byBegin
|
||||||
byEnd
|
byEnd
|
||||||
|
byPathname
|
||||||
)
|
)
|
||||||
|
|
||||||
type heightSpec struct {
|
type heightSpec struct {
|
||||||
@ -653,7 +655,7 @@ func defaultOptions() *Options {
|
|||||||
Man: false,
|
Man: false,
|
||||||
Fuzzy: true,
|
Fuzzy: true,
|
||||||
FuzzyAlgo: algo.FuzzyMatchV2,
|
FuzzyAlgo: algo.FuzzyMatchV2,
|
||||||
Scheme: "default",
|
Scheme: "", // Unknown
|
||||||
Extended: true,
|
Extended: true,
|
||||||
Phony: false,
|
Phony: false,
|
||||||
Case: CaseSmart,
|
Case: CaseSmart,
|
||||||
@ -664,7 +666,7 @@ func defaultOptions() *Options {
|
|||||||
Sort: 1000,
|
Sort: 1000,
|
||||||
Track: trackDisabled,
|
Track: trackDisabled,
|
||||||
Tac: false,
|
Tac: false,
|
||||||
Criteria: []criterion{byScore, byLength},
|
Criteria: []criterion{}, // Unknown
|
||||||
Multi: 0,
|
Multi: 0,
|
||||||
Ansi: false,
|
Ansi: false,
|
||||||
Mouse: true,
|
Mouse: true,
|
||||||
@ -804,16 +806,6 @@ func parseAlgo(str string) (algo.Algo, error) {
|
|||||||
return nil, errors.New("invalid algorithm (expected: v1 or v2)")
|
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) {
|
func parseBorder(str string, optional bool, allowLine bool) (tui.BorderShape, error) {
|
||||||
switch str {
|
switch str {
|
||||||
case "line":
|
case "line":
|
||||||
@ -1037,6 +1029,19 @@ func parseKeyChordsImpl(str string, message string) (map[tui.Event]string, error
|
|||||||
return chords, nil
|
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) {
|
func parseTiebreak(str string) ([]criterion, error) {
|
||||||
criteria := []criterion{byScore}
|
criteria := []criterion{byScore}
|
||||||
hasIndex := false
|
hasIndex := false
|
||||||
@ -1044,6 +1049,7 @@ func parseTiebreak(str string) ([]criterion, error) {
|
|||||||
hasLength := false
|
hasLength := false
|
||||||
hasBegin := false
|
hasBegin := false
|
||||||
hasEnd := false
|
hasEnd := false
|
||||||
|
hasPathname := false
|
||||||
check := func(notExpected *bool, name string) error {
|
check := func(notExpected *bool, name string) error {
|
||||||
if *notExpected {
|
if *notExpected {
|
||||||
return errors.New("duplicate sort criteria: " + name)
|
return errors.New("duplicate sort criteria: " + name)
|
||||||
@ -1065,6 +1071,11 @@ func parseTiebreak(str string) ([]criterion, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
criteria = append(criteria, byChunk)
|
criteria = append(criteria, byChunk)
|
||||||
|
case "pathname":
|
||||||
|
if err := check(&hasPathname, "pathname"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
criteria = append(criteria, byPathname)
|
||||||
case "length":
|
case "length":
|
||||||
if err := check(&hasLength, "length"); err != nil {
|
if err := check(&hasLength, "length"); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -2261,7 +2272,9 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
opts.Scheme = strings.ToLower(str)
|
if opts.Scheme, opts.Criteria, err = parseScheme(str); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
case "--expect":
|
case "--expect":
|
||||||
str, err := nextString("key names required")
|
str, err := nextString("key names required")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -3173,7 +3186,9 @@ func postProcessOptions(opts *Options) error {
|
|||||||
return errors.New("failed to start pprof profiles: " + err.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) {
|
func parseShellWords(str string) ([]string, error) {
|
||||||
@ -3223,7 +3238,26 @@ func ParseOptions(useDefaults bool, args []string) (*Options, error) {
|
|||||||
return nil, err
|
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 {
|
if err := validateOptions(opts); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -3231,6 +3265,17 @@ func ParseOptions(useDefaults bool, args []string) (*Options, error) {
|
|||||||
return opts, nil
|
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 {
|
func (opts *Options) extractReloadOnStart() string {
|
||||||
cmd := ""
|
cmd := ""
|
||||||
if actions, prs := opts.Keymap[tui.Start.AsEvent()]; prs {
|
if actions, prs := opts.Keymap[tui.Start.AsEvent()]; prs {
|
||||||
|
@ -69,6 +69,21 @@ func buildResult(item *Item, offsets []Offset, score int) Result {
|
|||||||
}
|
}
|
||||||
case byLength:
|
case byLength:
|
||||||
val = item.TrimLength()
|
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:
|
case byBegin, byEnd:
|
||||||
if validOffsetFound {
|
if validOffsetFound {
|
||||||
whitePrefixLen := 0
|
whitePrefixLen := 0
|
||||||
|
Loading…
x
Reference in New Issue
Block a user