summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJunegunn Choi <junegunn.c@gmail.com>2024-05-27 01:20:56 +0900
committerJunegunn Choi <junegunn.c@gmail.com>2024-05-27 01:35:05 +0900
commit2f51eb2b414f3e27f0d3ab3f4ef1a3f3a48c6d06 (patch)
tree6ad0a17d17eaa2dd18d2575375ed9d22cecd2337
parent0ccbd79e10813f7fb67cd29687203ef3cd0d5692 (diff)
Different marker for the first and last line of multi-line entries
Can be configured via `--marker-multi-line`
-rw-r--r--CHANGELOG.md6
-rw-r--r--man/man1/fzf.14
-rw-r--r--src/options.go297
-rw-r--r--src/terminal.go89
-rw-r--r--src/util/chars.go14
-rwxr-xr-xtest/test_go.rb54
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