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'
This commit is contained in:
Junegunn Choi 2021-03-12 19:51:28 +09:00
parent 7310370a31
commit 4c4c6e626e
No known key found for this signature in database
GPG Key ID: 254BC280FEF9C627
5 changed files with 87 additions and 29 deletions

View File

@ -1,10 +1,17 @@
CHANGELOG 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 - Added `select` and `deselect` action for unconditionally selecting or
deselecting a single item in `--multi` mode. Complements `toggle` action. 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 - Built with Go 1.16
0.25.1 0.25.1

View File

@ -442,7 +442,7 @@ e.g.
done'\fR done'\fR
.RE .RE
.TP .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 .RS
.B POSITION: (default: right) .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 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-/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. * \fBdefault\fR resets all options previously set to the default.
.RS .RS
@ -506,6 +509,8 @@ e.g.
--preview 'bat --style=numbers --color=always --highlight-line {2} {1}' \\ --preview 'bat --style=numbers --color=always --highlight-line {2} {1}' \\
--preview-window +{2}-/2\fR --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 .RE
.SS Scripting .SS Scripting

View File

@ -84,7 +84,7 @@ const usage = `usage: fzf [options]
[up|down|left|right][:SIZE[%]] [up|down|left|right][:SIZE[%]]
[:[no]wrap][:[no]cycle][:[no]follow][:[no]hidden] [:[no]wrap][:[no]cycle][:[no]follow][:[no]hidden]
[:rounded|sharp|noborder] [:rounded|sharp|noborder]
[:+SCROLL[-OFFSET]] [:+SCROLL[-OFFSET]][:~HEADER_LINES]
[:default] [:default]
Scripting Scripting
@ -161,15 +161,16 @@ const (
) )
type previewOpts struct { type previewOpts struct {
command string command string
position windowPosition position windowPosition
size sizeSpec size sizeSpec
scroll string scroll string
hidden bool hidden bool
wrap bool wrap bool
cycle bool cycle bool
follow bool follow bool
border tui.BorderShape border tui.BorderShape
headerLines int
} }
// Options stores the values of command-line options // Options stores the values of command-line options
@ -231,7 +232,7 @@ type Options struct {
} }
func defaultPreviewOpts(command string) previewOpts { 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 { func defaultOptions() *Options {
@ -1078,6 +1079,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]+|-/[1-9][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 { for _, token := range tokens {
switch token { switch token {
case "": case "":
@ -1114,7 +1116,9 @@ func parsePreviewWindow(opts *previewOpts, input string) {
case "nofollow": case "nofollow":
opts.follow = false opts.follow = false
default: 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") opts.size = parseSize(token, 99, "window size")
} else if offsetRegex.MatchString(token) { } else if offsetRegex.MatchString(token) {
opts.scroll = token[1:] opts.scroll = token[1:]
@ -1364,7 +1368,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[%]][: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": 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

@ -1295,18 +1295,37 @@ func (t *Terminal) renderPreviewSpinner() {
} }
} }
func (t *Terminal) renderPreviewText(unchanged bool) { func (t *Terminal) renderPreviewArea(unchanged bool) {
maxWidth := t.pwindow.Width()
lineNo := -t.previewer.offset
height := t.pwindow.Height()
if unchanged { if unchanged {
t.pwindow.MoveAndClear(0, 0) t.pwindow.MoveAndClear(0, 0) // Clear scroll offset display
} else { } else {
t.previewed.filled = false t.previewed.filled = false
t.pwindow.Erase() 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 var ansi *ansiState
for _, line := range t.previewer.lines { for _, line := range lines {
var lbg tui.Color = -1 var lbg tui.Color = -1
if ansi != nil { if ansi != nil {
ansi.lbg = -1 ansi.lbg = -1
@ -1354,9 +1373,6 @@ func (t *Terminal) renderPreviewText(unchanged bool) {
} }
lineNo++ lineNo++
} }
if !unchanged {
t.pwindow.FinishFill()
}
} }
func (t *Terminal) printPreview() { func (t *Terminal) printPreview() {
@ -1369,7 +1385,7 @@ func (t *Terminal) printPreview() {
t.previewer.version == t.previewed.version && t.previewer.version == t.previewed.version &&
t.previewer.offset == t.previewed.offset t.previewer.offset == t.previewed.offset
t.previewer.scrollable = t.previewer.offset > 0 || numLines > height t.previewer.scrollable = t.previewer.offset > 0 || numLines > height
t.renderPreviewText(unchanged) t.renderPreviewArea(unchanged)
t.renderPreviewSpinner() t.renderPreviewSpinner()
t.previewed.numLines = numLines t.previewed.numLines = numLines
t.previewed.version = t.previewer.version t.previewed.version = t.previewer.version
@ -1382,7 +1398,7 @@ func (t *Terminal) printPreviewDelayed() {
} }
t.previewer.scrollable = false t.previewer.scrollable = false
t.renderPreviewText(true) t.renderPreviewArea(true)
message := t.trimMessage("Loading ..", t.pwindow.Width()) message := t.trimMessage("Loading ..", t.pwindow.Width())
pos := t.pwindow.Width() - len(message) pos := t.pwindow.Width() - len(message)
@ -1929,7 +1945,7 @@ func (t *Terminal) Loop() {
cmd := util.ExecCommand(command, true) cmd := util.ExecCommand(command, true)
if pwindow != nil { if pwindow != nil {
height := pwindow.Height() 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() env := os.Environ()
lines := fmt.Sprintf("LINES=%d", height) lines := fmt.Sprintf("LINES=%d", height)
columns := fmt.Sprintf("COLUMNS=%d", pwindow.Width()) columns := fmt.Sprintf("COLUMNS=%d", pwindow.Width())
@ -2132,7 +2148,7 @@ func (t *Terminal) Loop() {
if t.previewer.following { if t.previewer.following {
t.previewer.offset = len(t.previewer.lines) - t.pwindow.Height() t.previewer.offset = len(t.previewer.lines) - t.pwindow.Height()
} else if result.offset >= 0 { } 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() t.printPreview()
case reqPreviewRefresh: case reqPreviewRefresh:
@ -2205,7 +2221,7 @@ func (t *Terminal) Loop() {
if t.previewOpts.cycle { if t.previewOpts.cycle {
newOffset = (newOffset + numLines) % numLines 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 { if t.previewer.offset != newOffset {
t.previewer.offset = newOffset t.previewer.offset = newOffset
req(reqPreviewRefresh) req(reqPreviewRefresh)

View File

@ -2016,6 +2016,32 @@ class TestGoFZF < TestBase
nil nil
end end
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 end
module TestShell module TestShell