Implement word wrapping in the list section
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled

This commit is contained in:
Junegunn Choi
2026-02-18 15:20:56 +09:00
parent b6411beaa1
commit 69e9abdab4
7 changed files with 228 additions and 119 deletions

View File

@@ -3,6 +3,14 @@ CHANGELOG
0.68.0 0.68.0
------ ------
- Implemented word wrapping in the list section
- Added `--wrap=word` (or `--wrap-word`) option and `toggle-wrap-word` action
for word-level line wrapping in the list section
- Changed default binding of `ctrl-/` and `alt-/` from `toggle-wrap` to
`toggle-wrap-word`
```sh
fzf --wrap=word
```
- Implemented word wrapping in the preview window - Implemented word wrapping in the preview window
- Added `wrap-word` flag for `--preview-window` to enable word-level wrapping - Added `wrap-word` flag for `--preview-window` to enable word-level wrapping
- Added `toggle-preview-wrap-word` action - Added `toggle-preview-wrap-word` action

View File

@@ -593,8 +593,11 @@ Highlight the whole current line
.B "\-\-cycle" .B "\-\-cycle"
Enable cyclic scroll Enable cyclic scroll
.TP .TP
.B "\-\-wrap" .BI "\-\-wrap" "[=MODE]"
Enable line wrap Enable line wrap. \fIMODE\fR can be \fBchar\fR (default) or \fBword\fR.
\fBword\fR mode wraps lines at word boundaries (spaces and tabs) instead of
at arbitrary character positions. \fB\-\-wrap\-word\fR is a synonym for
\fB\-\-wrap=word\fR.
.TP .TP
.BI "\-\-wrap\-sign" "=INDICATOR" .BI "\-\-wrap\-sign" "=INDICATOR"
Indicator for wrapped lines. The default is '↳ ' or '> ' depending on Indicator for wrapped lines. The default is '↳ ' or '> ' depending on
@@ -1968,7 +1971,8 @@ 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 \fIctrl\-/\fR \fIalt\-/\fR \fBtoggle\-wrap\fR
\fBtoggle\-wrap\-word\fR \fIctrl\-/\fR \fIalt\-/\fR
\fBtoggle+down\fR \fIctrl\-i (tab)\fR \fBtoggle+down\fR \fIctrl\-i (tab)\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)

View File

