parent
48ab87294b
commit
b8904a8c3e
5
fzf
5
fzf
@ -206,9 +206,10 @@ class FZF
|
|||||||
@expect = true
|
@expect = true
|
||||||
when /^--expect=(.*)$/
|
when /^--expect=(.*)$/
|
||||||
@expect = true
|
@expect = true
|
||||||
when '--toggle-sort'
|
when '--toggle-sort', '--tiebreak'
|
||||||
argv.shift
|
argv.shift
|
||||||
when '--tac', '--sync', '--toggle-sort', /^--toggle-sort=(.*)$/
|
when '--tac', '--no-tac', '--sync', '--no-sync', '--hscroll', '--no-hscroll',
|
||||||
|
/^--toggle-sort=(.*)$/, /^--tiebreak=(.*)$/
|
||||||
# XXX
|
# XXX
|
||||||
else
|
else
|
||||||
usage 1, "illegal option: #{o}"
|
usage 1, "illegal option: #{o}"
|
||||||
|
@ -66,6 +66,20 @@ Reverse the order of the input
|
|||||||
.RS
|
.RS
|
||||||
e.g. \fBhistory | fzf --tac --no-sort\fR
|
e.g. \fBhistory | fzf --tac --no-sort\fR
|
||||||
.RE
|
.RE
|
||||||
|
.TP
|
||||||
|
.BI "--tiebreak=" "STR"
|
||||||
|
Sort criterion to use when the scores are tied
|
||||||
|
.br
|
||||||
|
.R ""
|
||||||
|
.br
|
||||||
|
.BR length " Prefers item with shorter length"
|
||||||
|
.br
|
||||||
|
.BR begin " Prefers item with matched substring closer to the beginning"
|
||||||
|
.br
|
||||||
|
.BR end " Prefers item with matched substring closer to the end""
|
||||||
|
.br
|
||||||
|
.BR index " Prefers item that appeared earlier in the input stream"
|
||||||
|
.br
|
||||||
.SS Interface
|
.SS Interface
|
||||||
.TP
|
.TP
|
||||||
.B "-m, --multi"
|
.B "-m, --multi"
|
||||||
|
@ -55,6 +55,7 @@ func Run(options *Options) {
|
|||||||
|
|
||||||
opts := ParseOptions()
|
opts := ParseOptions()
|
||||||
sort := opts.Sort > 0
|
sort := opts.Sort > 0
|
||||||
|
rankTiebreak = opts.Tiebreak
|
||||||
|
|
||||||
if opts.Version {
|
if opts.Version {
|
||||||
fmt.Println(Version)
|
fmt.Println(Version)
|
||||||
|
36
src/item.go
36
src/item.go
@ -1,6 +1,8 @@
|
|||||||
package fzf
|
package fzf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"math"
|
||||||
|
|
||||||
"github.com/junegunn/fzf/src/curses"
|
"github.com/junegunn/fzf/src/curses"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -27,17 +29,21 @@ type Item struct {
|
|||||||
// Rank is used to sort the search result
|
// Rank is used to sort the search result
|
||||||
type Rank struct {
|
type Rank struct {
|
||||||
matchlen uint16
|
matchlen uint16
|
||||||
strlen uint16
|
tiebreak uint16
|
||||||
index uint32
|
index uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tiebreak criterion to use. Never changes once fzf is started.
|
||||||
|
var rankTiebreak tiebreak
|
||||||
|
|
||||||
// Rank calculates rank of the Item
|
// Rank calculates rank of the Item
|
||||||
func (i *Item) Rank(cache bool) Rank {
|
func (i *Item) Rank(cache bool) Rank {
|
||||||
if cache && (i.rank.matchlen > 0 || i.rank.strlen > 0) {
|
if cache && (i.rank.matchlen > 0 || i.rank.tiebreak > 0) {
|
||||||
return i.rank
|
return i.rank
|
||||||
}
|
}
|
||||||
matchlen := 0
|
matchlen := 0
|
||||||
prevEnd := 0
|
prevEnd := 0
|
||||||
|
minBegin := math.MaxUint16
|
||||||
for _, offset := range i.offsets {
|
for _, offset := range i.offsets {
|
||||||
begin := int(offset[0])
|
begin := int(offset[0])
|
||||||
end := int(offset[1])
|
end := int(offset[1])
|
||||||
@ -48,10 +54,30 @@ func (i *Item) Rank(cache bool) Rank {
|
|||||||
prevEnd = end
|
prevEnd = end
|
||||||
}
|
}
|
||||||
if end > begin {
|
if end > begin {
|
||||||
|
if begin < minBegin {
|
||||||
|
minBegin = begin
|
||||||
|
}
|
||||||
matchlen += end - begin
|
matchlen += end - begin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rank := Rank{uint16(matchlen), uint16(len(*i.text)), i.index}
|
var tiebreak uint16
|
||||||
|
switch rankTiebreak {
|
||||||
|
case byLength:
|
||||||
|
tiebreak = uint16(len(*i.text))
|
||||||
|
case byBegin:
|
||||||
|
// We can't just look at i.offsets[0][0] because it can be an inverse term
|
||||||
|
tiebreak = uint16(minBegin)
|
||||||
|
case byEnd:
|
||||||
|
if prevEnd > 0 {
|
||||||
|
tiebreak = uint16(1 + len(*i.text) - prevEnd)
|
||||||
|
} else {
|
||||||
|
// Empty offsets due to inverse terms.
|
||||||
|
tiebreak = 1
|
||||||
|
}
|
||||||
|
case byIndex:
|
||||||
|
tiebreak = 1
|
||||||
|
}
|
||||||
|
rank := Rank{uint16(matchlen), tiebreak, i.index}
|
||||||
if cache {
|
if cache {
|
||||||
i.rank = rank
|
i.rank = rank
|
||||||
}
|
}
|
||||||
@ -199,9 +225,9 @@ func compareRanks(irank Rank, jrank Rank, tac bool) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if irank.strlen < jrank.strlen {
|
if irank.tiebreak < jrank.tiebreak {
|
||||||
return true
|
return true
|
||||||
} else if irank.strlen > jrank.strlen {
|
} else if irank.tiebreak > jrank.tiebreak {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ func TestItemRank(t *testing.T) {
|
|||||||
strs := []string{"foo", "foobar", "bar", "baz"}
|
strs := []string{"foo", "foobar", "bar", "baz"}
|
||||||
item1 := Item{text: &strs[0], index: 1, offsets: []Offset{}}
|
item1 := Item{text: &strs[0], index: 1, offsets: []Offset{}}
|
||||||
rank1 := item1.Rank(true)
|
rank1 := item1.Rank(true)
|
||||||
if rank1.matchlen != 0 || rank1.strlen != 3 || rank1.index != 1 {
|
if rank1.matchlen != 0 || rank1.tiebreak != 3 || rank1.index != 1 {
|
||||||
t.Error(item1.Rank(true))
|
t.Error(item1.Rank(true))
|
||||||
}
|
}
|
||||||
// Only differ in index
|
// Only differ in index
|
||||||
|
@ -28,7 +28,8 @@ const usage = `usage: fzf [options]
|
|||||||
Search result
|
Search result
|
||||||
+s, --no-sort Do not sort the result
|
+s, --no-sort Do not sort the result
|
||||||
--tac Reverse the order of the input
|
--tac Reverse the order of the input
|
||||||
(e.g. 'history | fzf --tac --no-sort')
|
--tiebreak=CRI Sort criterion when the scores are tied;
|
||||||
|
[length|begin|end|index] (default: length)
|
||||||
|
|
||||||
Interface
|
Interface
|
||||||
-m, --multi Enable multi-select with tab/shift-tab
|
-m, --multi Enable multi-select with tab/shift-tab
|
||||||
@ -50,7 +51,6 @@ const usage = `usage: fzf [options]
|
|||||||
--expect=KEYS Comma-separated list of keys to complete fzf
|
--expect=KEYS Comma-separated list of keys to complete fzf
|
||||||
--toggle-sort=KEY Key to toggle sort
|
--toggle-sort=KEY Key to toggle sort
|
||||||
--sync Synchronous search for multi-staged filtering
|
--sync Synchronous search for multi-staged filtering
|
||||||
(e.g. 'fzf --multi | fzf --sync')
|
|
||||||
|
|
||||||
Environment variables
|
Environment variables
|
||||||
FZF_DEFAULT_COMMAND Default command to use when input is tty
|
FZF_DEFAULT_COMMAND Default command to use when input is tty
|
||||||
@ -78,6 +78,16 @@ const (
|
|||||||
CaseRespect
|
CaseRespect
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Sort criteria
|
||||||
|
type tiebreak int
|
||||||
|
|
||||||
|
const (
|
||||||
|
byLength tiebreak = iota
|
||||||
|
byBegin
|
||||||
|
byEnd
|
||||||
|
byIndex
|
||||||
|
)
|
||||||
|
|
||||||
// Options stores the values of command-line options
|
// Options stores the values of command-line options
|
||||||
type Options struct {
|
type Options struct {
|
||||||
Mode Mode
|
Mode Mode
|
||||||
@ -87,6 +97,7 @@ type Options struct {
|
|||||||
Delimiter *regexp.Regexp
|
Delimiter *regexp.Regexp
|
||||||
Sort int
|
Sort int
|
||||||
Tac bool
|
Tac bool
|
||||||
|
Tiebreak tiebreak
|
||||||
Multi bool
|
Multi bool
|
||||||
Ansi bool
|
Ansi bool
|
||||||
Mouse bool
|
Mouse bool
|
||||||
@ -116,6 +127,7 @@ func defaultOptions() *Options {
|
|||||||
Delimiter: nil,
|
Delimiter: nil,
|
||||||
Sort: 1000,
|
Sort: 1000,
|
||||||
Tac: false,
|
Tac: false,
|
||||||
|
Tiebreak: byLength,
|
||||||
Multi: false,
|
Multi: false,
|
||||||
Ansi: false,
|
Ansi: false,
|
||||||
Mouse: true,
|
Mouse: true,
|
||||||
@ -238,6 +250,22 @@ func parseKeyChords(str string, message string) []int {
|
|||||||
return chords
|
return chords
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseTiebreak(str string) tiebreak {
|
||||||
|
switch strings.ToLower(str) {
|
||||||
|
case "length":
|
||||||
|
return byLength
|
||||||
|
case "index":
|
||||||
|
return byIndex
|
||||||
|
case "begin":
|
||||||
|
return byBegin
|
||||||
|
case "end":
|
||||||
|
return byEnd
|
||||||
|
default:
|
||||||
|
errorExit("invalid sort criterion: " + str)
|
||||||
|
}
|
||||||
|
return byLength
|
||||||
|
}
|
||||||
|
|
||||||
func checkToggleSort(str string) int {
|
func checkToggleSort(str string) int {
|
||||||
keys := parseKeyChords(str, "key name required")
|
keys := parseKeyChords(str, "key name required")
|
||||||
if len(keys) != 1 {
|
if len(keys) != 1 {
|
||||||
@ -265,6 +293,8 @@ func parseOptions(opts *Options, allArgs []string) {
|
|||||||
opts.Filter = &filter
|
opts.Filter = &filter
|
||||||
case "--expect":
|
case "--expect":
|
||||||
opts.Expect = parseKeyChords(nextString(allArgs, &i, "key names required"), "key names required")
|
opts.Expect = parseKeyChords(nextString(allArgs, &i, "key names required"), "key names required")
|
||||||
|
case "--tiebreak":
|
||||||
|
opts.Tiebreak = parseTiebreak(nextString(allArgs, &i, "sort criterion required"))
|
||||||
case "--toggle-sort":
|
case "--toggle-sort":
|
||||||
opts.ToggleSort = checkToggleSort(nextString(allArgs, &i, "key name required"))
|
opts.ToggleSort = checkToggleSort(nextString(allArgs, &i, "key name required"))
|
||||||
case "-d", "--delimiter":
|
case "-d", "--delimiter":
|
||||||
@ -352,6 +382,8 @@ func parseOptions(opts *Options, allArgs []string) {
|
|||||||
opts.ToggleSort = checkToggleSort(value)
|
opts.ToggleSort = checkToggleSort(value)
|
||||||
} else if match, value := optString(arg, "--expect="); match {
|
} else if match, value := optString(arg, "--expect="); match {
|
||||||
opts.Expect = parseKeyChords(value, "key names required")
|
opts.Expect = parseKeyChords(value, "key names required")
|
||||||
|
} else if match, value := optString(arg, "--tiebreak="); match {
|
||||||
|
opts.Tiebreak = parseTiebreak(value)
|
||||||
} else {
|
} else {
|
||||||
errorExit("unknown option: " + arg)
|
errorExit("unknown option: " + arg)
|
||||||
}
|
}
|
||||||
|
@ -476,19 +476,66 @@ class TestGoFZF < TestBase
|
|||||||
|
|
||||||
def test_unicode_case
|
def test_unicode_case
|
||||||
tempname = TEMPNAME + Time.now.to_f.to_s
|
tempname = TEMPNAME + Time.now.to_f.to_s
|
||||||
File.open(tempname, 'w') do |f|
|
writelines tempname, %w[строКА1 СТРОКА2 строка3 Строка4]
|
||||||
f << %w[строКА1 СТРОКА2 строка3 Строка4].join($/)
|
|
||||||
f.sync
|
|
||||||
end
|
|
||||||
since = Time.now
|
|
||||||
while `cat #{tempname}`.split($/).length != 4 && (Time.now - since) < 10
|
|
||||||
sleep 0.1
|
|
||||||
end
|
|
||||||
assert_equal %w[СТРОКА2 Строка4], `cat #{tempname} | #{FZF} -fС`.split($/)
|
assert_equal %w[СТРОКА2 Строка4], `cat #{tempname} | #{FZF} -fС`.split($/)
|
||||||
assert_equal %w[строКА1 СТРОКА2 строка3 Строка4], `cat #{tempname} | #{FZF} -fс`.split($/)
|
assert_equal %w[строКА1 СТРОКА2 строка3 Строка4], `cat #{tempname} | #{FZF} -fс`.split($/)
|
||||||
rescue
|
rescue
|
||||||
File.unlink tempname
|
File.unlink tempname
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_tiebreak
|
||||||
|
tempname = TEMPNAME + Time.now.to_f.to_s
|
||||||
|
input = %w[
|
||||||
|
--foobar--------
|
||||||
|
-----foobar---
|
||||||
|
----foobar--
|
||||||
|
-------foobar-
|
||||||
|
]
|
||||||
|
writelines tempname, input
|
||||||
|
|
||||||
|
assert_equal input, `cat #{tempname} | #{FZF} -ffoobar --tiebreak=index`.split($/)
|
||||||
|
|
||||||
|
by_length = %w[
|
||||||
|
----foobar--
|
||||||
|
-----foobar---
|
||||||
|
-------foobar-
|
||||||
|
--foobar--------
|
||||||
|
]
|
||||||
|
assert_equal by_length, `cat #{tempname} | #{FZF} -ffoobar`.split($/)
|
||||||
|
assert_equal by_length, `cat #{tempname} | #{FZF} -ffoobar --tiebreak=length`.split($/)
|
||||||
|
|
||||||
|
by_begin = %w[
|
||||||
|
--foobar--------
|
||||||
|
----foobar--
|
||||||
|
-----foobar---
|
||||||
|
-------foobar-
|
||||||
|
]
|
||||||
|
assert_equal by_begin, `cat #{tempname} | #{FZF} -ffoobar --tiebreak=begin`.split($/)
|
||||||
|
assert_equal by_begin, `cat #{tempname} | #{FZF} -f"!z foobar" -x --tiebreak begin`.split($/)
|
||||||
|
|
||||||
|
assert_equal %w[
|
||||||
|
-------foobar-
|
||||||
|
----foobar--
|
||||||
|
-----foobar---
|
||||||
|
--foobar--------
|
||||||
|
], `cat #{tempname} | #{FZF} -ffoobar --tiebreak end`.split($/)
|
||||||
|
|
||||||
|
assert_equal input, `cat #{tempname} | #{FZF} -f"!z" -x --tiebreak end`.split($/)
|
||||||
|
rescue
|
||||||
|
File.unlink tempname
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def writelines path, lines, timeout = 10
|
||||||
|
File.open(path, 'w') do |f|
|
||||||
|
f << lines.join($/)
|
||||||
|
f.sync
|
||||||
|
end
|
||||||
|
since = Time.now
|
||||||
|
while `cat #{path}`.split($/).length != lines.length && (Time.now - since) < 10
|
||||||
|
sleep 0.1
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
module TestShell
|
module TestShell
|
||||||
|
Loading…
x
Reference in New Issue
Block a user