Add preview window option for setting the initial scroll offset

Close #1057
Close #2120

  # Initial scroll offset is set to the line number of each line of
  # git grep output *minus* 5 lines
  git grep --line-number '' |
    fzf --delimiter : --preview 'nl {1}' --preview-window +{2}-5
This commit is contained in:
Junegunn Choi 2020-07-27 00:15:25 +09:00
parent c0a83b27eb
commit 0f9cb5590e
No known key found for this signature in database
GPG Key ID: 254BC280FEF9C627
5 changed files with 97 additions and 17 deletions

View File

@ -26,6 +26,13 @@ CHANGELOG
# Preview window hidden by default, it appears when you first hit '?' # Preview window hidden by default, it appears when you first hit '?'
fzf --bind '?:preview:cat {}' --preview-window hidden fzf --bind '?:preview:cat {}' --preview-window hidden
``` ```
- Added preview window option for setting the initial scroll offset
```sh
# Initial scroll offset is set to the line number of each line of
# git grep output *minus* 5 lines
git grep --line-number '' |
fzf --delimiter : --preview 'nl {1}' --preview-window +{2}-5
```
- Added support for ANSI colors in `--prompt` string - Added support for ANSI colors in `--prompt` string
- Vim plugin - Vim plugin
- `tmux` layout option for using fzf-tmux - `tmux` layout option for using fzf-tmux

View File

@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf 1 "Jun 2020" "fzf 0.22.0" "fzf - a command-line fuzzy finder" .TH fzf 1 "Jul 2020" "fzf 0.22.0" "fzf - a command-line fuzzy finder"
.SH NAME .SH NAME
fzf - a command-line fuzzy finder fzf - a command-line fuzzy finder
@ -381,7 +381,7 @@ Preview window will be updated even when there is no match for the current
query if any of the placeholder expressions evaluates to a non-empty string. query if any of the placeholder expressions evaluates to a non-empty string.
.RE .RE
.TP .TP
.BI "--preview-window=" "[POSITION][:SIZE[%]][:noborder][:wrap][:hidden]" .BI "--preview-window=" "[POSITION][:SIZE[%]][:noborder][:wrap][:hidden][:+SCROLL[-OFFSET]]"
Determines the layout of the preview window. If the argument contains Determines the layout of the preview window. If the argument contains
\fB:hidden\fR, the preview window will be hidden by default until \fB:hidden\fR, the preview window will be hidden by default until
\fBtoggle-preview\fR action is triggered. Long lines are truncated by default. \fBtoggle-preview\fR action is triggered. Long lines are truncated by default.
@ -390,6 +390,12 @@ Line wrap can be enabled with \fB:wrap\fR flag.
If size is given as 0, preview window will not be visible, but fzf will still If size is given as 0, preview window will not be visible, but fzf will still
execute the command in the background. execute the command in the background.
\fB+SCROLL[-OFFSET]\fR determines the initial scroll offset of the preview
window. \fBSCROLL\fR can be either a numeric integer or a single-field index
expression that refers to a numeric integer. The optional \fB-OFFSET\fR part is
for adjusting the base offset so that you can see the text above it. It should
be given as a numeric integer.
.RS .RS
.B POSITION: (default: right) .B POSITION: (default: right)
\fBup \fBup
@ -400,8 +406,15 @@ execute the command in the background.
.RS .RS
e.g. e.g.
\fBfzf --preview="head {}" --preview-window=up:30% \fB# Non-default scroll window positions and sizes
fzf --preview="file {}" --preview-window=down:1\fR fzf --preview="head {}" --preview-window=up:30%
fzf --preview="file {}" --preview-window=down:1
# Initial scroll offset is set to the line number of each line of
# git grep output *minus* 5 lines
git grep --line-number '' |
fzf --delimiter : --preview 'nl {1}' --preview-window +{2}-5\fR
.RE .RE
.SS Scripting .SS Scripting
.TP .TP

View File

