Add preview action for --bind

Fix #2010
Fix #1638
This commit is contained in:
Junegunn Choi 2020-06-21 22:41:33 +09:00
parent c33258832e
commit 17dd833925
No known key found for this signature in database
GPG Key ID: 254BC280FEF9C627
4 changed files with 102 additions and 18 deletions

View File

@ -690,6 +690,7 @@ A key or an event can be bound to one or more of the following actions.
\fBpage-up\fR \fIpgup\fR
\fBhalf-page-down\fR
\fBhalf-page-up\fR
\fBpreview(...)\fR (see below for the details)
\fBpreview-down\fR \fIshift-down\fR
\fBpreview-up\fR \fIshift-up\fR
\fBpreview-page-down\fR
@ -783,6 +784,24 @@ e.g.
fzf --bind "change:reload:$RG_PREFIX {q} || true" \\
--ansi --phony --query "$INITIAL_QUERY"\fR
.SS PREVIEW BINDING
With \fBpreview(...)\fR action, you can specify multiple different preview
commands in addition to the default preview command given by \fB--preview\fR
option.
e.g.
# Default preview command with an extra preview binding
fzf --preview 'file {}' --bind '?:preview:cat {}'
# A preview binding with no default preview command
# (Preview window is initially empty)
fzf --bind '?:preview:cat {}'
# Preview window hidden by default, it appears when you first hit '?'
fzf --bind '?:preview:cat {}' --preview-window hidden
.SH AUTHOR
Junegunn Choi (\fIjunegunn.c@gmail.com\fR)

View File

