summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJunegunn Choi <junegunn.c@gmail.com>2024-10-01 19:15:17 +0900
committerJunegunn Choi <junegunn.c@gmail.com>2024-10-01 19:15:17 +0900
commit1a32220ca94ae897cab408a9eeaed094a8a739f1 (patch)
treeeac828f9fbc416c7d19fb74bc65e8e45d01b4786
parent4161403a1d6286f6ba7898b1f22f30d01d85b8dc (diff)
downloadfzf-1a32220ca94ae897cab408a9eeaed094a8a739f1.tar.gz
Add --gap option to put empty lines between items
-rw-r--r--CHANGELOG.md15
-rw-r--r--man/man1/fzf.13
-rw-r--r--src/options.go13
-rw-r--r--src/terminal.go61
-rwxr-xr-xtest/test_go.rb34
5 files changed, 105 insertions, 21 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 80b6491a..3ea88846 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,21 @@
CHANGELOG
=========
+0.56.0
+------
+- Added `--gap[=N]` option to display empty lines between items.
+ - This can be useful to visually separate adjacent multi-line items.
+ ```sh
+ # All bash functions, highlighted
+ declare -f | perl -0777 -pe 's/^}\n/}\0/gm' |
+ bat --plain --language bash --color always |
+ fzf --read0 --ansi --reverse --multi --highlight-line --gap
+ ```
+ - Or just to make the list easier to read. For single-line items, you probably want to set `--color gutter:-1` as well to hide the gutter.
+ ```sh
+ fzf --gap --color gutter:-1
+ ```
+
0.55.0
------
_Release highlights: https://junegunn.github.io/fzf/releases/0.55.0/_
diff --git a/man/man1/fzf.1 b/man/man1/fzf.1
index 321327ab..59e21815 100644
--- a/man/man1/fzf.1
+++ b/man/man1/fzf.1
@@ -208,6 +208,9 @@ Indicator for wrapped lines. The default is '↳ ' or '> ' depending on
.B "\-\-no\-multi\-line"
Disable multi-line display of items when using \fB\-\-read0\fR
.TP
+.BI "\-\-gap" "[=N]"
+Render empty lines between each item
+.TP
.B "\-\-keep\-right"
Keep the right end of the line visible when it's too long. Effective only when
the query string is empty.
diff --git a/src/options.go b/src/options.go
index 62c8c0c3..b0ab6b1d 100644
--- a/src/options.go
+++ b/src/options.go
@@ -56,6 +56,7 @@ Usage: fzf [options]
--wrap Enable line wrap
--wrap-sign=STR Indicator for wrapped lines
--no-multi-line Disable multi-line display of items when using --read0
+ --gap[=N] Render empty lines between each item
--keep-right Keep the right end of the line visible on overflow
--scroll-off=LINES Number of screen lines to keep above or below when
scrolling to the top or to the bottom (default: 0)
@@ -473,6 +474,7 @@ type Options struct {
Header []string
HeaderLines int
HeaderFirst bool
+ Gap int
Ellipsis *string
Scrollbar *string
Margin [4]sizeSpec
@@ -579,6 +581,7 @@ func defaultOptions() *Options {
Header: make([]string, 0),
HeaderLines: 0,
HeaderFirst: false,
+ Gap: 0,
Ellipsis: nil,
Scrollbar: nil,
Margin: defaultMargin(),
@@ -2343,6 +2346,12 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
opts.HeaderFirst = true
case "--no-header-first":
opts.HeaderFirst = false
+ case "--gap":
+ if opts.Gap, err = optionalNumeric(allArgs, &i, 1); err != nil {
+ return err
+ }
+ case "--no-gap":
+ opts.Gap = 0
case "--ellipsis":
str, err := nextString(allArgs, &i, "ellipsis string required")
if err != nil {
@@ -2630,6 +2639,10 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
if opts.HeaderLines, err = atoi(value); err != nil {
return err
}
+ } else if match, value := optString(arg, "--gap="); match {
+ if opts.Gap, err = atoi(value); err != nil {
+ return err
+ }
} else if match, value := optString(arg, "--ellipsis="); match {
str := firstLine(value)
opts.Ellipsis = &str
diff --git a/src/terminal.go b/src/terminal.go
index 535f5e3a..ce299de4 100644
--- a/src/terminal.go
+++ b/src/terminal.go
@@ -245,6 +245,7 @@ type Terminal struct {
hscroll bool
hscrollOff int
scrollOff int
+ gap int
wordRubout string
wordNext string
cx int
@@ -825,6 +826,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
headerVisible: true,
headerFirst: opts.HeaderFirst,
headerLines: opts.HeaderLines,
+ gap: opts.Gap,
header: []string{},
header0: opts.Header,
ansi: opts.Ansi,
@@ -1136,15 +1138,23 @@ func (t *Terminal) wrapCols() int {
return util.Max(t.window.Width()-(t.pointerLen+t.markerLen+1), 1)
}
+// Number of lines the item takes including the gap
func (t *Terminal) numItemLines(item *Item, atMost int) (int, bool) {
+ var numLines int
if !t.wrap && !t.multiLine {
- return 1, false
+ numLines = 1 + t.gap
+ return numLines, numLines > atMost
}
+ var overflow bool
if !t.wrap && t.multiLine {
- return item.text.NumLines(atMost)
+ numLines, overflow = item.text.NumLines(atMost)
+ } else {
+ var lines [][]rune
+ lines, overflow = item.text.Lines(t.multiLine, atMost, t.wrapCols(), t.wrapSignWidth, t.tabstop)
+ numLines = len(lines)
}
- lines, overflow := item.text.Lines(t.multiLine, atMost, t.wrapCols(), t.wrapSignWidth, t.tabstop)
- return len(lines), overflow
+ numLines += t.gap
+ return numLines, overflow || numLines > atMost
}
func (t *Terminal) itemLines(item *Item, atMost int) ([][]rune, bool) {
@@ -2050,6 +2060,21 @@ func (t *Terminal) printHeader() {
t.wrap = wrap
}
+func (t *Terminal) canSpanMultiLines() bool {
+ return t.multiLine || t.wrap || t.gap > 0
+}
+
+func (t *Terminal) renderEmptyLine(line int, barRange [2]int) {
+ t.move(line, 0, true)
+ t.markEmptyLine(line)
+ // If the screen is not filled with the list in non-multi-line mode,
+ // scrollbar is not visible at all. But in multi-line mode, we may need
+ // to redraw the scrollbar character at the end.
+ if t.canSpanMultiLines() {
+ t.prevLines[line].hasBar = t.printBar(line, true, barRange)
+ }
+}
+
func (t *Terminal) printList() {
t.constrain()
barLength, barStart := t.getScrollbar()
@@ -2070,14 +2095,7 @@ func (t *Terminal) printList() {
item := t.merger.Get(itemCount + t.offset)
line = t.printItem(item, line, maxy, itemCount, itemCount == t.cy-t.offset, barRange)
} else if !t.prevLines[line].empty {
- t.move(line, 0, true)
- t.markEmptyLine(line)
- // If the screen is not filled with the list in non-multi-line mode,
- // scrollbar is not visible at all. But in multi-line mode, we may need
- // to redraw the scrollbar character at the end.
- if t.multiLine || t.wrap {
- t.prevLines[line].hasBar = t.printBar(line, true, barRange)
- }
+ t.renderEmptyLine(line, barRange)
}
}
}
@@ -2125,9 +2143,6 @@ func (t *Terminal) printItem(result Result, line int, maxLine int, index int, cu
prevLine.queryLen == newLine.queryLen &&
prevLine.result == newLine.result {
t.prevLines[line].hasBar = printBar(line, false)
- if !t.multiLine && !t.wrap {
- return line
- }
return line + numLines - 1
}
@@ -2214,6 +2229,10 @@ func (t *Terminal) printItem(result Result, line int, maxLine int, index int, cu
}
finalLineNum = t.printHighlighted(result, base, match, false, true, line, maxLine, forceRedraw, preTask, postTask)
}
+ for i := 0; i < t.gap && finalLineNum < maxLine; i++ {
+ finalLineNum++
+ t.renderEmptyLine(finalLineNum, barRange)
+ }
return finalLineNum
}
@@ -2275,7 +2294,7 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
allOffsets := result.colorOffsets(charOffsets, t.theme, colBase, colMatch, current)
maxLines := 1
- if t.multiLine || t.wrap {
+ if t.canSpanMultiLines() {
maxLines = maxLineNum - lineNum + 1
}
lines, overflow := t.itemLines(item, maxLines)
@@ -2285,7 +2304,7 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
topCutoff := false
skipLines := 0
wrapped := false
- if t.multiLine || t.wrap {
+ if t.canSpanMultiLines() {
// Cut off the upper lines in the 'default' layout
if t.layout == layoutDefault && !current && maxLines == numItemLines && overflow {
lines, _ = t.itemLines(item, math.MaxInt)
@@ -4875,7 +4894,7 @@ func (t *Terminal) constrain() {
for tries := 0; tries < maxLines; tries++ {
numItems := maxLines
// How many items can be fit on screen including the current item?
- if (t.multiLine || t.wrap) && t.merger.Length() > 0 {
+ if t.canSpanMultiLines() && t.merger.Length() > 0 {
numItemsFound := 0
linesSum := 0
@@ -4930,12 +4949,12 @@ func (t *Terminal) constrain() {
for {
prevOffset := newOffset
numItems := t.merger.Length()
- itemLines := 1
- if (t.multiLine || t.wrap) && t.cy < numItems {
+ itemLines := 1 + t.gap
+ if t.canSpanMultiLines() && t.cy < numItems {
itemLines, _ = t.numItemLines(t.merger.Get(t.cy).item, maxLines)
}
linesBefore := t.cy - newOffset
- if t.multiLine || t.wrap {
+ if t.canSpanMultiLines() {
linesBefore = 0
for i := newOffset; i < t.cy && i < numItems; i++ {
lines, _ := t.numItemLines(t.merger.Get(i).item, maxLines-linesBefore-itemLines)
diff --git a/test/test_go.rb b/test/test_go.rb
index 8f627baf..2a462157 100755
--- a/test/test_go.rb
+++ b/test/test_go.rb
@@ -3392,6 +3392,40 @@ class TestGoFZF < TestBase
assert lines[1]&.end_with?('1000││')
end
end
+
+ def test_gap
+ tmux.send_keys %[seq 100 | #{FZF} --gap --border --reverse], :Enter
+ block = <<~BLOCK
+ ╭─────────────────
+ │ >
+ │ 100/100 ──────
+ │ > 1
+ │
+ │ 2
+ │
+ │ 3
+ │
+ │ 4
+ BLOCK
+ tmux.until { assert_block(block, _1) }
+ end
+
+ def test_gap_2
+ tmux.send_keys %[seq 100 | #{FZF} --gap=2 --border --reverse], :Enter
+ block = <<~BLOCK
+ ╭─────────────────
+ │ >
+ │ 100/100 ──────
+ │ > 1
+ │
+ │
+ │ 2
+ │
+ │
+ │ 3
+ BLOCK
+ tmux.until { assert_block(block, _1) }
+ end
end
module TestShell