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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 355 additions and 118 deletions

View File

@ -3,6 +3,16 @@ CHANGELOG
0.54.0 0.54.0
------ ------
- Implemented line wrap of long items
- `--wrap` option enables line wrap
- `--wrap-sign` customizes the sign for wrapped lines (default: `↳ `)
- `toggle-wrap` action toggles line wrap
```sh
history | fzf --tac --wrap --bind 'ctrl-/:toggle-wrap'
# You can press CTRL-/ to toggle line wrap in CTRL-R binding
export FZF_CTRL_R_OPTS=$'--bind ctrl-/:toggle-wrap --wrap-sign "\t↳ "'
```
- Added `--info-command` option for customizing the info line - Added `--info-command` option for customizing the info line
```sh ```sh
# Prepend the current cursor position in yellow # Prepend the current cursor position in yellow

View File

@ -198,6 +198,13 @@ the details.
.B "\-\-cycle" .B "\-\-cycle"
Enable cyclic scroll Enable cyclic scroll
.TP .TP
.B "\-\-wrap"
Enable line wrap
.TP
.BI "\-\-wrap\-sign" "=INDICATOR"
Indicator for wrapped lines. The default is '↳ ' or '> ' depending on
\fB\-\-no\-unicode\fR.
.TP
.B "\-\-no\-multi\-line" .B "\-\-no\-multi\-line"
Disable multi-line display of items when using \fB\-\-read0\fR Disable multi-line display of items when using \fB\-\-read0\fR
.TP .TP
@ -1490,6 +1497,7 @@ A key or an event can be bound to one or more of the following actions.
\fBtoggle\-sort\fR \fBtoggle\-sort\fR
\fBtoggle\-track\fR (toggle global tracking option (\fB\-\-track\fR)) \fBtoggle\-track\fR (toggle global tracking option (\fB\-\-track\fR))
\fBtoggle\-track\-current\fR (toggle tracking of the current item) \fBtoggle\-track\-current\fR (toggle tracking of the current item)
\fBtoggle\-wrap\fR
\fBtoggle+up\fR \fIbtab (shift\-tab)\fR \fBtoggle+up\fR \fIbtab (shift\-tab)\fR
\fBtrack\-current\fR (track the current item; automatically disabled if focus changes) \fBtrack\-current\fR (track the current item; automatically disabled if focus changes)
\fBtransform(...)\fR (transform states using the output of an external command) \fBtransform(...)\fR (transform states using the output of an external command)

View File

