Different marker for the first and last line of multi-line entries

Can be configured via `--marker-multi-line`
This commit is contained in:
Junegunn Choi 2024-05-27 01:20:56 +09:00
parent 0ccbd79e10
commit 2f51eb2b41
No known key found for this signature in database
GPG Key ID: 254BC280FEF9C627
6 changed files with 323 additions and 139 deletions

View File

@ -10,6 +10,12 @@ CHANGELOG
``` ```
- To disable multi-line display, use `--no-multi-line` - 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 - 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 - Native `--tmux` integration to replace fzf-tmux script
```sh ```sh
# --tmux [center|top|bottom|left|right][,SIZE[%]][,SIZE[%]] # --tmux [center|top|bottom|left|right][,SIZE[%]][,SIZE[%]]

View File

@ -455,6 +455,10 @@ Pointer to the current line (default: '▌' or '>' depending on \fB--no-unicode\
.BI "--marker=" "STR" .BI "--marker=" "STR"
Multi-select marker (default: '┃' or '>' depending on \fB--no-unicode\fR) Multi-select marker (default: '┃' or '>' depending on \fB--no-unicode\fR)
.TP .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" .BI "--header=" "STR"
The given string will be printed as the sticky header. The lines are displayed 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 in the given order from top to bottom regardless of \fB--layout\fR option, and

View File

@ -27,136 +27,137 @@ Author: Junegunn Choi <junegunn.c@gmail.com>
Usage: fzf [options] Usage: fzf [options]
Search Search
-x, --extended Extended-search mode -x, --extended Extended-search mode
(enabled by default; +x or --no-extended to disable) (enabled by default; +x or --no-extended to disable)
-e, --exact Enable Exact-match -e, --exact Enable Exact-match
-i, --ignore-case Case-insensitive match (default: smart-case match) -i, --ignore-case Case-insensitive match (default: smart-case match)
+i, --no-ignore-case Case-sensitive match +i, --no-ignore-case Case-sensitive match
--scheme=SCHEME Scoring scheme [default|path|history] --scheme=SCHEME Scoring scheme [default|path|history]
--literal Do not normalize latin script letters before matching --literal Do not normalize latin script letters before matching
-n, --nth=N[,..] Comma-separated list of field index expressions -n, --nth=N[,..] Comma-separated list of field index expressions
for limiting search scope. Each can be a non-zero for limiting search scope. Each can be a non-zero
integer or a range expression ([BEGIN]..[END]). integer or a range expression ([BEGIN]..[END]).
--with-nth=N[,..] Transform the presentation of each line using --with-nth=N[,..] Transform the presentation of each line using
field index expressions field index expressions
-d, --delimiter=STR Field delimiter regex (default: AWK-style) -d, --delimiter=STR Field delimiter regex (default: AWK-style)
+s, --no-sort Do not sort the result +s, --no-sort Do not sort the result
--track Track the current selection when the result is updated --track Track the current selection when the result is updated
--tac Reverse the order of the input --tac Reverse the order of the input
--disabled Do not perform search --disabled Do not perform search
--tiebreak=CRI[,..] Comma-separated list of sort criteria to apply --tiebreak=CRI[,..] Comma-separated list of sort criteria to apply
when the scores are tied [length|chunk|begin|end|index] when the scores are tied [length|chunk|begin|end|index]
(default: length) (default: length)
Interface Interface
-m, --multi[=MAX] Enable multi-select with tab/shift-tab -m, --multi[=MAX] Enable multi-select with tab/shift-tab
--no-mouse Disable mouse --no-mouse Disable mouse
--bind=KEYBINDS Custom key bindings. Refer to the man page. --bind=KEYBINDS Custom key bindings. Refer to the man page.
--cycle Enable cyclic scroll --cycle Enable cyclic scroll
--no-multi-line Disable multi-line display of items when using --read0 --no-multi-line Disable multi-line display of items when using --read0
--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)
--no-hscroll Disable horizontal scroll --no-hscroll Disable horizontal scroll
--hscroll-off=COLS Number of screen columns to keep to the right of the --hscroll-off=COLS Number of screen columns to keep to the right of the
highlighted substring (default: 10) highlighted substring (default: 10)
--filepath-word Make word-wise movements respect path separators --filepath-word Make word-wise movements respect path separators
--jump-labels=CHARS Label characters for jump mode --jump-labels=CHARS Label characters for jump mode
Layout Layout
--height=[~]HEIGHT[%] Display fzf window below the cursor with the given --height=[~]HEIGHT[%] Display fzf window below the cursor with the given
height instead of using fullscreen. height instead of using fullscreen.
A negative value is calculated as the terminal height A negative value is calculated as the terminal height
minus the given value. minus the given value.
If prefixed with '~', fzf will determine the height If prefixed with '~', fzf will determine the height
according to the input size. according to the input size.
--min-height=HEIGHT Minimum height when --height is given in percent --min-height=HEIGHT Minimum height when --height is given in percent
(default: 10) (default: 10)
--tmux=OPTS Start fzf in a tmux popup (requires tmux 3.3+) --tmux=OPTS Start fzf in a tmux popup (requires tmux 3.3+)
[center|top|bottom|left|right][,SIZE[%]][,SIZE[%]] [center|top|bottom|left|right][,SIZE[%]][,SIZE[%]]
--layout=LAYOUT Choose layout: [default|reverse|reverse-list] --layout=LAYOUT Choose layout: [default|reverse|reverse-list]
--border[=STYLE] Draw border around the finder --border[=STYLE] Draw border around the finder
[rounded|sharp|bold|block|thinblock|double|horizontal|vertical| [rounded|sharp|bold|block|thinblock|double|horizontal|vertical|
top|bottom|left|right|none] (default: rounded) top|bottom|left|right|none] (default: rounded)
--border-label=LABEL Label to print on the border --border-label=LABEL Label to print on the border
--border-label-pos=COL Position of the border label --border-label-pos=COL Position of the border label
[POSITIVE_INTEGER: columns from left| [POSITIVE_INTEGER: columns from left|
NEGATIVE_INTEGER: columns from right][:bottom] NEGATIVE_INTEGER: columns from right][:bottom]
(default: 0 or center) (default: 0 or center)
--margin=MARGIN Screen margin (TRBL | TB,RL | T,RL,B | T,R,B,L) --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) --padding=PADDING Padding inside border (TRBL | TB,RL | T,RL,B | T,R,B,L)
--info=STYLE Finder info style --info=STYLE Finder info style
[default|right|hidden|inline[-right][:PREFIX]] [default|right|hidden|inline[-right][:PREFIX]]
--separator=STR String to form horizontal separator on info line --separator=STR String to form horizontal separator on info line
--no-separator Hide info line separator --no-separator Hide info line separator
--scrollbar[=C1[C2]] Scrollbar character(s) (each for main and preview window) --scrollbar[=C1[C2]] Scrollbar character(s) (each for main and preview window)
--no-scrollbar Hide scrollbar --no-scrollbar Hide scrollbar
--prompt=STR Input prompt (default: '> ') --prompt=STR Input prompt (default: '> ')
--pointer=STR Pointer to the current line (default: '▌' or '>') --pointer=STR Pointer to the current line (default: '▌' or '>')
--marker=STR Multi-select marker (default: '┃' or '>') --marker=STR Multi-select marker (default: '┃' or '>')
--header=STR String to print as header --marker-multi-line=STR Multi-select marker for multi-line entries;
--header-lines=N The first N lines of the input are treated as header 3 elements for top, middle, and bottom (default: '')
--header-first Print header before the prompt line --header=STR String to print as header
--ellipsis=STR Ellipsis to show when line is truncated (default: '..') --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 Display
--ansi Enable processing of ANSI color codes --ansi Enable processing of ANSI color codes
--tabstop=SPACES Number of spaces for a tab character (default: 8) --tabstop=SPACES Number of spaces for a tab character (default: 8)
--color=COLSPEC Base scheme (dark|light|16|bw) and/or custom colors --color=COLSPEC Base scheme (dark|light|16|bw) and/or custom colors
--highlight-line Highlight the whole current line --highlight-line Highlight the whole current line
--no-bold Do not use bold text --no-bold Do not use bold text
History History
--history=FILE History file --history=FILE History file
--history-size=N Maximum number of history entries (default: 1000) --history-size=N Maximum number of history entries (default: 1000)
Preview Preview
--preview=COMMAND Command to preview highlighted line ({}) --preview=COMMAND Command to preview highlighted line ({})
--preview-window=OPT Preview window layout (default: right:50%) --preview-window=OPT Preview window layout (default: right:50%)
[up|down|left|right][,SIZE[%]] [up|down|left|right][,SIZE[%]]
[,[no]wrap][,[no]cycle][,[no]follow][,[no]hidden] [,[no]wrap][,[no]cycle][,[no]follow][,[no]hidden]
[,border-BORDER_OPT] [,border-BORDER_OPT]
[,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES] [,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES]
[,default][,<SIZE_THRESHOLD(ALTERNATIVE_LAYOUT)] [,default][,<SIZE_THRESHOLD(ALTERNATIVE_LAYOUT)]
--preview-label=LABEL --preview-label=LABEL
--preview-label-pos=N Same as --border-label and --border-label-pos, --preview-label-pos=N Same as --border-label and --border-label-pos,
but for preview window but for preview window
Scripting Scripting
-q, --query=STR Start the finder with the given query -q, --query=STR Start the finder with the given query
-1, --select-1 Automatically select the only match -1, --select-1 Automatically select the only match
-0, --exit-0 Exit immediately when there's no match -0, --exit-0 Exit immediately when there's no match
-f, --filter=STR Filter mode. Do not start interactive finder. -f, --filter=STR Filter mode. Do not start interactive finder.
--print-query Print query as the first line --print-query Print query as the first line
--expect=KEYS Comma-separated list of keys to complete fzf --expect=KEYS Comma-separated list of keys to complete fzf
--read0 Read input delimited by ASCII NUL characters --read0 Read input delimited by ASCII NUL characters
--print0 Print output delimited by ASCII NUL characters --print0 Print output delimited by ASCII NUL characters
--sync Synchronous search for multi-staged filtering --sync Synchronous search for multi-staged filtering
--with-shell=STR Shell command and flags to start child processes with --with-shell=STR Shell command and flags to start child processes with
--listen[=[ADDR:]PORT] Start HTTP server to receive actions (POST /) --listen[=[ADDR:]PORT] Start HTTP server to receive actions (POST /)
(To allow remote process execution, use --listen-unsafe) (To allow remote process execution, use --listen-unsafe)
Directory traversal (Only used when $FZF_DEFAULT_COMMAND is not set) Directory traversal (Only used when $FZF_DEFAULT_COMMAND is not set)
--walker=OPTS [file][,dir][,follow][,hidden] (default: file,follow,hidden) --walker=OPTS [file][,dir][,follow][,hidden] (default: file,follow,hidden)
--walker-root=DIR Root directory from which to start walker (default: .) --walker-root=DIR Root directory from which to start walker (default: .)
--walker-skip=DIRS Comma-separated list of directory names to skip --walker-skip=DIRS Comma-separated list of directory names to skip
(default: .git,node_modules) (default: .git,node_modules)
Shell integration Shell integration
--bash Print script to set up Bash shell integration --bash Print script to set up Bash shell integration
--zsh Print script to set up Zsh shell integration --zsh Print script to set up Zsh shell integration
--fish Print script to set up Fish shell integration --fish Print script to set up Fish shell integration
Help Help
--version Display version information and exit --version Display version information and exit
--help Show this message --help Show this message
--man Show man page --man Show man page
Environment variables Environment variables
FZF_DEFAULT_COMMAND Default command to use when input is tty 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 Default options (e.g. '--layout=reverse --info=inline')
FZF_DEFAULT_OPTS_FILE Location of the file to read default options from 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_API_KEY X-API-Key header for HTTP server (--listen)
` `
@ -438,6 +439,7 @@ type Options struct {
Prompt string Prompt string
Pointer *string Pointer *string
Marker *string Marker *string
MarkerMulti [3]string
Query string Query string
Select1 bool Select1 bool
Exit0 bool Exit0 bool
@ -1847,6 +1849,37 @@ func parseMargin(opt string, margin string) ([4]sizeSpec, error) {
return [4]sizeSpec{}, errors.New("invalid " + opt + ": " + margin) 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 { func parseOptions(opts *Options, allArgs []string) error {
var err error var err error
var historyMax int var historyMax int
@ -2186,19 +2219,27 @@ func parseOptions(opts *Options, allArgs []string) error {
return err return err
} }
case "--pointer": case "--pointer":
str, err := nextString(allArgs, &i, "pointer sign string required") str, err := nextString(allArgs, &i, "pointer sign required")
if err != nil { if err != nil {
return err return err
} }
str = firstLine(str) str = firstLine(str)
opts.Pointer = &str opts.Pointer = &str
case "--marker": case "--marker":
str, err := nextString(allArgs, &i, "selected sign string required") str, err := nextString(allArgs, &i, "marker sign required")
if err != nil { if err != nil {
return err return err
} }
str = firstLine(str) str = firstLine(str)
opts.Marker = &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": case "--sync":
opts.Sync = true opts.Sync = true
case "--no-sync", "--async": case "--no-sync", "--async":
@ -2439,6 +2480,10 @@ func parseOptions(opts *Options, allArgs []string) error {
} else if match, value := optString(arg, "--marker="); match { } else if match, value := optString(arg, "--marker="); match {
str := firstLine(value) str := firstLine(value)
opts.Marker = &str 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 { } else if match, value := optString(arg, "-n", "--nth="); match {
if opts.Nth, err = splitNth(value); err != nil { if opts.Nth, err = splitNth(value); err != nil {
return err return err
@ -2680,13 +2725,35 @@ func postProcessOptions(opts *Options) error {
opts.Pointer = &defaultPointer opts.Pointer = &defaultPointer
} }
markerLen := 1
if opts.Marker == nil { if opts.Marker == nil {
// "" looks better, but not all terminals render it correctly // "" looks better, but not all terminals render it correctly
defaultMarker := "┃" defaultMarker := "┃"
if !opts.Unicode { if !opts.Unicode {
defaultMarker = ">" defaultMarker = ">"
} }
opts.Marker = &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 // Default actions for CTRL-N / CTRL-P when --history is set

View File

@ -177,6 +177,15 @@ type fitpad struct {
type labelPrinter func(tui.Window, int) type labelPrinter func(tui.Window, int)
type markerClass int
const (
markerSingle markerClass = iota
markerTop
markerMiddle
markerBottom
)
type StatusItem struct { type StatusItem struct {
Index int `json:"index"` Index int `json:"index"`
Text string `json:"text"` Text string `json:"text"`
@ -218,6 +227,7 @@ type Terminal struct {
marker string marker string
markerLen int markerLen int
markerEmpty string markerEmpty string
markerMultiLine [3]string
queryLen [2]int queryLen [2]int
layout layoutType layout layoutType
fullscreen bool fullscreen bool
@ -755,6 +765,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
pointerLen: uniseg.StringWidth(*opts.Pointer), pointerLen: uniseg.StringWidth(*opts.Pointer),
marker: *opts.Marker, marker: *opts.Marker,
markerLen: uniseg.StringWidth(*opts.Marker), markerLen: uniseg.StringWidth(*opts.Marker),
markerMultiLine: opts.MarkerMulti,
wordRubout: wordRubout, wordRubout: wordRubout,
wordNext: wordNext, wordNext: wordNext,
cx: len(input), cx: len(input),
@ -1084,7 +1095,8 @@ func (t *Terminal) avgNumLines() int {
offset := util.Max(0, util.Min(t.offset, total-maxItems-1)) offset := util.Max(0, util.Min(t.offset, total-maxItems-1))
for idx := 0; idx < maxItems && idx+offset < total; idx++ { for idx := 0; idx < maxItems && idx+offset < total; idx++ {
item := t.merger.Get(idx + offset) item := t.merger.Get(idx + offset)
numLines += item.item.text.NumLines(maxItems) lines, _ := item.item.text.NumLines(maxItems)
numLines += lines
count++ count++
} }
if count == 0 { if count == 0 {
@ -1884,7 +1896,7 @@ func (t *Terminal) printHeader() {
t.printHighlighted(Result{item: item}, t.printHighlighted(Result{item: item},
tui.ColHeader, tui.ColHeader, false, false, line, line, true, 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 { if !t.multiLine {
return line 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) 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 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 { if current {
preTask := func(lineOffset int) { preTask := func(marker markerClass) {
if len(label) == 0 { if len(label) == 0 {
t.window.CPrint(tui.ColCurrentCursorEmpty, t.pointerEmpty) t.window.CPrint(tui.ColCurrentCursorEmpty, t.pointerEmpty)
} else { } else {
t.window.CPrint(tui.ColCurrentCursor, label) t.window.CPrint(tui.ColCurrentCursor, label)
} }
if selected { if selected {
t.window.CPrint(tui.ColCurrentMarker, t.marker) t.window.CPrint(tui.ColCurrentMarker, markerFor(marker))
} else { } else {
t.window.CPrint(tui.ColCurrentSelectedEmpty, t.markerEmpty) t.window.CPrint(tui.ColCurrentSelectedEmpty, t.markerEmpty)
} }
} }
finalLineNum = t.printHighlighted(result, tui.ColCurrent, tui.ColCurrentMatch, true, true, line, maxLine, forceRedraw, preTask, postTask) finalLineNum = t.printHighlighted(result, tui.ColCurrent, tui.ColCurrentMatch, true, true, line, maxLine, forceRedraw, preTask, postTask)
} else { } else {
preTask := func(lineOffset int) { preTask := func(marker markerClass) {
if len(label) == 0 { if len(label) == 0 {
t.window.CPrint(tui.ColCursorEmpty, t.pointerEmpty) t.window.CPrint(tui.ColCursorEmpty, t.pointerEmpty)
} else { } else {
t.window.CPrint(tui.ColCursor, label) t.window.CPrint(tui.ColCursor, label)
} }
if selected { if selected {
t.window.CPrint(tui.ColMarker, t.marker) t.window.CPrint(tui.ColMarker, markerFor(marker))
} else { } else {
t.window.Print(t.markerEmpty) 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 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 var displayWidth int
item := result.item item := result.item
matchOffsets := []Offset{} matchOffsets := []Offset{}
@ -2096,12 +2121,16 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
finalLineNum := lineNum finalLineNum := lineNum
numItemLines := 1 numItemLines := 1
cutoff := 0 cutoff := 0
if t.multiLine && t.layout == layoutDefault { overflow := false
topCutoff := false
if t.multiLine {
maxLines := maxLineNum - lineNum + 1 maxLines := maxLineNum - lineNum + 1
numItemLines = item.text.NumLines(maxLines) numItemLines, overflow = item.text.NumLines(maxLines)
// Cut off the upper lines in the 'default' layout // Cut off the upper lines in the 'default' layout
if !current && maxLines == numItemLines { if t.layout == layoutDefault && !current && maxLines == numItemLines && overflow {
cutoff = item.text.NumLines(math.MaxInt32) - maxLines actualLines, _ := item.text.NumLines(math.MaxInt32)
cutoff = actualLines - maxLines
topCutoff = true
} }
} }
for lineOffset := 0; from <= len(text) && (lineNum <= maxLineNum || maxLineNum == 0); lineOffset++ { 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) t.move(actualLineNum, 0, forceRedraw)
if preTask != nil { 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) maxWidth := t.window.Width() - (t.pointerLen + t.markerLen + 1)
@ -4502,7 +4558,7 @@ func (t *Terminal) constrain() {
linesSum := 0 linesSum := 0
add := func(i int) bool { 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 linesSum += lines
if linesSum >= numItems { if linesSum >= numItems {
if numItemsFound == 0 { if numItemsFound == 0 {
@ -4547,13 +4603,14 @@ func (t *Terminal) constrain() {
numItems := t.merger.Length() numItems := t.merger.Length()
itemLines := 1 itemLines := 1
if t.multiLine && t.cy < numItems { 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 linesBefore := t.cy - newOffset
if t.multiLine { if t.multiLine {
linesBefore = 0 linesBefore = 0
for i := newOffset; i < t.cy && i < numItems; i++ { 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) linesAfter := maxLines - (linesBefore + itemLines)

View File

@ -75,18 +75,18 @@ func (chars *Chars) Bytes() []byte {
return chars.slice return chars.slice
} }
func (chars *Chars) NumLines(atMost int) int { func (chars *Chars) NumLines(atMost int) (int, bool) {
lines := 1 lines := 1
if runes := chars.optionalRunes(); runes != nil { if runes := chars.optionalRunes(); runes != nil {
for _, r := range runes { for _, r := range runes {
if r == '\n' { if r == '\n' {
lines++ lines++
} }
if lines >= atMost { if lines > atMost {
return atMost return atMost, true
} }
} }
return lines return lines, false
} }
for idx := 0; idx < len(chars.slice); idx++ { for idx := 0; idx < len(chars.slice); idx++ {
@ -97,11 +97,11 @@ func (chars *Chars) NumLines(atMost int) int {
idx += found idx += found
lines++ lines++
if lines >= atMost { if lines > atMost {
return atMost return atMost, true
} }
} }
return lines return lines, false
} }
func (chars *Chars) optionalRunes() []rune { func (chars *Chars) optionalRunes() []rune {

View File

@ -2752,8 +2752,9 @@ class TestGoFZF < TestBase
def assert_block(expected, lines) def assert_block(expected, lines)
cols = expected.lines.map(&:chomp).map(&:length).max cols = expected.lines.map(&:chomp).map(&:length).max
actual = lines.reverse.take(expected.lines.length).reverse.map { _1[0, cols].rstrip + "\n" }.join top = lines.take(expected.lines.length).map { _1[0, cols].rstrip + "\n" }.join
assert_equal_org expected, actual bottom = lines.reverse.take(expected.lines.length).reverse.map { _1[0, cols].rstrip + "\n" }.join
assert_includes [top, bottom], expected
end end
def test_height_range_fit def test_height_range_fit
@ -3268,6 +3269,55 @@ class TestGoFZF < TestBase
tmux.send_keys '99' tmux.send_keys '99'
tmux.until { |lines| assert(lines.any? { |line| line.include?('0 / 0') }) } tmux.until { |lines| assert(lines.any? { |line| line.include?('0 / 0') }) }
end 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 end
module TestShell module TestShell