diff options
-rw-r--r-- | CHANGELOG.md | 6 | ||||
-rw-r--r-- | man/man1/fzf.1 | 4 | ||||
-rw-r--r-- | src/options.go | 297 | ||||
-rw-r--r-- | src/terminal.go | 89 | ||||
-rw-r--r-- | src/util/chars.go | 14 | ||||
-rwxr-xr-x | test/test_go.rb | 54 |
6 files changed, 324 insertions, 140 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 283446d5..5b4d55eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,12 @@ CHANGELOG ``` - To disable multi-line display, use `--no-multi-line` - The default `--pointer` and `--marker` have been changed from `>` to Unicode bar characters as they look better with multi-line items +- Added `--marker-multi-line` to customize the select marker for multi-line entries with the default set to `╻┃╹` + ``` + ╻First line + ┃... + ╹Last line + ``` - Native `--tmux` integration to replace fzf-tmux script ```sh # --tmux [center|top|bottom|left|right][,SIZE[%]][,SIZE[%]] diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index ef88d69a..d1c2cdc0 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -455,6 +455,10 @@ Pointer to the current line (default: '▌' or '>' depending on \fB--no-unicode\ .BI "--marker=" "STR" Multi-select marker (default: '┃' or '>' depending on \fB--no-unicode\fR) .TP +.BI "--marker-multi-line=" "STR" +Multi-select marker for multi-line entries. 3 elements for top, middle, and bottom. +(default: '╻┃╹' or '.|'' depending on \fB--no-unicode\fR) +.TP .BI "--header=" "STR" The given string will be printed as the sticky header. The lines are displayed in the given order from top to bottom regardless of \fB--layout\fR option, and diff --git a/src/options.go b/src/options.go index c5b939e4..461db313 100644 --- a/src/options.go +++ b/src/options.go @@ -27,136 +27,137 @@ Author: Junegunn Choi <junegunn.c@gmail.com> Usage: fzf [options] Search - -x, --extended Extended-search mode - (enabled by default; +x or --no-extended to disable) - -e, --exact Enable Exact-match - -i, --ignore-case Case-insensitive match (default: smart-case match) - +i, --no-ignore-case Case-sensitive match - --scheme=SCHEME Scoring scheme [default|path|history] - --literal Do not normalize latin script letters before matching - -n, --nth=N[,..] Comma-separated list of field index expressions - for limiting search scope. Each can be a non-zero - integer or a range expression ([BEGIN]..[END]). - --with-nth=N[,..] Transform the presentation of each line using - field index expressions - -d, --delimiter=STR Field delimiter regex (default: AWK-style) - +s, --no-sort Do not sort the result - --track Track the current selection when the result is updated - --tac Reverse the order of the input - --disabled Do not perform search - --tiebreak=CRI[,..] Comma-separated list of sort criteria to apply - when the scores are tied [length|chunk|begin|end|index] - (default: length) - + -x, --extended Extended-search mode + (enabled by default; +x or --no-extended to disable) + -e, --exact Enable Exact-match + -i, --ignore-case Case-insensitive match (default: smart-case match) + +i, --no-ignore-case Case-sensitive match + --scheme=SCHEME Scoring scheme [default|path|history] + --literal Do not normalize latin script letters before matching + -n, --nth=N[,..] Comma-separated list of field index expressions + for limiting search scope. Each can be a non-zero + integer or a range expression ([BEGIN]..[END]). + --with-nth=N[,..] Transform the presentation of each line using + field index expressions + -d, --delimiter=STR Field delimiter regex (default: AWK-style) + +s, --no-sort Do not sort the result + --track Track the current selection when the result is updated + --tac Reverse the order of the input + --disabled Do not perform search + --tiebreak=CRI[,..] Comma-separated list of sort criteria to apply + when the scores are tied [length|chunk|begin|end|index] + (default: length) Interface - -m, --multi[=MAX] Enable multi-select with tab/shift-tab - --no-mouse Disable mouse - --bind=KEYBINDS Custom key bindings. Refer to the man page. - --cycle Enable cyclic scroll - --no-multi-line Disable multi-line display of items when using --read0 - --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) - --no-hscroll Disable horizontal scroll - --hscroll-off=COLS Number of screen columns to keep to the right of the - highlighted substring (default: 10) - --filepath-word Make word-wise movements respect path separators - --jump-labels=CHARS Label characters for jump mode + -m, --multi[=MAX] Enable multi-select with tab/shift-tab + --no-mouse Disable mouse + --bind=KEYBINDS Custom key bindings. Refer to the man page. + --cycle Enable cyclic scroll + --no-multi-line Disable multi-line display of items when using --read0 + --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) + --no-hscroll Disable horizontal scroll + --hscroll-off=COLS Number of screen columns to keep to the right of the + highlighted substring (default: 10) + --filepath-word Make word-wise movements respect path separators + --jump-labels=CHARS Label characters for jump mode Layout - --height=[~]HEIGHT[%] Display fzf window below the cursor with the given - height instead of using fullscreen. - A negative value is calculated as the terminal height - minus the given value. - If prefixed with '~', fzf will determine the height - according to the input size. - --min-height=HEIGHT Minimum height when --height is given in percent - (default: 10) - --tmux=OPTS Start fzf in a tmux popup (requires tmux 3.3+) - [center|top|bottom|left|right][,SIZE[%]][,SIZE[%]] - --layout=LAYOUT Choose layout: [default|reverse|reverse-list] - --border[=STYLE] Draw border around the finder - [rounded|sharp|bold|block|thinblock|double|horizontal|vertical| - top|bottom|left|right|none] (default: rounded) - --border-label=LABEL Label to print on the border - --border-label-pos=COL Position of the border label - [POSITIVE_INTEGER: columns from left| - NEGATIVE_INTEGER: columns from right][:bottom] - (default: 0 or center) - --margin=MARGIN Screen margin (TRBL | TB,RL | T,RL,B | T,R,B,L) - --padding=PADDING Padding inside border (TRBL | TB,RL | T,RL,B | T,R,B,L) - --info=STYLE Finder info style - [default|right|hidden|inline[-right][:PREFIX]] - --separator=STR String to form horizontal separator on info line - --no-separator Hide info line separator - --scrollbar[=C1[C2]] Scrollbar character(s) (each for main and preview window) - --no-scrollbar Hide scrollbar - --prompt=STR Input prompt (default: '> ') - --pointer=STR Pointer to the current line (default: '▌' or '>') - --marker=STR Multi-select marker (default: '┃' or '>') - --header=STR String to print as header - --header-lines=N The first N lines of the input are treated as header - --header-first Print header before the prompt line - --ellipsis=STR Ellipsis to show when line is truncated (default: '..') + --height=[~]HEIGHT[%] Display fzf window below the cursor with the given + height instead of using fullscreen. + A negative value is calculated as the terminal height + minus the given value. + If prefixed with '~', fzf will determine the height + according to the input size. + --min-height=HEIGHT Minimum height when --height is given in percent + (default: 10) + --tmux=OPTS Start fzf in a tmux popup (requires tmux 3.3+) + [center|top|bottom|left|right][,SIZE[%]][,SIZE[%]] + --layout=LAYOUT Choose layout: [default|reverse|reverse-list] + --border[=STYLE] Draw border around the finder + [rounded|sharp|bold|block|thinblock|double|horizontal|vertical| + top|bottom|left|right|none] (default: rounded) + --border-label=LABEL Label to print on the border + --border-label-pos=COL Position of the border label + [POSITIVE_INTEGER: columns from left| + NEGATIVE_INTEGER: columns from right][:bottom] + (default: 0 or center) + --margin=MARGIN Screen margin (TRBL | TB,RL | T,RL,B | T,R,B,L) + --padding=PADDING Padding inside border (TRBL | TB,RL | T,RL,B | T,R,B,L) + --info=STYLE Finder info style + [default|right|hidden|inline[-right][:PREFIX]] + --separator=STR String to form horizontal separator on info line + --no-separator Hide info line separator + --scrollbar[=C1[C2]] Scrollbar character(s) (each for main and preview window) + --no-scrollbar Hide scrollbar + --prompt=STR Input prompt (default: '> ') + --pointer=STR Pointer to the current line (default: '▌' or '>') + --marker=STR Multi-select marker (default: '┃' or '>') + --marker-multi-line=STR Multi-select marker for multi-line entries; + 3 elements for top, middle, and bottom (default: '╻┃╹') + --header=STR String to print as header + --header-lines=N The first N lines of the input are treated as header + --header-first Print header before the prompt line + --ellipsis=STR Ellipsis to show when line is truncated (default: '..') Display - --ansi Enable processing of ANSI color codes - --tabstop=SPACES Number of spaces for a tab character (default: 8) - --color=COLSPEC Base scheme (dark|light|16|bw) and/or custom colors - --highlight-line Highlight the whole current line - --no-bold Do not use bold text + --ansi Enable processing of ANSI color codes + --tabstop=SPACES Number of spaces for a tab character (default: 8) + --color=COLSPEC Base scheme (dark|light|16|bw) and/or custom colors + --highlight-line Highlight the whole current line + --no-bold Do not use bold text History - --history=FILE History file - --history-size=N Maximum number of history entries (default: 1000) + --history=FILE History file + --history-size=N Maximum number of history entries (default: 1000) Preview - --preview=COMMAND Command to preview highlighted line ({}) - --preview-window=OPT Preview window layout (default: right:50%) - [up|down|left|right][,SIZE[%]] - [,[no]wrap][,[no]cycle][,[no]follow][,[no]hidden] - [,border-BORDER_OPT] - [,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES] - [,default][,<SIZE_THRESHOLD(ALTERNATIVE_LAYOUT)] + --preview=COMMAND Command to preview highlighted line ({}) + --preview-window=OPT Preview window layout (default: right:50%) + [up|down|left|right][,SIZE[%]] + [,[no]wrap][,[no]cycle][,[no]follow][,[no]hidden] + [,border-BORDER_OPT] + [,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES] + [,default][,<SIZE_THRESHOLD(ALTERNATIVE_LAYOUT)] --preview-label=LABEL - --preview-label-pos=N Same as --border-label and --border-label-pos, - but for preview window + --preview-label-pos=N Same as --border-label and --border-label-pos, + but for preview window Scripting - -q, --query=STR Start the finder with the given query - -1, --select-1 Automatically select the only match - -0, --exit-0 Exit immediately when there's no match - -f, --filter=STR Filter mode. Do not start interactive finder. - --print-query Print query as the first line - --expect=KEYS Comma-separated list of keys to complete fzf - --read0 Read input delimited by ASCII NUL characters - --print0 Print output delimited by ASCII NUL characters - --sync Synchronous search for multi-staged filtering - --with-shell=STR Shell command and flags to start child processes with - --listen[=[ADDR:]PORT] Start HTTP server to receive actions (POST /) - (To allow remote process execution, use --listen-unsafe) - - Directory traversal (Only used when $FZF_DEFAULT_COMMAND is not set) - --walker=OPTS [file][,dir][,follow][,hidden] (default: file,follow,hidden) - --walker-root=DIR Root directory from which to start walker (default: .) - --walker-skip=DIRS Comma-separated list of directory names to skip - (default: .git,node_modules) + -q, --query=STR Start the finder with the given query + -1, --select-1 Automatically select the only match + -0, --exit-0 Exit immediately when there's no match + -f, --filter=STR Filter mode. Do not start interactive finder. + --print-query Print query as the first line + --expect=KEYS Comma-separated list of keys to complete fzf + --read0 Read input delimited by ASCII NUL characters + --print0 Print output delimited by ASCII NUL characters + --sync Synchronous search for multi-staged filtering + --with-shell=STR Shell command and flags to start child processes with + --listen[=[ADDR:]PORT] Start HTTP server to receive actions (POST /) + (To allow remote process execution, use --listen-unsafe) + + Directory traversal (Only used when $FZF_DEFAULT_COMMAND is not set) + --walker=OPTS [file][,dir][,follow][,hidden] (default: file,follow,hidden) + --walker-root=DIR Root directory from which to start walker (default: .) + --walker-skip=DIRS Comma-separated list of directory names to skip + (default: .git,node_modules) Shell integration - --bash Print script to set up Bash shell integration - --zsh Print script to set up Zsh shell integration - --fish Print script to set up Fish shell integration + --bash Print script to set up Bash shell integration + --zsh Print script to set up Zsh shell integration + --fish Print script to set up Fish shell integration Help - --version Display version information and exit - --help Show this message - --man Show man page + --version Display version information and exit + --help Show this message + --man Show man page Environment variables - FZF_DEFAULT_COMMAND Default command to use when input is tty - FZF_DEFAULT_OPTS Default options (e.g. '--layout=reverse --info=inline') - FZF_DEFAULT_OPTS_FILE Location of the file to read default options from - FZF_API_KEY X-API-Key header for HTTP server (--listen) + FZF_DEFAULT_COMMAND Default command to use when input is tty + FZF_DEFAULT_OPTS Default options (e.g. '--layout=reverse --info=inline') + FZF_DEFAULT_OPTS_FILE Location of the file to read default options from + FZF_API_KEY X-API-Key header for HTTP server (--listen) ` @@ -438,6 +439,7 @@ type Options struct { Prompt string Pointer *string Marker *string + MarkerMulti [3]string Query string Select1 bool Exit0 bool @@ -1847,6 +1849,37 @@ func parseMargin(opt string, margin string) ([4]sizeSpec, error) { return [4]sizeSpec{}, errors.New("invalid " + opt + ": " + margin) } +func parseMarkerMultiLine(str string) ([3]string, error) { + gr := uniseg.NewGraphemes(str) + parts := []string{} + totalWidth := 0 + for gr.Next() { + s := string(gr.Runes()) + totalWidth += uniseg.StringWidth(s) + parts = append(parts, s) + } + + result := [3]string{} + if totalWidth != 3 && totalWidth != 6 { + return result, fmt.Errorf("invalid total marker width: %d (expected: 3 or 6)", totalWidth) + } + + expected := totalWidth / 3 + idx := 0 + for _, part := range parts { + expected -= uniseg.StringWidth(part) + result[idx] += part + if expected <= 0 { + idx++ + expected = totalWidth / 3 + } + if idx == 3 { + break + } + } + return result, nil +} + func parseOptions(opts *Options, allArgs []string) error { var err error var historyMax int @@ -2186,19 +2219,27 @@ func parseOptions(opts *Options, allArgs []string) error { return err } case "--pointer": - str, err := nextString(allArgs, &i, "pointer sign string required") + str, err := nextString(allArgs, &i, "pointer sign required") if err != nil { return err } str = firstLine(str) opts.Pointer = &str case "--marker": - str, err := nextString(allArgs, &i, "selected sign string required") + str, err := nextString(allArgs, &i, "marker sign required") if err != nil { return err } str = firstLine(str) opts.Marker = &str + case "--marker-multi-line": + str, err := nextString(allArgs, &i, "marker sign for multi-line entries required") + if err != nil { + return err + } + if opts.MarkerMulti, err = parseMarkerMultiLine(firstLine(str)); err != nil { + return err + } case "--sync": opts.Sync = true case "--no-sync", "--async": @@ -2439,6 +2480,10 @@ func parseOptions(opts *Options, allArgs []string) error { } else if match, value := optString(arg, "--marker="); match { str := firstLine(value) opts.Marker = &str + } else if match, value := optString(arg, "--marker-multi-line="); match { + if opts.MarkerMulti, err = parseMarkerMultiLine(firstLine(value)); err != nil { + return err + } } else if match, value := optString(arg, "-n", "--nth="); match { if opts.Nth, err = splitNth(value); err != nil { return err @@ -2680,13 +2725,35 @@ func postProcessOptions(opts *Options) error { opts.Pointer = &defaultPointer } + markerLen := 1 if opts.Marker == nil { - // "▏" looks better, but not all terminals render it correctly + // "▎" looks better, but not all terminals render it correctly defaultMarker := "┃" if !opts.Unicode { defaultMarker = ">" } opts.Marker = &defaultMarker + } else { + markerLen = uniseg.StringWidth(*opts.Marker) + } + + markerMultiLen := 1 + if len(opts.MarkerMulti[0]) == 0 { + if opts.Unicode { + opts.MarkerMulti = [3]string{"╻", "┃", "╹"} + } else { + opts.MarkerMulti = [3]string{".", "|", "'"} + } + } else { + markerMultiLen = uniseg.StringWidth(opts.MarkerMulti[0]) + } + if markerMultiLen > markerLen { + padded := *opts.Marker + " " + opts.Marker = &padded + } else if markerMultiLen < markerLen { + for idx := range opts.MarkerMulti { + opts.MarkerMulti[idx] += " " + } } // Default actions for CTRL-N / CTRL-P when --history is set diff --git a/src/terminal.go b/src/terminal.go index e2ea49fd..d5ad72a5 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -177,6 +177,15 @@ type fitpad struct { type labelPrinter func(tui.Window, int) +type markerClass int + +const ( + markerSingle markerClass = iota + markerTop + markerMiddle + markerBottom +) + type StatusItem struct { Index int `json:"index"` Text string `json:"text"` @@ -218,6 +227,7 @@ type Terminal struct { marker string markerLen int markerEmpty string + markerMultiLine [3]string queryLen [2]int layout layoutType fullscreen bool @@ -755,6 +765,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor pointerLen: uniseg.StringWidth(*opts.Pointer), marker: *opts.Marker, markerLen: uniseg.StringWidth(*opts.Marker), + markerMultiLine: opts.MarkerMulti, wordRubout: wordRubout, wordNext: wordNext, cx: len(input), @@ -1084,7 +1095,8 @@ func (t *Terminal) avgNumLines() int { offset := util.Max(0, util.Min(t.offset, total-maxItems-1)) for idx := 0; idx < maxItems && idx+offset < total; idx++ { item := t.merger.Get(idx + offset) - numLines += item.item.text.NumLines(maxItems) + lines, _ := item.item.text.NumLines(maxItems) + numLines += lines count++ } if count == 0 { @@ -1884,7 +1896,7 @@ func (t *Terminal) printHeader() { t.printHighlighted(Result{item: item}, tui.ColHeader, tui.ColHeader, false, false, line, line, true, - func(int) { t.window.Print(" ") }, nil) + func(markerClass) { t.window.Print(" ") }, nil) } } @@ -1964,7 +1976,8 @@ func (t *Terminal) printItem(result Result, line int, maxLine int, index int, cu if !t.multiLine { return line } - return line + item.text.NumLines(maxLine-line+1) - 1 + lines, _ := item.text.NumLines(maxLine - line + 1) + return line + lines - 1 } maxWidth := t.window.Width() - (t.pointerLen + t.markerLen + 1) @@ -1992,29 +2005,41 @@ func (t *Terminal) printItem(result Result, line int, maxLine int, index int, cu } var finalLineNum int + markerFor := func(markerClass markerClass) string { + marker := t.marker + switch markerClass { + case markerTop: + marker = t.markerMultiLine[0] + case markerMiddle: + marker = t.markerMultiLine[1] + case markerBottom: + marker = t.markerMultiLine[2] + } + return marker + } if current { - preTask := func(lineOffset int) { + preTask := func(marker markerClass) { if len(label) == 0 { t.window.CPrint(tui.ColCurrentCursorEmpty, t.pointerEmpty) } else { t.window.CPrint(tui.ColCurrentCursor, label) } if selected { - t.window.CPrint(tui.ColCurrentMarker, t.marker) + t.window.CPrint(tui.ColCurrentMarker, markerFor(marker)) } else { t.window.CPrint(tui.ColCurrentSelectedEmpty, t.markerEmpty) } } finalLineNum = t.printHighlighted(result, tui.ColCurrent, tui.ColCurrentMatch, true, true, line, maxLine, forceRedraw, preTask, postTask) } else { - preTask := func(lineOffset int) { + preTask := func(marker markerClass) { if len(label) == 0 { t.window.CPrint(tui.ColCursorEmpty, t.pointerEmpty) } else { t.window.CPrint(tui.ColCursor, label) } if selected { - t.window.CPrint(tui.ColMarker, t.marker) + t.window.CPrint(tui.ColMarker, markerFor(marker)) } else { t.window.Print(t.markerEmpty) } @@ -2070,7 +2095,7 @@ func (t *Terminal) overflow(runes []rune, max int) bool { return t.displayWidthWithLimit(runes, 0, max) > max } -func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMatch tui.ColorPair, current bool, match bool, lineNum int, maxLineNum int, forceRedraw bool, preTask func(int), postTask func(int, int)) int { +func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMatch tui.ColorPair, current bool, match bool, lineNum int, maxLineNum int, forceRedraw bool, preTask func(markerClass), postTask func(int, int)) int { var displayWidth int item := result.item matchOffsets := []Offset{} @@ -2096,12 +2121,16 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat finalLineNum := lineNum numItemLines := 1 cutoff := 0 - if t.multiLine && t.layout == layoutDefault { + overflow := false + topCutoff := false + if t.multiLine { maxLines := maxLineNum - lineNum + 1 - numItemLines = item.text.NumLines(maxLines) + numItemLines, overflow = item.text.NumLines(maxLines) // Cut off the upper lines in the 'default' layout - if !current && maxLines == numItemLines { - cutoff = item.text.NumLines(math.MaxInt32) - maxLines + if t.layout == layoutDefault && !current && maxLines == numItemLines && overflow { + actualLines, _ := item.text.NumLines(math.MaxInt32) + cutoff = actualLines - maxLines + topCutoff = true } } for lineOffset := 0; from <= len(text) && (lineNum <= maxLineNum || maxLineNum == 0); lineOffset++ { @@ -2151,7 +2180,34 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat t.move(actualLineNum, 0, forceRedraw) if preTask != nil { - preTask(lineOffset) + var marker markerClass + if numItemLines == 1 { + if !overflow { + marker = markerSingle + } else if topCutoff { + marker = markerBottom + } else { + marker = markerTop + } + } else { + if lineOffset == 0 { // First line + if topCutoff { + marker = markerMiddle + } else { + marker = markerTop + } + } else if lineOffset == numItemLines-1 { // Last line + if topCutoff || !overflow { + marker = markerBottom + } else { + marker = markerMiddle + } + } else { + marker = markerMiddle + } + } + + preTask(marker) } maxWidth := t.window.Width() - (t.pointerLen + t.markerLen + 1) @@ -4502,7 +4558,7 @@ func (t *Terminal) constrain() { linesSum := 0 add := func(i int) bool { - lines := t.merger.Get(i).item.text.NumLines(numItems - linesSum) + lines, _ := t.merger.Get(i).item.text.NumLines(numItems - linesSum) linesSum += lines if linesSum >= numItems { if numItemsFound == 0 { @@ -4547,13 +4603,14 @@ func (t *Terminal) constrain() { numItems := t.merger.Length() itemLines := 1 if t.multiLine && t.cy < numItems { - itemLines = t.merger.Get(t.cy).item.text.NumLines(maxLines) + itemLines, _ = t.merger.Get(t.cy).item.text.NumLines(maxLines) } linesBefore := t.cy - newOffset if t.multiLine { linesBefore = 0 for i := newOffset; i < t.cy && i < numItems; i++ { - linesBefore += t.merger.Get(i).item.text.NumLines(maxLines - linesBefore - itemLines) + lines, _ := t.merger.Get(i).item.text.NumLines(maxLines - linesBefore - itemLines) + linesBefore += lines } } linesAfter := maxLines - (linesBefore + itemLines) diff --git a/src/util/chars.go b/src/util/chars.go index 7c706ae8..82773f40 100644 --- a/src/util/chars.go +++ b/src/util/chars.go @@ -75,18 +75,18 @@ func (chars *Chars) Bytes() []byte { return chars.slice } -func (chars *Chars) NumLines(atMost int) int { +func (chars *Chars) NumLines(atMost int) (int, bool) { lines := 1 if runes := chars.optionalRunes(); runes != nil { for _, r := range runes { if r == '\n' { lines++ } - if lines >= atMost { - return atMost + if lines > atMost { + return atMost, true } } - return lines + return lines, false } for idx := 0; idx < len(chars.slice); idx++ { @@ -97,11 +97,11 @@ func (chars *Chars) NumLines(atMost int) int { idx += found lines++ - if lines >= atMost { - return atMost + if lines > atMost { + return atMost, true } } - return lines + return lines, false } func (chars *Chars) optionalRunes() []rune { diff --git a/test/test_go.rb b/test/test_go.rb index 06c3aefd..dfb338e8 100755 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -2752,8 +2752,9 @@ class TestGoFZF < TestBase def assert_block(expected, lines) cols = expected.lines.map(&:chomp).map(&:length).max - actual = lines.reverse.take(expected.lines.length).reverse.map { _1[0, cols].rstrip + "\n" }.join - assert_equal_org expected, actual + top = lines.take(expected.lines.length).map { _1[0, cols].rstrip + "\n" }.join + bottom = lines.reverse.take(expected.lines.length).reverse.map { _1[0, cols].rstrip + "\n" }.join + assert_includes [top, bottom], expected end def test_height_range_fit @@ -3268,6 +3269,55 @@ class TestGoFZF < TestBase tmux.send_keys '99' tmux.until { |lines| assert(lines.any? { |line| line.include?('0 / 0') }) } end + + def test_fzf_multi_line + tmux.send_keys %[(echo -en '0\\0'; echo -en '1\\n2\\0'; seq 1000) | fzf --read0 --multi --bind load:select-all --border rounded], :Enter + block = <<~BLOCK + │ ┃998 + │ ┃999 + │ ┃1000 + │ ╹ + │ ╻1 + │ ╹2 + │ >>0 + │ 3/3 (3) + │ > + ╰─────────── + BLOCK + tmux.until { assert_block(block, _1) } + tmux.send_keys :Up, :Up + block = <<~BLOCK + ╭─────── + │ >╻1 + │ >┃2 + │ >┃3 + BLOCK + tmux.until { assert_block(block, _1) } + + block = <<~BLOCK + │ >┃ + │ + │ > + ╰─── + BLOCK + tmux.until { assert_block(block, _1) } + end + + def test_fzf_multi_line_reverse + tmux.send_keys %[(echo -en '0\\0'; echo -en '1\\n2\\0'; seq 1000) | fzf --read0 --multi --bind load:select-all --border rounded --reverse], :Enter + block = <<~BLOCK + ╭─────────── + │ > + │ 3/3 (3) + │ >>0 + │ ╻1 + │ ╹2 + │ ╻1 + │ ┃2 + │ ┃3 + BLOCK + tmux.until { assert_block(block, _1) } + end end module TestShell |