diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index d1257c84..3e7b1ee7 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -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) diff --git a/src/options.go b/src/options.go index cb070ec2..e43f5e1b 100644 --- a/src/options.go +++ b/src/options.go @@ -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": diff --git a/src/terminal.go b/src/terminal.go index 56da73a9..a4bcb4bd 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -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() diff --git a/test/test_go.rb b/test/test_go.rb index 2b865ea9..e1e22575 100755 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -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