mirror of
https://github.com/junegunn/fzf.git
synced 2025-11-09 03:43:49 -05:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e2d96d5e6 |
13
CHANGELOG.md
13
CHANGELOG.md
@@ -1,6 +1,19 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
0.67.0
|
||||
------
|
||||
- Added `--freeze-left=N` option to keep the leftmost N columns visible.
|
||||
```sh
|
||||
# Keeps the file name column fixed and always visible
|
||||
git grep --line-number --color=always -- '' |
|
||||
fzf --ansi --delimiter : --freeze-left 1
|
||||
|
||||
# Used with --keep-right
|
||||
git grep --line-number --color=always -- '' |
|
||||
fzf --ansi --delimiter : --freeze-left 1 --keep-right
|
||||
```
|
||||
|
||||
0.66.1
|
||||
------
|
||||
- Bug fixes
|
||||
|
||||
@@ -629,6 +629,9 @@ Render empty lines between each item
|
||||
The given string will be repeated to draw a horizontal line on each gap
|
||||
(default: '┈' or '\-' depending on \fB\-\-no\-unicode\fR).
|
||||
.TP
|
||||
.BI "\-\-freeze\-left=" "N"
|
||||
Number of fields to freeze on the left.
|
||||
.TP
|
||||
.B "\-\-keep\-right"
|
||||
Keep the right end of the line visible when it's too long. Effective only when
|
||||
the query string is empty.
|
||||
|
||||
@@ -104,6 +104,7 @@ Usage: fzf [options]
|
||||
--gap[=N] Render empty lines between each item
|
||||
--gap-line[=STR] Draw horizontal line on each gap using the string
|
||||
(default: '┈' or '-')
|
||||
--freeze-left=N Number of fields to freeze on the left
|
||||
--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
|
||||
scrolling to the top or to the bottom (default: 0)
|
||||
@@ -562,6 +563,7 @@ type Options struct {
|
||||
Case Case
|
||||
Normalize bool
|
||||
Nth []Range
|
||||
FreezeLeft int
|
||||
WithNth func(Delimiter) func([]Token, int32) string
|
||||
AcceptNth func(Delimiter) func([]Token, int32) string
|
||||
Delimiter Delimiter
|
||||
@@ -2695,6 +2697,10 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
|
||||
if opts.Nth, err = splitNth(str); err != nil {
|
||||
return err
|
||||
}
|
||||
case "--freeze-left":
|
||||
if opts.FreezeLeft, err = nextInt("number of fields required"); err != nil {
|
||||
return err
|
||||
}
|
||||
case "--with-nth":
|
||||
str, err := nextString("nth expression required")
|
||||
if err != nil {
|
||||
@@ -3338,6 +3344,10 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
|
||||
return errors.New("empty jump labels")
|
||||
}
|
||||
|
||||
if opts.FreezeLeft < 0 {
|
||||
return errors.New("number of fields to freeze must be a non-negative integer")
|
||||
}
|
||||
|
||||
if validateJumpLabels {
|
||||
for _, r := range opts.JumpLabels {
|
||||
if r < 32 || r > 126 {
|
||||
|
||||
147
src/terminal.go
147
src/terminal.go
@@ -331,6 +331,7 @@ type Terminal struct {
|
||||
scrollbar string
|
||||
previewScrollbar string
|
||||
ansi bool
|
||||
freezeLeft int
|
||||
nthAttr tui.Attr
|
||||
nth []Range
|
||||
nthCurrent []Range
|
||||
@@ -1050,6 +1051,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
|
||||
footer: opts.Footer,
|
||||
header0: opts.Header,
|
||||
ansi: opts.Ansi,
|
||||
freezeLeft: opts.FreezeLeft,
|
||||
nthAttr: opts.Theme.Nth.Attr,
|
||||
nth: opts.Nth,
|
||||
nthCurrent: opts.Nth,
|
||||
@@ -3525,6 +3527,18 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
|
||||
}
|
||||
allOffsets := result.colorOffsets(charOffsets, nthOffsets, t.theme, colBase, colMatch, t.nthAttr, hidden)
|
||||
|
||||
// Determine split offset for horizontal scrolling with freeze
|
||||
splitOffset := -1
|
||||
if t.hscroll && !t.wrap && t.freezeLeft > 0 {
|
||||
tokens := Tokenize(item.text.ToString(), t.delimiter)
|
||||
if len(tokens) == 0 {
|
||||
splitOffset = 0
|
||||
} else {
|
||||
token := tokens[util.Min(t.freezeLeft, len(tokens))-1]
|
||||
splitOffset = int(token.prefixLength) + token.text.Length() - token.text.TrailingWhitespaces()
|
||||
}
|
||||
}
|
||||
|
||||
maxLines := 1
|
||||
if t.canSpanMultiLines() {
|
||||
maxLines = maxLineNum - lineNum + 1
|
||||
@@ -3594,6 +3608,10 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
|
||||
break
|
||||
}
|
||||
}
|
||||
splitOffsetLocal := 0
|
||||
if splitOffset >= 0 && splitOffset > from && splitOffset < from+len(line) {
|
||||
splitOffsetLocal = splitOffset - from
|
||||
}
|
||||
from += len(line)
|
||||
if lineOffset < skipLines {
|
||||
continue
|
||||
@@ -3667,69 +3685,88 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
|
||||
wrapped = true
|
||||
}
|
||||
|
||||
displayWidth = t.displayWidthWithLimit(line, 0, 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]
|
||||
el := int32(len(ellipsis))
|
||||
b += el - diff
|
||||
e += el - diff
|
||||
b = util.Max32(b, el)
|
||||
if rightTrim {
|
||||
e = util.Min32(e, int32(maxWidth-ellipsisWidth))
|
||||
}
|
||||
offsets[idx].offset[0] = b
|
||||
offsets[idx].offset[1] = util.Max32(b, e)
|
||||
}
|
||||
frozen := line[:splitOffsetLocal]
|
||||
rest := line[splitOffsetLocal:]
|
||||
displayWidthSum := 0
|
||||
for fidx, runes := range [][]rune{frozen, rest} {
|
||||
if len(runes) == 0 {
|
||||
continue
|
||||
}
|
||||
if t.hscroll {
|
||||
if t.keepRight && pos == nil {
|
||||
trimmed, diff := t.trimLeft(line, maxWidth, ellipsisWidth)
|
||||
transformOffsets(diff, false)
|
||||
line = append(ellipsis, trimmed...)
|
||||
} else if !t.overflow(line[:maxe], maxWidth-ellipsisWidth) {
|
||||
// Stri..
|
||||
line, _ = t.trimRight(line, maxWidth-ellipsisWidth)
|
||||
line = append(line, ellipsis...)
|
||||
if splitOffsetLocal > 0 && fidx == 1 {
|
||||
for idx := range offsets {
|
||||
offsets[idx].offset[0] -= int32(splitOffsetLocal)
|
||||
offsets[idx].offset[1] -= int32(splitOffsetLocal)
|
||||
}
|
||||
maxe -= splitOffsetLocal
|
||||
}
|
||||
displayWidth = t.displayWidthWithLimit(runes, 0, 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(runes))
|
||||
transformOffsets := func(diff int32, rightTrim bool) {
|
||||
for idx, offset := range offsets {
|
||||
b, e := offset.offset[0], offset.offset[1]
|
||||
el := int32(len(ellipsis))
|
||||
b += el - diff
|
||||
e += el - diff
|
||||
b = util.Max32(b, el)
|
||||
if rightTrim {
|
||||
e = util.Min32(e, int32(maxWidth-ellipsisWidth))
|
||||
}
|
||||
offsets[idx].offset[0] = b
|
||||
offsets[idx].offset[1] = util.Max32(b, e)
|
||||
}
|
||||
}
|
||||
if t.hscroll {
|
||||
if fidx > 0 && t.keepRight && pos == nil {
|
||||
trimmed, diff := t.trimLeft(runes, maxWidth, ellipsisWidth)
|
||||
transformOffsets(diff, false)
|
||||
runes = append(ellipsis, trimmed...)
|
||||
} else if fidx == 0 || !t.overflow(runes[:maxe], maxWidth-ellipsisWidth) {
|
||||
// Stri..
|
||||
runes, _ = t.trimRight(runes, maxWidth-ellipsisWidth)
|
||||
runes = append(runes, ellipsis...)
|
||||
} else {
|
||||
// Stri..
|
||||
rightTrim := false
|
||||
if t.overflow(runes[maxe:], ellipsisWidth) {
|
||||
runes = append(runes[:maxe], ellipsis...)
|
||||
rightTrim = true
|
||||
}
|
||||
// ..ri..
|
||||
var diff int32
|
||||
runes, diff = t.trimLeft(runes, maxWidth, ellipsisWidth)
|
||||
|
||||
// Transform offsets
|
||||
transformOffsets(diff, rightTrim)
|
||||
runes = append(ellipsis, runes...)
|
||||
}
|
||||
} else {
|
||||
// Stri..
|
||||
rightTrim := false
|
||||
if t.overflow(line[maxe:], ellipsisWidth) {
|
||||
line = append(line[:maxe], ellipsis...)
|
||||
rightTrim = true
|
||||
runes, _ = t.trimRight(runes, maxWidth-ellipsisWidth)
|
||||
runes = append(runes, ellipsis...)
|
||||
|
||||
for idx, offset := range offsets {
|
||||
offsets[idx].offset[0] = util.Min32(offset.offset[0], int32(maxWidth-len(ellipsis)))
|
||||
offsets[idx].offset[1] = util.Min32(offset.offset[1], int32(maxWidth))
|
||||
}
|
||||
// ..ri..
|
||||
var diff int32
|
||||
line, diff = t.trimLeft(line, maxWidth, ellipsisWidth)
|
||||
|
||||
// Transform offsets
|
||||
transformOffsets(diff, rightTrim)
|
||||
line = append(ellipsis, line...)
|
||||
}
|
||||
displayWidth = t.displayWidthWithLimit(runes, 0, displayWidth)
|
||||
}
|
||||
displayWidthSum += displayWidth
|
||||
|
||||
if maxWidth > 0 {
|
||||
color := colBase
|
||||
if hidden {
|
||||
color = color.WithFg(t.theme.Nomatch)
|
||||
}
|
||||
t.printColoredString(t.window, runes, offsets, color)
|
||||
} else {
|
||||
line, _ = t.trimRight(line, maxWidth-ellipsisWidth)
|
||||
line = append(line, ellipsis...)
|
||||
|
||||
for idx, offset := range offsets {
|
||||
offsets[idx].offset[0] = util.Min32(offset.offset[0], int32(maxWidth-len(ellipsis)))
|
||||
offsets[idx].offset[1] = util.Min32(offset.offset[1], int32(maxWidth))
|
||||
}
|
||||
break
|
||||
}
|
||||
displayWidth = t.displayWidthWithLimit(line, 0, displayWidth)
|
||||
}
|
||||
|
||||
if maxWidth > 0 {
|
||||
color := colBase
|
||||
if hidden {
|
||||
color = color.WithFg(t.theme.Nomatch)
|
||||
}
|
||||
t.printColoredString(t.window, line, offsets, color)
|
||||
maxWidth -= displayWidth
|
||||
}
|
||||
if postTask != nil {
|
||||
postTask(actualLineNum, displayWidth, wasWrapped, forceRedraw, lbg)
|
||||
postTask(actualLineNum, displayWidthSum, wasWrapped, forceRedraw, lbg)
|
||||
} else {
|
||||
t.markOtherLine(actualLineNum)
|
||||
}
|
||||
|
||||
@@ -1190,6 +1190,17 @@ class TestCore < TestInteractive
|
||||
tmux.until { |lines| assert lines.any_include?('9999␊10000') }
|
||||
end
|
||||
|
||||
def test_freeze_left_keep_right
|
||||
tmux.send_keys %[seq 10000 | #{FZF} --read0 --delimiter "\n" --freeze-left 3 --keep-right --ellipsis XX --no-multi-line --bind space:toggle-multi-line], :Enter
|
||||
tmux.until { |lines| assert_match(/^> 1␊2␊3XX.*10000␊$/, lines[-3]) }
|
||||
tmux.send_keys '5'
|
||||
tmux.until { |lines| assert_match(/^> 1␊2␊3␊4␊5␊.*XX$/, lines[-3]) }
|
||||
tmux.send_keys :Space
|
||||
tmux.until { |lines| assert lines.any_include?('> 1') }
|
||||
tmux.send_keys :Space
|
||||
tmux.until { |lines| assert lines.any_include?('1␊2␊3␊4␊5␊') }
|
||||
end
|
||||
|
||||
def test_backward_eof
|
||||
tmux.send_keys "echo foo | #{FZF} --bind 'backward-eof:reload(seq 100)'", :Enter
|
||||
tmux.until { |lines| lines.item_count == 1 && lines.match_count == 1 }
|
||||
|
||||
Reference in New Issue
Block a user