mirror of
https://github.com/Llewellynvdm/fzf.git
synced 2024-11-24 21:57:36 +00:00
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:
parent
724f8a1d45
commit
70bf8bc35d
10
CHANGELOG.md
10
CHANGELOG.md
@ -3,6 +3,16 @@ CHANGELOG
|
||||
|
||||
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
|
||||
```sh
|
||||
# Prepend the current cursor position in yellow
|
||||
|
@ -198,6 +198,13 @@ the details.
|
||||
.B "\-\-cycle"
|
||||
Enable cyclic scroll
|
||||
.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"
|
||||
Disable multi-line display of items when using \fB\-\-read0\fR
|
||||
.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\-track\fR (toggle global tracking option (\fB\-\-track\fR))
|
||||
\fBtoggle\-track\-current\fR (toggle tracking of the current item)
|
||||
\fBtoggle\-wrap\fR
|
||||
\fBtoggle+up\fR \fIbtab (shift\-tab)\fR
|
||||
\fBtrack\-current\fR (track the current item; automatically disabled if focus changes)
|
||||
\fBtransform(...)\fR (transform states using the output of an external command)
|
||||
|
@ -58,73 +58,74 @@ func _() {
|
||||
_ = x[actToggleTrack-47]
|
||||
_ = x[actToggleTrackCurrent-48]
|
||||
_ = x[actToggleHeader-49]
|
||||
_ = x[actTrackCurrent-50]
|
||||
_ = x[actUntrackCurrent-51]
|
||||
_ = x[actDown-52]
|
||||
_ = x[actUp-53]
|
||||
_ = x[actPageUp-54]
|
||||
_ = x[actPageDown-55]
|
||||
_ = x[actPosition-56]
|
||||
_ = x[actHalfPageUp-57]
|
||||
_ = x[actHalfPageDown-58]
|
||||
_ = x[actOffsetUp-59]
|
||||
_ = x[actOffsetDown-60]
|
||||
_ = x[actOffsetMiddle-61]
|
||||
_ = x[actJump-62]
|
||||
_ = x[actJumpAccept-63]
|
||||
_ = x[actPrintQuery-64]
|
||||
_ = x[actRefreshPreview-65]
|
||||
_ = x[actReplaceQuery-66]
|
||||
_ = x[actToggleSort-67]
|
||||
_ = x[actShowPreview-68]
|
||||
_ = x[actHidePreview-69]
|
||||
_ = x[actTogglePreview-70]
|
||||
_ = x[actTogglePreviewWrap-71]
|
||||
_ = x[actTransform-72]
|
||||
_ = x[actTransformBorderLabel-73]
|
||||
_ = x[actTransformHeader-74]
|
||||
_ = x[actTransformPreviewLabel-75]
|
||||
_ = x[actTransformPrompt-76]
|
||||
_ = x[actTransformQuery-77]
|
||||
_ = x[actPreview-78]
|
||||
_ = x[actChangePreview-79]
|
||||
_ = x[actChangePreviewWindow-80]
|
||||
_ = x[actPreviewTop-81]
|
||||
_ = x[actPreviewBottom-82]
|
||||
_ = x[actPreviewUp-83]
|
||||
_ = x[actPreviewDown-84]
|
||||
_ = x[actPreviewPageUp-85]
|
||||
_ = x[actPreviewPageDown-86]
|
||||
_ = x[actPreviewHalfPageUp-87]
|
||||
_ = x[actPreviewHalfPageDown-88]
|
||||
_ = x[actPrevHistory-89]
|
||||
_ = x[actPrevSelected-90]
|
||||
_ = x[actPrint-91]
|
||||
_ = x[actPut-92]
|
||||
_ = x[actNextHistory-93]
|
||||
_ = x[actNextSelected-94]
|
||||
_ = x[actExecute-95]
|
||||
_ = x[actExecuteSilent-96]
|
||||
_ = x[actExecuteMulti-97]
|
||||
_ = x[actSigStop-98]
|
||||
_ = x[actFirst-99]
|
||||
_ = x[actLast-100]
|
||||
_ = x[actReload-101]
|
||||
_ = x[actReloadSync-102]
|
||||
_ = x[actDisableSearch-103]
|
||||
_ = x[actEnableSearch-104]
|
||||
_ = x[actSelect-105]
|
||||
_ = x[actDeselect-106]
|
||||
_ = x[actUnbind-107]
|
||||
_ = x[actRebind-108]
|
||||
_ = x[actBecome-109]
|
||||
_ = x[actShowHeader-110]
|
||||
_ = x[actHideHeader-111]
|
||||
_ = x[actToggleWrap-50]
|
||||
_ = x[actTrackCurrent-51]
|
||||
_ = x[actUntrackCurrent-52]
|
||||
_ = x[actDown-53]
|
||||
_ = x[actUp-54]
|
||||
_ = x[actPageUp-55]
|
||||
_ = x[actPageDown-56]
|
||||
_ = x[actPosition-57]
|
||||
_ = x[actHalfPageUp-58]
|
||||
_ = x[actHalfPageDown-59]
|
||||
_ = x[actOffsetUp-60]
|
||||
_ = x[actOffsetDown-61]
|
||||
_ = x[actOffsetMiddle-62]
|
||||
_ = x[actJump-63]
|
||||
_ = x[actJumpAccept-64]
|
||||
_ = x[actPrintQuery-65]
|
||||
_ = x[actRefreshPreview-66]
|
||||
_ = x[actReplaceQuery-67]
|
||||
_ = x[actToggleSort-68]
|
||||
_ = x[actShowPreview-69]
|
||||
_ = x[actHidePreview-70]
|
||||
_ = x[actTogglePreview-71]
|
||||
_ = x[actTogglePreviewWrap-72]
|
||||
_ = x[actTransform-73]
|
||||
_ = x[actTransformBorderLabel-74]
|
||||
_ = x[actTransformHeader-75]
|
||||
_ = x[actTransformPreviewLabel-76]
|
||||
_ = x[actTransformPrompt-77]
|
||||
_ = x[actTransformQuery-78]
|
||||
_ = x[actPreview-79]
|
||||
_ = x[actChangePreview-80]
|
||||
_ = x[actChangePreviewWindow-81]
|
||||
_ = x[actPreviewTop-82]
|
||||
_ = x[actPreviewBottom-83]
|
||||
_ = x[actPreviewUp-84]
|
||||
_ = x[actPreviewDown-85]
|
||||
_ = x[actPreviewPageUp-86]
|
||||
_ = x[actPreviewPageDown-87]
|
||||
_ = x[actPreviewHalfPageUp-88]
|
||||
_ = x[actPreviewHalfPageDown-89]
|
||||
_ = x[actPrevHistory-90]
|
||||
_ = x[actPrevSelected-91]
|
||||
_ = x[actPrint-92]
|
||||
_ = x[actPut-93]
|
||||
_ = x[actNextHistory-94]
|
||||
_ = x[actNextSelected-95]
|
||||
_ = x[actExecute-96]
|
||||
_ = x[actExecuteSilent-97]
|
||||
_ = x[actExecuteMulti-98]
|
||||
_ = x[actSigStop-99]
|
||||
_ = x[actFirst-100]
|
||||
_ = x[actLast-101]
|
||||
_ = x[actReload-102]
|
||||
_ = x[actReloadSync-103]
|
||||
_ = x[actDisableSearch-104]
|
||||
_ = x[actEnableSearch-105]
|
||||
_ = x[actSelect-106]
|
||||
_ = x[actDeselect-107]
|
||||
_ = x[actUnbind-108]
|
||||
_ = x[actRebind-109]
|
||||
_ = x[actBecome-110]
|
||||
_ = 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 {
|
||||
if i < 0 || i >= actionType(len(_actionType_index)-1) {
|
||||
|
@ -53,6 +53,8 @@ Usage: fzf [options]
|
||||
--no-mouse Disable mouse
|
||||
--bind=KEYBINDS Custom key bindings. Refer to the man page.
|
||||
--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
|
||||
--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
|
||||
@ -435,6 +437,8 @@ type Options struct {
|
||||
MinHeight int
|
||||
Layout layoutType
|
||||
Cycle bool
|
||||
Wrap bool
|
||||
WrapSign *string
|
||||
MultiLine bool
|
||||
CursorLine bool
|
||||
KeepRight bool
|
||||
@ -543,6 +547,7 @@ func defaultOptions() *Options {
|
||||
MinHeight: 10,
|
||||
Layout: layoutDefault,
|
||||
Cycle: false,
|
||||
Wrap: false,
|
||||
MultiLine: true,
|
||||
KeepRight: false,
|
||||
Hscroll: true,
|
||||
@ -1366,6 +1371,8 @@ func parseActionList(masked string, original string, prevActions []*action, putA
|
||||
appendAction(actToggleTrackCurrent)
|
||||
case "toggle-header":
|
||||
appendAction(actToggleHeader)
|
||||
case "toggle-wrap":
|
||||
appendAction(actToggleWrap)
|
||||
case "show-header":
|
||||
appendAction(actShowHeader)
|
||||
case "hide-header":
|
||||
@ -2163,6 +2170,16 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
|
||||
opts.CursorLine = false
|
||||
case "--no-cycle":
|
||||
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":
|
||||
opts.MultiLine = true
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
} else if match, value := optString(arg, "--wrap-sign="); match {
|
||||
opts.WrapSign = &value
|
||||
} else if match, value := optString(arg, "--prompt="); match {
|
||||
opts.Prompt = value
|
||||
} else if match, value := optString(arg, "--pointer="); match {
|
||||
|
186
src/terminal.go
186
src/terminal.go
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -226,3 +226,85 @@ func (chars *Chars) Prepend(prefix string) {
|
||||
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
|
||||
}
|
||||
|
@ -1,6 +1,9 @@
|
||||
package util
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestToCharsAscii(t *testing.T) {
|
||||
chars := ToChars([]byte("foobar"))
|
||||
@ -44,3 +47,37 @@ func TestTrimLength(t *testing.T) {
|
||||
check(" h o ", 5)
|
||||
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)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user