summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJunegunn Choi <junegunn.c@gmail.com>2021-03-12 19:51:28 +0900
committerJunegunn Choi <junegunn.c@gmail.com>2021-03-12 20:32:27 +0900
commit4c4c6e626e48575602d7ee86fb9a8ee0816a0a11 (patch)
tree9f8a890cfe426f51d8066201c5f5d05a7f4d3aa5
parent7310370a31fd7f4735caaebdc270578355119c3b (diff)
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'
-rw-r--r--CHANGELOG.md9
-rw-r--r--man/man1/fzf.17
-rw-r--r--src/options.go30
-rw-r--r--src/terminal.go44
-rwxr-xr-xtest/test_go.rb26
5 files changed, 87 insertions, 29 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 136d8354..82422907 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 486a0ad8..d4388940 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 0e612559..29c90373 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 3fab90fd..9fa11212 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 175a800f..7ec519b1 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