mirror of
https://github.com/junegunn/fzf.git
synced 2026-02-27 03:52:32 +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)
|
- Added fish completion support (#4605) (@lalvarezt)
|
||||||
- zsh: Handle multi-line history selection (#4595) (@LangLangBart)
|
- zsh: Handle multi-line history selection (#4595) (@LangLangBart)
|
||||||
- Bug fixes
|
- Bug fixes
|
||||||
|
- Fixed `--preview-window follow` not working correctly with wrapping (#3243, #4258)
|
||||||
- Fixed symlinks to directories being returned as files (#4676) (@skk64)
|
- Fixed symlinks to directories being returned as files (#4676) (@skk64)
|
||||||
- Fixed SIGHUP signal handling (#4668) (@LangLangBart)
|
- Fixed SIGHUP signal handling (#4668) (@LangLangBart)
|
||||||
- Fixed preview process not killed on exit (#4667)
|
- Fixed preview process not killed on exit (#4667)
|
||||||
|
|||||||
@@ -4093,6 +4093,80 @@ func extractPassThroughs(line string) ([]string, string) {
|
|||||||
return passThroughs, transformed
|
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 {
|
func (t *Terminal) wordWrapAnsiLine(line string, maxWidth int, wrapSignWidth int) []string {
|
||||||
if maxWidth <= 0 {
|
if maxWidth <= 0 {
|
||||||
return []string{line}
|
return []string{line}
|
||||||
@@ -5704,7 +5778,7 @@ func (t *Terminal) Loop() error {
|
|||||||
t.previewer.lines = result.lines
|
t.previewer.lines = result.lines
|
||||||
t.previewer.spinner = result.spinner
|
t.previewer.spinner = result.spinner
|
||||||
if t.hasPreviewWindow() && t.previewer.following.Enabled() {
|
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 {
|
} else if result.offset >= 0 {
|
||||||
t.previewer.offset = util.Constrain(result.offset, t.activePreviewOpts.headerLines, len(t.previewer.lines)-1)
|
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 {
|
for i, line := range allLines {
|
||||||
lines := WrapLine(line, w.posx, w.width, w.tabstop, w.wrapSignWidth)
|
lines := WrapLine(line, w.posx, w.width, w.tabstop, w.wrapSignWidth)
|
||||||
for j, wl := range lines {
|
for j, wl := range lines {
|
||||||
w.stderrInternal(wl.Text, false, resetCode)
|
if w.posx < w.width {
|
||||||
w.posx += wl.DisplayWidth
|
w.stderrInternal(wl.Text, false, resetCode)
|
||||||
|
w.posx += wl.DisplayWidth
|
||||||
|
}
|
||||||
|
|
||||||
// Wrap line
|
// Wrap line
|
||||||
if j < len(lines)-1 || i < len(allLines)-1 {
|
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.renderWrapSign(style)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
w.renderGraphemes(wl.Text, style)
|
if w.lastX < w.width {
|
||||||
|
w.renderGraphemes(wl.Text, style)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if w.lastX >= w.width {
|
if w.lastX >= w.width {
|
||||||
|
|||||||
@@ -1377,7 +1377,8 @@ func WrapLine(input string, prefixLength int, initialMax int, tabstop int, wrapS
|
|||||||
width := 0
|
width := 0
|
||||||
line := ""
|
line := ""
|
||||||
gr := uniseg.NewGraphemes(input)
|
gr := uniseg.NewGraphemes(input)
|
||||||
max := initialMax
|
maxWidth := initialMax
|
||||||
|
contMax := max(1, initialMax-wrapSignWidth)
|
||||||
for gr.Next() {
|
for gr.Next() {
|
||||||
rs := gr.Runes()
|
rs := gr.Runes()
|
||||||
str := string(rs)
|
str := string(rs)
|
||||||
@@ -1392,14 +1393,14 @@ func WrapLine(input string, prefixLength int, initialMax int, tabstop int, wrapS
|
|||||||
}
|
}
|
||||||
width += w
|
width += w
|
||||||
|
|
||||||
if prefixLength+width <= max {
|
if prefixLength+width <= maxWidth {
|
||||||
line += str
|
line += str
|
||||||
} else {
|
} else {
|
||||||
lines = append(lines, WrappedLine{string(line), width - w})
|
lines = append(lines, WrappedLine{string(line), width - w})
|
||||||
line = str
|
line = str
|
||||||
prefixLength = 0
|
prefixLength = 0
|
||||||
width = w
|
width = w
|
||||||
max = initialMax - wrapSignWidth
|
maxWidth = contMax
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lines = append(lines, WrappedLine{string(line), width})
|
lines = append(lines, WrappedLine{string(line), width})
|
||||||
|
|||||||
@@ -383,6 +383,16 @@ class TestPreview < TestInteractive
|
|||||||
end
|
end
|
||||||
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
|
def test_close
|
||||||
tmux.send_keys "seq 100 | #{FZF} --preview 'echo foo' --bind ctrl-c:close", :Enter
|
tmux.send_keys "seq 100 | #{FZF} --preview 'echo foo' --bind ctrl-c:close", :Enter
|
||||||
tmux.until { |lines| assert_equal 100, lines.match_count }
|
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.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 { |lines| assert_equal 10, lines.match_count }
|
||||||
tmux.until do |lines|
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user