@@ -75,115 +75,116 @@ func _() {
_ = x[actToggleTrackCurrent-64] _ = x[actToggleTrackCurrent-64]
_ = x[actToggleHeader-65] _ = x[actToggleHeader-65]
_ = x[actToggleWrap-66] _ = x[actToggleWrap-66]
_ = x[actToggleMultiLine-67] _ = x[actToggleWrapWord-67]
_ = x[actToggleHscroll-68] _ = x[actToggleMultiLine-68]
_ = x[actToggleRaw-69] _ = x[actToggleHscroll-69]
_ = x[actEnableRaw-70] _ = x[actToggleRaw-70]
_ = x[actDisableRaw-71] _ = x[actEnableRaw-71]
_ = x[actTrackCurrent-72] _ = x[actDisableRaw-72]
_ = x[actToggleInput-73] _ = x[actTrackCurrent-73]
_ = x[actHideInput-74] _ = x[actToggleInput-74]
_ = x[actShowInput-75] _ = x[actHideInput-75]
_ = x[actUntrackCurrent-76] _ = x[actShowInput-76]
_ = x[actDown-77] _ = x[actUntrackCurrent-77]
_ = x[actDownMatch-78] _ = x[actDown-78]
_ = x[actUp-79] _ = x[actDownMatch-79]
_ = x[actUpMatch-80] _ = x[actUp-80]
_ = x[actPageUp-81] _ = x[actUpMatch-81]
_ = x[actPageDown-82] _ = x[actPageUp-82]
_ = x[actPosition-83] _ = x[actPageDown-83]
_ = x[actHalfPageUp-84] _ = x[actPosition-84]
_ = x[actHalfPageDown-85] _ = x[actHalfPageUp-85]
_ = x[actOffsetUp-86] _ = x[actHalfPageDown-86]
_ = x[actOffsetDown-87] _ = x[actOffsetUp-87]
_ = x[actOffsetMiddle-88] _ = x[actOffsetDown-88]
_ = x[actJump-89] _ = x[actOffsetMiddle-89]
_ = x[actJumpAccept-90] _ = x[actJump-90]
_ = x[actPrintQuery-91] _ = x[actJumpAccept-91]
_ = x[actRefreshPreview-92] _ = x[actPrintQuery-92]
_ = x[actReplaceQuery-93] _ = x[actRefreshPreview-93]
_ = x[actToggleSort-94] _ = x[actReplaceQuery-94]
_ = x[actShowPreview-95] _ = x[actToggleSort-95]
_ = x[actHidePreview-96] _ = x[actShowPreview-96]
_ = x[actTogglePreview-97] _ = x[actHidePreview-97]
_ = x[actTogglePreviewWrap-98] _ = x[actTogglePreview-98]
_ = x[actTogglePreviewWrapWord-99] _ = x[actTogglePreviewWrap-99]
_ = x[actTransform-100] _ = x[actTogglePreviewWrapWord-100]
_ = x[actTransformBorderLabel-101] _ = x[actTransform-101]
_ = x[actTransformGhost-102] _ = x[actTransformBorderLabel-102]
_ = x[actTransformHeader-103] _ = x[actTransformGhost-103]
_ = x[actTransformFooter-104] _ = x[actTransformHeader-104]
_ = x[actTransformHeaderLabel-105] _ = x[actTransformFooter-105]
_ = x[actTransformFooterLabel-106] _ = x[actTransformHeaderLabel-106]
_ = x[actTransformInputLabel-107] _ = x[actTransformFooterLabel-107]
_ = x[actTransformListLabel-108] _ = x[actTransformInputLabel-108]
_ = x[actTransformNth-109] _ = x[actTransformListLabel-109]
_ = x[actTransformPointer-110] _ = x[actTransformNth-110]
_ = x[actTransformPreviewLabel-111] _ = x[actTransformPointer-111]
_ = x[actTransformPrompt-112] _ = x[actTransformPreviewLabel-112]
_ = x[actTransformQuery-113] _ = x[actTransformPrompt-113]
_ = x[actTransformSearch-114] _ = x[actTransformQuery-114]
_ = x[actTrigger-115] _ = x[actTransformSearch-115]
_ = x[actBgTransform-116] _ = x[actTrigger-116]
_ = x[actBgTransformBorderLabel-117] _ = x[actBgTransform-117]
_ = x[actBgTransformGhost-118] _ = x[actBgTransformBorderLabel-118]
_ = x[actBgTransformHeader-119] _ = x[actBgTransformGhost-119]
_ = x[actBgTransformFooter-120] _ = x[actBgTransformHeader-120]
_ = x[actBgTransformHeaderLabel-121] _ = x[actBgTransformFooter-121]
_ = x[actBgTransformFooterLabel-122] _ = x[actBgTransformHeaderLabel-122]
_ = x[actBgTransformInputLabel-123] _ = x[actBgTransformFooterLabel-123]
_ = x[actBgTransformListLabel-124] _ = x[actBgTransformInputLabel-124]
_ = x[actBgTransformNth-125] _ = x[actBgTransformListLabel-125]
_ = x[actBgTransformPointer-126] _ = x[actBgTransformNth-126]
_ = x[actBgTransformPreviewLabel-127] _ = x[actBgTransformPointer-127]
_ = x[actBgTransformPrompt-128] _ = x[actBgTransformPreviewLabel-128]
_ = x[actBgTransformQuery-129] _ = x[actBgTransformPrompt-129]
_ = x[actBgTransformSearch-130] _ = x[actBgTransformQuery-130]
_ = x[actBgCancel-131] _ = x[actBgTransformSearch-131]
_ = x[actSearch-132] _ = x[actBgCancel-132]
_ = x[actPreview-133] _ = x[actSearch-133]
_ = x[actPreviewTop-134] _ = x[actPreview-134]
_ = x[actPreviewBottom-135] _ = x[actPreviewTop-135]
_ = x[actPreviewUp-136] _ = x[actPreviewBottom-136]
_ = x[actPreviewDown-137] _ = x[actPreviewUp-137]
_ = x[actPreviewPageUp-138] _ = x[actPreviewDown-138]
_ = x[actPreviewPageDown-139] _ = x[actPreviewPageUp-139]
_ = x[actPreviewHalfPageUp-140] _ = x[actPreviewPageDown-140]
_ = x[actPreviewHalfPageDown-141] _ = x[actPreviewHalfPageUp-141]
_ = x[actPrevHistory-142] _ = x[actPreviewHalfPageDown-142]
_ = x[actPrevSelected-143] _ = x[actPrevHistory-143]
_ = x[actPrint-144] _ = x[actPrevSelected-144]
_ = x[actPut-145] _ = x[actPrint-145]
_ = x[actNextHistory-146] _ = x[actPut-146]
_ = x[actNextSelected-147] _ = x[actNextHistory-147]
_ = x[actExecute-148] _ = x[actNextSelected-148]
_ = x[actExecuteSilent-149] _ = x[actExecute-149]
_ = x[actExecuteMulti-150] _ = x[actExecuteSilent-150]
_ = x[actSigStop-151] _ = x[actExecuteMulti-151]
_ = x[actBest-152] _ = x[actSigStop-152]
_ = x[actFirst-153] _ = x[actBest-153]
_ = x[actLast-154] _ = x[actFirst-154]
_ = x[actReload-155] _ = x[actLast-155]
_ = x[actReloadSync-156] _ = x[actReload-156]
_ = x[actDisableSearch-157] _ = x[actReloadSync-157]
_ = x[actEnableSearch-158] _ = x[actDisableSearch-158]
_ = x[actSelect-159] _ = x[actEnableSearch-159]
_ = x[actDeselect-160] _ = x[actSelect-160]
_ = x[actUnbind-161] _ = x[actDeselect-161]
_ = x[actRebind-162] _ = x[actUnbind-162]
_ = x[actToggleBind-163] _ = x[actRebind-163]
_ = x[actBecome-164] _ = x[actToggleBind-164]
_ = x[actShowHeader-165] _ = x[actBecome-165]
_ = x[actHideHeader-166] _ = x[actShowHeader-166]
_ = x[actBell-167] _ = x[actHideHeader-167]
_ = x[actExclude-168] _ = x[actBell-168]
_ = x[actExcludeMulti-169] _ = x[actExclude-169]
_ = x[actAsync-170] _ = x[actExcludeMulti-170]
_ = x[actAsync-171]
} }
const _actionType_name = "actIgnoreactStartactClickactInvalidactBracketedPasteBeginactBracketedPasteEndactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactBackwardSubWordactCancelactChangeBorderLabelactChangeGhostactChangeHeaderactChangeFooteractChangeHeaderLabelactChangeFooterLabelactChangeInputLabelactChangeListLabelactChangeMultiactChangeNthactChangePointeractChangePreviewactChangePreviewLabelactChangePreviewWindowactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactForwardSubWordactKillLineactKillWordactKillSubWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactBackwardKillSubWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleMultiLineactToggleHscrollactToggleRawactEnableRawactDisableRawactTrackCurrentactToggleInputactHideInputactShowInputactUntrackCurrentactDownactDownMatchactUpactUpMatchactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTogglePreviewWrapWordactTransformactTransformBorderLabelactTransformGhostactTransformHeaderactTransformFooteractTransformHeaderLabelactTransformFooterLabelactTransformInputLabelactTransformListLabelactTransformNthactTransformPointeractTransformPreviewLabelactTransformPromptactTransformQueryactTransformSearchactTriggeractBgTransformactBgTransformBorderLabelactBgTransformGhostactBgTransformHeaderactBgTransformFooteractBgTransformHeaderLabelactBgTransformFooterLabelactBgTransformInputLabelactBgTransformListLabelactBgTransformNthactBgTransformPointeractBgTransformPreviewLabelactBgTransformPromptactBgTransformQueryactBgTransformSearchactBgCancelactSearchactPreviewactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactBestactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactToggleBindactBecomeactShowHeaderactHideHeaderactBellactExcludeactExcludeMultiactAsync" const _actionType_name = "actIgnoreactStartactClickactInvalidactBracketedPasteBeginactBracketedPasteEndactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactBackwardSubWordactCancelactChangeBorderLabelactChangeGhostactChangeHeaderactChangeFooteractChangeHeaderLabelactChangeFooterLabelactChangeInputLabelactChangeListLabelactChangeMultiactChangeNthactChangePointeractChangePreviewactChangePreviewLabelactChangePreviewWindowactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactForwardSubWordactKillLineactKillWordactKillSubWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactBackwardKillSubWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleWrapWordactToggleMultiLineactToggleHscrollactToggleRawactEnableRawactDisableRawactTrackCurrentactToggleInputactHideInputactShowInputactUntrackCurrentactDownactDownMatchactUpactUpMatchactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTogglePreviewWrapWordactTransformactTransformBorderLabelactTransformGhostactTransformHeaderactTransformFooteractTransformHeaderLabelactTransformFooterLabelactTransformInputLabelactTransformListLabelactTransformNthactTransformPointeractTransformPreviewLabelactTransformPromptactTransformQueryactTransformSearchactTriggeractBgTransformactBgTransformBorderLabelactBgTransformGhostactBgTransformHeaderactBgTransformFooteractBgTransformHeaderLabelactBgTransformFooterLabelactBgTransformInputLabelactBgTransformListLabelactBgTransformNthactBgTransformPointeractBgTransformPreviewLabelactBgTransformPromptactBgTransformQueryactBgTransformSearchactBgCancelactSearchactPreviewactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactBestactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactToggleBindactBecomeactShowHeaderactHideHeaderactBellactExcludeactExcludeMultiactAsync"
var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 57, 77, 84, 92, 110, 118, 127, 144, 165, 180, 201, 225, 240, 258, 267, 287, 301, 316, 331, 351, 371, 390, 408, 422, 434, 450, 466, 487, 509, 524, 538, 552, 565, 582, 590, 603, 619, 631, 639, 653, 667, 684, 695, 706, 720, 738, 755, 762, 781, 803, 815, 829, 838, 853, 865, 878, 889, 900, 912, 926, 947, 962, 975, 993, 1009, 1021, 1033, 1046, 1061, 1075, 1087, 1099, 1116, 1123, 1135, 1140, 1150, 1159, 1170, 1181, 1194, 1209, 1220, 1233, 1248, 1255, 1268, 1281, 1298, 1313, 1326, 1340, 1354, 1370, 1390, 1414, 1426, 1449, 1466, 1484, 1502, 1525, 1548, 1570, 1591, 1606, 1625, 1649, 1667, 1684, 1702, 1712, 1726, 1751, 1770, 1790, 1810, 1835, 1860, 1884, 1907, 1924, 1945, 1971, 1991, 2010, 2030, 2041, 2050, 2060, 2073, 2089, 2101, 2115, 2131, 2149, 2169, 2191, 2205, 2220, 2228, 2234, 2248, 2263, 2273, 2289, 2304, 2314, 2321, 2329, 2336, 2345, 2358, 2374, 2389, 2398, 2409, 2418, 2427, 2440, 2449, 2462, 2475, 2482, 2492, 2507, 2515} var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 57, 77, 84, 92, 110, 118, 127, 144, 165, 180, 201, 225, 240, 258, 267, 287, 301, 316, 331, 351, 371, 390, 408, 422, 434, 450, 466, 487, 509, 524, 538, 552, 565, 582, 590, 603, 619, 631, 639, 653, 667, 684, 695, 706, 720, 738, 755, 762, 781, 803, 815, 829, 838, 853, 865, 878, 889, 900, 912, 926, 947, 962, 975, 992, 1010, 1026, 1038, 1050, 1063, 1078, 1092, 1104, 1116, 1133, 1140, 1152, 1157, 1167, 1176, 1187, 1198, 1211, 1226, 1237, 1250, 1265, 1272, 1285, 1298, 1315, 1330, 1343, 1357, 1371, 1387, 1407, 1431, 1443, 1466, 1483, 1501, 1519, 1542, 1565, 1587, 1608, 1623, 1642, 1666, 1684, 1701, 1719, 1729, 1743, 1768, 1787, 1807, 1827, 1852, 1877, 1901, 1924, 1941, 1962, 1988, 2008, 2027, 2047, 2058, 2067, 2077, 2090, 2106, 2118, 2132, 2148, 2166, 2186, 2208, 2222, 2237, 2245, 2251, 2265, 2280, 2290, 2306, 2321, 2331, 2338, 2346, 2353, 2362, 2375, 2391, 2406, 2415, 2426, 2435, 2444, 2457, 2466, 2479, 2492, 2499, 2509, 2524, 2532}
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

