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
=========
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

View File

@ -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

View File

@ -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":

View File

@ -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)

View File

@ -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