summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJunegunn Choi <junegunn.c@gmail.com>2024-06-23 02:00:16 +0900
committerJunegunn Choi <junegunn.c@gmail.com>2024-06-23 02:34:04 +0900
commit882e739b53276b277c1e213432b6d18bded39654 (patch)
tree77d9ff0fc4c69ee7320c693df99e01618fda0bd4
parent7c2ffd3fef3f9131ee448a5f40d91835c8bd814d (diff)
Add --wrap option and 'toggle-wrap' action
-rw-r--r--CHANGELOG.md3
-rw-r--r--man/man1/fzf.13
-rw-r--r--src/actiontype_string.go129
-rw-r--r--src/options.go9
-rw-r--r--src/terminal.go167
-rw-r--r--src/util/chars.go78
-rw-r--r--src/util/chars_test.go30
7 files changed, 305 insertions, 114 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d7b5a9e1..207e6ad3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,9 @@ CHANGELOG
0.54.0
------
+- Added `--wrap` option to enable line wrap and added `toggle-wrap` action
+ ```sh
+ history | fzf --tac --wrap --bind 'ctrl-/:toggle-wrap'
- Added `--info-command` option for customizing the info line
```sh
# Prepend the current cursor position in yellow
diff --git a/man/man1/fzf.1 b/man/man1/fzf.1
index 136aa1c1..94a3190e 100644
--- a/man/man1/fzf.1
+++ b/man/man1/fzf.1
@@ -198,6 +198,9 @@ the details.
.B "--cycle"
Enable cyclic scroll
.TP
+.B "--wrap"
+Enable line wrap
+.TP
.B "--no-multi-line"
Disable multi-line display of items when using \fB--read0\fR
.TP
diff --git a/src/actiontype_string.go b/src/actiontype_string.go
index 03ec74c4..f5221f8a 100644
--- a/src/actiontype_string.go
+++ b/src/actiontype_string.go
@@ -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) {
diff --git a/src/options.go b/src/options.go
index fdf5f8dd..aad0837b 100644
--- a/src/options.go
+++ b/src/options.go
@@ -53,6 +53,7 @@ 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
--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 +436,7 @@ type Options struct {
MinHeight int
Layout layoutType
Cycle bool
+ Wrap bool
MultiLine bool
CursorLine bool
KeepRight bool
@@ -543,6 +545,7 @@ func defaultOptions() *Options {
MinHeight: 10,
Layout: layoutDefault,
Cycle: false,
+ Wrap: false,
MultiLine: true,
KeepRight: false,
Hscroll: true,
@@ -1366,6 +1369,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 +2168,10 @@ 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 "--multi-line":
opts.MultiLine = true
case "--no-multi-line":
diff --git a/src/terminal.go b/src/terminal.go
index a9e2f48e..3a5aee48 100644
--- a/src/terminal.go
+++ b/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,8 @@ type Terminal struct {
infoCommand string
infoStyle infoStyle
infoPrefix string
+ wrap bool
+ wrapSign string
separator labelPrinter
separatorLen int
spinner []string
@@ -446,6 +449,7 @@ const (
actToggleTrack
actToggleTrackCurrent
actToggleHeader
+ actToggleWrap
actTrackCurrent
actUntrackCurrent
actDown
@@ -787,6 +791,8 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
input: input,
multi: opts.Multi,
multiLine: opts.ReadZero && opts.MultiLine,
+ wrap: opts.Wrap,
+ wrapSign: "↳ ",
sort: opts.Sort > 0,
toggleSort: opts.ToggleSort,
track: opts.Track,
@@ -877,6 +883,8 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
}
if t.unicode {
t.borderWidth = uniseg.StringWidth("│")
+ } else {
+ t.wrapSign = "> "
}
if opts.Scrollbar == nil {
if t.unicode && t.borderWidth == 1 {
@@ -1103,10 +1111,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)-2, 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.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.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 +1151,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++
}
@@ -1951,6 +1986,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) {
@@ -1975,6 +2013,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() {
@@ -2002,7 +2041,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)
}
}
@@ -2035,7 +2074,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
@@ -2044,27 +2084,30 @@ 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 -= 2
+ }
if fillSpaces > 0 {
t.window.CPrint(color, strings.Repeat(" ", fillSpaces))
}
@@ -2075,6 +2118,9 @@ func (t *Terminal) printItem(result Result, line int, maxLine int, index int, cu
t.window.Print(strings.Repeat(" ", fillSpaces))
}
newLine.width = width
+ if wrapped {
+ newLine.width += 2
+ }
}
// When width is 0, line is completely cleared. We need to redraw scrollbar
newLine.hasBar = printBar(lineNum, forceRedraw || width == 0)
@@ -2172,7 +2218,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{}
@@ -2191,57 +2237,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
+ offsets := []colorOffset{}
+ for _, offset := range allOffsets {
+ if offset.offset[0] >= int32(from+len(line)) {
+ allOffsets = allOffsets[len(offsets):]
+ break
+ }
- line := text[from:]
- if t.multiLine {
- for idx, r := range text[from:] {
- if r == '\n' {
- line = line[:idx]
- break
- }
+ if offset.offset[0] < int32(from) {
+ continue
}
- }
- offsets := []colorOffset{}
- for _, offset := range allOffsets {
- if offset.offset[0] >= int32(from) && offset.offset[1] <= int32(from+len(line)) {
+ 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):]
- break
- }
- }
+ dupe := offset
+ dupe.offset[0] = int32(from + len(line))
- from += len(line) + 1
+ offset.offset[0] -= int32(from)
+ offset.offset[1] = int32(from + len(line))
+ offsets = append(offsets, offset)
- if cutoff > 0 {
- cutoff--
- lineOffset--
- continue
+ allOffsets = append([]colorOffset{dupe}, allOffsets[len(offsets):]...)
+ break
+ }
}
+ from += len(line)
var maxe int
for _, offset := range offsets {
@@ -2288,6 +2340,20 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
}
maxWidth := t.window.Width() - (t.pointerLen + t.markerLen + 1)
+ wasWrapped := false
+ if wrapped {
+ maxWidth -= 2
+ 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
+ }
+
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)
@@ -2344,7 +2410,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)
}
@@ -4356,6 +4422,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
@@ -4738,12 +4807,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 {
@@ -4787,14 +4856,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
}
}
diff --git a/src/util/chars.go b/src/util/chars.go
index 82773f40..f167d41f 100644
--- a/src/util/chars.go
+++ b/src/util/chars.go
@@ -226,3 +226,81 @@ func (chars *Chars) Prepend(prefix string) {
chars.slice = append([]byte(prefix), chars.slice...)
}
}
+
+func (chars *Chars) Lines(multiLine bool, maxLines int, wrapCols 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 {
+ _, overflowIdx := RunesWidth(line, 0, tabstop, wrapCols)
+ 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
+}
diff --git a/src/util/chars_test.go b/src/util/chars_test.go
index b7983f30..912156b3 100644
--- a/src/util/chars_test.go
+++ b/src/util/chars_test.go
@@ -1,6 +1,9 @@
package util
-import "testing"
+import (
+ "fmt"
+ "testing"
+)
func TestToCharsAscii(t *testing.T) {
chars := ToChars([]byte("foobar"))
@@ -44,3 +47,28 @@ func TestTrimLength(t *testing.T) {
check(" h o ", 5)
check(" ", 0)
}
+
+func TestCharsLines(t *testing.T) {
+ chars := ToChars([]byte("abc\n한글\ndef"))
+ for _, ml := range []bool{true, false} {
+ // No wrap
+ lines, overflow := chars.Lines(ml, 1, 0, 8)
+ fmt.Println(lines, overflow)
+ lines, overflow = chars.Lines(ml, 2, 0, 8)
+ fmt.Println(lines, overflow)
+ lines, overflow = chars.Lines(ml, 3, 0, 8)
+ fmt.Println(lines, overflow)
+
+ // Wrap
+ lines, overflow = chars.Lines(ml, 4, 2, 8)
+ fmt.Println(lines, overflow)
+ lines, overflow = chars.Lines(ml, 100, 1, 8)
+ fmt.Println(lines, overflow)
+
+ chars = ToChars([]byte("abc\n한글\ndef\n\n\n"))
+ lines, overflow = chars.Lines(ml, 100, 100, 8)
+ fmt.Println(lines, overflow)
+ numLines, overflow := chars.NumLines(8)
+ fmt.Println(numLines, overflow)
+ }
+}