mirror of
https://github.com/junegunn/fzf.git
synced 2026-02-21 00:58:42 +08:00
Fix --preview-window follow not working correctly with wrapping
Fix #3243 Fix #4258
This commit is contained in:
@@ -36,6 +36,7 @@ CHANGELOG
|
||||
- Added fish completion support (#4605) (@lalvarezt)
|
||||
- zsh: Handle multi-line history selection (#4595) (@LangLangBart)
|
||||
- Bug fixes
|
||||
- Fixed `--preview-window follow` not working correctly with wrapping (#3243, #4258)
|
||||
- Fixed symlinks to directories being returned as files (#4676) (@skk64)
|
||||
- Fixed SIGHUP signal handling (#4668) (@LangLangBart)
|
||||
- Fixed preview process not killed on exit (#4667)
|
||||
|
||||
@@ -4093,6 +4093,80 @@ func extractPassThroughs(line string) ([]string, string) {
|
||||
return passThroughs, transformed
|
||||
}
|
||||
|
||||
// followOffset computes the correct content-line offset for follow mode,
|
||||
// accounting for line wrapping in the preview window.
|
||||
func (t *Terminal) followOffset() int {
|
||||
lines := t.previewer.lines
|
||||
headerLines := t.activePreviewOpts.headerLines
|
||||
height := t.pwindow.Height() - headerLines
|
||||
if height <= 0 || len(lines) <= headerLines {
|
||||
return headerLines
|
||||
}
|
||||
|
||||
body := lines[headerLines:]
|
||||
if !t.activePreviewOpts.wrap {
|
||||
return max(t.previewer.offset, headerLines+len(body)-height)
|
||||
}
|
||||
|
||||
maxWidth := t.pwindow.Width()
|
||||
visualLines := 0
|
||||
for i := len(body) - 1; i >= 0; i-- {
|
||||
h := t.previewLineHeight(body[i], maxWidth)
|
||||
if visualLines+h > height {
|
||||
return headerLines + i + 1
|
||||
}
|
||||
visualLines += h
|
||||
}
|
||||
return headerLines
|
||||
}
|
||||
|
||||
// previewLineHeight estimates the number of visual lines a preview content line
|
||||
// occupies when wrapping is enabled.
|
||||
func (t *Terminal) previewLineHeight(line string, maxWidth int) int {
|
||||
if maxWidth <= 0 {
|
||||
return 1
|
||||
}
|
||||
|
||||
// For word-wrap mode, count the sub-lines produced by word wrapping.
|
||||
// Each sub-line may still char-wrap if it contains a word longer than the width.
|
||||
if t.activePreviewOpts.wrapWord {
|
||||
subLines := t.wordWrapAnsiLine(line, maxWidth, t.wrapSignWidth)
|
||||
total := 0
|
||||
for i, sub := range subLines {
|
||||
prefixWidth := 0
|
||||
cols := maxWidth
|
||||
if i > 0 {
|
||||
prefixWidth = t.wrapSignWidth
|
||||
cols -= t.wrapSignWidth
|
||||
}
|
||||
w := t.ansiLineWidth(sub, prefixWidth)
|
||||
if cols <= 0 {
|
||||
cols = 1
|
||||
}
|
||||
total += max(1, (w+cols-1)/cols)
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
// For char-wrap, compute visible width and divide by available width.
|
||||
w := t.ansiLineWidth(line, 0)
|
||||
if w <= maxWidth {
|
||||
return 1
|
||||
}
|
||||
remaining := w - maxWidth
|
||||
contWidth := max(1, maxWidth-t.wrapSignWidth)
|
||||
return 1 + (remaining+contWidth-1)/contWidth
|
||||
}
|
||||
|
||||
// ansiLineWidth computes the display width of a string, skipping ANSI escape sequences.
|
||||
// prefixWidth is the visual offset where the content starts (e.g. wrap sign width for
|
||||
// continuation lines), used for correct tab stop alignment.
|
||||
func (t *Terminal) ansiLineWidth(line string, prefixWidth int) int {
|
||||
trimmed, _, _ := extractColor(line, nil, nil)
|
||||
_, width := t.processTabs([]rune(trimmed), prefixWidth)
|
||||
return width - prefixWidth
|
||||
}
|
||||
|
||||
func (t *Terminal) wordWrapAnsiLine(line string, maxWidth int, wrapSignWidth int) []string {
|
||||
if maxWidth <= 0 {
|
||||
return []string{line}
|
||||
@@ -5704,7 +5778,7 @@ func (t *Terminal) Loop() error {
|
||||
t.previewer.lines = result.lines
|
||||
t.previewer.spinner = result.spinner
|
||||
if t.hasPreviewWindow() && t.previewer.following.Enabled() {
|
||||
t.previewer.offset = max(t.previewer.offset, len(t.previewer.lines)-(t.pwindow.Height()-t.activePreviewOpts.headerLines))
|
||||
t.previewer.offset = t.followOffset()
|
||||
} else if result.offset >= 0 {
|
||||
t.previewer.offset = util.Constrain(result.offset, t.activePreviewOpts.headerLines, len(t.previewer.lines)-1)
|
||||
}
|
||||
|
||||
@@ -1423,8 +1423,10 @@ func (w *LightWindow) fill(str string, resetCode string) FillReturn {
|
||||
for i, line := range allLines {
|
||||
lines := WrapLine(line, w.posx, w.width, w.tabstop, w.wrapSignWidth)
|
||||
for j, wl := range lines {
|
||||
w.stderrInternal(wl.Text, false, resetCode)
|
||||
w.posx += wl.DisplayWidth
|
||||
if w.posx < w.width {
|
||||
w.stderrInternal(wl.Text, false, resetCode)
|
||||
w.posx += wl.DisplayWidth
|
||||
}
|
||||
|
||||
// Wrap line
|
||||
if j < len(lines)-1 || i < len(allLines)-1 {
|
||||
|
||||
@@ -971,7 +971,9 @@ func (w *TcellWindow) fillString(text string, pair ColorPair) FillReturn {
|
||||
w.renderWrapSign(style)
|
||||
}
|
||||
}
|
||||
w.renderGraphemes(wl.Text, style)
|
||||
if w.lastX < w.width {
|
||||
w.renderGraphemes(wl.Text, style)
|
||||
}
|
||||
}
|
||||
}
|
||||
if w.lastX >= w.width {
|
||||
|
||||
@@ -1377,7 +1377,8 @@ func WrapLine(input string, prefixLength int, initialMax int, tabstop int, wrapS
|
||||
width := 0
|
||||
line := ""
|
||||
gr := uniseg.NewGraphemes(input)
|
||||
max := initialMax
|
||||
maxWidth := initialMax
|
||||
contMax := max(1, initialMax-wrapSignWidth)
|
||||
for gr.Next() {
|
||||
rs := gr.Runes()
|
||||
str := string(rs)
|
||||
@@ -1392,14 +1393,14 @@ func WrapLine(input string, prefixLength int, initialMax int, tabstop int, wrapS
|
||||
}
|
||||
width += w
|
||||
|
||||
if prefixLength+width <= max {
|
||||
if prefixLength+width <= maxWidth {
|
||||
line += str
|
||||
} else {
|
||||
lines = append(lines, WrappedLine{string(line), width - w})
|
||||
line = str
|
||||
prefixLength = 0
|
||||
width = w
|
||||
max = initialMax - wrapSignWidth
|
||||
maxWidth = contMax
|
||||
}
|
||||
}
|
||||
lines = append(lines, WrappedLine{string(line), width})
|
||||
|
||||
@@ -383,6 +383,16 @@ class TestPreview < TestInteractive
|
||||
end
|
||||
end
|
||||
|
||||
def test_preview_follow_wrap
|
||||
tmux.send_keys "seq 1 | #{FZF} --preview 'seq 1000' --preview-window right,2,follow,wrap", :Enter
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.until do |lines|
|
||||
assert_includes lines[-4], '│ 10 │'
|
||||
assert_includes lines[-3], '│ ↳ │'
|
||||
assert_includes lines[-2], '│ ↳ │'
|
||||
end
|
||||
end
|
||||
|
||||
def test_close
|
||||
tmux.send_keys "seq 100 | #{FZF} --preview 'echo foo' --bind ctrl-c:close", :Enter
|
||||
tmux.until { |lines| assert_equal 100, lines.match_count }
|
||||
@@ -541,7 +551,7 @@ class TestPreview < TestInteractive
|
||||
tmux.send_keys "seq 10 | #{FZF} --preview-border rounded --preview-window '~5,2,+0,<100000(~0,+100,wrap,noinfo)' --preview 'seq 1000'", :Enter
|
||||
tmux.until { |lines| assert_equal 10, lines.match_count }
|
||||
tmux.until do |lines|
|
||||
assert_equal ['╭────╮', '│ 10 │', '│ ↳ 0│', '│ 10 │', '│ ↳ 1│'], lines.take(5).map(&:strip)
|
||||
assert_equal ['╭────╮', '│ 10 │', '│ ↳ │', '│ 10 │', '│ ↳ │'], lines.take(5).map(&:strip)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
Reference in New Issue
Block a user