From 1a32220ca94ae897cab408a9eeaed094a8a739f1 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 1 Oct 2024 19:15:17 +0900 Subject: [PATCH] Add --gap option to put empty lines between items --- CHANGELOG.md | 15 ++++++++++++ man/man1/fzf.1 | 3 +++ src/options.go | 13 +++++++++++ src/terminal.go | 61 ++++++++++++++++++++++++++++++++----------------- test/test_go.rb | 34 +++++++++++++++++++++++++++ 5 files changed, 105 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80b6491..3ea8884 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 321327a..59e2181 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 62c8c0c..b0ab6b1 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 535f5e3..ce299de 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 8f627ba..2a46215 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