Add jump and jump-accept actions for --bind

jump and jump-accept implement EasyMotion-like movement in fzf.
Suggested by @mhrebenyuk. Close #569.
This commit is contained in:
Junegunn Choi 2016-05-18 02:06:52 +09:00
parent 4adebfc856
commit 6d235bceee
No known key found for this signature in database
GPG Key ID: 254BC280FEF9C627
4 changed files with 117 additions and 13 deletions

View File

@ -36,6 +36,9 @@ const (
// History
defaultHistoryMax int = 1000
// Jump labels
defaultJumpLabels string = "qwertyuiopasdfghjklzxcvbnm1234567890QWERTYUIOPASDFGHJKLZXCVBNM"
)
// fzf events

View File

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

View File

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

View File

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