@ -58,73 +58,74 @@ func _() {
_ = x[actToggleTrack-47] _ = x[actToggleTrack-47]
_ = x[actToggleTrackCurrent-48] _ = x[actToggleTrackCurrent-48]
_ = x[actToggleHeader-49] _ = x[actToggleHeader-49]
_ = x[actTrackCurrent-50] _ = x[actToggleWrap-50]
_ = x[actUntrackCurrent-51] _ = x[actTrackCurrent-51]
_ = x[actDown-52] _ = x[actUntrackCurrent-52]
_ = x[actUp-53] _ = x[actDown-53]
_ = x[actPageUp-54] _ = x[actUp-54]
_ = x[actPageDown-55] _ = x[actPageUp-55]
_ = x[actPosition-56] _ = x[actPageDown-56]
_ = x[actHalfPageUp-57] _ = x[actPosition-57]
_ = x[actHalfPageDown-58] _ = x[actHalfPageUp-58]
_ = x[actOffsetUp-59] _ = x[actHalfPageDown-59]
_ = x[actOffsetDown-60] _ = x[actOffsetUp-60]
_ = x[actOffsetMiddle-61] _ = x[actOffsetDown-61]
_ = x[actJump-62] _ = x[actOffsetMiddle-62]
_ = x[actJumpAccept-63] _ = x[actJump-63]
_ = x[actPrintQuery-64] _ = x[actJumpAccept-64]
_ = x[actRefreshPreview-65] _ = x[actPrintQuery-65]
_ = x[actReplaceQuery-66] _ = x[actRefreshPreview-66]
_ = x[actToggleSort-67] _ = x[actReplaceQuery-67]
_ = x[actShowPreview-68] _ = x[actToggleSort-68]
_ = x[actHidePreview-69] _ = x[actShowPreview-69]
_ = x[actTogglePreview-70] _ = x[actHidePreview-70]
_ = x[actTogglePreviewWrap-71] _ = x[actTogglePreview-71]
_ = x[actTransform-72] _ = x[actTogglePreviewWrap-72]
_ = x[actTransformBorderLabel-73] _ = x[actTransform-73]
_ = x[actTransformHeader-74] _ = x[actTransformBorderLabel-74]
_ = x[actTransformPreviewLabel-75] _ = x[actTransformHeader-75]
_ = x[actTransformPrompt-76] _ = x[actTransformPreviewLabel-76]
_ = x[actTransformQuery-77] _ = x[actTransformPrompt-77]
_ = x[actPreview-78] _ = x[actTransformQuery-78]
_ = x[actChangePreview-79] _ = x[actPreview-79]
_ = x[actChangePreviewWindow-80] _ = x[actChangePreview-80]
_ = x[actPreviewTop-81] _ = x[actChangePreviewWindow-81]
_ = x[actPreviewBottom-82] _ = x[actPreviewTop-82]
_ = x[actPreviewUp-83] _ = x[actPreviewBottom-83]
_ = x[actPreviewDown-84] _ = x[actPreviewUp-84]
_ = x[actPreviewPageUp-85] _ = x[actPreviewDown-85]
_ = x[actPreviewPageDown-86] _ = x[actPreviewPageUp-86]
_ = x[actPreviewHalfPageUp-87] _ = x[actPreviewPageDown-87]
_ = x[actPreviewHalfPageDown-88] _ = x[actPreviewHalfPageUp-88]
_ = x[actPrevHistory-89] _ = x[actPreviewHalfPageDown-89]
_ = x[actPrevSelected-90] _ = x[actPrevHistory-90]
_ = x[actPrint-91] _ = x[actPrevSelected-91]
_ = x[actPut-92] _ = x[actPrint-92]
_ = x[actNextHistory-93] _ = x[actPut-93]
_ = x[actNextSelected-94] _ = x[actNextHistory-94]
_ = x[actExecute-95] _ = x[actNextSelected-95]
_ = x[actExecuteSilent-96] _ = x[actExecute-96]
_ = x[actExecuteMulti-97] _ = x[actExecuteSilent-97]
_ = x[actSigStop-98] _ = x[actExecuteMulti-98]
_ = x[actFirst-99] _ = x[actSigStop-99]
_ = x[actLast-100] _ = x[actFirst-100]
_ = x[actReload-101] _ = x[actLast-101]
_ = x[actReloadSync-102] _ = x[actReload-102]
_ = x[actDisableSearch-103] _ = x[actReloadSync-103]
_ = x[actEnableSearch-104] _ = x[actDisableSearch-104]
_ = x[actSelect-105] _ = x[actEnableSearch-105]
_ = x[actDeselect-106] _ = x[actSelect-106]
_ = x[actUnbind-107] _ = x[actDeselect-107]
_ = x[actRebind-108] _ = x[actUnbind-108]
_ = x[actBecome-109] _ = x[actRebind-109]
_ = x[actShowHeader-110] _ = x[actBecome-110]
_ = x[actHideHeader-111] _ = x[actShowHeader-111]
_ = x[actHideHeader-112]
} }
const _actionType_name = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeHeaderactChangeMultiactChangePreviewLabelactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactTrackCurrentactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformHeaderactTransformPreviewLabelactTransformPromptactTransformQueryactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactBecomeactShowHeaderactHideHeader" const _actionType_name = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeHeaderactChangeMultiactChangePreviewLabelactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactTrackCurrentactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformHeaderactTransformPreviewLabelactTransformPromptactTransformQueryactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactBecomeactShowHeaderactHideHeader"
var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 42, 50, 68, 76, 85, 102, 123, 138, 159, 183, 198, 207, 227, 242, 256, 277, 292, 306, 320, 333, 350, 358, 371, 387, 399, 407, 421, 435, 446, 457, 475, 492, 499, 518, 530, 544, 553, 568, 580, 593, 604, 615, 627, 641, 662, 677, 692, 709, 716, 721, 730, 741, 752, 765, 780, 791, 804, 819, 826, 839, 852, 869, 884, 897, 911, 925, 941, 961, 973, 996, 1014, 1038, 1056, 1073, 1083, 1099, 1121, 1134, 1150, 1162, 1176, 1192, 1210, 1230, 1252, 1266, 1281, 1289, 1295, 1309, 1324, 1334, 1350, 1365, 1375, 1383, 1390, 1399, 1412, 1428, 1443, 1452, 1463, 1472, 1481, 1490, 1503, 1516} var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 42, 50, 68, 76, 85, 102, 123, 138, 159, 183, 198, 207, 227, 242, 256, 277, 292, 306, 320, 333, 350, 358, 371, 387, 399, 407, 421, 435, 446, 457, 475, 492, 499, 518, 530, 544, 553, 568, 580, 593, 604, 615, 627, 641, 662, 677, 690, 705, 722, 729, 734, 743, 754, 765, 778, 793, 804, 817, 832, 839, 852, 865, 882, 897, 910, 924, 938, 954, 974, 986, 1009, 1027, 1051, 1069, 1086, 1096, 1112, 1134, 1147, 1163, 1175, 1189, 1205, 1223, 1243, 1265, 1279, 1294, 1302, 1308, 1322, 1337, 1347, 1363, 1378, 1388, 1396, 1403, 1412, 1425, 1441, 1456, 1465, 1476, 1485, 1494, 1503, 1516, 1529}
func (i actionType) String() string { func (i actionType) String() string {
if i < 0 || i >= actionType(len(_actionType_index)-1) { if i < 0 || i >= actionType(len(_actionType_index)-1) {

View File

@ -53,6 +53,8 @@ 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
--wrap Enable line wrap
--wrap-sign=STR Indicator for wrapped lines
--no-multi-line Disable multi-line display of items when using --read0 --no-multi-line Disable multi-line display of items when using --read0
--keep-right Keep the right end of the line visible on overflow --keep-right Keep the right end of the line visible on overflow
--scroll-off=LINES Number of screen lines to keep above or below when --scroll-off=LINES Number of screen lines to keep above or below when
@ -435,6 +437,8 @@ type Options struct {
MinHeight int MinHeight int
Layout layoutType Layout layoutType
Cycle bool Cycle bool
Wrap bool
WrapSign *string
MultiLine bool MultiLine bool
CursorLine bool CursorLine bool
KeepRight bool KeepRight bool
@ -543,6 +547,7 @@ func defaultOptions() *Options {
MinHeight: 10, MinHeight: 10,
Layout: layoutDefault, Layout: layoutDefault,
Cycle: false, Cycle: false,
Wrap: false,
MultiLine: true, MultiLine: true,
KeepRight: false, KeepRight: false,
Hscroll: true, Hscroll: true,
@ -1366,6 +1371,8 @@ func parseActionList(masked string, original string, prevActions []*action, putA
appendAction(actToggleTrackCurrent) appendAction(actToggleTrackCurrent)
case "toggle-header": case "toggle-header":
appendAction(actToggleHeader) appendAction(actToggleHeader)
case "toggle-wrap":
appendAction(actToggleWrap)
case "show-header": case "show-header":
appendAction(actShowHeader) appendAction(actShowHeader)
case "hide-header": case "hide-header":
@ -2163,6 +2170,16 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
opts.CursorLine = false opts.CursorLine = false
case "--no-cycle": case "--no-cycle":
opts.Cycle = false opts.Cycle = false
case "--wrap":
opts.Wrap = true
case "--no-wrap":
opts.Wrap = false
case "--wrap-sign":
str, err := nextString(allArgs, &i, "wrap sign required")
if err != nil {
return err
}
opts.WrapSign = &str
case "--multi-line": case "--multi-line":
opts.MultiLine = true opts.MultiLine = true
case "--no-multi-line": case "--no-multi-line":
@ -2513,6 +2530,8 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
if err := parseLabelPosition(&opts.PreviewLabel, value); err != nil { if err := parseLabelPosition(&opts.PreviewLabel, value); err != nil {
return err return err
} }
} else if match, value := optString(arg, "--wrap-sign="); match {
opts.WrapSign = &value
} else if match, value := optString(arg, "--prompt="); match { } else if match, value := optString(arg, "--prompt="); match {
opts.Prompt = value opts.Prompt = value
} else if match, value := optString(arg, "--pointer="); match { } else if match, value := optString(arg, "--pointer="); match {

View File

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

View File

@ -226,3 +226,85 @@ func (chars *Chars) Prepend(prefix string) {
chars.slice = append([]byte(prefix), chars.slice...) chars.slice = append([]byte(prefix), chars.slice...)
} }
} }
func (chars *Chars) Lines(multiLine bool, maxLines int, wrapCols int, wrapSignWidth int, tabstop int) ([][]rune, bool) {
text := make([]rune, chars.Length())
copy(text, chars.ToRunes())
lines := [][]rune{}
overflow := false
if !multiLine {
lines = append(lines, text)
} else {
from := 0
for off := 0; off < len(text); off++ {
if text[off] == '\n' {
lines = append(lines, text[from:off+1]) // Include '\n'
from = off + 1
if len(lines) >= maxLines {
break
}
}
}
var lastLine []rune
if from < len(text) {
lastLine = text[from:]
}
overflow = false
if len(lines) >= maxLines {
overflow = true
} else {
lines = append(lines, lastLine)
}
}
// If wrapping is disabled, we're done
if wrapCols == 0 {
return lines, overflow
}
wrapped := [][]rune{}
for _, line := range lines {
// Remove trailing '\n' and remember if it was there
newline := len(line) > 0 && line[len(line)-1] == '\n'
if newline {
line = line[:len(line)-1]
}
for {
cols := wrapCols
if len(wrapped) > 0 {
cols -= wrapSignWidth
}
_, overflowIdx := RunesWidth(line, 0, tabstop, cols)
if overflowIdx >= 0 {
// Might be a wide character
if overflowIdx == 0 {
overflowIdx = 1
}
if len(wrapped) >= maxLines {
return wrapped, true
}
wrapped = append(wrapped, line[:overflowIdx])
line = line[overflowIdx:]
continue
}
// Restore trailing '\n'
if newline {
line = append(line, '\n')
}
if len(wrapped) >= maxLines {
return wrapped, true
}
wrapped = append(wrapped, line)
break
}
}
return wrapped, false
}

View File

@ -1,6 +1,9 @@
package util package util
import "testing" import (
"fmt"
"testing"
)
func TestToCharsAscii(t *testing.T) { func TestToCharsAscii(t *testing.T) {
chars := ToChars([]byte("foobar")) chars := ToChars([]byte("foobar"))
@ -44,3 +47,37 @@ func TestTrimLength(t *testing.T) {
check(" h o ", 5) check(" h o ", 5)
check(" ", 0) check(" ", 0)
} }
func TestCharsLines(t *testing.T) {
chars := ToChars([]byte("abcdef\n가나다\n\tdef"))
check := func(multiLine bool, maxLines int, wrapCols int, wrapSignWidth int, tabstop int, expectedNumLines int, expectedOverflow bool) {
lines, overflow := chars.Lines(multiLine, maxLines, wrapCols, wrapSignWidth, tabstop)
fmt.Println(lines, overflow)
if len(lines) != expectedNumLines || overflow != expectedOverflow {
t.Errorf("Invalid result: %d %v (expected %d %v)", len(lines), overflow, expectedNumLines, expectedOverflow)
}
}
// No wrap
check(true, 1, 0, 0, 8, 1, true)
check(true, 2, 0, 0, 8, 2, true)
check(true, 3, 0, 0, 8, 3, false)
// Wrap (2)
check(true, 4, 2, 0, 8, 4, true)
check(true, 5, 2, 0, 8, 5, true)
check(true, 6, 2, 0, 8, 6, true)
check(true, 7, 2, 0, 8, 7, true)
check(true, 8, 2, 0, 8, 8, true)
check(true, 9, 2, 0, 8, 9, false)
check(true, 9, 2, 0, 1, 8, false) // Smaller tab size
// With wrap sign (3 + 1)
check(true, 100, 3, 1, 1, 8, false)
// With wrap sign (3 + 2)
check(true, 100, 3, 2, 1, 12, false)
// With wrap sign (3 + 2) and no multi-line
check(false, 100, 3, 2, 1, 13, false)
}