mirror of
https://github.com/Llewellynvdm/fzf.git
synced 2025-02-08 15:08:30 +00:00
--multi to take optional argument to limit the number of selection
Close #1718 Related #688
This commit is contained in:
parent
a2e9366c84
commit
072066c49c
@ -1,6 +1,7 @@
|
|||||||
package fzf
|
package fzf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -27,6 +28,7 @@ const (
|
|||||||
spinnerDuration = 200 * time.Millisecond
|
spinnerDuration = 200 * time.Millisecond
|
||||||
previewCancelWait = 500 * time.Millisecond
|
previewCancelWait = 500 * time.Millisecond
|
||||||
maxPatternLength = 300
|
maxPatternLength = 300
|
||||||
|
maxMulti = math.MaxInt32
|
||||||
|
|
||||||
// Matcher
|
// Matcher
|
||||||
numPartitionsMultiplier = 8
|
numPartitionsMultiplier = 8
|
||||||
|
@ -38,7 +38,7 @@ const usage = `usage: fzf [options]
|
|||||||
(default: length)
|
(default: length)
|
||||||
|
|
||||||
Interface
|
Interface
|
||||||
-m, --multi Enable multi-select with tab/shift-tab
|
-m, --multi[=MAX] Enable multi-select with tab/shift-tab
|
||||||
--no-mouse Disable mouse
|
--no-mouse Disable mouse
|
||||||
--bind=KEYBINDS Custom key bindings. Refer to the man page.
|
--bind=KEYBINDS Custom key bindings. Refer to the man page.
|
||||||
--cycle Enable cyclic scroll
|
--cycle Enable cyclic scroll
|
||||||
@ -162,7 +162,7 @@ type Options struct {
|
|||||||
Sort int
|
Sort int
|
||||||
Tac bool
|
Tac bool
|
||||||
Criteria []criterion
|
Criteria []criterion
|
||||||
Multi bool
|
Multi int
|
||||||
Ansi bool
|
Ansi bool
|
||||||
Mouse bool
|
Mouse bool
|
||||||
Theme *tui.ColorTheme
|
Theme *tui.ColorTheme
|
||||||
@ -215,7 +215,7 @@ func defaultOptions() *Options {
|
|||||||
Sort: 1000,
|
Sort: 1000,
|
||||||
Tac: false,
|
Tac: false,
|
||||||
Criteria: []criterion{byScore, byLength},
|
Criteria: []criterion{byScore, byLength},
|
||||||
Multi: false,
|
Multi: 0,
|
||||||
Ansi: false,
|
Ansi: false,
|
||||||
Mouse: true,
|
Mouse: true,
|
||||||
Theme: tui.EmptyTheme(),
|
Theme: tui.EmptyTheme(),
|
||||||
@ -314,13 +314,14 @@ func nextInt(args []string, i *int, message string) int {
|
|||||||
return atoi(args[*i])
|
return atoi(args[*i])
|
||||||
}
|
}
|
||||||
|
|
||||||
func optionalNumeric(args []string, i *int) int {
|
func optionalNumeric(args []string, i *int, defaultValue int) int {
|
||||||
if len(args) > *i+1 {
|
if len(args) > *i+1 {
|
||||||
if strings.IndexAny(args[*i+1], "0123456789") == 0 {
|
if strings.IndexAny(args[*i+1], "0123456789") == 0 {
|
||||||
*i++
|
*i++
|
||||||
|
return atoi(args[*i])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return 1 // Don't care
|
return defaultValue
|
||||||
}
|
}
|
||||||
|
|
||||||
func splitNth(str string) []Range {
|
func splitNth(str string) []Range {
|
||||||
@ -1033,7 +1034,7 @@ func parseOptions(opts *Options, allArgs []string) {
|
|||||||
case "--with-nth":
|
case "--with-nth":
|
||||||
opts.WithNth = splitNth(nextString(allArgs, &i, "nth expression required"))
|
opts.WithNth = splitNth(nextString(allArgs, &i, "nth expression required"))
|
||||||
case "-s", "--sort":
|
case "-s", "--sort":
|
||||||
opts.Sort = optionalNumeric(allArgs, &i)
|
opts.Sort = optionalNumeric(allArgs, &i, 1)
|
||||||
case "+s", "--no-sort":
|
case "+s", "--no-sort":
|
||||||
opts.Sort = 0
|
opts.Sort = 0
|
||||||
case "--tac":
|
case "--tac":
|
||||||
@ -1045,9 +1046,9 @@ func parseOptions(opts *Options, allArgs []string) {
|
|||||||
case "+i":
|
case "+i":
|
||||||
opts.Case = CaseRespect
|
opts.Case = CaseRespect
|
||||||
case "-m", "--multi":
|
case "-m", "--multi":
|
||||||
opts.Multi = true
|
opts.Multi = optionalNumeric(allArgs, &i, maxMulti)
|
||||||
case "+m", "--no-multi":
|
case "+m", "--no-multi":
|
||||||
opts.Multi = false
|
opts.Multi = 0
|
||||||
case "--ansi":
|
case "--ansi":
|
||||||
opts.Ansi = true
|
opts.Ansi = true
|
||||||
case "--no-ansi":
|
case "--no-ansi":
|
||||||
@ -1190,6 +1191,8 @@ func parseOptions(opts *Options, allArgs []string) {
|
|||||||
opts.WithNth = splitNth(value)
|
opts.WithNth = splitNth(value)
|
||||||
} else if match, _ := optString(arg, "-s", "--sort="); match {
|
} else if match, _ := optString(arg, "-s", "--sort="); match {
|
||||||
opts.Sort = 1 // Don't care
|
opts.Sort = 1 // Don't care
|
||||||
|
} else if match, value := optString(arg, "-s", "--multi="); match {
|
||||||
|
opts.Multi = atoi(value)
|
||||||
} else if match, value := optString(arg, "--height="); match {
|
} else if match, value := optString(arg, "--height="); match {
|
||||||
opts.Height = parseHeight(value)
|
opts.Height = parseHeight(value)
|
||||||
} else if match, value := optString(arg, "--min-height="); match {
|
} else if match, value := optString(arg, "--min-height="); match {
|
||||||
|
@ -76,7 +76,7 @@ type Terminal struct {
|
|||||||
xoffset int
|
xoffset int
|
||||||
yanked []rune
|
yanked []rune
|
||||||
input []rune
|
input []rune
|
||||||
multi bool
|
multi int
|
||||||
sort bool
|
sort bool
|
||||||
toggleSort bool
|
toggleSort bool
|
||||||
delimiter Delimiter
|
delimiter Delimiter
|
||||||
@ -750,8 +750,12 @@ func (t *Terminal) printInfo() {
|
|||||||
output += " -S"
|
output += " -S"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if t.multi && len(t.selected) > 0 {
|
if len(t.selected) > 0 {
|
||||||
|
if t.multi == maxMulti {
|
||||||
output += fmt.Sprintf(" (%d)", len(t.selected))
|
output += fmt.Sprintf(" (%d)", len(t.selected))
|
||||||
|
} else {
|
||||||
|
output += fmt.Sprintf(" (%d/%d)", len(t.selected), t.multi)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if t.progress > 0 && t.progress < 100 {
|
if t.progress > 0 && t.progress < 100 {
|
||||||
output += fmt.Sprintf(" (%d%%)", t.progress)
|
output += fmt.Sprintf(" (%d%%)", t.progress)
|
||||||
@ -1426,9 +1430,18 @@ func (t *Terminal) buildPlusList(template string, forcePlus bool) (bool, []*Item
|
|||||||
return true, sels
|
return true, sels
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Terminal) selectItem(item *Item) {
|
func (t *Terminal) selectItem(item *Item) bool {
|
||||||
|
if len(t.selected) >= t.multi {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if _, found := t.selected[item.Index()]; found {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
t.selected[item.Index()] = selectedItem{time.Now(), item}
|
t.selected[item.Index()] = selectedItem{time.Now(), item}
|
||||||
t.version++
|
t.version++
|
||||||
|
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Terminal) deselectItem(item *Item) {
|
func (t *Terminal) deselectItem(item *Item) {
|
||||||
@ -1436,12 +1449,12 @@ func (t *Terminal) deselectItem(item *Item) {
|
|||||||
t.version++
|
t.version++
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Terminal) toggleItem(item *Item) {
|
func (t *Terminal) toggleItem(item *Item) bool {
|
||||||
if _, found := t.selected[item.Index()]; !found {
|
if _, found := t.selected[item.Index()]; !found {
|
||||||
t.selectItem(item)
|
return t.selectItem(item)
|
||||||
} else {
|
|
||||||
t.deselectItem(item)
|
|
||||||
}
|
}
|
||||||
|
t.deselectItem(item)
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Terminal) killPreview(code int) {
|
func (t *Terminal) killPreview(code int) {
|
||||||
@ -1687,11 +1700,12 @@ func (t *Terminal) Loop() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
toggle := func() {
|
toggle := func() bool {
|
||||||
if t.cy < t.merger.Length() {
|
if t.cy < t.merger.Length() && t.toggleItem(t.merger.Get(t.cy).item) {
|
||||||
t.toggleItem(t.merger.Get(t.cy).item)
|
|
||||||
req(reqInfo)
|
req(reqInfo)
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
scrollPreview := func(amount int) {
|
scrollPreview := func(amount int) {
|
||||||
if !t.previewer.more {
|
if !t.previewer.more {
|
||||||
@ -1813,27 +1827,42 @@ func (t *Terminal) Loop() {
|
|||||||
t.cx--
|
t.cx--
|
||||||
}
|
}
|
||||||
case actSelectAll:
|
case actSelectAll:
|
||||||
if t.multi {
|
if t.multi > 0 {
|
||||||
for i := 0; i < t.merger.Length(); i++ {
|
for i := 0; i < t.merger.Length(); i++ {
|
||||||
t.selectItem(t.merger.Get(i).item)
|
if !t.selectItem(t.merger.Get(i).item) {
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
req(reqList, reqInfo)
|
req(reqList, reqInfo)
|
||||||
}
|
}
|
||||||
case actDeselectAll:
|
case actDeselectAll:
|
||||||
if t.multi {
|
if t.multi > 0 {
|
||||||
t.selected = make(map[int32]selectedItem)
|
t.selected = make(map[int32]selectedItem)
|
||||||
t.version++
|
t.version++
|
||||||
req(reqList, reqInfo)
|
req(reqList, reqInfo)
|
||||||
}
|
}
|
||||||
case actToggle:
|
case actToggle:
|
||||||
if t.multi && t.merger.Length() > 0 {
|
if t.multi > 0 && t.merger.Length() > 0 && toggle() {
|
||||||
toggle()
|
|
||||||
req(reqList)
|
req(reqList)
|
||||||
}
|
}
|
||||||
case actToggleAll:
|
case actToggleAll:
|
||||||
if t.multi {
|
if t.multi > 0 {
|
||||||
|
prevIndexes := make(map[int]struct{})
|
||||||
|
for i := 0; i < t.merger.Length() && len(t.selected) > 0; i++ {
|
||||||
|
item := t.merger.Get(i).item
|
||||||
|
if _, found := t.selected[item.Index()]; found {
|
||||||
|
prevIndexes[i] = struct{}{}
|
||||||
|
t.deselectItem(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for i := 0; i < t.merger.Length(); i++ {
|
for i := 0; i < t.merger.Length(); i++ {
|
||||||
t.toggleItem(t.merger.Get(i).item)
|
if _, found := prevIndexes[i]; !found {
|
||||||
|
item := t.merger.Get(i).item
|
||||||
|
if !t.selectItem(item) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
req(reqList, reqInfo)
|
req(reqList, reqInfo)
|
||||||
}
|
}
|
||||||
@ -1848,14 +1877,12 @@ func (t *Terminal) Loop() {
|
|||||||
}
|
}
|
||||||
return doAction(action{t: actToggleUp}, mapkey)
|
return doAction(action{t: actToggleUp}, mapkey)
|
||||||
case actToggleDown:
|
case actToggleDown:
|
||||||
if t.multi && t.merger.Length() > 0 {
|
if t.multi > 0 && t.merger.Length() > 0 && toggle() {
|
||||||
toggle()
|
|
||||||
t.vmove(-1, true)
|
t.vmove(-1, true)
|
||||||
req(reqList)
|
req(reqList)
|
||||||
}
|
}
|
||||||
case actToggleUp:
|
case actToggleUp:
|
||||||
if t.multi && t.merger.Length() > 0 {
|
if t.multi > 0 && t.merger.Length() > 0 && toggle() {
|
||||||
toggle()
|
|
||||||
t.vmove(1, true)
|
t.vmove(1, true)
|
||||||
req(reqList)
|
req(reqList)
|
||||||
}
|
}
|
||||||
@ -1959,7 +1986,7 @@ func (t *Terminal) Loop() {
|
|||||||
if me.S != 0 {
|
if me.S != 0 {
|
||||||
// Scroll
|
// Scroll
|
||||||
if t.window.Enclose(my, mx) && t.merger.Length() > 0 {
|
if t.window.Enclose(my, mx) && t.merger.Length() > 0 {
|
||||||
if t.multi && me.Mod {
|
if t.multi > 0 && me.Mod {
|
||||||
toggle()
|
toggle()
|
||||||
}
|
}
|
||||||
t.vmove(me.S, true)
|
t.vmove(me.S, true)
|
||||||
@ -1999,7 +2026,7 @@ func (t *Terminal) Loop() {
|
|||||||
t.cx = mx + t.xoffset
|
t.cx = mx + t.xoffset
|
||||||
} else if my >= min {
|
} else if my >= min {
|
||||||
// List
|
// List
|
||||||
if t.vset(t.offset+my-min) && t.multi && me.Mod {
|
if t.vset(t.offset+my-min) && t.multi > 0 && me.Mod {
|
||||||
toggle()
|
toggle()
|
||||||
}
|
}
|
||||||
req(reqList)
|
req(reqList)
|
||||||
|
@ -371,6 +371,65 @@ class TestGoFZF < TestBase
|
|||||||
assert_equal %w[3 2 5 6 8 7], readonce.split($INPUT_RECORD_SEPARATOR)
|
assert_equal %w[3 2 5 6 8 7], readonce.split($INPUT_RECORD_SEPARATOR)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_multi_max
|
||||||
|
tmux.send_keys "seq 1 10 | #{FZF} -m 3 --bind A:select-all,T:toggle-all --preview 'echo [{+}]/{}'", :Enter
|
||||||
|
|
||||||
|
tmux.until { |lines| lines.item_count == 10 }
|
||||||
|
|
||||||
|
tmux.send_keys '1'
|
||||||
|
tmux.until do |lines|
|
||||||
|
lines[1].include?('[1]/1') && lines[-2].include?('2/10')
|
||||||
|
end
|
||||||
|
|
||||||
|
tmux.send_keys 'A'
|
||||||
|
tmux.until do |lines|
|
||||||
|
lines[1].include?('[1 10]/1') && lines[-2].include?('2/10 (2/3)')
|
||||||
|
end
|
||||||
|
|
||||||
|
tmux.send_keys :BSpace
|
||||||
|
tmux.until { |lines| lines[-2].include?('10/10 (2/3)') }
|
||||||
|
|
||||||
|
tmux.send_keys 'T'
|
||||||
|
tmux.until do |lines|
|
||||||
|
lines[1].include?('[2 3 4]/1') && lines[-2].include?('10/10 (3/3)')
|
||||||
|
end
|
||||||
|
|
||||||
|
%w[T A].each do |key|
|
||||||
|
tmux.send_keys key
|
||||||
|
tmux.until do |lines|
|
||||||
|
lines[1].include?('[1 5 6]/1') && lines[-2].include?('10/10 (3/3)')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
tmux.send_keys :BTab
|
||||||
|
tmux.until do |lines|
|
||||||
|
lines[1].include?('[5 6]/2') && lines[-2].include?('10/10 (2/3)')
|
||||||
|
end
|
||||||
|
|
||||||
|
[:BTab, :BTab, 'A'].each do |key|
|
||||||
|
tmux.send_keys key
|
||||||
|
tmux.until do |lines|
|
||||||
|
lines[1].include?('[5 6 2]/3') && lines[-2].include?('10/10 (3/3)')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
tmux.send_keys '2'
|
||||||
|
tmux.until { |lines| lines[-2].include?('1/10 (3/3)') }
|
||||||
|
|
||||||
|
tmux.send_keys 'T'
|
||||||
|
tmux.until do |lines|
|
||||||
|
lines[1].include?('[5 6]/2') && lines[-2].include?('1/10 (2/3)')
|
||||||
|
end
|
||||||
|
|
||||||
|
tmux.send_keys :BSpace
|
||||||
|
tmux.until { |lines| lines[-2].include?('10/10 (2/3)') }
|
||||||
|
|
||||||
|
tmux.send_keys 'A'
|
||||||
|
tmux.until do |lines|
|
||||||
|
lines[1].include?('[5 6 1]/1') && lines[-2].include?('10/10 (3/3)')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def test_with_nth
|
def test_with_nth
|
||||||
[true, false].each do |multi|
|
[true, false].each do |multi|
|
||||||
tmux.send_keys "(echo ' 1st 2nd 3rd/';
|
tmux.send_keys "(echo ' 1st 2nd 3rd/';
|
||||||
|
Loading…
x
Reference in New Issue
Block a user