@@ -95,7 +95,7 @@ Usage: fzf [options]
-m, --multi[=MAX] Enable multi-select with tab/shift-tab -m, --multi[=MAX] Enable multi-select with tab/shift-tab
--highlight-line Highlight the whole current line --highlight-line Highlight the whole current line
--cycle Enable cyclic scroll --cycle Enable cyclic scroll
--wrap Enable line wrap --wrap[=MODE] Enable line wrap (char|word, default: char)
--wrap-sign=STR Indicator for wrapped lines --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
--raw Enable raw mode (show non-matching items) --raw Enable raw mode (show non-matching items)
@@ -606,6 +606,7 @@ type Options struct {
Layout layoutType Layout layoutType
Cycle bool Cycle bool
Wrap bool Wrap bool
WrapWord bool
WrapSign *string WrapSign *string
MultiLine bool MultiLine bool
CursorLine bool CursorLine bool
@@ -740,6 +741,7 @@ func defaultOptions() *Options {
Layout: layoutDefault, Layout: layoutDefault,
Cycle: false, Cycle: false,
Wrap: false, Wrap: false,
WrapWord: false,
MultiLine: true, MultiLine: true,
KeepRight: false, KeepRight: false,
Hscroll: true, Hscroll: true,
@@ -1804,6 +1806,8 @@ func parseActionList(masked string, original string, prevActions []*action, putA
appendAction(actToggleHeader) appendAction(actToggleHeader)
case "toggle-wrap": case "toggle-wrap":
appendAction(actToggleWrap) appendAction(actToggleWrap)
case "toggle-wrap-word":
appendAction(actToggleWrapWord)
case "toggle-multi-line": case "toggle-multi-line":
appendAction(actToggleMultiLine) appendAction(actToggleMultiLine)
case "toggle-hscroll": case "toggle-hscroll":
@@ -2853,9 +2857,29 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
case "--no-cycle": case "--no-cycle":
opts.Cycle = false opts.Cycle = false
case "--wrap": case "--wrap":
opts.Wrap = true given, str := optionalNextString()
if given {
switch str {
case "char":
opts.Wrap = true
opts.WrapWord = false
case "word":
opts.Wrap = true
opts.WrapWord = true
default:
return errors.New("invalid wrap mode: " + str + " (expected: char or word)")
}
} else {
opts.Wrap = true
}
case "--no-wrap": case "--no-wrap":
opts.Wrap = false opts.Wrap = false
opts.WrapWord = false
case "--wrap-word":
opts.Wrap = true
opts.WrapWord = true
case "--no-wrap-word":
opts.WrapWord = false
case "--wrap-sign": case "--wrap-sign":
str, err := nextString("wrap sign required") str, err := nextString("wrap sign required")
if err != nil { if err != nil {

View File

@@ -250,6 +250,7 @@ type Terminal struct {
infoStyle infoStyle infoStyle infoStyle
infoPrefix string infoPrefix string
wrap bool wrap bool
wrapWord bool
wrapSign string wrapSign string
wrapSignWidth int wrapSignWidth int
ghost string ghost string
@@ -585,6 +586,7 @@ const (
actToggleTrackCurrent actToggleTrackCurrent
actToggleHeader actToggleHeader
actToggleWrap actToggleWrap
actToggleWrapWord
actToggleMultiLine actToggleMultiLine
actToggleHscroll actToggleHscroll
actToggleRaw actToggleRaw
@@ -830,8 +832,8 @@ func defaultKeymap() map[tui.Event][]*action {
if !util.IsWindows() { if !util.IsWindows() {
add(tui.CtrlZ, actSigStop) add(tui.CtrlZ, actSigStop)
} }
add(tui.CtrlSlash, actToggleWrap) add(tui.CtrlSlash, actToggleWrapWord)
addEvent(tui.AltKey('/'), actToggleWrap) addEvent(tui.AltKey('/'), actToggleWrapWord)
addEvent(tui.AltKey('b'), actBackwardWord) addEvent(tui.AltKey('b'), actBackwardWord)
add(tui.ShiftLeft, actBackwardWord) add(tui.ShiftLeft, actBackwardWord)
@@ -1014,6 +1016,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
multi: opts.Multi, multi: opts.Multi,
multiLine: opts.ReadZero && opts.MultiLine, multiLine: opts.ReadZero && opts.MultiLine,
wrap: opts.Wrap, wrap: opts.Wrap,
wrapWord: opts.WrapWord,
sort: opts.Sort > 0, sort: opts.Sort > 0,
toggleSort: opts.ToggleSort, toggleSort: opts.ToggleSort,
track: opts.Track, track: opts.Track,
@@ -1635,7 +1638,7 @@ func (t *Terminal) numItemLines(item *Item, atMost int) (int, bool) {
numLines, overflow = item.text.NumLines(atMost) numLines, overflow = item.text.NumLines(atMost)
} else { } else {
var lines [][]rune var lines [][]rune
lines, overflow = item.text.Lines(t.multiLine, atMost, t.wrapCols(), t.wrapSignWidth, t.tabstop) lines, overflow = item.text.Lines(t.multiLine, atMost, t.wrapCols(), t.wrapSignWidth, t.tabstop, t.wrapWord)
numLines = len(lines) numLines = len(lines)
} }
numLines += t.gap numLines += t.gap
@@ -1651,7 +1654,7 @@ func (t *Terminal) itemLines(item *Item, atMost int) ([][]rune, bool) {
copy(text, item.text.ToRunes()) copy(text, item.text.ToRunes())
return [][]rune{text}, false return [][]rune{text}, false
} }
return item.text.Lines(t.multiLine, atMost, t.wrapCols(), t.wrapSignWidth, t.tabstop) return item.text.Lines(t.multiLine, atMost, t.wrapCols(), t.wrapSignWidth, t.tabstop, t.wrapWord)
} }
// 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
@@ -6721,8 +6724,16 @@ 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: case actToggleWrap, actToggleWrapWord:
t.wrap = !t.wrap if a.t == actToggleWrapWord {
t.wrapWord = !t.wrapWord
t.wrap = t.wrapWord
} else {
t.wrap = !t.wrap
if !t.wrap {
t.wrapWord = false
}
}
t.clearNumLinesCache() t.clearNumLinesCache()
req(reqList, reqHeader) req(reqList, reqHeader)
case actToggleMultiLine: case actToggleMultiLine:

View File

@@ -249,7 +249,7 @@ func (chars *Chars) Prepend(prefix string) {
} }
} }
func (chars *Chars) Lines(multiLine bool, maxLines int, wrapCols int, wrapSignWidth int, tabstop int) ([][]rune, bool) { func (chars *Chars) Lines(multiLine bool, maxLines int, wrapCols int, wrapSignWidth int, tabstop int, wrapWord bool) ([][]rune, bool) {
text := make([]rune, chars.Length()) text := make([]rune, chars.Length())
copy(text, chars.ToRunes()) copy(text, chars.ToRunes())
@@ -307,6 +307,19 @@ func (chars *Chars) Lines(multiLine bool, maxLines int, wrapCols int, wrapSignWi
if overflowIdx == 0 { if overflowIdx == 0 {
overflowIdx = 1 overflowIdx = 1
} }
if wrapWord {
// Find last space/tab at or before overflowIdx
breakIdx := -1
for k := overflowIdx; k > 0; k-- {
if line[k-1] == ' ' || line[k-1] == '\t' {
breakIdx = k
break
}
}
if breakIdx > 0 {
overflowIdx = breakIdx
}
}
if len(wrapped) >= maxLines { if len(wrapped) >= maxLines {
return wrapped, true return wrapped, true
} }

View File

@@ -51,7 +51,7 @@ func TestTrimLength(t *testing.T) {
func TestCharsLines(t *testing.T) { func TestCharsLines(t *testing.T) {
chars := ToChars([]byte("abcdef\n가나다\n\tdef")) chars := ToChars([]byte("abcdef\n가나다\n\tdef"))
check := func(multiLine bool, maxLines int, wrapCols int, wrapSignWidth int, tabstop int, expectedNumLines int, expectedOverflow bool) { 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) lines, overflow := chars.Lines(multiLine, maxLines, wrapCols, wrapSignWidth, tabstop, false)
fmt.Println(lines, overflow) fmt.Println(lines, overflow)
if len(lines) != expectedNumLines || overflow != expectedOverflow { if len(lines) != expectedNumLines || overflow != expectedOverflow {
t.Errorf("Invalid result: %d %v (expected %d %v)", len(lines), overflow, expectedNumLines, expectedOverflow) t.Errorf("Invalid result: %d %v (expected %d %v)", len(lines), overflow, expectedNumLines, expectedOverflow)
@@ -81,3 +81,51 @@ func TestCharsLines(t *testing.T) {
// With wrap sign (3 + 2) and no multi-line // With wrap sign (3 + 2) and no multi-line
check(false, 100, 3, 2, 1, 13, false) check(false, 100, 3, 2, 1, 13, false)
} }
func TestCharsLinesWrapWord(t *testing.T) {
// "hello world foo bar" with width 12 should break at word boundaries
chars := ToChars([]byte("hello world foo bar"))
lines, overflow := chars.Lines(false, 100, 12, 0, 8, true)
// "hello world " (12) | "foo bar" (7)
if len(lines) != 2 || overflow {
t.Errorf("Expected 2 lines, got %d (overflow: %v): %v", len(lines), overflow, lines)
}
if string(lines[0]) != "hello world " {
t.Errorf("Expected first line 'hello world ', got %q", string(lines[0]))
}
if string(lines[1]) != "foo bar" {
t.Errorf("Expected second line 'foo bar', got %q", string(lines[1]))
}
// No word boundary: a single long word falls back to character wrap
chars2 := ToChars([]byte("abcdefghijklmnop"))
lines2, _ := chars2.Lines(false, 100, 10, 0, 8, true)
if len(lines2) != 2 {
t.Errorf("Expected 2 lines for long word, got %d: %v", len(lines2), lines2)
}
if string(lines2[0]) != "abcdefghij" {
t.Errorf("Expected first line 'abcdefghij', got %q", string(lines2[0]))
}
// Tab as word boundary
chars3 := ToChars([]byte("hello\tworld"))
lines3, _ := chars3.Lines(false, 100, 7, 0, 8, true)
// "hello\t" should break at tab (width of tab at pos 5 with tabstop 8 = 3, total width = 8 > 7)
// Actually RunesWidth: 'h'=1,'e'=1,'l'=1,'l'=1,'o'=1,'\t'=3 = 8 > 7, overflowIdx=5
// Then word-wrap scans back and finds no space/tab before idx 5 (tab IS at idx 5 but we check line[k-1])
// Wait - let me think: overflowIdx=5, we check k=5 -> line[4]='o', k=4 -> line[3]='l'... no space/tab found
// Falls back to character wrap: "hello" | "\tworld"
if len(lines3) < 2 {
t.Errorf("Expected at least 2 lines for tab test, got %d: %v", len(lines3), lines3)
}
// wrapWord=false still character-wraps
chars4 := ToChars([]byte("hello world"))
lines4, _ := chars4.Lines(false, 100, 8, 0, 8, false)
if len(lines4) != 2 {
t.Errorf("Expected 2 lines with wrapWord=false, got %d: %v", len(lines4), lines4)
}
if string(lines4[0]) != "hello wo" {
t.Errorf("Expected first line 'hello wo', got %q", string(lines4[0]))
}
}