Add --wrap option and 'toggle-wrap' action (#3887)

* `--wrap`
* `--wrap-sign`
* `toggle-wrap`

Close #3619
Close #2236
Close #577
Close #461
This commit is contained in:
Junegunn Choi
2024-06-25 17:08:47 +09:00
committed by GitHub
parent 724f8a1d45
commit 70bf8bc35d
7 changed files with 355 additions and 118 deletions

View File

@@ -155,6 +155,7 @@ type eachLine struct {
type itemLine struct {
firstLine int
numLines int
cy int
current bool
selected bool
@@ -215,6 +216,9 @@ type Terminal struct {
infoCommand string
infoStyle infoStyle
infoPrefix string
wrap bool
wrapSign string
wrapSignWidth int
separator labelPrinter
separatorLen int
spinner []string
@@ -446,6 +450,7 @@ const (
actToggleTrack
actToggleTrackCurrent
actToggleHeader
actToggleWrap
actTrackCurrent
actUntrackCurrent
actDown
@@ -787,6 +792,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
input: input,
multi: opts.Multi,
multiLine: opts.ReadZero && opts.MultiLine,
wrap: opts.Wrap,
sort: opts.Sort > 0,
toggleSort: opts.ToggleSort,
track: opts.Track,
@@ -876,8 +882,15 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
t.separator, t.separatorLen = t.ansiLabelPrinter(bar, &tui.ColSeparator, true)
}
if t.unicode {
t.wrapSign = "↳ "
t.borderWidth = uniseg.StringWidth("│")
} else {
t.wrapSign = "> "
}
if opts.WrapSign != nil {
t.wrapSign = *opts.WrapSign
}
t.wrapSign, t.wrapSignWidth = t.processTabs([]rune(t.wrapSign), 0)
if opts.Scrollbar == nil {
if t.unicode && t.borderWidth == 1 {
t.scrollbar = "│"
@@ -1067,8 +1080,11 @@ func (t *Terminal) parsePrompt(prompt string) (func(), int) {
}
output := func() {
line := t.promptLine()
wrap := t.wrap
t.wrap = false
t.printHighlighted(
Result{item: item}, tui.ColPrompt, tui.ColPrompt, false, false, line, line, true, nil, nil)
t.wrap = wrap
}
_, promptLen := t.processTabs([]rune(trimmed), 0)
@@ -1103,10 +1119,37 @@ func getScrollbar(perLine int, total int, height int, offset int) (int, int) {
return barLength, barStart
}
func (t *Terminal) wrapCols() int {
if !t.wrap {
return 0 // No wrap
}
return util.Max(t.window.Width()-(t.pointerLen+t.markerLen+1), 1)
}
func (t *Terminal) numItemLines(item *Item, atMost int) (int, bool) {
if !t.wrap && !t.multiLine {
return 1, false
}
if !t.wrap && t.multiLine {
return item.text.NumLines(atMost)
}
lines, overflow := item.text.Lines(t.multiLine, atMost, t.wrapCols(), t.wrapSignWidth, t.tabstop)
return len(lines), overflow
}
func (t *Terminal) itemLines(item *Item, atMost int) ([][]rune, bool) {
if !t.wrap && !t.multiLine {
text := make([]rune, item.text.Length())
copy(text, item.text.ToRunes())
return [][]rune{text}, false
}
return item.text.Lines(t.multiLine, atMost, t.wrapCols(), t.wrapSignWidth, t.tabstop)
}
// 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 {
if !t.wrap && !t.multiLine {
return 1
}
@@ -1116,8 +1159,8 @@ func (t *Terminal) avgNumLines() int {
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)
lines, _ := item.item.text.NumLines(maxItems)
result := t.merger.Get(idx + offset)
lines, _ := t.numItemLines(result.item, maxItems)
numLines += lines
count++
}
@@ -1964,6 +2007,9 @@ func (t *Terminal) printHeader() {
case layoutDefault, layoutReverseList:
needReverse = true
}
// Wrapping is not supported for header
wrap := t.wrap
t.wrap = false
for idx, lineStr := range append(append([]string{}, t.header0...), t.header...) {
line := idx
if needReverse && idx < len(t.header0) {
@@ -1988,6 +2034,7 @@ func (t *Terminal) printHeader() {
tui.ColHeader, tui.ColHeader, false, false, line, line, true,
func(markerClass) { t.window.Print(" ") }, nil)
}
t.wrap = wrap
}
func (t *Terminal) printList() {
@@ -2015,7 +2062,7 @@ func (t *Terminal) printList() {
// 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 {
if t.multiLine || t.wrap {
t.prevLines[line].hasBar = t.printBar(line, true, barRange)
}
}
@@ -2048,7 +2095,8 @@ func (t *Terminal) printItem(result Result, line int, maxLine int, index int, cu
}
// Avoid unnecessary redraw
newLine := itemLine{firstLine: line, cy: index + t.offset, current: current, selected: selected, label: label,
numLines, _ := t.numItemLines(item, maxLine-line+1)
newLine := itemLine{firstLine: line, numLines: numLines, 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
@@ -2057,37 +2105,46 @@ func (t *Terminal) printItem(result Result, line int, maxLine int, index int, cu
}
if !forceRedraw &&
prevLine.numLines == newLine.numLines &&
prevLine.current == newLine.current &&
prevLine.selected == newLine.selected &&
prevLine.label == newLine.label &&
prevLine.queryLen == newLine.queryLen &&
prevLine.result == newLine.result {
t.prevLines[line].hasBar = printBar(line, false)
if !t.multiLine {
if !t.multiLine && !t.wrap {
return line
}
lines, _ := item.text.NumLines(maxLine - line + 1)
return line + lines - 1
return line + numLines - 1
}
maxWidth := t.window.Width() - (t.pointerLen + t.markerLen + 1)
postTask := func(lineNum int, width int) {
postTask := func(lineNum int, width int, wrapped bool) {
if (current || selected) && t.highlightLine {
color := tui.ColSelected
if current {
color = tui.ColCurrent
}
fillSpaces := maxWidth - width
if wrapped {
fillSpaces -= t.wrapSignWidth
}
if fillSpaces > 0 {
t.window.CPrint(color, strings.Repeat(" ", fillSpaces))
}
newLine.width = maxWidth
} else {
fillSpaces := t.prevLines[lineNum].width - width
if wrapped {
fillSpaces -= t.wrapSignWidth
}
if fillSpaces > 0 {
t.window.Print(strings.Repeat(" ", fillSpaces))
}
newLine.width = width
if wrapped {
newLine.width += t.wrapSignWidth
}
}
// When width is 0, line is completely cleared. We need to redraw scrollbar
newLine.hasBar = printBar(lineNum, forceRedraw || width == 0)
@@ -2185,7 +2242,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(markerClass), 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, bool)) int {
var displayWidth int
item := result.item
matchOffsets := []Offset{}
@@ -2204,57 +2261,63 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
}
allOffsets := result.colorOffsets(charOffsets, t.theme, colBase, colMatch, current)
from := 0
text := make([]rune, item.text.Length())
copy(text, item.text.ToRunes())
maxLines := 1
if t.multiLine || t.wrap {
maxLines = maxLineNum - lineNum + 1
}
lines, overflow := t.itemLines(item, maxLines)
numItemLines := len(lines)
finalLineNum := lineNum
numItemLines := 1
cutoff := 0
overflow := false
topCutoff := false
if t.multiLine {
maxLines := maxLineNum - lineNum + 1
numItemLines, overflow = item.text.NumLines(maxLines)
wrapped := false
if t.multiLine || t.wrap {
// Cut off the upper lines in the 'default' layout
if t.layout == layoutDefault && !current && maxLines == numItemLines && overflow {
actualLines, _ := item.text.NumLines(math.MaxInt32)
cutoff = actualLines - maxLines
lines, _ = t.itemLines(item, math.MaxInt)
// To see if the first visible line is wrapped, we need to check the last cut-off line
prevLine := lines[len(lines)-maxLines-1]
if len(prevLine) == 0 || prevLine[len(prevLine)-1] != '\n' {
wrapped = true
}
lines = lines[len(lines)-maxLines:]
topCutoff = true
}
}
for lineOffset := 0; from <= len(text) && (lineNum <= maxLineNum || maxLineNum == 0); lineOffset++ {
from := 0
for lineOffset := 0; lineOffset < len(lines) && (lineNum <= maxLineNum || maxLineNum == 0); lineOffset++ {
line := lines[lineOffset]
finalLineNum = lineNum
line := text[from:]
if t.multiLine {
for idx, r := range text[from:] {
if r == '\n' {
line = line[:idx]
break
}
}
}
offsets := []colorOffset{}
for _, offset := range allOffsets {
if offset.offset[0] >= int32(from) && offset.offset[1] <= int32(from+len(line)) {
if offset.offset[0] >= int32(from+len(line)) {
allOffsets = allOffsets[len(offsets):]
break
}
if offset.offset[0] < int32(from) {
continue
}
if offset.offset[1] < int32(from+len(line)) {
offset.offset[0] -= int32(from)
offset.offset[1] -= int32(from)
offsets = append(offsets, offset)
} else {
allOffsets = allOffsets[len(offsets):]
dupe := offset
dupe.offset[0] = int32(from + len(line))
offset.offset[0] -= int32(from)
offset.offset[1] = int32(from + len(line))
offsets = append(offsets, offset)
allOffsets = append([]colorOffset{dupe}, allOffsets[len(offsets):]...)
break
}
}
from += len(line) + 1
if cutoff > 0 {
cutoff--
lineOffset--
continue
}
from += len(line)
var maxe int
for _, offset := range offsets {
@@ -2301,10 +2364,24 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
}
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))
wasWrapped := false
if wrapped {
maxWidth -= t.wrapSignWidth
t.window.CPrint(colBase.WithAttr(tui.Dim), t.wrapSign)
wrapped = false
wasWrapped = true
}
if len(line) > 0 && line[len(line)-1] == '\n' {
line = line[:len(line)-1]
} else {
wrapped = true
}
displayWidth = t.displayWidthWithLimit(line, 0, maxWidth)
if displayWidth > maxWidth {
if !t.wrap && displayWidth > maxWidth {
ellipsis, ellipsisWidth := util.Truncate(t.ellipsis, maxWidth/2)
maxe = util.Constrain(maxe+util.Min(maxWidth/2-ellipsisWidth, t.hscrollOff), 0, len(line))
transformOffsets := func(diff int32, rightTrim bool) {
for idx, offset := range offsets {
b, e := offset.offset[0], offset.offset[1]
@@ -2357,7 +2434,7 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
t.printColoredString(t.window, line, offsets, colBase)
if postTask != nil {
postTask(actualLineNum, displayWidth)
postTask(actualLineNum, displayWidth, wasWrapped)
} else {
t.markOtherLine(actualLineNum)
}
@@ -4369,6 +4446,9 @@ func (t *Terminal) Loop() error {
case actToggleHeader:
t.headerVisible = !t.headerVisible
req(reqList, reqInfo, reqPrompt, reqHeader)
case actToggleWrap:
t.wrap = !t.wrap
req(reqList, reqHeader)
case actTrackCurrent:
if t.track == trackDisabled {
t.track = trackCurrent
@@ -4751,12 +4831,12 @@ func (t *Terminal) constrain() {
for tries := 0; tries < maxLines; tries++ {
numItems := maxLines
// How many items can be fit on screen including the current item?
if t.multiLine && t.merger.Length() > 0 {
if (t.multiLine || t.wrap) && t.merger.Length() > 0 {
numItemsFound := 0
linesSum := 0
add := func(i int) bool {
lines, _ := t.merger.Get(i).item.text.NumLines(numItems - linesSum)
lines, _ := t.numItemLines(t.merger.Get(i).item, numItems-linesSum)
linesSum += lines
if linesSum >= numItems {
if numItemsFound == 0 {
@@ -4800,14 +4880,14 @@ func (t *Terminal) constrain() {
prevOffset := newOffset
numItems := t.merger.Length()
itemLines := 1
if t.multiLine && t.cy < numItems {
itemLines, _ = t.merger.Get(t.cy).item.text.NumLines(maxLines)
if (t.multiLine || t.wrap) && t.cy < numItems {
itemLines, _ = t.numItemLines(t.merger.Get(t.cy).item, maxLines)
}
linesBefore := t.cy - newOffset
if t.multiLine {
if t.multiLine || t.wrap {
linesBefore = 0
for i := newOffset; i < t.cy && i < numItems; i++ {
lines, _ := t.merger.Get(i).item.text.NumLines(maxLines - linesBefore - itemLines)
lines, _ := t.numItemLines(t.merger.Get(i).item, maxLines-linesBefore-itemLines)
linesBefore += lines
}
}