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
|
||||
=========
|
||||
|
||||
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
|
||||
------
|
||||
_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)
|
||||
* [Vim plugin](#vim-plugin)
|
||||
* [Advanced topics](#advanced-topics)
|
||||
* [Customizing for different types of input](#customizing-for-different-types-of-input)
|
||||
* [Performance](#performance)
|
||||
* [Executing external programs](#executing-external-programs)
|
||||
* [Turning into a different process](#turning-into-a-different-process)
|
||||
@ -718,6 +719,22 @@ See [README-VIM.md](README-VIM.md).
|
||||
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
|
||||
|
||||
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
|
||||
to your `$FZF_DEFAULT_OPTS`.
|
||||
- `--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
|
||||
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
|
||||
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
|
||||
fzf - a command-line fuzzy finder
|
||||
@ -76,7 +76,8 @@ Generic scoring scheme designed to work well with any type of input.
|
||||
.RS
|
||||
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
|
||||
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
|
||||
|
||||
@ -90,6 +91,13 @@ more weight to the chronological ordering. This also sets
|
||||
.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
|
||||
.BI "\-\-algo=" TYPE
|
||||
Fuzzy matching algorithm (default: v2)
|
||||
@ -144,6 +152,8 @@ Comma-separated list of sort criteria to apply when the scores are tied.
|
||||
.br
|
||||
.BR chunk " Prefers line with shorter matched chunk (delimited by whitespaces)"
|
||||
.br
|
||||
.BR pathname " Prefers line with matched substring in the file name of the path"
|
||||
.br
|
||||
.BR begin " Prefers line with matched substring closer to the beginning"
|
||||
.br
|
||||
.BR end " Prefers line with matched substring closer to the end"
|
||||
@ -1128,9 +1138,9 @@ Show man page
|
||||
.SH ENVIRONMENT VARIABLES
|
||||
.TP
|
||||
.B FZF_DEFAULT_COMMAND
|
||||
Default command to use when input is tty. On *nix systems, fzf runs the command
|
||||
with \fB$SHELL \-c\fR if \fBSHELL\fR is set, otherwise with \fBsh \-c\fR, so in
|
||||
this case make sure that the command is POSIX-compliant.
|
||||
Default command to use when input is a TTY device. On *nix systems, fzf runs
|
||||
the command with \fB$SHELL \-c\fR if \fBSHELL\fR is set, otherwise with \fBsh
|
||||
\-c\fR, so in this case make sure that the command is POSIX-compliant.
|
||||
.TP
|
||||
.B FZF_DEFAULT_OPTS
|
||||
Default options.
|
||||
|
@ -1081,7 +1081,7 @@ endfunction
|
||||
|
||||
function! s:cmd(bang, ...) abort
|
||||
let args = copy(a:000)
|
||||
let opts = { 'options': ['--multi'] }
|
||||
let opts = { 'options': ['--multi', '--scheme', 'path'] }
|
||||
if len(args) && isdirectory(expand(args[-1]))
|
||||
let opts.dir = substitute(substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g'), '[/\\]*$', '/', '')
|
||||
if s:is_win && !&shellslash
|
||||
|
@ -188,6 +188,9 @@ func Run(opts *Options) (int, error) {
|
||||
forward = false
|
||||
case byBegin:
|
||||
forward = true
|
||||
case byPathname:
|
||||
withPos = true
|
||||
forward = false
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user