mirror of
https://github.com/Llewellynvdm/fzf.git
synced 2024-12-23 03:19:01 +00:00
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:
parent
4adebfc856
commit
6d235bceee
@ -36,6 +36,9 @@ const (
|
||||
|
||||
// History
|
||||
defaultHistoryMax int = 1000
|
||||
|
||||
// Jump labels
|
||||
defaultJumpLabels string = "qwertyuiopasdfghjklzxcvbnm1234567890QWERTYUIOPASDFGHJKLZXCVBNM"
|
||||
)
|
||||
|
||||
// fzf events
|
||||
|
@ -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) {
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user