@ -80,7 +80,7 @@ const usage = `usage: fzf [options]
Preview Preview
--preview=COMMAND Command to preview highlighted line ({}) --preview=COMMAND Command to preview highlighted line ({})
--preview-window=OPT Preview window layout (default: right:50%) --preview-window=OPT Preview window layout (default: right:50%)
[up|down|left|right][:SIZE[%]][:wrap][:hidden] [up|down|left|right][:SIZE[%]][:wrap][:hidden][:+SCROLL[-OFFSET]]
Scripting Scripting
-q, --query=STR Start the finder with the given query -q, --query=STR Start the finder with the given query
@ -159,6 +159,7 @@ type previewOpts struct {
command string command string
position windowPosition position windowPosition
size sizeSpec size sizeSpec
scroll string
hidden bool hidden bool
wrap bool wrap bool
border bool border bool
@ -260,7 +261,7 @@ func defaultOptions() *Options {
ToggleSort: false, ToggleSort: false,
Expect: make(map[int]string), Expect: make(map[int]string),
Keymap: make(map[int][]action), Keymap: make(map[int][]action),
Preview: previewOpts{"", posRight, sizeSpec{50, true}, false, false, true}, Preview: previewOpts{"", posRight, sizeSpec{50, true}, "", false, false, true},
PrintQuery: false, PrintQuery: false,
ReadZero: false, ReadZero: false,
Printer: func(str string) { fmt.Println(str) }, Printer: func(str string) { fmt.Println(str) },
@ -994,6 +995,7 @@ func parsePreviewWindow(opts *previewOpts, input string) {
tokens := strings.Split(input, ":") tokens := strings.Split(input, ":")
sizeRegex := regexp.MustCompile("^[0-9]+%?$") sizeRegex := regexp.MustCompile("^[0-9]+%?$")
offsetRegex := regexp.MustCompile("^\\+([0-9]+|{[0-9]+})(-[0-9]+)?$")
for _, token := range tokens { for _, token := range tokens {
switch token { switch token {
case "": case "":
@ -1016,8 +1018,10 @@ func parsePreviewWindow(opts *previewOpts, input string) {
default: default:
if sizeRegex.MatchString(token) { if sizeRegex.MatchString(token) {
opts.size = parseSize(token, 99, "window size") opts.size = parseSize(token, 99, "window size")
} else if offsetRegex.MatchString(token) {
opts.scroll = token[1:]
} else { } else {
errorExit("invalid preview window layout: " + input) errorExit("invalid preview window option: " + token)
} }
} }
} }
@ -1270,7 +1274,7 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Preview.command = "" opts.Preview.command = ""
case "--preview-window": case "--preview-window":
parsePreviewWindow(&opts.Preview, parsePreviewWindow(&opts.Preview,
nextString(allArgs, &i, "preview window layout required: [up|down|left|right][:SIZE[%]][:noborder][:wrap][:hidden]")) nextString(allArgs, &i, "preview window layout required: [up|down|left|right][:SIZE[%]][:noborder][:wrap][:hidden][:+SCROLL[-OFFSET]]"))
case "--height": case "--height":
opts.Height = parseHeight(nextString(allArgs, &i, "height required: HEIGHT[%]")) opts.Height = parseHeight(nextString(allArgs, &i, "height required: HEIGHT[%]"))
case "--min-height": case "--min-height":

View File

@ -262,6 +262,11 @@ type previewRequest struct {
list []*Item list []*Item
} }
type previewResult struct {
content string
offset int
}
func toActions(types ...actionType) []action { func toActions(types ...actionType) []action {
actions := make([]action, len(types)) actions := make([]action, len(types))
for idx, t := range types { for idx, t := range types {
@ -1347,6 +1352,39 @@ func cleanTemporaryFiles() {
activeTempFiles = []string{} activeTempFiles = []string{}
} }
func (t *Terminal) replacePlaceholder(template string, forcePlus bool, input string, list []*Item) string {
return replacePlaceholder(
template, t.ansi, t.delimiter, t.printsep, forcePlus, input, list)
}
// Ascii to positive integer
func atopi(s string) int {
n, e := strconv.Atoi(strings.ReplaceAll(s, "'", ""))
if e != nil || n < 1 {
return 0
}
return n
}
func (t *Terminal) evaluateScrollOffset(list []*Item) int {
offsetExpr := t.replacePlaceholder(t.preview.scroll, false, "", list)
nums := strings.Split(offsetExpr, "-")
switch len(nums) {
case 0:
return 0
case 1, 2:
base := atopi(nums[0])
if base == 0 {
return 0
} else if len(nums) == 1 {
return base - 1
}
return base - atopi(nums[1]) - 1
default:
return 0
}
}
func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, printsep string, forcePlus bool, query string, allItems []*Item) string { func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, printsep string, forcePlus bool, query string, allItems []*Item) string {
current := allItems[:1] current := allItems[:1]
selected := allItems[1:] selected := allItems[1:]
@ -1445,7 +1483,7 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo
if !valid { if !valid {
return return
} }
command := replacePlaceholder(template, t.ansi, t.delimiter, t.printsep, forcePlus, string(t.input), list) command := t.replacePlaceholder(template, forcePlus, string(t.input), list)
cmd := util.ExecCommand(command, false) cmd := util.ExecCommand(command, false)
if !background { if !background {
cmd.Stdin = os.Stdin cmd.Stdin = os.Stdin
@ -1629,8 +1667,8 @@ func (t *Terminal) Loop() {
}) })
// We don't display preview window if no match // We don't display preview window if no match
if items[0] != nil { if items[0] != nil {
command := replacePlaceholder(commandTemplate, command := t.replacePlaceholder(commandTemplate, false, string(t.Input()), items)
t.ansi, t.delimiter, t.printsep, false, string(t.Input()), items) offset := t.evaluateScrollOffset(items)
cmd := util.ExecCommand(command, true) cmd := util.ExecCommand(command, true)
if t.pwindow != nil { if t.pwindow != nil {
env := os.Environ() env := os.Environ()
@ -1673,11 +1711,11 @@ func (t *Terminal) Loop() {
cmd.Wait() cmd.Wait()
finishChan <- true finishChan <- true
if out.Len() > 0 || !<-updateChan { if out.Len() > 0 || !<-updateChan {
t.reqBox.Set(reqPreviewDisplay, out.String()) t.reqBox.Set(reqPreviewDisplay, previewResult{out.String(), offset})
} }
cleanTemporaryFiles() cleanTemporaryFiles()
} else { } else {
t.reqBox.Set(reqPreviewDisplay, "") t.reqBox.Set(reqPreviewDisplay, previewResult{"", 0})
} }
} }
}() }()
@ -1751,9 +1789,10 @@ func (t *Terminal) Loop() {
return exitNoMatch return exitNoMatch
}) })
case reqPreviewDisplay: case reqPreviewDisplay:
t.previewer.text = value.(string) result := value.(previewResult)
t.previewer.text = result.content
t.previewer.lines = strings.Count(t.previewer.text, "\n") t.previewer.lines = strings.Count(t.previewer.text, "\n")
t.previewer.offset = 0 t.previewer.offset = util.Constrain(result.offset, 0, t.previewer.lines-1)
t.printPreview() t.printPreview()
case reqPreviewRefresh: case reqPreviewRefresh:
t.printPreview() t.printPreview()
@ -2172,8 +2211,7 @@ func (t *Terminal) Loop() {
valid = !slot || query valid = !slot || query
} }
if valid { if valid {
command := replacePlaceholder(a.a, command := t.replacePlaceholder(a.a, false, string(t.input), list)
t.ansi, t.delimiter, t.printsep, false, string(t.input), list)
newCommand = &command newCommand = &command
} }
} }

View File

@ -1787,6 +1787,24 @@ class TestGoFZF < TestBase
tmux.until { |lines| refute_includes lines[1], '2' } tmux.until { |lines| refute_includes lines[1], '2' }
tmux.until { |lines| assert_includes lines[1], '[111]' } tmux.until { |lines| assert_includes lines[1], '[111]' }
end end
def test_preview_scroll_begin_constant
tmux.send_keys "echo foo 123 321 | #{FZF} --preview 'seq 1000' --preview-window left:+123", :Enter
tmux.until { |lines| lines.item_count == 1 }
tmux.until { |lines| assert_match %r{123.*123/1000}, lines[1] }
end
def test_preview_scroll_begin_expr
tmux.send_keys "echo foo 123 321 | #{FZF} --preview 'seq 1000' --preview-window left:+{3}", :Enter
tmux.until { |lines| lines.item_count == 1 }
tmux.until { |lines| assert_match %r{321.*321/1000}, lines[1] }
end
def test_preview_scroll_begin_and_offset
tmux.send_keys "echo foo 123 321 | #{FZF} --preview 'seq 1000' --preview-window left:+{2}-2", :Enter
tmux.until { |lines| lines.item_count == 1 }
tmux.until { |lines| assert_match %r{121.*121/1000}, lines[1] }
end
end end
module TestShell module TestShell