mirror of
https://github.com/Llewellynvdm/fzf.git
synced 2024-11-21 20:35:11 +00:00
Implement multi-line display of multi-line items
This commit is contained in:
parent
5b204c54f9
commit
04db44067d
@ -162,6 +162,9 @@ the details.
|
||||
.B "--cycle"
|
||||
Enable cyclic scroll
|
||||
.TP
|
||||
.B "--no-multi-line"
|
||||
Disable multi-line display of items when using \fB--read0\fR
|
||||
.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.
|
||||
@ -204,13 +207,19 @@ height minus the given value.
|
||||
fzf --height=-1
|
||||
|
||||
When prefixed with \fB~\fR, fzf will automatically determine the height in the
|
||||
range according to the input size. Note that adaptive height is not compatible
|
||||
with top/bottom margin and padding given in percent size. It is also not
|
||||
compatible with a negative height value.
|
||||
range according to the input size.
|
||||
|
||||
# Will not take up 100% of the screen
|
||||
seq 5 | fzf --height=~100%
|
||||
|
||||
Adaptive height has the following limitations:
|
||||
.br
|
||||
* Cannot be used with top/bottom margin and padding given in percent size
|
||||
.br
|
||||
* Negative value is not allowed
|
||||
.br
|
||||
* It will not find the right size when there are multi-line items
|
||||
|
||||
.TP
|
||||
.BI "--min-height=" "HEIGHT"
|
||||
Minimum height when \fB--height\fR is given in percent (default: 10).
|
||||
|
@ -45,6 +45,7 @@ const Usage = `usage: fzf [options]
|
||||
--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)
|
||||
@ -409,6 +410,7 @@ type Options struct {
|
||||
MinHeight int
|
||||
Layout layoutType
|
||||
Cycle bool
|
||||
MultiLine bool
|
||||
CursorLine bool
|
||||
KeepRight bool
|
||||
Hscroll bool
|
||||
@ -506,6 +508,7 @@ func defaultOptions() *Options {
|
||||
MinHeight: 10,
|
||||
Layout: layoutDefault,
|
||||
Cycle: false,
|
||||
MultiLine: true,
|
||||
KeepRight: false,
|
||||
Hscroll: true,
|
||||
HscrollOff: 10,
|
||||
@ -2062,6 +2065,10 @@ func parseOptions(opts *Options, allArgs []string) error {
|
||||
opts.CursorLine = false
|
||||
case "--no-cycle":
|
||||
opts.Cycle = false
|
||||
case "--multi-line":
|
||||
opts.MultiLine = true
|
||||
case "--no-multi-line":
|
||||
opts.MultiLine = false
|
||||
case "--keep-right":
|
||||
opts.KeepRight = true
|
||||
case "--no-keep-right":
|
||||
|
@ -15,6 +15,7 @@ type Offset [2]int32
|
||||
type colorOffset struct {
|
||||
offset [2]int32
|
||||
color tui.ColorPair
|
||||
match bool
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
@ -109,7 +110,7 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
|
||||
if len(itemColors) == 0 {
|
||||
var offsets []colorOffset
|
||||
for _, off := range matchOffsets {
|
||||
offsets = append(offsets, colorOffset{offset: [2]int32{off[0], off[1]}, color: colMatch})
|
||||
offsets = append(offsets, colorOffset{offset: [2]int32{off[0], off[1]}, color: colMatch, match: true})
|
||||
}
|
||||
return offsets
|
||||
}
|
||||
@ -193,12 +194,13 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
|
||||
}
|
||||
}
|
||||
colors = append(colors, colorOffset{
|
||||
offset: [2]int32{int32(start), int32(idx)}, color: color})
|
||||
offset: [2]int32{int32(start), int32(idx)}, color: color, match: true})
|
||||
} else {
|
||||
ansi := itemColors[curr-1]
|
||||
colors = append(colors, colorOffset{
|
||||
offset: [2]int32{int32(start), int32(idx)},
|
||||
color: ansiToColorPair(ansi, colBase)})
|
||||
color: ansiToColorPair(ansi, colBase),
|
||||
match: false})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
534
src/terminal.go
534
src/terminal.go
@ -148,14 +148,25 @@ type eachLine struct {
|
||||
}
|
||||
|
||||
type itemLine struct {
|
||||
offset int
|
||||
current bool
|
||||
selected bool
|
||||
label string
|
||||
queryLen int
|
||||
width int
|
||||
bar bool
|
||||
result Result
|
||||
firstLine int
|
||||
cy int
|
||||
current bool
|
||||
selected bool
|
||||
label string
|
||||
queryLen int
|
||||
width int
|
||||
hasBar bool
|
||||
result Result
|
||||
empty bool
|
||||
other bool
|
||||
}
|
||||
|
||||
func (t *Terminal) markEmptyLine(line int) {
|
||||
t.prevLines[line] = itemLine{firstLine: line, empty: true}
|
||||
}
|
||||
|
||||
func (t *Terminal) markOtherLine(line int) {
|
||||
t.prevLines[line] = itemLine{firstLine: line, other: true}
|
||||
}
|
||||
|
||||
type fitpad struct {
|
||||
@ -163,8 +174,6 @@ type fitpad struct {
|
||||
pad int
|
||||
}
|
||||
|
||||
var emptyLine = itemLine{}
|
||||
|
||||
type labelPrinter func(tui.Window, int)
|
||||
|
||||
type StatusItem struct {
|
||||
@ -224,6 +233,7 @@ type Terminal struct {
|
||||
yanked []rune
|
||||
input []rune
|
||||
multi int
|
||||
multiLine bool
|
||||
sort bool
|
||||
toggleSort bool
|
||||
track trackOption
|
||||
@ -739,6 +749,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
|
||||
yanked: []rune{},
|
||||
input: input,
|
||||
multi: opts.Multi,
|
||||
multiLine: opts.ReadZero && opts.MultiLine,
|
||||
sort: opts.Sort > 0,
|
||||
toggleSort: opts.ToggleSort,
|
||||
track: opts.Track,
|
||||
@ -1009,8 +1020,9 @@ func (t *Terminal) parsePrompt(prompt string) (func(), int) {
|
||||
}
|
||||
}
|
||||
output := func() {
|
||||
line := t.promptLine()
|
||||
t.printHighlighted(
|
||||
Result{item: item}, tui.ColPrompt, tui.ColPrompt, false, false)
|
||||
Result{item: item}, tui.ColPrompt, tui.ColPrompt, false, false, line, line, true, nil, nil)
|
||||
}
|
||||
_, promptLen := t.processTabs([]rune(trimmed), 0)
|
||||
|
||||
@ -1031,22 +1043,45 @@ func (t *Terminal) noSeparatorLine() bool {
|
||||
return noSeparatorLine(t.infoStyle, t.separatorLen > 0)
|
||||
}
|
||||
|
||||
func getScrollbar(total int, height int, offset int) (int, int) {
|
||||
if total == 0 || total <= height {
|
||||
func getScrollbar(perLine int, total int, height int, offset int) (int, int) {
|
||||
if total == 0 || total*perLine <= height {
|
||||
return 0, 0
|
||||
}
|
||||
barLength := util.Max(1, height*height/total)
|
||||
barLength := util.Max(1, height*height/(total*perLine))
|
||||
var barStart int
|
||||
if total == height {
|
||||
barStart = 0
|
||||
} else {
|
||||
barStart = (height - barLength) * offset / (total - height)
|
||||
barStart = util.Min(height-barLength, (height*perLine-barLength)*offset/(total*perLine-height))
|
||||
}
|
||||
return barLength, barStart
|
||||
}
|
||||
|
||||
// Estimate the average number of lines per item. Instead of going through all
|
||||
// items, we only check a few items around the current cursor position.
|
||||
func (t *Terminal) avgNumLines() int {
|
||||
if !t.multiLine {
|
||||
return 1
|
||||
}
|
||||
|
||||
maxItems := t.maxItems()
|
||||
numLines := 0
|
||||
count := 0
|
||||
total := t.merger.Length()
|
||||
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)
|
||||
count++
|
||||
}
|
||||
if count == 0 {
|
||||
return 1
|
||||
}
|
||||
return numLines / count
|
||||
}
|
||||
|
||||
func (t *Terminal) getScrollbar() (int, int) {
|
||||
return getScrollbar(t.merger.Length(), t.maxItems(), t.offset)
|
||||
return getScrollbar(t.avgNumLines(), t.merger.Length(), t.maxItems(), t.offset)
|
||||
}
|
||||
|
||||
// Input returns current query string
|
||||
@ -1622,7 +1657,6 @@ func (t *Terminal) placeCursor() {
|
||||
}
|
||||
|
||||
func (t *Terminal) printPrompt() {
|
||||
t.move(t.promptLine(), 0, true)
|
||||
t.prompt()
|
||||
|
||||
before, after := t.updatePromptOffset()
|
||||
@ -1645,6 +1679,10 @@ func (t *Terminal) trimMessage(message string, maxWidth int) string {
|
||||
func (t *Terminal) printInfo() {
|
||||
pos := 0
|
||||
line := t.promptLine()
|
||||
move := func(y int, x int, clear bool) {
|
||||
t.move(y, x, clear)
|
||||
t.markOtherLine(y)
|
||||
}
|
||||
printSpinner := func() {
|
||||
if t.reading {
|
||||
duration := int64(spinnerDuration)
|
||||
@ -1663,7 +1701,7 @@ func (t *Terminal) printInfo() {
|
||||
str = string(trimmed)
|
||||
width = maxWidth
|
||||
}
|
||||
t.move(line, pos, t.separatorLen == 0)
|
||||
move(line, pos, t.separatorLen == 0)
|
||||
if t.reading {
|
||||
t.window.CPrint(tui.ColSpinner, str)
|
||||
} else {
|
||||
@ -1672,7 +1710,6 @@ func (t *Terminal) printInfo() {
|
||||
pos += width
|
||||
}
|
||||
printSeparator := func(fillLength int, pad bool) {
|
||||
// --------_
|
||||
if t.separatorLen > 0 {
|
||||
t.separator(t.window, fillLength)
|
||||
t.window.Print(" ")
|
||||
@ -1682,12 +1719,12 @@ func (t *Terminal) printInfo() {
|
||||
}
|
||||
switch t.infoStyle {
|
||||
case infoDefault:
|
||||
t.move(line+1, 0, t.separatorLen == 0)
|
||||
move(line+1, 0, t.separatorLen == 0)
|
||||
printSpinner()
|
||||
t.window.Print(" ") // Margin
|
||||
pos = 2
|
||||
case infoRight:
|
||||
t.move(line+1, 0, false)
|
||||
move(line+1, 0, false)
|
||||
case infoInlineRight:
|
||||
pos = t.promptLen + t.queryLen[0] + t.queryLen[1] + 1
|
||||
case infoInline:
|
||||
@ -1695,7 +1732,7 @@ func (t *Terminal) printInfo() {
|
||||
printInfoPrefix()
|
||||
case infoHidden:
|
||||
if t.separatorLen > 0 {
|
||||
t.move(line+1, 0, false)
|
||||
move(line+1, 0, false)
|
||||
printSeparator(t.window.Width()-1, false)
|
||||
}
|
||||
return
|
||||
@ -1755,7 +1792,7 @@ func (t *Terminal) printInfo() {
|
||||
|
||||
if t.infoStyle == infoInlineRight {
|
||||
if len(t.infoPrefix) == 0 {
|
||||
t.move(line, pos, false)
|
||||
move(line, pos, false)
|
||||
newPos := util.Max(pos, t.window.Width()-util.StringWidth(output)-3)
|
||||
t.window.Print(strings.Repeat(" ", newPos-pos))
|
||||
pos = newPos
|
||||
@ -1779,7 +1816,7 @@ func (t *Terminal) printInfo() {
|
||||
|
||||
if t.infoStyle == infoInlineRight {
|
||||
if t.separatorLen > 0 {
|
||||
t.move(line+1, 0, false)
|
||||
move(line+1, 0, false)
|
||||
printSeparator(t.window.Width()-1, false)
|
||||
}
|
||||
return
|
||||
@ -1829,10 +1866,9 @@ func (t *Terminal) printHeader() {
|
||||
text: util.ToChars([]byte(trimmed)),
|
||||
colors: colors}
|
||||
|
||||
t.move(line, 0, true)
|
||||
t.window.Print(" ")
|
||||
t.printHighlighted(Result{item: item},
|
||||
tui.ColHeader, tui.ColHeader, false, false)
|
||||
tui.ColHeader, tui.ColHeader, false, false, line, line, true,
|
||||
func(int) { t.window.Print(" ") }, nil)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1840,53 +1876,66 @@ func (t *Terminal) printList() {
|
||||
t.constrain()
|
||||
barLength, barStart := t.getScrollbar()
|
||||
|
||||
maxy := t.maxItems()
|
||||
maxy := t.maxItems() - 1
|
||||
count := t.merger.Length() - t.offset
|
||||
for j := 0; j < maxy; j++ {
|
||||
i := j
|
||||
if t.layout == layoutDefault {
|
||||
i = maxy - 1 - j
|
||||
}
|
||||
line := i + 2 + t.visibleHeaderLines()
|
||||
if t.noSeparatorLine() {
|
||||
line--
|
||||
}
|
||||
if i < count {
|
||||
t.printItem(t.merger.Get(i+t.offset), line, i, i == t.cy-t.offset, i >= barStart && i < barStart+barLength)
|
||||
} else if t.prevLines[i] != emptyLine || t.prevLines[i].offset != line {
|
||||
t.prevLines[i] = emptyLine
|
||||
|
||||
// Start line
|
||||
startLine := 2 + t.visibleHeaderLines()
|
||||
if t.noSeparatorLine() {
|
||||
startLine--
|
||||
}
|
||||
maxy += startLine
|
||||
|
||||
barRange := [2]int{startLine + barStart, startLine + barStart + barLength}
|
||||
for line, itemCount := startLine, 0; line <= maxy; line, itemCount = line+1, itemCount+1 {
|
||||
if itemCount < count {
|
||||
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.prevLines[line].hasBar = t.printBar(line, true, barRange)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Terminal) printItem(result Result, line int, i int, current bool, bar bool) {
|
||||
func (t *Terminal) printBar(lineNum int, forceRedraw bool, barRange [2]int) bool {
|
||||
hasBar := lineNum >= barRange[0] && lineNum < barRange[1]
|
||||
if len(t.scrollbar) > 0 && (hasBar != t.prevLines[lineNum].hasBar || forceRedraw) {
|
||||
t.move(lineNum, t.window.Width()-1, true)
|
||||
if hasBar {
|
||||
t.window.CPrint(tui.ColScrollbar, t.scrollbar)
|
||||
}
|
||||
}
|
||||
return hasBar
|
||||
}
|
||||
|
||||
func (t *Terminal) printItem(result Result, line int, maxLine int, index int, current bool, barRange [2]int) int {
|
||||
item := result.item
|
||||
_, selected := t.selected[item.Index()]
|
||||
label := ""
|
||||
if t.jumping != jumpDisabled {
|
||||
if i < len(t.jumpLabels) {
|
||||
if index < len(t.jumpLabels) {
|
||||
// Striped
|
||||
current = i%2 == 0
|
||||
label = t.jumpLabels[i:i+1] + strings.Repeat(" ", t.pointerLen-1)
|
||||
current = index%2 == 0
|
||||
label = t.jumpLabels[index:index+1] + strings.Repeat(" ", t.pointerLen-1)
|
||||
}
|
||||
} else if current {
|
||||
label = t.pointer
|
||||
}
|
||||
|
||||
// Avoid unnecessary redraw
|
||||
newLine := itemLine{offset: line, current: current, selected: selected, label: label,
|
||||
result: result, queryLen: len(t.input), width: 0, bar: bar}
|
||||
prevLine := t.prevLines[i]
|
||||
forceRedraw := prevLine.offset != newLine.offset
|
||||
printBar := func() {
|
||||
if len(t.scrollbar) > 0 && (bar != prevLine.bar || forceRedraw) {
|
||||
t.prevLines[i].bar = bar
|
||||
t.move(line, t.window.Width()-1, true)
|
||||
if bar {
|
||||
t.window.CPrint(tui.ColScrollbar, t.scrollbar)
|
||||
}
|
||||
}
|
||||
newLine := itemLine{firstLine: line, cy: index + t.offset, current: current, selected: selected, label: label,
|
||||
result: result, queryLen: len(t.input), width: 0, hasBar: line >= barRange[0] && line < barRange[1]}
|
||||
prevLine := t.prevLines[line]
|
||||
forceRedraw := prevLine.other || prevLine.firstLine != newLine.firstLine
|
||||
printBar := func(lineNum int, forceRedraw bool) bool {
|
||||
return t.printBar(lineNum, forceRedraw, barRange)
|
||||
}
|
||||
|
||||
if !forceRedraw &&
|
||||
@ -1895,33 +1944,64 @@ func (t *Terminal) printItem(result Result, line int, i int, current bool, bar b
|
||||
prevLine.label == newLine.label &&
|
||||
prevLine.queryLen == newLine.queryLen &&
|
||||
prevLine.result == newLine.result {
|
||||
printBar()
|
||||
return
|
||||
t.prevLines[line].hasBar = printBar(line, false)
|
||||
if !t.multiLine {
|
||||
return line
|
||||
}
|
||||
return line + item.text.NumLines(maxLine-line+1) - 1
|
||||
}
|
||||
|
||||
t.move(line, 0, forceRedraw)
|
||||
maxWidth := t.window.Width() - (t.pointerLen + t.markerLen + 1)
|
||||
postTask := func(lineNum int, width int) {
|
||||
if (current || selected) && t.highlightLine {
|
||||
color := tui.ColSelected
|
||||
if current {
|
||||
color = tui.ColCurrent
|
||||
}
|
||||
fillSpaces := maxWidth - width
|
||||
if fillSpaces > 0 {
|
||||
t.window.CPrint(color, strings.Repeat(" ", fillSpaces))
|
||||
}
|
||||
newLine.width = maxWidth
|
||||
} else {
|
||||
fillSpaces := t.prevLines[lineNum].width - width
|
||||
if fillSpaces > 0 {
|
||||
t.window.Print(strings.Repeat(" ", fillSpaces))
|
||||
}
|
||||
newLine.width = width
|
||||
}
|
||||
// When width is 0, line is completely cleared. We need to redraw scrollbar
|
||||
newLine.hasBar = printBar(lineNum, forceRedraw || width == 0)
|
||||
t.prevLines[lineNum] = newLine
|
||||
}
|
||||
|
||||
var finalLineNum int
|
||||
if current {
|
||||
if len(label) == 0 {
|
||||
t.window.CPrint(tui.ColCurrentCursorEmpty, t.pointerEmpty)
|
||||
} else {
|
||||
t.window.CPrint(tui.ColCurrentCursor, label)
|
||||
preTask := func(lineOffset int) {
|
||||
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)
|
||||
} else {
|
||||
t.window.CPrint(tui.ColCurrentSelectedEmpty, t.markerEmpty)
|
||||
}
|
||||
}
|
||||
if selected {
|
||||
t.window.CPrint(tui.ColCurrentMarker, t.marker)
|
||||
} else {
|
||||
t.window.CPrint(tui.ColCurrentSelectedEmpty, t.markerEmpty)
|
||||
}
|
||||
newLine.width = t.printHighlighted(result, tui.ColCurrent, tui.ColCurrentMatch, true, true)
|
||||
finalLineNum = t.printHighlighted(result, tui.ColCurrent, tui.ColCurrentMatch, true, true, line, maxLine, forceRedraw, preTask, postTask)
|
||||
} else {
|
||||
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)
|
||||
} else {
|
||||
t.window.Print(t.markerEmpty)
|
||||
preTask := func(lineOffset int) {
|
||||
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)
|
||||
} else {
|
||||
t.window.Print(t.markerEmpty)
|
||||
}
|
||||
}
|
||||
var base, match tui.ColorPair
|
||||
if selected {
|
||||
@ -1931,27 +2011,9 @@ func (t *Terminal) printItem(result Result, line int, i int, current bool, bar b
|
||||
base = tui.ColNormal
|
||||
match = tui.ColMatch
|
||||
}
|
||||
newLine.width = t.printHighlighted(result, base, match, false, true)
|
||||
finalLineNum = t.printHighlighted(result, base, match, false, true, line, maxLine, forceRedraw, preTask, postTask)
|
||||
}
|
||||
if (current || selected) && t.highlightLine {
|
||||
color := tui.ColSelected
|
||||
if current {
|
||||
color = tui.ColCurrent
|
||||
}
|
||||
maxWidth := t.window.Width() - (t.pointerLen + t.markerLen + 1)
|
||||
fillSpaces := maxWidth - newLine.width
|
||||
newLine.width = maxWidth
|
||||
if fillSpaces > 0 {
|
||||
t.window.CPrint(color, strings.Repeat(" ", fillSpaces))
|
||||
}
|
||||
} else {
|
||||
fillSpaces := prevLine.width - newLine.width
|
||||
if fillSpaces > 0 {
|
||||
t.window.Print(strings.Repeat(" ", fillSpaces))
|
||||
}
|
||||
}
|
||||
printBar()
|
||||
t.prevLines[i] = newLine
|
||||
return finalLineNum
|
||||
}
|
||||
|
||||
func (t *Terminal) trimRight(runes []rune, width int) ([]rune, bool) {
|
||||
@ -1992,12 +2054,9 @@ 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) 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(int), postTask func(int, int)) int {
|
||||
var displayWidth int
|
||||
item := result.item
|
||||
|
||||
// Overflow
|
||||
text := make([]rune, item.text.Length())
|
||||
copy(text, item.text.ToRunes())
|
||||
matchOffsets := []Offset{}
|
||||
var pos *[]int
|
||||
if match && t.merger.pattern != nil {
|
||||
@ -2012,69 +2071,138 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
|
||||
}
|
||||
sort.Sort(ByOrder(charOffsets))
|
||||
}
|
||||
var maxe int
|
||||
for _, offset := range charOffsets {
|
||||
maxe = util.Max(maxe, int(offset[1]))
|
||||
}
|
||||
allOffsets := result.colorOffsets(charOffsets, t.theme, colBase, colMatch, current)
|
||||
|
||||
offsets := result.colorOffsets(charOffsets, t.theme, colBase, colMatch, current)
|
||||
maxWidth := t.window.Width() - (t.pointerLen + t.markerLen + 1)
|
||||
ellipsis, ellipsisWidth := util.Truncate(t.ellipsis, maxWidth/2)
|
||||
maxe = util.Constrain(maxe+util.Min(maxWidth/2-ellipsisWidth, t.hscrollOff), 0, len(text))
|
||||
displayWidth := t.displayWidthWithLimit(text, 0, maxWidth)
|
||||
if displayWidth > maxWidth {
|
||||
transformOffsets := func(diff int32, rightTrim bool) {
|
||||
for idx, offset := range offsets {
|
||||
b, e := offset.offset[0], offset.offset[1]
|
||||
el := int32(len(ellipsis))
|
||||
b += el - diff
|
||||
e += el - diff
|
||||
b = util.Max32(b, el)
|
||||
if rightTrim {
|
||||
e = util.Min32(e, int32(maxWidth-ellipsisWidth))
|
||||
from := 0
|
||||
text := make([]rune, item.text.Length())
|
||||
copy(text, item.text.ToRunes())
|
||||
|
||||
finalLineNum := lineNum
|
||||
numItemLines := 1
|
||||
cutoff := 0
|
||||
if t.multiLine && t.layout == layoutDefault {
|
||||
maxLines := maxLineNum - lineNum + 1
|
||||
numItemLines = item.text.NumLines(maxLines)
|
||||
// Cut off the upper lines in the 'default' layout
|
||||
if !current && maxLines == numItemLines {
|
||||
cutoff = item.text.NumLines(math.MaxInt32) - maxLines
|
||||
}
|
||||
}
|
||||
for lineOffset := 0; from <= len(text) && (lineNum <= maxLineNum || maxLineNum == 0); lineOffset++ {
|
||||
finalLineNum = lineNum
|
||||
|
||||
line := text[from:]
|
||||
if t.multiLine {
|
||||
for idx, r := range text[from:] {
|
||||
if r == '\n' {
|
||||
line = line[:idx]
|
||||
break
|
||||
}
|
||||
offsets[idx].offset[0] = b
|
||||
offsets[idx].offset[1] = util.Max32(b, e)
|
||||
}
|
||||
}
|
||||
if t.hscroll {
|
||||
if t.keepRight && pos == nil {
|
||||
trimmed, diff := t.trimLeft(text, maxWidth-ellipsisWidth)
|
||||
transformOffsets(diff, false)
|
||||
text = append(ellipsis, trimmed...)
|
||||
} else if !t.overflow(text[:maxe], maxWidth-ellipsisWidth) {
|
||||
// Stri..
|
||||
text, _ = t.trimRight(text, maxWidth-ellipsisWidth)
|
||||
text = append(text, ellipsis...)
|
||||
|
||||
offsets := []colorOffset{}
|
||||
for _, offset := range allOffsets {
|
||||
if offset.offset[0] >= int32(from) && offset.offset[1] <= int32(from+len(line)) {
|
||||
offset.offset[0] -= int32(from)
|
||||
offset.offset[1] -= int32(from)
|
||||
offsets = append(offsets, offset)
|
||||
} else {
|
||||
// Stri..
|
||||
rightTrim := false
|
||||
if t.overflow(text[maxe:], ellipsisWidth) {
|
||||
text = append(text[:maxe], ellipsis...)
|
||||
rightTrim = true
|
||||
}
|
||||
// ..ri..
|
||||
var diff int32
|
||||
text, diff = t.trimLeft(text, maxWidth-ellipsisWidth)
|
||||
|
||||
// Transform offsets
|
||||
transformOffsets(diff, rightTrim)
|
||||
text = append(ellipsis, text...)
|
||||
}
|
||||
} else {
|
||||
text, _ = t.trimRight(text, maxWidth-ellipsisWidth)
|
||||
text = append(text, ellipsis...)
|
||||
|
||||
for idx, offset := range offsets {
|
||||
offsets[idx].offset[0] = util.Min32(offset.offset[0], int32(maxWidth-len(ellipsis)))
|
||||
offsets[idx].offset[1] = util.Min32(offset.offset[1], int32(maxWidth))
|
||||
allOffsets = allOffsets[len(offsets):]
|
||||
break
|
||||
}
|
||||
}
|
||||
displayWidth = t.displayWidthWithLimit(text, 0, displayWidth)
|
||||
|
||||
from += len(line) + 1
|
||||
|
||||
if cutoff > 0 {
|
||||
cutoff--
|
||||
lineOffset--
|
||||
continue
|
||||
}
|
||||
|
||||
var maxe int
|
||||
for _, offset := range offsets {
|
||||
if offset.match {
|
||||
maxe = util.Max(maxe, int(offset.offset[1]))
|
||||
}
|
||||
}
|
||||
|
||||
actualLineNum := lineNum
|
||||
if t.layout == layoutDefault {
|
||||
actualLineNum = (lineNum - lineOffset) + (numItemLines - lineOffset) - 1
|
||||
}
|
||||
t.move(actualLineNum, 0, forceRedraw)
|
||||
|
||||
if preTask != nil {
|
||||
preTask(lineOffset)
|
||||
}
|
||||
|
||||
maxWidth := t.window.Width() - (t.pointerLen + t.markerLen + 1)
|
||||
ellipsis, ellipsisWidth := util.Truncate(t.ellipsis, maxWidth/2)
|
||||
maxe = util.Constrain(maxe+util.Min(maxWidth/2-ellipsisWidth, t.hscrollOff), 0, len(line))
|
||||
displayWidth = t.displayWidthWithLimit(line, 0, maxWidth)
|
||||
if displayWidth > maxWidth {
|
||||
transformOffsets := func(diff int32, rightTrim bool) {
|
||||
for idx, offset := range offsets {
|
||||
b, e := offset.offset[0], offset.offset[1]
|
||||
el := int32(len(ellipsis))
|
||||
b += el - diff
|
||||
e += el - diff
|
||||
b = util.Max32(b, el)
|
||||
if rightTrim {
|
||||
e = util.Min32(e, int32(maxWidth-ellipsisWidth))
|
||||
}
|
||||
offsets[idx].offset[0] = b
|
||||
offsets[idx].offset[1] = util.Max32(b, e)
|
||||
}
|
||||
}
|
||||
if t.hscroll {
|
||||
if t.keepRight && pos == nil {
|
||||
trimmed, diff := t.trimLeft(line, maxWidth-ellipsisWidth)
|
||||
transformOffsets(diff, false)
|
||||
line = append(ellipsis, trimmed...)
|
||||
} else if !t.overflow(line[:maxe], maxWidth-ellipsisWidth) {
|
||||
// Stri..
|
||||
line, _ = t.trimRight(line, maxWidth-ellipsisWidth)
|
||||
line = append(line, ellipsis...)
|
||||
} else {
|
||||
// Stri..
|
||||
rightTrim := false
|
||||
if t.overflow(line[maxe:], ellipsisWidth) {
|
||||
line = append(line[:maxe], ellipsis...)
|
||||
rightTrim = true
|
||||
}
|
||||
// ..ri..
|
||||
var diff int32
|
||||
line, diff = t.trimLeft(line, maxWidth-ellipsisWidth)
|
||||
|
||||
// Transform offsets
|
||||
transformOffsets(diff, rightTrim)
|
||||
line = append(ellipsis, line...)
|
||||
}
|
||||
} else {
|
||||
line, _ = t.trimRight(line, maxWidth-ellipsisWidth)
|
||||
line = append(line, ellipsis...)
|
||||
|
||||
for idx, offset := range offsets {
|
||||
offsets[idx].offset[0] = util.Min32(offset.offset[0], int32(maxWidth-len(ellipsis)))
|
||||
offsets[idx].offset[1] = util.Min32(offset.offset[1], int32(maxWidth))
|
||||
}
|
||||
}
|
||||
displayWidth = t.displayWidthWithLimit(line, 0, displayWidth)
|
||||
}
|
||||
|
||||
t.printColoredString(t.window, line, offsets, colBase)
|
||||
if postTask != nil {
|
||||
postTask(actualLineNum, displayWidth)
|
||||
} else {
|
||||
t.markOtherLine(actualLineNum)
|
||||
}
|
||||
lineNum += 1
|
||||
}
|
||||
|
||||
t.printColoredString(t.window, text, offsets, colBase)
|
||||
return displayWidth
|
||||
return finalLineNum
|
||||
}
|
||||
|
||||
func (t *Terminal) printColoredString(window tui.Window, text []rune, offsets []colorOffset, colBase tui.ColorPair) {
|
||||
@ -2172,7 +2300,7 @@ func (t *Terminal) renderPreviewArea(unchanged bool) {
|
||||
}
|
||||
|
||||
effectiveHeight := height - headerLines
|
||||
barLength, barStart := getScrollbar(len(body), effectiveHeight, util.Min(len(body)-effectiveHeight, t.previewer.offset-headerLines))
|
||||
barLength, barStart := getScrollbar(1, len(body), effectiveHeight, util.Min(len(body)-effectiveHeight, t.previewer.offset-headerLines))
|
||||
t.renderPreviewScrollbar(headerLines, barLength, barStart)
|
||||
}
|
||||
|
||||
@ -4022,7 +4150,7 @@ func (t *Terminal) Loop() error {
|
||||
if pbarDragging {
|
||||
effectiveHeight := t.pwindow.Height() - headerLines
|
||||
numLines := len(t.previewer.lines) - headerLines
|
||||
barLength, _ := getScrollbar(numLines, effectiveHeight, util.Min(numLines-effectiveHeight, t.previewer.offset-headerLines))
|
||||
barLength, _ := getScrollbar(1, numLines, effectiveHeight, util.Min(numLines-effectiveHeight, t.previewer.offset-headerLines))
|
||||
if barLength > 0 {
|
||||
y := my - t.pwindow.Top() - headerLines - barLength/2
|
||||
y = util.Constrain(y, 0, effectiveHeight-barLength)
|
||||
@ -4068,7 +4196,8 @@ func (t *Terminal) Loop() error {
|
||||
total := t.merger.Length()
|
||||
prevOffset := t.offset
|
||||
// barStart = (maxItems - barLength) * t.offset / (total - maxItems)
|
||||
t.offset = int(math.Ceil(float64(newBarStart) * float64(total-maxItems) / float64(maxItems-barLength)))
|
||||
perLine := t.avgNumLines()
|
||||
t.offset = int(math.Ceil(float64(newBarStart) * float64(total*perLine-maxItems) / float64(maxItems*perLine-barLength)))
|
||||
t.cy = t.offset + t.cy - prevOffset
|
||||
req(reqList)
|
||||
}
|
||||
@ -4076,11 +4205,18 @@ func (t *Terminal) Loop() error {
|
||||
break
|
||||
}
|
||||
|
||||
// There can be empty lines after the list in multi-line mode
|
||||
prevLine := t.prevLines[my]
|
||||
if prevLine.empty {
|
||||
break
|
||||
}
|
||||
|
||||
// Double-click on an item
|
||||
cy := prevLine.cy
|
||||
if me.Double && mx < t.window.Width()-1 {
|
||||
// Double-click
|
||||
if my >= min {
|
||||
if t.vset(t.offset+my-min) && t.cy < t.merger.Length() {
|
||||
if t.vset(cy) && t.cy < t.merger.Length() {
|
||||
return doActions(actionsFor(tui.DoubleClick))
|
||||
}
|
||||
}
|
||||
@ -4093,7 +4229,7 @@ func (t *Terminal) Loop() error {
|
||||
// Prompt
|
||||
t.cx = mxCons + t.xoffset
|
||||
} else if my >= min {
|
||||
t.vset(t.offset + my - min)
|
||||
t.vset(cy)
|
||||
req(reqList)
|
||||
evt := tui.RightClick
|
||||
if me.Mod {
|
||||
@ -4312,26 +4448,66 @@ func (t *Terminal) Loop() error {
|
||||
func (t *Terminal) constrain() {
|
||||
// count of items to display allowed by filtering
|
||||
count := t.merger.Length()
|
||||
// count of lines can be displayed
|
||||
height := t.maxItems()
|
||||
maxItems := t.maxItems()
|
||||
|
||||
t.cy = util.Constrain(t.cy, 0, util.Max(0, count-1))
|
||||
// May need to try again after adjusting the offset
|
||||
for tries := 0; tries < maxItems; tries++ {
|
||||
height := maxItems
|
||||
// How many items can be fit on screen including the current item?
|
||||
if t.multiLine && t.merger.Length() > 0 {
|
||||
actualHeight := 0
|
||||
linesSum := 0
|
||||
|
||||
minOffset := util.Max(t.cy-height+1, 0)
|
||||
maxOffset := util.Max(util.Min(count-height, t.cy), 0)
|
||||
t.offset = util.Constrain(t.offset, minOffset, maxOffset)
|
||||
if t.scrollOff == 0 {
|
||||
return
|
||||
}
|
||||
add := func(i int) bool {
|
||||
lines := t.merger.Get(i).item.text.NumLines(height - linesSum)
|
||||
linesSum += lines
|
||||
if linesSum >= height {
|
||||
if actualHeight == 0 {
|
||||
actualHeight = 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
actualHeight++
|
||||
return true
|
||||
}
|
||||
|
||||
scrollOff := util.Min(height/2, t.scrollOff)
|
||||
for {
|
||||
prevOffset := t.offset
|
||||
if t.cy-t.offset < scrollOff {
|
||||
t.offset = util.Max(minOffset, t.offset-1)
|
||||
for i := t.offset; i < t.merger.Length(); i++ {
|
||||
if !add(i) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// We can possibly fit more items "before" the offset on screen
|
||||
if linesSum < height {
|
||||
for i := t.offset - 1; i >= 0; i-- {
|
||||
if !add(i) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
height = actualHeight
|
||||
}
|
||||
if t.cy-t.offset >= height-scrollOff {
|
||||
t.offset = util.Min(maxOffset, t.offset+1)
|
||||
|
||||
t.cy = util.Constrain(t.cy, 0, util.Max(0, count-1))
|
||||
minOffset := util.Max(t.cy-height+1, 0)
|
||||
maxOffset := util.Max(util.Min(count-height, t.cy), 0)
|
||||
prevOffset := t.offset
|
||||
t.offset = util.Constrain(t.offset, minOffset, maxOffset)
|
||||
if t.scrollOff > 0 {
|
||||
scrollOff := util.Min(height/2, t.scrollOff)
|
||||
for {
|
||||
prevOffset := t.offset
|
||||
if t.cy-t.offset < scrollOff {
|
||||
t.offset = util.Max(minOffset, t.offset-1)
|
||||
}
|
||||
if t.cy-t.offset >= height-scrollOff {
|
||||
t.offset = util.Min(maxOffset, t.offset+1)
|
||||
}
|
||||
if t.offset == prevOffset {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if t.offset == prevOffset {
|
||||
break
|
||||
|
@ -1,6 +1,7 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
@ -74,6 +75,35 @@ func (chars *Chars) Bytes() []byte {
|
||||
return chars.slice
|
||||
}
|
||||
|
||||
func (chars *Chars) NumLines(atMost int) int {
|
||||
lines := 1
|
||||
if runes := chars.optionalRunes(); runes != nil {
|
||||
for _, r := range runes {
|
||||
if r == '\n' {
|
||||
lines++
|
||||
}
|
||||
if lines >= atMost {
|
||||
return atMost
|
||||
}
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
for idx := 0; idx < len(chars.slice); idx++ {
|
||||
found := bytes.IndexByte(chars.slice[idx:], '\n')
|
||||
if found < 0 {
|
||||
break
|
||||
}
|
||||
|
||||
idx += found
|
||||
lines++
|
||||
if lines >= atMost {
|
||||
return atMost
|
||||
}
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
func (chars *Chars) optionalRunes() []rune {
|
||||
if chars.inBytes {
|
||||
return nil
|
||||
|
@ -2128,7 +2128,7 @@ class TestGoFZF < TestBase
|
||||
end
|
||||
|
||||
def test_keep_right
|
||||
tmux.send_keys "seq 10000 | #{FZF} --read0 --keep-right", :Enter
|
||||
tmux.send_keys "seq 10000 | #{FZF} --read0 --keep-right --no-multi-line", :Enter
|
||||
tmux.until { |lines| assert lines.any_include?('9999␊10000') }
|
||||
end
|
||||
|
||||
@ -3398,7 +3398,7 @@ module TestShell
|
||||
|
||||
def test_ctrl_r_multiline
|
||||
# NOTE: Current bash implementation shows an extra new line if there's
|
||||
# only enty in the history
|
||||
# only entry in the history
|
||||
tmux.send_keys ':', :Enter
|
||||
tmux.send_keys 'echo "foo', :Enter, 'bar"', :Enter
|
||||
tmux.until { |lines| assert_equal %w[foo bar], lines[-2..] }
|
||||
|
Loading…
Reference in New Issue
Block a user