--multi to take optional argument to limit the number of selection

Close #1718
Related #688
This commit is contained in:
Junegunn Choi 2019-11-02 12:55:26 +09:00
parent a2e9366c84
commit 072066c49c
No known key found for this signature in database
GPG Key ID: 254BC280FEF9C627
4 changed files with 123 additions and 32 deletions

View File

@ -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

View File

@ -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 {

View File

@ -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 {
output += fmt.Sprintf(" (%d)", len(t.selected)) if t.multi == maxMulti {
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)

View File

@ -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/';