diff --git a/src/constants.go b/src/constants.go index 2e41e8be..eb9262df 100644 --- a/src/constants.go +++ b/src/constants.go @@ -36,6 +36,9 @@ const ( // History defaultHistoryMax int = 1000 + + // Jump labels + defaultJumpLabels string = "qwertyuiopasdfghjklzxcvbnm1234567890QWERTYUIOPASDFGHJKLZXCVBNM" ) // fzf events diff --git a/src/options.go b/src/options.go index ea7d717c..089b4c2f 100644 --- a/src/options.go +++ b/src/options.go @@ -45,6 +45,7 @@ const usage = `usage: fzf [options] --hscroll-off=COL Number of screen columns to keep to the right of the highlighted substring (default: 10) --inline-info Display finder info inline with the query + --jump-labels=CHARS Label characters for jump and jump-accept --prompt=STR Input prompt (default: '> ') --bind=KEYBINDS Custom key bindings. Refer to the man page. --history=FILE History file @@ -112,6 +113,7 @@ type Options struct { Hscroll bool HscrollOff int InlineInfo bool + JumpLabels string Prompt string Query string Select1 bool @@ -153,6 +155,7 @@ func defaultOptions() *Options { Hscroll: true, HscrollOff: 10, InlineInfo: false, + JumpLabels: defaultJumpLabels, Prompt: "> ", Query: "", Select1: false, @@ -553,6 +556,10 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, str string) keymap[key] = actForwardChar case "forward-word": keymap[key] = actForwardWord + case "jump": + keymap[key] = actJump + case "jump-accept": + keymap[key] = actJumpAccept case "kill-line": keymap[key] = actKillLine case "kill-word": @@ -804,6 +811,8 @@ func parseOptions(opts *Options, allArgs []string) { opts.InlineInfo = true case "--no-inline-info": opts.InlineInfo = false + case "--jump-labels": + opts.JumpLabels = nextString(allArgs, &i, "label characters required") case "-1", "--select-1": opts.Select1 = true case "+1", "--no-select-1": @@ -891,6 +900,8 @@ func parseOptions(opts *Options, allArgs []string) { opts.Tabstop = atoi(value) } else if match, value := optString(arg, "--hscroll-off="); match { opts.HscrollOff = atoi(value) + } else if match, value := optString(arg, "--jump-labels="); match { + opts.JumpLabels = value } else { errorExit("unknown option: " + arg) } @@ -908,6 +919,10 @@ func parseOptions(opts *Options, allArgs []string) { if opts.Tabstop < 1 { errorExit("tab stop must be a positive integer") } + + if len(opts.JumpLabels) == 0 { + errorExit("empty jump labels") + } } func postProcessOptions(opts *Options) { diff --git a/src/terminal.go b/src/terminal.go index 771cad78..4f611ebe 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -19,6 +19,14 @@ import ( "github.com/junegunn/go-runewidth" ) +type jumpMode int + +const ( + jumpDisabled jumpMode = iota + jumpEnabled + jumpAcceptEnabled +) + // Terminal represents terminal input/output type Terminal struct { initDelay time.Duration @@ -50,6 +58,8 @@ type Terminal struct { count int progress int reading bool + jumping jumpMode + jumpLabels string merger *Merger selected map[int32]selectedItem reqBox *util.EventBox @@ -88,6 +98,7 @@ const ( reqInfo reqHeader reqList + reqJump reqRefresh reqRedraw reqClose @@ -133,6 +144,8 @@ const ( actUp actPageUp actPageDown + actJump + actJumpAccept actPrintQuery actToggleSort actPreviousHistory @@ -235,6 +248,8 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { header0: header, ansi: opts.Ansi, reading: true, + jumping: jumpDisabled, + jumpLabels: opts.JumpLabels, merger: EmptyMerger, selected: make(map[int32]selectedItem), reqBox: util.NewEventBox(), @@ -497,15 +512,25 @@ func (t *Terminal) printList() { } t.move(line, 0, true) if i < count { - t.printItem(t.merger.Get(i+t.offset), i == t.cy-t.offset) + t.printItem(t.merger.Get(i+t.offset), i, i == t.cy-t.offset) } } } -func (t *Terminal) printItem(item *Item, current bool) { +func (t *Terminal) printItem(item *Item, i int, current bool) { _, selected := t.selected[item.Index()] + label := " " + if t.jumping != jumpDisabled { + if i < len(t.jumpLabels) { + // Striped + current = i%2 == 0 + label = t.jumpLabels[i : i+1] + } + } else if current { + label = ">" + } + C.CPrint(C.ColCursor, true, label) if current { - C.CPrint(C.ColCursor, true, ">") if selected { C.CPrint(C.ColSelected, true, ">") } else { @@ -513,7 +538,6 @@ func (t *Terminal) printItem(item *Item, current bool) { } t.printHighlighted(item, true, C.ColCurrent, C.ColCurrentMatch, true) } else { - C.CPrint(C.ColCursor, true, " ") if selected { C.CPrint(C.ColSelected, true, ">") } else { @@ -806,6 +830,11 @@ func (t *Terminal) Loop() { t.printInfo() case reqList: t.printList() + case reqJump: + if t.merger.Length() == 0 { + t.jumping = jumpDisabled + } + t.printList() case reqHeader: t.printHeader() case reqRefresh: @@ -1025,6 +1054,12 @@ func (t *Terminal) Loop() { case actPageDown: t.vmove(-(t.maxItems() - 1)) req(reqList) + case actJump: + t.jumping = jumpEnabled + req(reqJump) + case actJumpAccept: + t.jumping = jumpAcceptEnabled + req(reqJump) case actBackwardWord: t.cx = findLastMatch("[^[:alnum:]][[:alnum:]]", string(t.input[:t.cx])) + 1 case actForwardWord: @@ -1104,18 +1139,32 @@ func (t *Terminal) Loop() { } return true } - action := t.keymap[event.Type] + changed := false mapkey := event.Type - if event.Type == C.Rune { - mapkey = int(event.Char) + int(C.AltZ) - if act, prs := t.keymap[mapkey]; prs { - action = act + if t.jumping == jumpDisabled { + action := t.keymap[mapkey] + if mapkey == C.Rune { + mapkey = int(event.Char) + int(C.AltZ) + if act, prs := t.keymap[mapkey]; prs { + action = act + } } + if !doAction(action, mapkey) { + continue + } + changed = string(previousInput) != string(t.input) + } else { + if mapkey == C.Rune { + if idx := strings.IndexRune(t.jumpLabels, event.Char); idx >= 0 { + t.cy = idx + t.offset + if t.jumping == jumpAcceptEnabled { + req(reqClose) + } + } + } + t.jumping = jumpDisabled + req(reqList) } - if !doAction(action, mapkey) { - continue - } - changed := string(previousInput) != string(t.input) t.mutex.Unlock() // Must be unlocked before touching reqBox if changed { diff --git a/test/test_go.rb b/test/test_go.rb index 02bf08d2..8f336932 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -1181,6 +1181,43 @@ class TestGoFZF < TestBase tmux.send_keys :Enter end + def test_jump + tmux.send_keys "seq 1000 | #{fzf "--multi --jump-labels 12345 --bind 'ctrl-j:jump'"}", :Enter + tmux.until { |lines| lines[-2] == ' 1000/1000' } + tmux.send_keys 'C-j' + tmux.until { |lines| lines[-7] == '5 5' } + tmux.until { |lines| lines[-8] == ' 6' } + tmux.send_keys '5' + tmux.until { |lines| lines[-7] == '> 5' } + tmux.send_keys :Tab + tmux.until { |lines| lines[-7] == ' >5' } + tmux.send_keys 'C-j' + tmux.until { |lines| lines[-7] == '5>5' } + tmux.send_keys '2' + tmux.until { |lines| lines[-4] == '> 2' } + tmux.send_keys :Tab + tmux.until { |lines| lines[-4] == ' >2' } + tmux.send_keys 'C-j' + tmux.until { |lines| lines[-7] == '5>5' } + + # Press any key other than jump labels to cancel jump + tmux.send_keys '6' + tmux.until { |lines| lines[-3] == '> 1' } + tmux.send_keys :Tab + tmux.until { |lines| lines[-3] == '>>1' } + tmux.send_keys :Enter + assert_equal %w[5 2 1], readonce.split($/) + end + + def test_jump_accept + tmux.send_keys "seq 1000 | #{fzf "--multi --jump-labels 12345 --bind 'ctrl-j:jump-accept'"}", :Enter + tmux.until { |lines| lines[-2] == ' 1000/1000' } + tmux.send_keys 'C-j' + tmux.until { |lines| lines[-7] == '5 5' } + tmux.send_keys '3' + assert_equal '3', readonce.chomp + end + private def writelines path, lines File.unlink path while File.exists? path