mirror of
https://github.com/junegunn/fzf.git
synced 2025-08-05 14:42:11 -07: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:
@@ -36,6 +36,9 @@ const (
|
|||||||
|
|
||||||
// History
|
// History
|
||||||
defaultHistoryMax int = 1000
|
defaultHistoryMax int = 1000
|
||||||
|
|
||||||
|
// Jump labels
|
||||||
|
defaultJumpLabels string = "qwertyuiopasdfghjklzxcvbnm1234567890QWERTYUIOPASDFGHJKLZXCVBNM"
|
||||||
)
|
)
|
||||||
|
|
||||||
// fzf events
|
// 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
|
--hscroll-off=COL Number of screen columns to keep to the right of the
|
||||||
highlighted substring (default: 10)
|
highlighted substring (default: 10)
|
||||||
--inline-info Display finder info inline with the query
|
--inline-info Display finder info inline with the query
|
||||||
|
--jump-labels=CHARS Label characters for jump and jump-accept
|
||||||
--prompt=STR Input prompt (default: '> ')
|
--prompt=STR Input prompt (default: '> ')
|
||||||
--bind=KEYBINDS Custom key bindings. Refer to the man page.
|
--bind=KEYBINDS Custom key bindings. Refer to the man page.
|
||||||
--history=FILE History file
|
--history=FILE History file
|
||||||
@@ -112,6 +113,7 @@ type Options struct {
|
|||||||
Hscroll bool
|
Hscroll bool
|
||||||
HscrollOff int
|
HscrollOff int
|
||||||
InlineInfo bool
|
InlineInfo bool
|
||||||
|
JumpLabels string
|
||||||
Prompt string
|
Prompt string
|
||||||
Query string
|
Query string
|
||||||
Select1 bool
|
Select1 bool
|
||||||
@@ -153,6 +155,7 @@ func defaultOptions() *Options {
|
|||||||
Hscroll: true,
|
Hscroll: true,
|
||||||
HscrollOff: 10,
|
HscrollOff: 10,
|
||||||
InlineInfo: false,
|
InlineInfo: false,
|
||||||
|
JumpLabels: defaultJumpLabels,
|
||||||
Prompt: "> ",
|
Prompt: "> ",
|
||||||
Query: "",
|
Query: "",
|
||||||
Select1: false,
|
Select1: false,
|
||||||
@@ -553,6 +556,10 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, str string)
|
|||||||
keymap[key] = actForwardChar
|
keymap[key] = actForwardChar
|
||||||
case "forward-word":
|
case "forward-word":
|
||||||
keymap[key] = actForwardWord
|
keymap[key] = actForwardWord
|
||||||
|
case "jump":
|
||||||
|
keymap[key] = actJump
|
||||||
|
case "jump-accept":
|
||||||
|
keymap[key] = actJumpAccept
|
||||||
case "kill-line":
|
case "kill-line":
|
||||||
keymap[key] = actKillLine
|
keymap[key] = actKillLine
|
||||||
case "kill-word":
|
case "kill-word":
|
||||||
@@ -804,6 +811,8 @@ func parseOptions(opts *Options, allArgs []string) {
|
|||||||
opts.InlineInfo = true
|
opts.InlineInfo = true
|
||||||
case "--no-inline-info":
|
case "--no-inline-info":
|
||||||
opts.InlineInfo = false
|
opts.InlineInfo = false
|
||||||
|
case "--jump-labels":
|
||||||
|
opts.JumpLabels = nextString(allArgs, &i, "label characters required")
|
||||||
case "-1", "--select-1":
|
case "-1", "--select-1":
|
||||||
opts.Select1 = true
|
opts.Select1 = true
|
||||||
case "+1", "--no-select-1":
|
case "+1", "--no-select-1":
|
||||||
@@ -891,6 +900,8 @@ func parseOptions(opts *Options, allArgs []string) {
|
|||||||
opts.Tabstop = atoi(value)
|
opts.Tabstop = atoi(value)
|
||||||
} else if match, value := optString(arg, "--hscroll-off="); match {
|
} else if match, value := optString(arg, "--hscroll-off="); match {
|
||||||
opts.HscrollOff = atoi(value)
|
opts.HscrollOff = atoi(value)
|
||||||
|
} else if match, value := optString(arg, "--jump-labels="); match {
|
||||||
|
opts.JumpLabels = value
|
||||||
} else {
|
} else {
|
||||||
errorExit("unknown option: " + arg)
|
errorExit("unknown option: " + arg)
|
||||||
}
|
}
|
||||||
@@ -908,6 +919,10 @@ func parseOptions(opts *Options, allArgs []string) {
|
|||||||
if opts.Tabstop < 1 {
|
if opts.Tabstop < 1 {
|
||||||
errorExit("tab stop must be a positive integer")
|
errorExit("tab stop must be a positive integer")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(opts.JumpLabels) == 0 {
|
||||||
|
errorExit("empty jump labels")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func postProcessOptions(opts *Options) {
|
func postProcessOptions(opts *Options) {
|
||||||
|
@@ -19,6 +19,14 @@ import (
|
|||||||
"github.com/junegunn/go-runewidth"
|
"github.com/junegunn/go-runewidth"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type jumpMode int
|
||||||
|
|
||||||
|
const (
|
||||||
|
jumpDisabled jumpMode = iota
|
||||||
|
jumpEnabled
|
||||||
|
jumpAcceptEnabled
|
||||||
|
)
|
||||||
|
|
||||||
// Terminal represents terminal input/output
|
// Terminal represents terminal input/output
|
||||||
type Terminal struct {
|
type Terminal struct {
|
||||||
initDelay time.Duration
|
initDelay time.Duration
|
||||||
@@ -50,6 +58,8 @@ type Terminal struct {
|
|||||||
count int
|
count int
|
||||||
progress int
|
progress int
|
||||||
reading bool
|
reading bool
|
||||||
|
jumping jumpMode
|
||||||
|
jumpLabels string
|
||||||
merger *Merger
|
merger *Merger
|
||||||
selected map[int32]selectedItem
|
selected map[int32]selectedItem
|
||||||
reqBox *util.EventBox
|
reqBox *util.EventBox
|
||||||
@@ -88,6 +98,7 @@ const (
|
|||||||
reqInfo
|
reqInfo
|
||||||
reqHeader
|
reqHeader
|
||||||
reqList
|
reqList
|
||||||
|
reqJump
|
||||||
reqRefresh
|
reqRefresh
|
||||||
reqRedraw
|
reqRedraw
|
||||||
reqClose
|
reqClose
|
||||||
@@ -133,6 +144,8 @@ const (
|
|||||||
actUp
|
actUp
|
||||||
actPageUp
|
actPageUp
|
||||||
actPageDown
|
actPageDown
|
||||||
|
actJump
|
||||||
|
actJumpAccept
|
||||||
actPrintQuery
|
actPrintQuery
|
||||||
actToggleSort
|
actToggleSort
|
||||||
actPreviousHistory
|
actPreviousHistory
|
||||||
@@ -235,6 +248,8 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
|||||||
header0: header,
|
header0: header,
|
||||||
ansi: opts.Ansi,
|
ansi: opts.Ansi,
|
||||||
reading: true,
|
reading: true,
|
||||||
|
jumping: jumpDisabled,
|
||||||
|
jumpLabels: opts.JumpLabels,
|
||||||
merger: EmptyMerger,
|
merger: EmptyMerger,
|
||||||
selected: make(map[int32]selectedItem),
|
selected: make(map[int32]selectedItem),
|
||||||
reqBox: util.NewEventBox(),
|
reqBox: util.NewEventBox(),
|
||||||
@@ -497,15 +512,25 @@ func (t *Terminal) printList() {
|
|||||||
}
|
}
|
||||||
t.move(line, 0, true)
|
t.move(line, 0, true)
|
||||||
if i < count {
|
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()]
|
_, 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 {
|
if current {
|
||||||
C.CPrint(C.ColCursor, true, ">")
|
|
||||||
if selected {
|
if selected {
|
||||||
C.CPrint(C.ColSelected, true, ">")
|
C.CPrint(C.ColSelected, true, ">")
|
||||||
} else {
|
} else {
|
||||||
@@ -513,7 +538,6 @@ func (t *Terminal) printItem(item *Item, current bool) {
|
|||||||
}
|
}
|
||||||
t.printHighlighted(item, true, C.ColCurrent, C.ColCurrentMatch, true)
|
t.printHighlighted(item, true, C.ColCurrent, C.ColCurrentMatch, true)
|
||||||
} else {
|
} else {
|
||||||
C.CPrint(C.ColCursor, true, " ")
|
|
||||||
if selected {
|
if selected {
|
||||||
C.CPrint(C.ColSelected, true, ">")
|
C.CPrint(C.ColSelected, true, ">")
|
||||||
} else {
|
} else {
|
||||||
@@ -806,6 +830,11 @@ func (t *Terminal) Loop() {
|
|||||||
t.printInfo()
|
t.printInfo()
|
||||||
case reqList:
|
case reqList:
|
||||||
t.printList()
|
t.printList()
|
||||||
|
case reqJump:
|
||||||
|
if t.merger.Length() == 0 {
|
||||||
|
t.jumping = jumpDisabled
|
||||||
|
}
|
||||||
|
t.printList()
|
||||||
case reqHeader:
|
case reqHeader:
|
||||||
t.printHeader()
|
t.printHeader()
|
||||||
case reqRefresh:
|
case reqRefresh:
|
||||||
@@ -1025,6 +1054,12 @@ func (t *Terminal) Loop() {
|
|||||||
case actPageDown:
|
case actPageDown:
|
||||||
t.vmove(-(t.maxItems() - 1))
|
t.vmove(-(t.maxItems() - 1))
|
||||||
req(reqList)
|
req(reqList)
|
||||||
|
case actJump:
|
||||||
|
t.jumping = jumpEnabled
|
||||||
|
req(reqJump)
|
||||||
|
case actJumpAccept:
|
||||||
|
t.jumping = jumpAcceptEnabled
|
||||||
|
req(reqJump)
|
||||||
case actBackwardWord:
|
case actBackwardWord:
|
||||||
t.cx = findLastMatch("[^[:alnum:]][[:alnum:]]", string(t.input[:t.cx])) + 1
|
t.cx = findLastMatch("[^[:alnum:]][[:alnum:]]", string(t.input[:t.cx])) + 1
|
||||||
case actForwardWord:
|
case actForwardWord:
|
||||||
@@ -1104,18 +1139,32 @@ func (t *Terminal) Loop() {
|
|||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
action := t.keymap[event.Type]
|
changed := false
|
||||||
mapkey := event.Type
|
mapkey := event.Type
|
||||||
if event.Type == C.Rune {
|
if t.jumping == jumpDisabled {
|
||||||
mapkey = int(event.Char) + int(C.AltZ)
|
action := t.keymap[mapkey]
|
||||||
if act, prs := t.keymap[mapkey]; prs {
|
if mapkey == C.Rune {
|
||||||
action = act
|
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
|
t.mutex.Unlock() // Must be unlocked before touching reqBox
|
||||||
|
|
||||||
if changed {
|
if changed {
|
||||||
|
@@ -1181,6 +1181,43 @@ class TestGoFZF < TestBase
|
|||||||
tmux.send_keys :Enter
|
tmux.send_keys :Enter
|
||||||
end
|
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
|
private
|
||||||
def writelines path, lines
|
def writelines path, lines
|
||||||
File.unlink path while File.exists? path
|
File.unlink path while File.exists? path
|
||||||
|
Reference in New Issue
Block a user