diff --git a/src/constants.go b/src/constants.go index a49e1fe1..48ba2b7d 100644 --- a/src/constants.go +++ b/src/constants.go @@ -1,6 +1,7 @@ package fzf import ( + "math" "os" "time" @@ -27,6 +28,7 @@ const ( spinnerDuration = 200 * time.Millisecond previewCancelWait = 500 * time.Millisecond maxPatternLength = 300 + maxMulti = math.MaxInt32 // Matcher numPartitionsMultiplier = 8 diff --git a/src/options.go b/src/options.go index bd2a038e..5f3ea498 100644 --- a/src/options.go +++ b/src/options.go @@ -38,7 +38,7 @@ const usage = `usage: fzf [options] (default: length) Interface - -m, --multi Enable multi-select with tab/shift-tab + -m, --multi[=MAX] Enable multi-select with tab/shift-tab --no-mouse Disable mouse --bind=KEYBINDS Custom key bindings. Refer to the man page. --cycle Enable cyclic scroll @@ -162,7 +162,7 @@ type Options struct { Sort int Tac bool Criteria []criterion - Multi bool + Multi int Ansi bool Mouse bool Theme *tui.ColorTheme @@ -215,7 +215,7 @@ func defaultOptions() *Options { Sort: 1000, Tac: false, Criteria: []criterion{byScore, byLength}, - Multi: false, + Multi: 0, Ansi: false, Mouse: true, Theme: tui.EmptyTheme(), @@ -314,13 +314,14 @@ func nextInt(args []string, i *int, message string) int { 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 strings.IndexAny(args[*i+1], "0123456789") == 0 { *i++ + return atoi(args[*i]) } } - return 1 // Don't care + return defaultValue } func splitNth(str string) []Range { @@ -1033,7 +1034,7 @@ func parseOptions(opts *Options, allArgs []string) { case "--with-nth": opts.WithNth = splitNth(nextString(allArgs, &i, "nth expression required")) case "-s", "--sort": - opts.Sort = optionalNumeric(allArgs, &i) + opts.Sort = optionalNumeric(allArgs, &i, 1) case "+s", "--no-sort": opts.Sort = 0 case "--tac": @@ -1045,9 +1046,9 @@ func parseOptions(opts *Options, allArgs []string) { case "+i": opts.Case = CaseRespect case "-m", "--multi": - opts.Multi = true + opts.Multi = optionalNumeric(allArgs, &i, maxMulti) case "+m", "--no-multi": - opts.Multi = false + opts.Multi = 0 case "--ansi": opts.Ansi = true case "--no-ansi": @@ -1190,6 +1191,8 @@ func parseOptions(opts *Options, allArgs []string) { opts.WithNth = splitNth(value) } else if match, _ := optString(arg, "-s", "--sort="); match { 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 { opts.Height = parseHeight(value) } else if match, value := optString(arg, "--min-height="); match { diff --git a/src/terminal.go b/src/terminal.go index bb29d1d5..34df2105 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -76,7 +76,7 @@ type Terminal struct { xoffset int yanked []rune input []rune - multi bool + multi int sort bool toggleSort bool delimiter Delimiter @@ -750,8 +750,12 @@ func (t *Terminal) printInfo() { output += " -S" } } - if t.multi && len(t.selected) > 0 { - output += fmt.Sprintf(" (%d)", len(t.selected)) + if len(t.selected) > 0 { + 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 { output += fmt.Sprintf(" (%d%%)", t.progress) @@ -1426,9 +1430,18 @@ func (t *Terminal) buildPlusList(template string, forcePlus bool) (bool, []*Item 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.version++ + + return true } func (t *Terminal) deselectItem(item *Item) { @@ -1436,12 +1449,12 @@ func (t *Terminal) deselectItem(item *Item) { t.version++ } -func (t *Terminal) toggleItem(item *Item) { +func (t *Terminal) toggleItem(item *Item) bool { if _, found := t.selected[item.Index()]; !found { - t.selectItem(item) - } else { - t.deselectItem(item) + return t.selectItem(item) } + t.deselectItem(item) + return true } func (t *Terminal) killPreview(code int) { @@ -1687,11 +1700,12 @@ func (t *Terminal) Loop() { } } } - toggle := func() { - if t.cy < t.merger.Length() { - t.toggleItem(t.merger.Get(t.cy).item) + toggle := func() bool { + if t.cy < t.merger.Length() && t.toggleItem(t.merger.Get(t.cy).item) { req(reqInfo) + return true } + return false } scrollPreview := func(amount int) { if !t.previewer.more { @@ -1813,27 +1827,42 @@ func (t *Terminal) Loop() { t.cx-- } case actSelectAll: - if t.multi { + if t.multi > 0 { 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) } case actDeselectAll: - if t.multi { + if t.multi > 0 { t.selected = make(map[int32]selectedItem) t.version++ req(reqList, reqInfo) } case actToggle: - if t.multi && t.merger.Length() > 0 { - toggle() + if t.multi > 0 && t.merger.Length() > 0 && toggle() { req(reqList) } 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++ { - 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) } @@ -1848,14 +1877,12 @@ func (t *Terminal) Loop() { } return doAction(action{t: actToggleUp}, mapkey) case actToggleDown: - if t.multi && t.merger.Length() > 0 { - toggle() + if t.multi > 0 && t.merger.Length() > 0 && toggle() { t.vmove(-1, true) req(reqList) } case actToggleUp: - if t.multi && t.merger.Length() > 0 { - toggle() + if t.multi > 0 && t.merger.Length() > 0 && toggle() { t.vmove(1, true) req(reqList) } @@ -1959,7 +1986,7 @@ func (t *Terminal) Loop() { if me.S != 0 { // Scroll if t.window.Enclose(my, mx) && t.merger.Length() > 0 { - if t.multi && me.Mod { + if t.multi > 0 && me.Mod { toggle() } t.vmove(me.S, true) @@ -1999,7 +2026,7 @@ func (t *Terminal) Loop() { t.cx = mx + t.xoffset } else if my >= min { // 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() } req(reqList) diff --git a/test/test_go.rb b/test/test_go.rb index 86a6bf45..ac0b1f8e 100755 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -371,6 +371,65 @@ class TestGoFZF < TestBase assert_equal %w[3 2 5 6 8 7], readonce.split($INPUT_RECORD_SEPARATOR) 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 [true, false].each do |multi| tmux.send_keys "(echo ' 1st 2nd 3rd/';