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