diff options
author | Junegunn Choi <junegunn.c@gmail.com> | 2024-06-23 02:00:16 +0900 |
---|---|---|
committer | Junegunn Choi <junegunn.c@gmail.com> | 2024-06-23 02:34:04 +0900 |
commit | 882e739b53276b277c1e213432b6d18bded39654 (patch) | |
tree | 77d9ff0fc4c69ee7320c693df99e01618fda0bd4 | |
parent | 7c2ffd3fef3f9131ee448a5f40d91835c8bd814d (diff) |
Add --wrap option and 'toggle-wrap' action
-rw-r--r-- | CHANGELOG.md | 3 | ||||
-rw-r--r-- | man/man1/fzf.1 | 3 | ||||
-rw-r--r-- | src/actiontype_string.go | 129 | ||||
-rw-r--r-- | src/options.go | 9 | ||||
-rw-r--r-- | src/terminal.go | 167 | ||||
-rw-r--r-- | src/util/chars.go | 78 | ||||
-rw-r--r-- | src/util/chars_test.go | 30 |
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) + } +} |