From 4c4c6e626e48575602d7ee86fb9a8ee0816a0a11 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 12 Mar 2021 19:51:28 +0900 Subject: [PATCH] Add support for preview window header Fix #2373 # Display top 3 lines as the fixed header fzf --preview 'bat --style=header,grid --color=always {}' --preview-window '~3' --- CHANGELOG.md | 9 ++++++++- man/man1/fzf.1 | 7 ++++++- src/options.go | 30 +++++++++++++++++------------- src/terminal.go | 44 ++++++++++++++++++++++++++++++-------------- test/test_go.rb | 26 ++++++++++++++++++++++++++ 5 files changed, 87 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 136d835..8242290 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,17 @@ CHANGELOG ========= -0.25.2 +0.26.0 ------ +- Added support for fixed header in preview window + ```sh + # Display top 3 lines as the fixed header + fzf --preview 'bat --style=header,grid --color=always {}' --preview-window '~3' + ``` - Added `select` and `deselect` action for unconditionally selecting or deselecting a single item in `--multi` mode. Complements `toggle` action. +- Sigificant performance improvement in ANSI code processing +- Bug fixes and improvements - Built with Go 1.16 0.25.1 diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 486a0ad..d438894 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -442,7 +442,7 @@ e.g. done'\fR .RE .TP -.BI "--preview-window=" "[POSITION][:SIZE[%]][:rounded|sharp|noborder][:[no]wrap][:[no]follow][:[no]cycle][:[no]hidden][:+SCROLL[-OFFSET]][:default]" +.BI "--preview-window=" "[POSITION][:SIZE[%]][:rounded|sharp|noborder][:[no]wrap][:[no]follow][:[no]cycle][:[no]hidden][:+SCROLL[-OFFSET]][:~HEADER_LINES][:default]" .RS .B POSITION: (default: right) @@ -487,6 +487,9 @@ for adjusting the base offset so that you can see the text above it. It should be given as a numeric integer (\fB-INTEGER\fR), or as a denominator form (\fB-/INTEGER\fR) for specifying a fraction of the preview window height. +* \fB~HEADER_LINES\fR keeps the top N lines as the fixed header so that they +are always visible. + * \fBdefault\fR resets all options previously set to the default. .RS @@ -506,6 +509,8 @@ e.g. --preview 'bat --style=numbers --color=always --highlight-line {2} {1}' \\ --preview-window +{2}-/2\fR + # Display top 3 lines as the fixed header + fzf --preview 'bat --style=header,grid --color=always {}' --preview-window '~3' .RE .SS Scripting diff --git a/src/options.go b/src/options.go index 0e61255..29c9037 100644 --- a/src/options.go +++ b/src/options.go @@ -84,7 +84,7 @@ const usage = `usage: fzf [options] [up|down|left|right][:SIZE[%]] [:[no]wrap][:[no]cycle][:[no]follow][:[no]hidden] [:rounded|sharp|noborder] - [:+SCROLL[-OFFSET]] + [:+SCROLL[-OFFSET]][:~HEADER_LINES] [:default] Scripting @@ -161,15 +161,16 @@ const ( ) type previewOpts struct { - command string - position windowPosition - size sizeSpec - scroll string - hidden bool - wrap bool - cycle bool - follow bool - border tui.BorderShape + command string + position windowPosition + size sizeSpec + scroll string + hidden bool + wrap bool + cycle bool + follow bool + border tui.BorderShape + headerLines int } // Options stores the values of command-line options @@ -231,7 +232,7 @@ type Options struct { } func defaultPreviewOpts(command string) previewOpts { - return previewOpts{command, posRight, sizeSpec{50, true}, "", false, false, false, false, tui.BorderRounded} + return previewOpts{command, posRight, sizeSpec{50, true}, "", false, false, false, false, tui.BorderRounded, 0} } func defaultOptions() *Options { @@ -1078,6 +1079,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]+|-/[1-9][0-9]*)?$") + headerRegex := regexp.MustCompile("^~(0|[1-9][0-9]*)$") for _, token := range tokens { switch token { case "": @@ -1114,7 +1116,9 @@ func parsePreviewWindow(opts *previewOpts, input string) { case "nofollow": opts.follow = false default: - if sizeRegex.MatchString(token) { + if headerRegex.MatchString(token) { + opts.headerLines = atoi(token[1:]) + } else if sizeRegex.MatchString(token) { opts.size = parseSize(token, 99, "window size") } else if offsetRegex.MatchString(token) { opts.scroll = token[1:] @@ -1364,7 +1368,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[%]][:rounded|sharp|noborder][:wrap][:cycle][:hidden][:+SCROLL[-OFFSET]][:default]")) + nextString(allArgs, &i, "preview window layout required: [up|down|left|right][:SIZE[%]][:rounded|sharp|noborder][:wrap][:cycle][:hidden][:+SCROLL[-OFFSET]][:~HEADER_LINES][:default]")) 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 3fab90f..9fa1121 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -1295,18 +1295,37 @@ func (t *Terminal) renderPreviewSpinner() { } } -func (t *Terminal) renderPreviewText(unchanged bool) { - maxWidth := t.pwindow.Width() - lineNo := -t.previewer.offset - height := t.pwindow.Height() +func (t *Terminal) renderPreviewArea(unchanged bool) { if unchanged { - t.pwindow.MoveAndClear(0, 0) + t.pwindow.MoveAndClear(0, 0) // Clear scroll offset display } else { t.previewed.filled = false t.pwindow.Erase() } + + height := t.pwindow.Height() + header := []string{} + body := t.previewer.lines + headerLines := t.previewOpts.headerLines + // Do not enable preview header lines if it's value is too large + if headerLines > 0 && headerLines < util.Min(len(body), height) { + header = t.previewer.lines[0:headerLines] + body = t.previewer.lines[headerLines:] + // Always redraw header + t.renderPreviewText(height, header, 0, false) + t.pwindow.MoveAndClear(t.pwindow.Y(), 0) + } + t.renderPreviewText(height, body, -t.previewer.offset+headerLines, unchanged) + + if !unchanged { + t.pwindow.FinishFill() + } +} + +func (t *Terminal) renderPreviewText(height int, lines []string, lineNo int, unchanged bool) { + maxWidth := t.pwindow.Width() var ansi *ansiState - for _, line := range t.previewer.lines { + for _, line := range lines { var lbg tui.Color = -1 if ansi != nil { ansi.lbg = -1 @@ -1354,9 +1373,6 @@ func (t *Terminal) renderPreviewText(unchanged bool) { } lineNo++ } - if !unchanged { - t.pwindow.FinishFill() - } } func (t *Terminal) printPreview() { @@ -1369,7 +1385,7 @@ func (t *Terminal) printPreview() { t.previewer.version == t.previewed.version && t.previewer.offset == t.previewed.offset t.previewer.scrollable = t.previewer.offset > 0 || numLines > height - t.renderPreviewText(unchanged) + t.renderPreviewArea(unchanged) t.renderPreviewSpinner() t.previewed.numLines = numLines t.previewed.version = t.previewer.version @@ -1382,7 +1398,7 @@ func (t *Terminal) printPreviewDelayed() { } t.previewer.scrollable = false - t.renderPreviewText(true) + t.renderPreviewArea(true) message := t.trimMessage("Loading ..", t.pwindow.Width()) pos := t.pwindow.Width() - len(message) @@ -1929,7 +1945,7 @@ func (t *Terminal) Loop() { cmd := util.ExecCommand(command, true) if pwindow != nil { height := pwindow.Height() - initialOffset = util.Max(0, t.evaluateScrollOffset(items, height)) + initialOffset = util.Max(0, t.evaluateScrollOffset(items, util.Max(0, height-t.previewOpts.headerLines))) env := os.Environ() lines := fmt.Sprintf("LINES=%d", height) columns := fmt.Sprintf("COLUMNS=%d", pwindow.Width()) @@ -2132,7 +2148,7 @@ func (t *Terminal) Loop() { if t.previewer.following { t.previewer.offset = len(t.previewer.lines) - t.pwindow.Height() } else if result.offset >= 0 { - t.previewer.offset = util.Constrain(result.offset, 0, len(t.previewer.lines)-1) + t.previewer.offset = util.Constrain(result.offset, t.previewOpts.headerLines, len(t.previewer.lines)-1) } t.printPreview() case reqPreviewRefresh: @@ -2205,7 +2221,7 @@ func (t *Terminal) Loop() { if t.previewOpts.cycle { newOffset = (newOffset + numLines) % numLines } - newOffset = util.Constrain(newOffset, 0, numLines-1) + newOffset = util.Constrain(newOffset, t.previewOpts.headerLines, numLines-1) if t.previewer.offset != newOffset { t.previewer.offset = newOffset req(reqPreviewRefresh) diff --git a/test/test_go.rb b/test/test_go.rb index 175a800..7ec519b 100755 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -2016,6 +2016,32 @@ class TestGoFZF < TestBase nil end end + + def test_preview_header + tmux.send_keys "seq 100 | #{FZF} --bind ctrl-k:preview-up+preview-up,ctrl-j:preview-down+preview-down+preview-down --preview 'seq 1000' --preview-window 'top:+{1}:~3'", :Enter + tmux.until { |lines| assert_equal 100, lines.item_count } + top5 = ->(lines) { lines.drop(1).take(5).map { |s| s[/[0-9]+/] } } + tmux.until do |lines| + assert_includes lines[1], '4/1000' + assert_equal(%w[1 2 3 4 5], top5[lines]) + end + tmux.send_keys '55' + tmux.until do |lines| + assert_equal 1, lines.match_count + assert_equal(%w[1 2 3 55 56], top5[lines]) + end + tmux.send_keys 'C-J' + tmux.until do |lines| + assert_equal(%w[1 2 3 58 59], top5[lines]) + end + tmux.send_keys :BSpace + tmux.until do |lines| + assert_equal 19, lines.match_count + assert_equal(%w[1 2 3 5 6], top5[lines]) + end + tmux.send_keys 'C-K' + tmux.until { |lines| assert_equal(%w[1 2 3 4 5], top5[lines]) } + end end module TestShell