diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a76a48..b4d8153 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,13 @@ CHANGELOG # Preview window hidden by default, it appears when you first hit '?' 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 - Vim plugin - `tmux` layout option for using fzf-tmux diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 878b30b..ed8328c 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -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 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 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. .RE .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 \fB:hidden\fR, the preview window will be hidden by default until \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 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 .B POSITION: (default: right) \fBup @@ -400,8 +406,15 @@ execute the command in the background. .RS e.g. - \fBfzf --preview="head {}" --preview-window=up:30% - fzf --preview="file {}" --preview-window=down:1\fR + \fB# Non-default scroll window positions and sizes + 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 .SS Scripting .TP diff --git a/src/options.go b/src/options.go index e43f5e1..811615e 100644 --- a/src/options.go +++ b/src/options.go @@ -80,7 +80,7 @@ const usage = `usage: fzf [options] Preview --preview=COMMAND Command to preview highlighted line ({}) --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 -q, --query=STR Start the finder with the given query @@ -159,6 +159,7 @@ type previewOpts struct { command string position windowPosition size sizeSpec + scroll string hidden bool wrap bool border bool @@ -260,7 +261,7 @@ func defaultOptions() *Options { ToggleSort: false, Expect: make(map[int]string), 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, ReadZero: false, Printer: func(str string) { fmt.Println(str) }, @@ -994,6 +995,7 @@ func parsePreviewWindow(opts *previewOpts, input string) { tokens := strings.Split(input, ":") sizeRegex := regexp.MustCompile("^[0-9]+%?$") + offsetRegex := regexp.MustCompile("^\\+([0-9]+|{[0-9]+})(-[0-9]+)?$") for _, token := range tokens { switch token { case "": @@ -1016,8 +1018,10 @@ func parsePreviewWindow(opts *previewOpts, input string) { default: if sizeRegex.MatchString(token) { opts.size = parseSize(token, 99, "window size") + } else if offsetRegex.MatchString(token) { + opts.scroll = token[1:] } 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 = "" case "--preview-window": 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": opts.Height = parseHeight(nextString(allArgs, &i, "height required: HEIGHT[%]")) case "--min-height": diff --git a/src/terminal.go b/src/terminal.go index e6bda16..c144569 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -262,6 +262,11 @@ type previewRequest struct { list []*Item } +type previewResult struct { + content string + offset int +} + func toActions(types ...actionType) []action { actions := make([]action, len(types)) for idx, t := range types { @@ -1347,6 +1352,39 @@ func cleanTemporaryFiles() { 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 { current := allItems[:1] selected := allItems[1:] @@ -1445,7 +1483,7 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo if !valid { 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) if !background { cmd.Stdin = os.Stdin @@ -1629,8 +1667,8 @@ func (t *Terminal) Loop() { }) // We don't display preview window if no match if items[0] != nil { - command := replacePlaceholder(commandTemplate, - t.ansi, t.delimiter, t.printsep, false, string(t.Input()), items) + command := t.replacePlaceholder(commandTemplate, false, string(t.Input()), items) + offset := t.evaluateScrollOffset(items) cmd := util.ExecCommand(command, true) if t.pwindow != nil { env := os.Environ() @@ -1673,11 +1711,11 @@ func (t *Terminal) Loop() { cmd.Wait() finishChan <- true if out.Len() > 0 || !<-updateChan { - t.reqBox.Set(reqPreviewDisplay, out.String()) + t.reqBox.Set(reqPreviewDisplay, previewResult{out.String(), offset}) } cleanTemporaryFiles() } else { - t.reqBox.Set(reqPreviewDisplay, "") + t.reqBox.Set(reqPreviewDisplay, previewResult{"", 0}) } } }() @@ -1751,9 +1789,10 @@ func (t *Terminal) Loop() { return exitNoMatch }) 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.offset = 0 + t.previewer.offset = util.Constrain(result.offset, 0, t.previewer.lines-1) t.printPreview() case reqPreviewRefresh: t.printPreview() @@ -2172,8 +2211,7 @@ func (t *Terminal) Loop() { valid = !slot || query } if valid { - command := replacePlaceholder(a.a, - t.ansi, t.delimiter, t.printsep, false, string(t.input), list) + command := t.replacePlaceholder(a.a, false, string(t.input), list) newCommand = &command } } diff --git a/test/test_go.rb b/test/test_go.rb index e1e2257..75e84ec 100755 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -1787,6 +1787,24 @@ class TestGoFZF < TestBase tmux.until { |lines| refute_includes lines[1], '2' } tmux.until { |lines| assert_includes lines[1], '[111]' } 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 module TestShell