Add --gap option to put empty lines between items

This commit is contained in:
Junegunn Choi 2024-10-01 19:15:17 +09:00
parent 4161403a1d
commit 1a32220ca9
No known key found for this signature in database
GPG Key ID: 254BC280FEF9C627
5 changed files with 105 additions and 21 deletions

View File

@ -1,6 +1,21 @@
CHANGELOG 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 0.55.0
------ ------
_Release highlights: https://junegunn.github.io/fzf/releases/0.55.0/_ _Release highlights: https://junegunn.github.io/fzf/releases/0.55.0/_

View File

@ -208,6 +208,9 @@ Indicator for wrapped lines. The default is '↳ ' or '> ' depending on
.B "\-\-no\-multi\-line" .B "\-\-no\-multi\-line"
Disable multi-line display of items when using \fB\-\-read0\fR Disable multi-line display of items when using \fB\-\-read0\fR
.TP .TP
.BI "\-\-gap" "[=N]"
Render empty lines between each item
.TP
.B "\-\-keep\-right" .B "\-\-keep\-right"
Keep the right end of the line visible when it's too long. Effective only when Keep the right end of the line visible when it's too long. Effective only when
the query string is empty. the query string is empty.

View File

@ -56,6 +56,7 @@ Usage: fzf [options]
--wrap Enable line wrap --wrap Enable line wrap
--wrap-sign=STR Indicator for wrapped lines --wrap-sign=STR Indicator for wrapped lines
--no-multi-line Disable multi-line display of items when using --read0 --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 --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 --scroll-off=LINES Number of screen lines to keep above or below when
scrolling to the top or to the bottom (default: 0) scrolling to the top or to the bottom (default: 0)
@ -473,6 +474,7 @@ type Options struct {
Header []string Header []string
HeaderLines int HeaderLines int
HeaderFirst bool HeaderFirst bool
Gap int
Ellipsis *string Ellipsis *string
Scrollbar *string Scrollbar *string
Margin [4]sizeSpec Margin [4]sizeSpec
@ -579,6 +581,7 @@ func defaultOptions() *Options {
Header: make([]string, 0), Header: make([]string, 0),
HeaderLines: 0, HeaderLines: 0,
HeaderFirst: false, HeaderFirst: false,
Gap: 0,
Ellipsis: nil, Ellipsis: nil,
Scrollbar: nil, Scrollbar: nil,
Margin: defaultMargin(), Margin: defaultMargin(),
@ -2343,6 +2346,12 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
opts.HeaderFirst = true opts.HeaderFirst = true
case "--no-header-first": case "--no-header-first":
opts.HeaderFirst = false 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": case "--ellipsis":
str, err := nextString(allArgs, &i, "ellipsis string required") str, err := nextString(allArgs, &i, "ellipsis string required")
if err != nil { if err != nil {
@ -2630,6 +2639,10 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
if opts.HeaderLines, err = atoi(value); err != nil { if opts.HeaderLines, err = atoi(value); err != nil {
return err 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 { } else if match, value := optString(arg, "--ellipsis="); match {
str := firstLine(value) str := firstLine(value)
opts.Ellipsis = &str opts.Ellipsis = &str

View File

@ -245,6 +245,7 @@ type Terminal struct {
hscroll bool hscroll bool
hscrollOff int hscrollOff int
scrollOff int scrollOff int
gap int
wordRubout string wordRubout string
wordNext string wordNext string
cx int cx int
@ -825,6 +826,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
headerVisible: true, headerVisible: true,
headerFirst: opts.HeaderFirst, headerFirst: opts.HeaderFirst,
headerLines: opts.HeaderLines, headerLines: opts.HeaderLines,
gap: opts.Gap,
header: []string{}, header: []string{},
header0: opts.Header, header0: opts.Header,
ansi: opts.Ansi, ansi: opts.Ansi,
@ -1136,15 +1138,23 @@ func (t *Terminal) wrapCols() int {
return util.Max(t.window.Width()-(t.pointerLen+t.markerLen+1), 1) 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) { func (t *Terminal) numItemLines(item *Item, atMost int) (int, bool) {
var numLines int
if !t.wrap && !t.multiLine { if !t.wrap && !t.multiLine {
return 1, false numLines = 1 + t.gap
return numLines, numLines > atMost
} }
var overflow bool
if !t.wrap && t.multiLine { 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) numLines += t.gap
return len(lines), overflow return numLines, overflow || numLines > atMost
} }
func (t *Terminal) itemLines(item *Item, atMost int) ([][]rune, bool) { func (t *Terminal) itemLines(item *Item, atMost int) ([][]rune, bool) {
@ -2050,6 +2060,21 @@ func (t *Terminal) printHeader() {
t.wrap = wrap 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() { func (t *Terminal) printList() {
t.constrain() t.constrain()
barLength, barStart := t.getScrollbar() barLength, barStart := t.getScrollbar()
@ -2070,14 +2095,7 @@ func (t *Terminal) printList() {
item := t.merger.Get(itemCount + t.offset) item := t.merger.Get(itemCount + t.offset)
line = t.printItem(item, line, maxy, itemCount, itemCount == t.cy-t.offset, barRange) line = t.printItem(item, line, maxy, itemCount, itemCount == t.cy-t.offset, barRange)
} else if !t.prevLines[line].empty { } else if !t.prevLines[line].empty {
t.move(line, 0, true) t.renderEmptyLine(line, barRange)
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)
}
} }
} }
} }
@ -2125,9 +2143,6 @@ func (t *Terminal) printItem(result Result, line int, maxLine int, index int, cu
prevLine.queryLen == newLine.queryLen && prevLine.queryLen == newLine.queryLen &&
prevLine.result == newLine.result { prevLine.result == newLine.result {
t.prevLines[line].hasBar = printBar(line, false) t.prevLines[line].hasBar = printBar(line, false)
if !t.multiLine && !t.wrap {
return line
}
return line + numLines - 1 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) 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 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) allOffsets := result.colorOffsets(charOffsets, t.theme, colBase, colMatch, current)
maxLines := 1 maxLines := 1
if t.multiLine || t.wrap { if t.canSpanMultiLines() {
maxLines = maxLineNum - lineNum + 1 maxLines = maxLineNum - lineNum + 1
} }
lines, overflow := t.itemLines(item, maxLines) lines, overflow := t.itemLines(item, maxLines)
@ -2285,7 +2304,7 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
topCutoff := false topCutoff := false
skipLines := 0 skipLines := 0
wrapped := false wrapped := false
if t.multiLine || t.wrap { if t.canSpanMultiLines() {
// Cut off the upper lines in the 'default' layout // Cut off the upper lines in the 'default' layout
if t.layout == layoutDefault && !current && maxLines == numItemLines && overflow { if t.layout == layoutDefault && !current && maxLines == numItemLines && overflow {
lines, _ = t.itemLines(item, math.MaxInt) lines, _ = t.itemLines(item, math.MaxInt)
@ -4875,7 +4894,7 @@ func (t *Terminal) constrain() {
for tries := 0; tries < maxLines; tries++ { for tries := 0; tries < maxLines; tries++ {
numItems := maxLines numItems := maxLines
// How many items can be fit on screen including the current item? // 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 numItemsFound := 0
linesSum := 0 linesSum := 0
@ -4930,12 +4949,12 @@ func (t *Terminal) constrain() {
for { for {
prevOffset := newOffset prevOffset := newOffset
numItems := t.merger.Length() numItems := t.merger.Length()
itemLines := 1 itemLines := 1 + t.gap
if (t.multiLine || t.wrap) && t.cy < numItems { if t.canSpanMultiLines() && t.cy < numItems {
itemLines, _ = t.numItemLines(t.merger.Get(t.cy).item, maxLines) itemLines, _ = t.numItemLines(t.merger.Get(t.cy).item, maxLines)
} }
linesBefore := t.cy - newOffset linesBefore := t.cy - newOffset
if t.multiLine || t.wrap { if t.canSpanMultiLines() {
linesBefore = 0 linesBefore = 0
for i := newOffset; i < t.cy && i < numItems; i++ { for i := newOffset; i < t.cy && i < numItems; i++ {
lines, _ := t.numItemLines(t.merger.Get(i).item, maxLines-linesBefore-itemLines) lines, _ := t.numItemLines(t.merger.Get(i).item, maxLines-linesBefore-itemLines)

View File

@ -3392,6 +3392,40 @@ class TestGoFZF < TestBase
assert lines[1]&.end_with?('1000││') assert lines[1]&.end_with?('1000││')
end end
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 end
module TestShell module TestShell