@ -684,7 +684,7 @@ func init() {
// Backreferences are not supported.
// "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|')
executeRegexp = regexp.MustCompile(
`(?si)[:+](execute(?:-multi|-silent)?|reload):.+|[:+](execute(?:-multi|-silent)?|reload)(\([^)]*\)|\[[^\]]*\]|~[^~]*~|![^!]*!|@[^@]*@|\#[^\#]*\#|\$[^\$]*\$|%[^%]*%|\^[^\^]*\^|&[^&]*&|\*[^\*]*\*|;[^;]*;|/[^/]*/|\|[^\|]*\|)`)
`(?si)[:+](execute(?:-multi|-silent)?|reload|preview):.+|[:+](execute(?:-multi|-silent)?|reload|preview)(\([^)]*\)|\[[^\]]*\]|~[^~]*~|![^!]*!|@[^@]*@|\#[^\#]*\#|\$[^\$]*\$|%[^%]*%|\^[^\^]*\^|&[^&]*&|\*[^\*]*\*|;[^;]*;|/[^/]*/|\|[^\|]*\|)`)
}
func parseKeymap(keymap map[int][]action, str string) {
@ -696,6 +696,8 @@ func parseKeymap(keymap map[int][]action, str string) {
prefix := symbol + "execute"
if strings.HasPrefix(src[1:], "reload") {
prefix = symbol + "reload"
} else if strings.HasPrefix(src[1:], "preview") {
prefix = symbol + "preview"
} else if src[len(prefix)] == '-' {
c := src[len(prefix)+1]
if c == 's' || c == 'S' {
@ -863,6 +865,8 @@ func parseKeymap(keymap map[int][]action, str string) {
switch t {
case actReload:
offset = len("reload")
case actPreview:
offset = len("preview")
case actExecuteSilent:
offset = len("execute-silent")
case actExecuteMulti:
@ -900,6 +904,8 @@ func isExecuteAction(str string) actionType {
switch prefix {
case "reload":
return actReload
case "preview":
return actPreview
case "execute":
return actExecute
case "execute-silent":

View File

@ -229,6 +229,7 @@ const (
actToggleSort
actTogglePreview
actTogglePreviewWrap
actPreview
actPreviewUp
actPreviewDown
actPreviewPageUp
@ -256,6 +257,11 @@ type searchRequest struct {
command *string
}
type previewRequest struct {
template string
list []*Item
}
func toActions(types ...actionType) []action {
actions := make([]action, len(types))
for idx, t := range types {
@ -327,6 +333,17 @@ func trimQuery(query string) []rune {
return []rune(strings.Replace(query, "\t", " ", -1))
}
func hasPreviewAction(opts *Options) bool {
for _, actions := range opts.Keymap {
for _, action := range actions {
if action.t == actPreview {
return true
}
}
}
return false
}
// NewTerminal returns new Terminal object
func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
input := trimQuery(opts.Query)
@ -344,7 +361,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
delay = initialDelay
}
var previewBox *util.EventBox
if len(opts.Preview.command) > 0 {
if len(opts.Preview.command) > 0 || hasPreviewAction(opts) {
previewBox = util.NewEventBox()
}
strongAttr := tui.Bold
@ -1584,20 +1601,23 @@ func (t *Terminal) Loop() {
if t.hasPreviewer() {
go func() {
for {
var request []*Item
var items []*Item
var commandTemplate string
t.previewBox.Wait(func(events *util.Events) {
for req, value := range *events {
switch req {
case reqPreviewEnqueue:
request = value.([]*Item)
request := value.(previewRequest)
commandTemplate = request.template
items = request.list
}
}
events.Clear()
})
// We don't display preview window if no match
if request[0] != nil {
command := replacePlaceholder(t.preview.command,
t.ansi, t.delimiter, t.printsep, false, string(t.Input()), request)
if items[0] != nil {
command := replacePlaceholder(commandTemplate,
t.ansi, t.delimiter, t.printsep, false, string(t.Input()), items)
cmd := util.ExecCommand(command, true)
if t.pwindow != nil {
env := os.Environ()
@ -1660,11 +1680,11 @@ func (t *Terminal) Loop() {
t.killPreview(code)
}
refreshPreview := func() {
if t.isPreviewEnabled() {
_, list := t.buildPlusList(t.preview.command, false)
refreshPreview := func(command string) {
if len(command) > 0 && t.isPreviewEnabled() {
_, list := t.buildPlusList(command, false)
t.cancelPreview()
t.previewBox.Set(reqPreviewEnqueue, list)
t.previewBox.Set(reqPreviewEnqueue, previewRequest{command, list})
}
}
@ -1694,7 +1714,7 @@ func (t *Terminal) Loop() {
if focusedIndex != currentIndex || version != t.version {
version = t.version
focusedIndex = currentIndex
refreshPreview()
refreshPreview(t.preview.command)
}
case reqJump:
if t.merger.Length() == 0 {
@ -1760,6 +1780,14 @@ func (t *Terminal) Loop() {
}
}
}
togglePreview := func(enabled bool) {
if t.previewer.enabled != enabled {
t.previewer.enabled = enabled
t.tui.Clear()
t.resizeWindows()
req(reqPrompt, reqList, reqInfo, reqHeader)
}
}
toggle := func() bool {
if t.cy < t.merger.Length() && t.toggleItem(t.merger.Get(t.cy).item) {
req(reqInfo)
@ -1808,17 +1836,15 @@ func (t *Terminal) Loop() {
return false
case actTogglePreview:
if t.hasPreviewer() {
t.previewer.enabled = !t.previewer.enabled
t.tui.Clear()
t.resizeWindows()
togglePreview(!t.previewer.enabled)
if t.previewer.enabled {
valid, list := t.buildPlusList(t.preview.command, false)
if valid {
t.cancelPreview()
t.previewBox.Set(reqPreviewEnqueue, list)
t.previewBox.Set(reqPreviewEnqueue,
previewRequest{t.preview.command, list})
}
}
req(reqPrompt, reqList, reqInfo, reqHeader)
}
case actTogglePreviewWrap:
if t.hasPreviewWindow() {
@ -1852,8 +1878,11 @@ func (t *Terminal) Loop() {
}
case actPrintQuery:
req(reqPrintQuery)
case actPreview:
togglePreview(true)
refreshPreview(a.a)
case actRefreshPreview:
refreshPreview()
refreshPreview(t.preview.command)
case actReplaceQuery:
if t.cy >= 0 && t.cy < t.merger.Length() {
t.input = t.merger.Get(t.cy).item.text.ToRunes()

View File

@ -1757,6 +1757,36 @@ class TestGoFZF < TestBase
tmux.send_keys :BSpace
tmux.until { |lines| lines.item_count == 100 && lines.match_count == 100 }
end
def test_preview_bindings_with_default_preview
tmux.send_keys "seq 10 | #{FZF} --preview 'echo [{}]' --bind 'a:preview(echo [{}{}]),b:preview(echo [{}{}{}]),c:refresh-preview'", :Enter
tmux.until { |lines| lines.item_count == 10 }
tmux.until { |lines| assert_includes lines[1], '[1]' }
tmux.send_keys 'a'
tmux.until { |lines| assert_includes lines[1], '[11]' }
tmux.send_keys 'c'
tmux.until { |lines| assert_includes lines[1], '[1]' }
tmux.send_keys 'b'
tmux.until { |lines| assert_includes lines[1], '[111]' }
tmux.send_keys :Up
tmux.until { |lines| assert_includes lines[1], '[2]' }
end
def test_preview_bindings_without_default_preview
tmux.send_keys "seq 10 | #{FZF} --bind 'a:preview(echo [{}{}]),b:preview(echo [{}{}{}]),c:refresh-preview'", :Enter
tmux.until { |lines| lines.item_count == 10 }
tmux.until { |lines| refute_includes lines[1], '1' }
tmux.send_keys 'a'
tmux.until { |lines| assert_includes lines[1], '[11]' }
tmux.send_keys 'c' # does nothing
tmux.until { |lines| assert_includes lines[1], '[11]' }
tmux.send_keys 'b'
tmux.until { |lines| assert_includes lines[1], '[111]' }
tmux.send_keys 9
tmux.until { |lines| lines.match_count == 1 }
tmux.until { |lines| refute_includes lines[1], '2' }
tmux.until { |lines| assert_includes lines[1], '[111]' }
end
end
module TestShell