mirror of
https://github.com/junegunn/fzf.git
synced 2026-05-25 17:58:50 +08:00
Add --preview-window=next position (#4801)
Places preview adjacent to input on the list side: above input in the
default layout, below it in --layout=reverse.
fzf --preview 'cat {}' --preview-window=next
Close #4798
This commit is contained in:
@@ -16,6 +16,7 @@ CHANGELOG
|
|||||||
else echo accept
|
else echo accept
|
||||||
fi'
|
fi'
|
||||||
```
|
```
|
||||||
|
- New `--preview-window=next` position that places the preview adjacent to the input section, on the list side: above the input in the default layout, below it in `--layout=reverse` (#4798).
|
||||||
- Bug fixes
|
- Bug fixes
|
||||||
- `change-preview-window` no longer resets `wrap` / `wrap-word` state set via `toggle-preview-wrap` / `toggle-preview-wrap-word`. Layout fields still snap to the preset, so cycling and the empty-token reset behave as before. The new spec can still override by including `wrap` or `nowrap` explicitly. (#4791)
|
- `change-preview-window` no longer resets `wrap` / `wrap-word` state set via `toggle-preview-wrap` / `toggle-preview-wrap-word`. Layout fields still snap to the preset, so cycling and the empty-token reset behave as before. The new spec can still override by including `wrap` or `nowrap` explicitly. (#4791)
|
||||||
|
|
||||||
|
|||||||
@@ -993,9 +993,14 @@ border line.
|
|||||||
\fBdown
|
\fBdown
|
||||||
\fBleft
|
\fBleft
|
||||||
\fBright
|
\fBright
|
||||||
|
\fBnext
|
||||||
|
|
||||||
\fRDetermines the layout of the preview window.
|
\fRDetermines the layout of the preview window.
|
||||||
|
|
||||||
|
* \fBnext\fR places the preview window adjacent to the input section, on
|
||||||
|
the list side: above the input in the default layout, below the input
|
||||||
|
in \fB\-\-layout=reverse\fR.
|
||||||
|
|
||||||
* If the argument contains \fB:hidden\fR, the preview window will be hidden by
|
* If the argument contains \fB:hidden\fR, the preview window will be hidden by
|
||||||
default until \fBtoggle\-preview\fR action is triggered.
|
default until \fBtoggle\-preview\fR action is triggered.
|
||||||
|
|
||||||
|
|||||||
+13
-4
@@ -160,7 +160,7 @@ Usage: fzf [options]
|
|||||||
PREVIEW WINDOW
|
PREVIEW WINDOW
|
||||||
--preview=COMMAND Command to preview highlighted line ({})
|
--preview=COMMAND Command to preview highlighted line ({})
|
||||||
--preview-window=OPT Preview window layout (default: right:50%)
|
--preview-window=OPT Preview window layout (default: right:50%)
|
||||||
[up|down|left|right][,SIZE[%]]
|
[up|down|left|right|next][,SIZE[%]]
|
||||||
[,[no]wrap[-word]][,[no]cycle][,[no]follow][,[no]info]
|
[,[no]wrap[-word]][,[no]cycle][,[no]follow][,[no]info]
|
||||||
[,[no]hidden][,border-STYLE]
|
[,[no]hidden][,border-STYLE]
|
||||||
[,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES]
|
[,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES]
|
||||||
@@ -332,6 +332,7 @@ const (
|
|||||||
posLeft
|
posLeft
|
||||||
posRight
|
posRight
|
||||||
posCenter
|
posCenter
|
||||||
|
posNext // adjacent to the input section, on the list side
|
||||||
)
|
)
|
||||||
|
|
||||||
type tmuxOptions struct {
|
type tmuxOptions struct {
|
||||||
@@ -391,7 +392,7 @@ func (o *previewOpts) Toggle() {
|
|||||||
o.hidden = !o.hidden
|
o.hidden = !o.hidden
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *previewOpts) Border() tui.BorderShape {
|
func (o *previewOpts) Border(layout layoutType) tui.BorderShape {
|
||||||
shape := o.border
|
shape := o.border
|
||||||
if shape == tui.BorderLine {
|
if shape == tui.BorderLine {
|
||||||
switch o.position {
|
switch o.position {
|
||||||
@@ -403,6 +404,12 @@ func (o *previewOpts) Border() tui.BorderShape {
|
|||||||
shape = tui.BorderRight
|
shape = tui.BorderRight
|
||||||
case posRight:
|
case posRight:
|
||||||
shape = tui.BorderLeft
|
shape = tui.BorderLeft
|
||||||
|
case posNext:
|
||||||
|
if layout == layoutReverse {
|
||||||
|
shape = tui.BorderBottom
|
||||||
|
} else {
|
||||||
|
shape = tui.BorderTop
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return shape
|
return shape
|
||||||
@@ -512,7 +519,7 @@ func parseLabelPosition(opts *labelOpts, arg string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a previewOpts) aboveOrBelow() bool {
|
func (a previewOpts) aboveOrBelow() bool {
|
||||||
return a.size.size > 0 && (a.position == posUp || a.position == posDown)
|
return a.size.size > 0 && (a.position == posUp || a.position == posDown || a.position == posNext)
|
||||||
}
|
}
|
||||||
|
|
||||||
type previewOptsCompare int
|
type previewOptsCompare int
|
||||||
@@ -2352,6 +2359,8 @@ func parsePreviewWindowImpl(opts *previewOpts, input string) error {
|
|||||||
opts.position = posLeft
|
opts.position = posLeft
|
||||||
case "right":
|
case "right":
|
||||||
opts.position = posRight
|
opts.position = posRight
|
||||||
|
case "next":
|
||||||
|
opts.position = posNext
|
||||||
case "rounded", "border", "border-rounded":
|
case "rounded", "border", "border-rounded":
|
||||||
opts.border = tui.BorderRounded
|
opts.border = tui.BorderRounded
|
||||||
case "border-line":
|
case "border-line":
|
||||||
@@ -3158,7 +3167,7 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
|
|||||||
case "--no-preview":
|
case "--no-preview":
|
||||||
opts.Preview.command = ""
|
opts.Preview.command = ""
|
||||||
case "--preview-window":
|
case "--preview-window":
|
||||||
str, err := nextString("preview window layout required: [up|down|left|right][,SIZE[%]][,border-STYLE][,wrap][,cycle][,hidden][,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES][,default]")
|
str, err := nextString("preview window layout required: [up|down|left|right|next][,SIZE[%]][,border-STYLE][,wrap][,cycle][,hidden][,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES][,default]")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
+203
-108
@@ -988,7 +988,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
|
|||||||
// Minimum height required to render fzf excluding margin and padding
|
// Minimum height required to render fzf excluding margin and padding
|
||||||
effectiveMinHeight := minHeight
|
effectiveMinHeight := minHeight
|
||||||
if previewBox != nil && opts.Preview.aboveOrBelow() {
|
if previewBox != nil && opts.Preview.aboveOrBelow() {
|
||||||
effectiveMinHeight += 1 + borderLines(opts.Preview.Border())
|
effectiveMinHeight += 1 + borderLines(opts.Preview.Border(opts.Layout))
|
||||||
}
|
}
|
||||||
if opts.noSeparatorLine() {
|
if opts.noSeparatorLine() {
|
||||||
effectiveMinHeight--
|
effectiveMinHeight--
|
||||||
@@ -1652,12 +1652,11 @@ func (t *Terminal) parsePrompt(prompt string) (func(), int) {
|
|||||||
wrap := t.wrap
|
wrap := t.wrap
|
||||||
t.wrap = false
|
t.wrap = false
|
||||||
t.withWindow(t.inputWindow, func() {
|
t.withWindow(t.inputWindow, func() {
|
||||||
line := t.promptLine()
|
|
||||||
preTask := func(markerClass) int {
|
preTask := func(markerClass) int {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
t.printHighlighted(
|
t.printHighlighted(
|
||||||
Result{item: item}, tui.ColPrompt, tui.ColPrompt, false, false, false, line, line, true, preTask, nil, 0)
|
Result{item: item}, tui.ColPrompt, tui.ColPrompt, false, false, false, 0, 0, true, preTask, nil, 0)
|
||||||
})
|
})
|
||||||
t.wrap = wrap
|
t.wrap = wrap
|
||||||
}
|
}
|
||||||
@@ -2105,12 +2104,13 @@ func calculateSize(base int, size sizeSpec, occupied int, minSize int) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *Terminal) minPreviewSize(opts *previewOpts) (int, int) {
|
func (t *Terminal) minPreviewSize(opts *previewOpts) (int, int) {
|
||||||
minPreviewWidth := 1 + borderColumns(opts.Border(), t.borderWidth)
|
border := opts.Border(t.layout)
|
||||||
minPreviewHeight := 1 + borderLines(opts.Border())
|
minPreviewWidth := 1 + borderColumns(border, t.borderWidth)
|
||||||
|
minPreviewHeight := 1 + borderLines(border)
|
||||||
|
|
||||||
switch opts.position {
|
switch opts.position {
|
||||||
case posLeft, posRight:
|
case posLeft, posRight:
|
||||||
if len(t.scrollbar) > 0 && !opts.Border().HasRight() {
|
if len(t.scrollbar) > 0 && !border.HasRight() {
|
||||||
// Need a column to show scrollbar
|
// Need a column to show scrollbar
|
||||||
minPreviewWidth++
|
minPreviewWidth++
|
||||||
}
|
}
|
||||||
@@ -2195,7 +2195,7 @@ func (t *Terminal) adjustMarginAndPadding() (int, int, [4]int, [4]int) {
|
|||||||
if t.needPreviewWindow() {
|
if t.needPreviewWindow() {
|
||||||
minPreviewWidth, minPreviewHeight := t.minPreviewSize(t.activePreviewOpts)
|
minPreviewWidth, minPreviewHeight := t.minPreviewSize(t.activePreviewOpts)
|
||||||
switch t.activePreviewOpts.position {
|
switch t.activePreviewOpts.position {
|
||||||
case posUp, posDown:
|
case posUp, posDown, posNext:
|
||||||
minAreaHeight += minPreviewHeight
|
minAreaHeight += minPreviewHeight
|
||||||
minAreaWidth = max(minPreviewWidth, minAreaWidth)
|
minAreaWidth = max(minPreviewWidth, minAreaWidth)
|
||||||
case posLeft, posRight:
|
case posLeft, posRight:
|
||||||
@@ -2220,7 +2220,7 @@ func (t *Terminal) hasHeaderWindow() bool {
|
|||||||
if t.hasHeaderLinesWindow() {
|
if t.hasHeaderLinesWindow() {
|
||||||
return len(t.header0) > 0
|
return len(t.header0) > 0
|
||||||
}
|
}
|
||||||
if t.headerBorderShape.Visible() {
|
if t.headerBorderShape.Visible() || t.headerFirst {
|
||||||
return len(t.header0)+t.headerLines > 0
|
return len(t.header0)+t.headerLines > 0
|
||||||
}
|
}
|
||||||
return t.inputBorderShape.Visible()
|
return t.inputBorderShape.Visible()
|
||||||
@@ -2258,6 +2258,9 @@ func (t *Terminal) determineHeaderLinesShape() (bool, tui.BorderShape) {
|
|||||||
|
|
||||||
// Use header window instead
|
// Use header window instead
|
||||||
if len(t.header0) == 0 {
|
if len(t.header0) == 0 {
|
||||||
|
if t.headerFirst && shape == tui.BorderPhantom {
|
||||||
|
return true, shape
|
||||||
|
}
|
||||||
return false, t.headerBorderShape
|
return false, t.headerBorderShape
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2450,7 +2453,56 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
|
|||||||
hasHeaderWindow := t.hasHeaderWindow()
|
hasHeaderWindow := t.hasHeaderWindow()
|
||||||
hasFooterWindow := len(t.footer) > 0
|
hasFooterWindow := len(t.footer) > 0
|
||||||
hasHeaderLinesWindow, headerLinesShape := t.determineHeaderLinesShape()
|
hasHeaderLinesWindow, headerLinesShape := t.determineHeaderLinesShape()
|
||||||
hasInputWindow := !t.inputless && (t.inputBorderShape.Visible() || hasHeaderWindow || hasHeaderLinesWindow)
|
// computePreviewSize returns the size resizePreviewWindows will compute
|
||||||
|
// for opts and the minimum size for that axis: height/minPreviewHeight
|
||||||
|
// for vertical positions, width/minPreviewWidth for horizontal.
|
||||||
|
computePreviewSize := func(opts *previewOpts) (int, int) {
|
||||||
|
minPreviewWidth, minPreviewHeight := t.minPreviewSize(opts)
|
||||||
|
switch opts.position {
|
||||||
|
case posUp, posDown, posNext:
|
||||||
|
minWindowHeight := minHeight
|
||||||
|
if t.inputless {
|
||||||
|
minWindowHeight--
|
||||||
|
}
|
||||||
|
if t.noSeparatorLine() {
|
||||||
|
minWindowHeight--
|
||||||
|
}
|
||||||
|
return calculateSize(height, opts.size, minWindowHeight, minPreviewHeight), minPreviewHeight
|
||||||
|
case posLeft, posRight:
|
||||||
|
minListWidth := minWidth
|
||||||
|
if t.listBorderShape.HasLeft() {
|
||||||
|
minListWidth += 2
|
||||||
|
}
|
||||||
|
if t.listBorderShape.HasRight() {
|
||||||
|
minListWidth++
|
||||||
|
}
|
||||||
|
return calculateSize(width, opts.size, minListWidth, minPreviewWidth), minPreviewWidth
|
||||||
|
}
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
|
// Walk the threshold chain to determine the previewOpts that
|
||||||
|
// resizePreviewWindows will actually settle on. We need this here
|
||||||
|
// because hasInputWindow and the availableLines adjustment below run
|
||||||
|
// before resizePreviewWindows, and t.activePreviewOpts still holds the
|
||||||
|
// previous frame's resolution.
|
||||||
|
effectivePreviewOpts := &t.previewOpts
|
||||||
|
if t.needPreviewWindow() {
|
||||||
|
opts := &t.previewOpts
|
||||||
|
for {
|
||||||
|
if opts.size.size == 0 || opts.threshold <= 0 || opts.alternative == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if actual, _ := computePreviewSize(opts); actual >= opts.threshold {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
opts = opts.alternative
|
||||||
|
if opts.hidden {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
effectivePreviewOpts = opts
|
||||||
|
}
|
||||||
|
hasInputWindow := !t.inputless && (t.inputBorderShape.Visible() || hasHeaderWindow || hasHeaderLinesWindow || effectivePreviewOpts.position == posNext)
|
||||||
inputWindowHeight := 2
|
inputWindowHeight := 2
|
||||||
if t.noSeparatorLine() {
|
if t.noSeparatorLine() {
|
||||||
inputWindowHeight--
|
inputWindowHeight--
|
||||||
@@ -2470,9 +2522,9 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
|
|||||||
|
|
||||||
// FIXME: Needed?
|
// FIXME: Needed?
|
||||||
if t.needPreviewWindow() {
|
if t.needPreviewWindow() {
|
||||||
_, minPreviewHeight := t.minPreviewSize(t.activePreviewOpts)
|
switch effectivePreviewOpts.position {
|
||||||
switch t.activePreviewOpts.position {
|
case posUp, posDown, posNext:
|
||||||
case posUp, posDown:
|
_, minPreviewHeight := t.minPreviewSize(effectivePreviewOpts)
|
||||||
availableLines -= minPreviewHeight
|
availableLines -= minPreviewHeight
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2624,6 +2676,50 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
|
|||||||
// Set up preview window
|
// Set up preview window
|
||||||
noBorder := tui.MakeBorderStyle(tui.BorderNone, t.unicode)
|
noBorder := tui.MakeBorderStyle(tui.BorderNone, t.unicode)
|
||||||
cleanLeft := []int{}
|
cleanLeft := []int{}
|
||||||
|
// previewNextSize is pheight when the preview is placed adjacent to
|
||||||
|
// the input (position == "next"); inputBorderTop() reads it through the
|
||||||
|
// closure to push input past the preview band.
|
||||||
|
previewNextSize := 0
|
||||||
|
// inputBorderTop returns the canvas Y at which the input border window
|
||||||
|
// should be placed. It depends on wborder/t.window (set by the preview
|
||||||
|
// case), the layout, --header-first, and previewNextSize (set when
|
||||||
|
// posNext is active). Used both for placing the preview adjacent to
|
||||||
|
// input and later for placing the input window itself.
|
||||||
|
inputBorderTop := func() int {
|
||||||
|
w := t.wborder
|
||||||
|
if w == nil {
|
||||||
|
w = t.window
|
||||||
|
}
|
||||||
|
hasSeparateHeader := hasHeaderWindow && t.headerBorderShape != tui.BorderInline
|
||||||
|
hasSeparateHeaderLines := hasHeaderLinesWindow && headerLinesShape != tui.BorderInline
|
||||||
|
if (hasSeparateHeader || hasSeparateHeaderLines) && t.headerFirst {
|
||||||
|
switch t.layout {
|
||||||
|
case layoutDefault:
|
||||||
|
btop := w.Top() + w.Height() + previewNextSize
|
||||||
|
if hasHeaderWindow && hasHeaderLinesWindow {
|
||||||
|
btop += headerLinesHeight
|
||||||
|
}
|
||||||
|
return btop
|
||||||
|
case layoutReverse:
|
||||||
|
btop := w.Top() - inputBorderHeight - previewNextSize
|
||||||
|
if hasHeaderWindow && hasHeaderLinesWindow {
|
||||||
|
btop -= headerLinesHeight
|
||||||
|
}
|
||||||
|
return btop
|
||||||
|
case layoutReverseList:
|
||||||
|
return w.Top() + w.Height() + previewNextSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch t.layout {
|
||||||
|
case layoutDefault:
|
||||||
|
return w.Top() + w.Height() + headerBorderHeight + headerLinesHeight + previewNextSize
|
||||||
|
case layoutReverse:
|
||||||
|
return w.Top() - shrink + footerBorderHeight - previewNextSize
|
||||||
|
case layoutReverseList:
|
||||||
|
return w.Top() + w.Height() + headerBorderHeight + previewNextSize
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
if forcePreview || t.needPreviewWindow() {
|
if forcePreview || t.needPreviewWindow() {
|
||||||
var resizePreviewWindows func(previewOpts *previewOpts)
|
var resizePreviewWindows func(previewOpts *previewOpts)
|
||||||
resizePreviewWindows = func(previewOpts *previewOpts) {
|
resizePreviewWindows = func(previewOpts *previewOpts) {
|
||||||
@@ -2635,7 +2731,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
|
|||||||
createPreviewWindow := func(y int, x int, w int, h int) {
|
createPreviewWindow := func(y int, x int, w int, h int) {
|
||||||
pwidth := w
|
pwidth := w
|
||||||
pheight := h
|
pheight := h
|
||||||
shape := previewOpts.Border()
|
shape := previewOpts.Border(t.layout)
|
||||||
previewBorder := tui.MakeBorderStyle(shape, t.unicode)
|
previewBorder := tui.MakeBorderStyle(shape, t.unicode)
|
||||||
t.pborder = t.tui.NewWindow(y, x, w, h, tui.WindowPreview, previewBorder, false)
|
t.pborder = t.tui.NewWindow(y, x, w, h, tui.WindowPreview, previewBorder, false)
|
||||||
pwidth -= borderColumns(shape, bw)
|
pwidth -= borderColumns(shape, bw)
|
||||||
@@ -2656,17 +2752,12 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
|
|||||||
t.pwindow.Erase()
|
t.pwindow.Erase()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
minPreviewWidth, minPreviewHeight := t.minPreviewSize(previewOpts)
|
// Shared boilerplate for vertical positions (posUp/posDown/posNext):
|
||||||
switch previewOpts.position {
|
// compute pheight, apply the threshold alternative, honor hidden,
|
||||||
case posUp, posDown:
|
// and update listStickToRight. Returns (pheight, true) when the
|
||||||
minWindowHeight := minHeight
|
// caller should return early.
|
||||||
if t.inputless {
|
computeVerticalSize := func() (int, bool) {
|
||||||
minWindowHeight--
|
pheight, minPreviewHeight := computePreviewSize(previewOpts)
|
||||||
}
|
|
||||||
if t.noSeparatorLine() {
|
|
||||||
minWindowHeight--
|
|
||||||
}
|
|
||||||
pheight := calculateSize(height, previewOpts.size, minWindowHeight, minPreviewHeight)
|
|
||||||
if hasThreshold && pheight < previewOpts.threshold {
|
if hasThreshold && pheight < previewOpts.threshold {
|
||||||
t.activePreviewOpts = previewOpts.alternative
|
t.activePreviewOpts = previewOpts.alternative
|
||||||
if forcePreview {
|
if forcePreview {
|
||||||
@@ -2675,22 +2766,27 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
|
|||||||
if !previewOpts.alternative.hidden {
|
if !previewOpts.alternative.hidden {
|
||||||
resizePreviewWindows(previewOpts.alternative)
|
resizePreviewWindows(previewOpts.alternative)
|
||||||
}
|
}
|
||||||
return
|
return 0, true
|
||||||
}
|
}
|
||||||
if forcePreview {
|
if forcePreview {
|
||||||
previewOpts.hidden = false
|
previewOpts.hidden = false
|
||||||
}
|
}
|
||||||
if previewOpts.hidden {
|
if previewOpts.hidden {
|
||||||
return
|
return 0, true
|
||||||
}
|
}
|
||||||
|
listStickToRight = listStickToRight && !previewOpts.Border(t.layout).HasRight()
|
||||||
listStickToRight = listStickToRight && !previewOpts.Border().HasRight()
|
|
||||||
if listStickToRight {
|
if listStickToRight {
|
||||||
innerWidth++
|
innerWidth++
|
||||||
width++
|
width++
|
||||||
}
|
}
|
||||||
|
return util.Constrain(pheight, minPreviewHeight, availableLines), false
|
||||||
pheight = util.Constrain(pheight, minPreviewHeight, availableLines)
|
}
|
||||||
|
switch previewOpts.position {
|
||||||
|
case posUp, posDown:
|
||||||
|
pheight, done := computeVerticalSize()
|
||||||
|
if done {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if previewOpts.position == posUp {
|
if previewOpts.position == posUp {
|
||||||
innerBorderFn(marginInt[0]+pheight, marginInt[3], width, height-pheight)
|
innerBorderFn(marginInt[0]+pheight, marginInt[3], width, height-pheight)
|
||||||
@@ -2703,15 +2799,32 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
|
|||||||
innerMarginInt[0]+shift+inlineTopLines, innerMarginInt[3], innerWidth, innerHeight-pheight-shrink-inlineTopLines-inlineBottomLines, tui.WindowList, noBorder, true)
|
innerMarginInt[0]+shift+inlineTopLines, innerMarginInt[3], innerWidth, innerHeight-pheight-shrink-inlineTopLines-inlineBottomLines, tui.WindowList, noBorder, true)
|
||||||
createPreviewWindow(marginInt[0]+height-pheight, marginInt[3], width, pheight)
|
createPreviewWindow(marginInt[0]+height-pheight, marginInt[3], width, pheight)
|
||||||
}
|
}
|
||||||
|
case posNext:
|
||||||
|
pheight, done := computeVerticalSize()
|
||||||
|
if done {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
previewNextSize = pheight
|
||||||
|
|
||||||
|
if t.layout == layoutReverse {
|
||||||
|
// [(header)][input][preview]([header])[list]: reuse posUp's
|
||||||
|
// wborder/list math; input is pulled back up by pheight in
|
||||||
|
// its positioning. Preview sits directly below input.
|
||||||
|
innerBorderFn(marginInt[0]+pheight, marginInt[3], width, height-pheight)
|
||||||
|
t.window = t.tui.NewWindow(
|
||||||
|
innerMarginInt[0]+pheight+shift+inlineTopLines, innerMarginInt[3], innerWidth, innerHeight-pheight-shrink-inlineTopLines-inlineBottomLines, tui.WindowList, noBorder, true)
|
||||||
|
createPreviewWindow(inputBorderTop()+inputBorderHeight, marginInt[3], width, pheight)
|
||||||
|
} else {
|
||||||
|
// [list]([header])[preview][input][(header)]: reuse posDown's
|
||||||
|
// wborder/list math; input is pushed down by pheight in its
|
||||||
|
// positioning. Preview sits directly above input.
|
||||||
|
innerBorderFn(marginInt[0], marginInt[3], width, height-pheight)
|
||||||
|
t.window = t.tui.NewWindow(
|
||||||
|
innerMarginInt[0]+shift+inlineTopLines, innerMarginInt[3], innerWidth, innerHeight-pheight-shrink-inlineTopLines-inlineBottomLines, tui.WindowList, noBorder, true)
|
||||||
|
createPreviewWindow(inputBorderTop()-pheight, marginInt[3], width, pheight)
|
||||||
|
}
|
||||||
case posLeft, posRight:
|
case posLeft, posRight:
|
||||||
minListWidth := minWidth
|
pwidth, _ := computePreviewSize(previewOpts)
|
||||||
if t.listBorderShape.HasLeft() {
|
|
||||||
minListWidth += 2
|
|
||||||
}
|
|
||||||
if t.listBorderShape.HasRight() {
|
|
||||||
minListWidth++
|
|
||||||
}
|
|
||||||
pwidth := calculateSize(width, previewOpts.size, minListWidth, minPreviewWidth)
|
|
||||||
if hasThreshold && pwidth < previewOpts.threshold {
|
if hasThreshold && pwidth < previewOpts.threshold {
|
||||||
t.activePreviewOpts = previewOpts.alternative
|
t.activePreviewOpts = previewOpts.alternative
|
||||||
if forcePreview {
|
if forcePreview {
|
||||||
@@ -2743,7 +2856,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
|
|||||||
|
|
||||||
// Clear characters on the margin
|
// Clear characters on the margin
|
||||||
// fzf --bind 'space:toggle-preview' --preview ':' --preview-window left,1,border-none --footer-border --footer f --header h --header-border
|
// fzf --bind 'space:toggle-preview' --preview ':' --preview-window left,1,border-none --footer-border --footer f --header h --header-border
|
||||||
if !previewOpts.Border().HasRight() {
|
if !previewOpts.Border(t.layout).HasRight() {
|
||||||
cleanLeft = append(cleanLeft, -2)
|
cleanLeft = append(cleanLeft, -2)
|
||||||
}
|
}
|
||||||
// fzf --bind 'space:toggle-preview' --preview ':' --preview-window left,1 --footer-border --footer f --header h --header-border
|
// fzf --bind 'space:toggle-preview' --preview ':' --preview-window left,1 --footer-border --footer f --header h --header-border
|
||||||
@@ -2758,7 +2871,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
|
|||||||
// fzf --preview 'seq 500' --preview-window border-left --border
|
// fzf --preview 'seq 500' --preview-window border-left --border
|
||||||
// fzf --preview 'seq 500' --preview-window border-left --border --list-border
|
// fzf --preview 'seq 500' --preview-window border-left --border --list-border
|
||||||
// fzf --preview 'seq 500' --preview-window border-left --border --input-border
|
// fzf --preview 'seq 500' --preview-window border-left --border --input-border
|
||||||
listStickToRight = t.borderShape.HasRight() && !previewOpts.Border().HasRight()
|
listStickToRight = t.borderShape.HasRight() && !previewOpts.Border(t.layout).HasRight()
|
||||||
if listStickToRight {
|
if listStickToRight {
|
||||||
innerWidth++
|
innerWidth++
|
||||||
width++
|
width++
|
||||||
@@ -2847,37 +2960,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if hasInputWindow {
|
if hasInputWindow {
|
||||||
var btop int
|
btop := inputBorderTop()
|
||||||
// Inline sections live inside the list frame, so they don't participate
|
|
||||||
// in --header-first repositioning; only non-inline sections do.
|
|
||||||
hasNonInlineHeader := hasHeaderWindow && t.headerBorderShape != tui.BorderInline
|
|
||||||
hasNonInlineHeaderLines := hasHeaderLinesWindow && headerLinesShape != tui.BorderInline
|
|
||||||
if (hasNonInlineHeader || hasNonInlineHeaderLines) && t.headerFirst {
|
|
||||||
switch t.layout {
|
|
||||||
case layoutDefault:
|
|
||||||
btop = w.Top() + w.Height()
|
|
||||||
// If both headers are present, the header lines are displayed with the list
|
|
||||||
if hasHeaderWindow && hasHeaderLinesWindow {
|
|
||||||
btop += headerLinesHeight
|
|
||||||
}
|
|
||||||
case layoutReverse:
|
|
||||||
btop = w.Top() - inputBorderHeight
|
|
||||||
if hasHeaderWindow && hasHeaderLinesWindow {
|
|
||||||
btop -= headerLinesHeight
|
|
||||||
}
|
|
||||||
case layoutReverseList:
|
|
||||||
btop = w.Top() + w.Height()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
switch t.layout {
|
|
||||||
case layoutDefault:
|
|
||||||
btop = w.Top() + w.Height() + headerBorderHeight + headerLinesHeight
|
|
||||||
case layoutReverse:
|
|
||||||
btop = w.Top() - shrink + footerBorderHeight
|
|
||||||
case layoutReverseList:
|
|
||||||
btop = w.Top() + w.Height() + headerBorderHeight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
shift := 0
|
shift := 0
|
||||||
if !t.inputBorderShape.HasLeft() && t.listBorderShape.HasLeft() {
|
if !t.inputBorderShape.HasLeft() && t.listBorderShape.HasLeft() {
|
||||||
shift += t.borderWidth + 1
|
shift += t.borderWidth + 1
|
||||||
@@ -2901,11 +2984,11 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
|
|||||||
var btop int
|
var btop int
|
||||||
if hasInputWindow && t.headerFirst {
|
if hasInputWindow && t.headerFirst {
|
||||||
if t.layout == layoutReverse {
|
if t.layout == layoutReverse {
|
||||||
btop = w.Top() - shrink + footerBorderHeight
|
btop = w.Top() - shrink + footerBorderHeight - previewNextSize
|
||||||
} else if t.layout == layoutReverseList {
|
} else if t.layout == layoutReverseList {
|
||||||
btop = w.Top() + w.Height() + inputBorderHeight
|
btop = w.Top() + w.Height() + inputBorderHeight + previewNextSize
|
||||||
} else {
|
} else {
|
||||||
btop = w.Top() + w.Height() + inputBorderHeight + headerLinesHeight
|
btop = w.Top() + w.Height() + inputBorderHeight + headerLinesHeight + previewNextSize
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if t.layout == layoutReverse {
|
if t.layout == layoutReverse {
|
||||||
@@ -2936,7 +3019,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
|
|||||||
|
|
||||||
if headerFirst {
|
if headerFirst {
|
||||||
if t.layout == layoutDefault {
|
if t.layout == layoutDefault {
|
||||||
btop = w.Top() + w.Height() + inputBorderHeight
|
btop = w.Top() + w.Height() + inputBorderHeight + previewNextSize
|
||||||
} else if t.layout == layoutReverse {
|
} else if t.layout == layoutReverse {
|
||||||
btop = w.Top() - headerLinesHeight - inputBorderHeight
|
btop = w.Top() - headerLinesHeight - inputBorderHeight
|
||||||
} else {
|
} else {
|
||||||
@@ -3004,7 +3087,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
|
|||||||
}
|
}
|
||||||
t.printLabel(t.wborder, listLabel, t.listLabelOpts, listLabelLen, t.listBorderShape, false)
|
t.printLabel(t.wborder, listLabel, t.listLabelOpts, listLabelLen, t.listBorderShape, false)
|
||||||
t.printLabel(t.border, t.borderLabel, t.borderLabelOpts, t.borderLabelLen, t.borderShape, false)
|
t.printLabel(t.border, t.borderLabel, t.borderLabelOpts, t.borderLabelLen, t.borderShape, false)
|
||||||
t.printLabel(t.pborder, t.previewLabel, t.previewLabelOpts, t.previewLabelLen, t.activePreviewOpts.Border(), false)
|
t.printLabel(t.pborder, t.previewLabel, t.previewLabelOpts, t.previewLabelLen, t.activePreviewOpts.Border(t.layout), false)
|
||||||
t.printLabel(t.inputBorder, t.inputLabel, t.inputLabelOpts, t.inputLabelLen, t.inputBorderShape, false)
|
t.printLabel(t.inputBorder, t.inputLabel, t.inputLabelOpts, t.inputLabelLen, t.inputBorderShape, false)
|
||||||
t.printLabel(t.headerBorder, t.headerLabel, t.headerLabelOpts, t.headerLabelLen, t.headerBorderShape, false)
|
t.printLabel(t.headerBorder, t.headerLabel, t.headerLabelOpts, t.headerLabelLen, t.headerBorderShape, false)
|
||||||
t.printLabel(t.footerBorder, t.footerLabel, t.footerLabelOpts, t.footerLabelLen, t.footerBorderShape, false)
|
t.printLabel(t.footerBorder, t.footerLabel, t.footerLabelOpts, t.footerLabelLen, t.footerBorderShape, false)
|
||||||
@@ -3113,23 +3196,6 @@ func (t *Terminal) updatePromptOffset() ([]rune, []rune) {
|
|||||||
return before, after
|
return before, after
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Terminal) promptLine() int {
|
|
||||||
if t.inputWindow != nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
if t.headerFirst {
|
|
||||||
max := t.window.Height() - 1
|
|
||||||
if max <= 0 { // Extremely short terminal
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
if !t.noSeparatorLine() {
|
|
||||||
max--
|
|
||||||
}
|
|
||||||
return min(t.visibleHeaderLinesInList(), max)
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Terminal) placeCursor() {
|
func (t *Terminal) placeCursor() {
|
||||||
if t.inputless {
|
if t.inputless {
|
||||||
return
|
return
|
||||||
@@ -3145,7 +3211,7 @@ func (t *Terminal) placeCursor() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
x = min(x, t.window.Width()-1)
|
x = min(x, t.window.Width()-1)
|
||||||
t.move(t.promptLine(), x, false)
|
t.move(0, x, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Terminal) printPrompt() {
|
func (t *Terminal) printPrompt() {
|
||||||
@@ -3199,7 +3265,7 @@ func (t *Terminal) printInfoImpl() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
pos := 0
|
pos := 0
|
||||||
line := t.promptLine()
|
line := 0
|
||||||
maxHeight := t.window.Height()
|
maxHeight := t.window.Height()
|
||||||
move := func(y int, x int, clear bool) bool {
|
move := func(y int, x int, clear bool) bool {
|
||||||
if y < 0 || y >= maxHeight {
|
if y < 0 || y >= maxHeight {
|
||||||
@@ -3534,12 +3600,6 @@ func (t *Terminal) headerIndentImpl(base int, borderShape tui.BorderShape) int {
|
|||||||
|
|
||||||
func (t *Terminal) printHeaderImpl(window tui.Window, borderShape tui.BorderShape, lines1 []string, lines2 []Item) {
|
func (t *Terminal) printHeaderImpl(window tui.Window, borderShape tui.BorderShape, lines1 []string, lines2 []Item) {
|
||||||
max := t.window.Height()
|
max := t.window.Height()
|
||||||
if !t.inputless && t.inputWindow == nil && window == nil && t.headerFirst {
|
|
||||||
max--
|
|
||||||
if !t.noSeparatorLine() {
|
|
||||||
max--
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var state *ansiState
|
var state *ansiState
|
||||||
needReverse := false
|
needReverse := false
|
||||||
switch t.layout {
|
switch t.layout {
|
||||||
@@ -4883,11 +4943,11 @@ func (t *Terminal) renderPreviewScrollbar(yoff int, barLength int, barStart int)
|
|||||||
t.previewer.xw = xw
|
t.previewer.xw = xw
|
||||||
}
|
}
|
||||||
xshift := -1 - t.borderWidth
|
xshift := -1 - t.borderWidth
|
||||||
if !t.activePreviewOpts.Border().HasRight() {
|
if !t.activePreviewOpts.Border(t.layout).HasRight() {
|
||||||
xshift = -1
|
xshift = -1
|
||||||
}
|
}
|
||||||
yshift := 1
|
yshift := 1
|
||||||
if !t.activePreviewOpts.Border().HasTop() {
|
if !t.activePreviewOpts.Border(t.layout).HasTop() {
|
||||||
yshift = 0
|
yshift = 0
|
||||||
}
|
}
|
||||||
for i := yoff; i < height; i++ {
|
for i := yoff; i < height; i++ {
|
||||||
@@ -5864,13 +5924,13 @@ func (t *Terminal) Loop() error {
|
|||||||
if t.activePreviewOpts.aboveOrBelow() {
|
if t.activePreviewOpts.aboveOrBelow() {
|
||||||
if t.activePreviewOpts.size.percent {
|
if t.activePreviewOpts.size.percent {
|
||||||
newContentHeight := int(float64(contentHeight) * 100. / (100. - t.activePreviewOpts.size.size))
|
newContentHeight := int(float64(contentHeight) * 100. / (100. - t.activePreviewOpts.size.size))
|
||||||
contentHeight = max(contentHeight+1+borderLines(t.activePreviewOpts.Border()), newContentHeight)
|
contentHeight = max(contentHeight+1+borderLines(t.activePreviewOpts.Border(t.layout)), newContentHeight)
|
||||||
} else {
|
} else {
|
||||||
contentHeight += int(t.activePreviewOpts.size.size) + borderLines(t.activePreviewOpts.Border())
|
contentHeight += int(t.activePreviewOpts.size.size) + borderLines(t.activePreviewOpts.Border(t.layout))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Minimum height if preview window can appear
|
// Minimum height if preview window can appear
|
||||||
contentHeight = max(contentHeight, 1+borderLines(t.activePreviewOpts.Border()))
|
contentHeight = max(contentHeight, 1+borderLines(t.activePreviewOpts.Border(t.layout)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return min(termHeight, contentHeight+pad)
|
return min(termHeight, contentHeight+pad)
|
||||||
@@ -6252,7 +6312,7 @@ func (t *Terminal) Loop() error {
|
|||||||
case reqRedrawBorderLabel:
|
case reqRedrawBorderLabel:
|
||||||
t.printLabel(t.border, t.borderLabel, t.borderLabelOpts, t.borderLabelLen, t.borderShape, true)
|
t.printLabel(t.border, t.borderLabel, t.borderLabelOpts, t.borderLabelLen, t.borderShape, true)
|
||||||
case reqRedrawPreviewLabel:
|
case reqRedrawPreviewLabel:
|
||||||
t.printLabel(t.pborder, t.previewLabel, t.previewLabelOpts, t.previewLabelLen, t.activePreviewOpts.Border(), true)
|
t.printLabel(t.pborder, t.previewLabel, t.previewLabelOpts, t.previewLabelLen, t.activePreviewOpts.Border(t.layout), true)
|
||||||
case reqReinit, reqResize, reqFullRedraw, reqRedraw:
|
case reqReinit, reqResize, reqFullRedraw, reqRedraw:
|
||||||
if req == reqReinit {
|
if req == reqReinit {
|
||||||
t.tui.Resume(t.fullscreen, true)
|
t.tui.Resume(t.fullscreen, true)
|
||||||
@@ -7585,6 +7645,20 @@ func (t *Terminal) Loop() error {
|
|||||||
} else if t.listBorderShape.HasRight() && t.pborder.EncloseY(my) && mx == t.wborder.Left()+t.wborder.Width()-1 {
|
} else if t.listBorderShape.HasRight() && t.pborder.EncloseY(my) && mx == t.wborder.Left()+t.wborder.Width()-1 {
|
||||||
pborderDragging = 1
|
pborderDragging = 1
|
||||||
}
|
}
|
||||||
|
case posNext:
|
||||||
|
if t.layout == layoutReverse {
|
||||||
|
if t.pborder.Enclose(my, mx) && my == t.pborder.Top()+t.pborder.Height()-1 {
|
||||||
|
pborderDragging = 0
|
||||||
|
} else if t.listBorderShape.HasTop() && t.pborder.EncloseX(mx) && my == t.wborder.Top() {
|
||||||
|
pborderDragging = 1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if t.pborder.Enclose(my, mx) && my == t.pborder.Top() {
|
||||||
|
pborderDragging = 0
|
||||||
|
} else if t.listBorderShape.HasBottom() && t.pborder.EncloseX(mx) && my == t.wborder.Top()+t.wborder.Height()-1 {
|
||||||
|
pborderDragging = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7608,6 +7682,27 @@ func (t *Terminal) Loop() error {
|
|||||||
prevSize = t.pwindow.Width()
|
prevSize = t.pwindow.Width()
|
||||||
offset := mx - t.pborder.Left()
|
offset := mx - t.pborder.Left()
|
||||||
newSize = prevSize - offset
|
newSize = prevSize - offset
|
||||||
|
case posNext:
|
||||||
|
prevSize = t.pwindow.Height()
|
||||||
|
// In posNext, header/header-lines sections may sit
|
||||||
|
// between preview and list. When the list border is
|
||||||
|
// dragged (pborderDragging == 1), subtract that gap
|
||||||
|
// so the initial click does not jump.
|
||||||
|
headerGap := 0
|
||||||
|
if pborderDragging == 1 && t.wborder != nil {
|
||||||
|
if t.layout == layoutReverse {
|
||||||
|
headerGap = t.wborder.Top() - (t.pborder.Top() + t.pborder.Height())
|
||||||
|
} else {
|
||||||
|
headerGap = t.pborder.Top() - (t.wborder.Top() + t.wborder.Height())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if t.layout == layoutReverse {
|
||||||
|
diff := t.pborder.Height() - prevSize
|
||||||
|
newSize = my - t.pborder.Top() - diff + 1 - headerGap
|
||||||
|
} else {
|
||||||
|
offset := my - t.pborder.Top()
|
||||||
|
newSize = prevSize - offset - headerGap
|
||||||
|
}
|
||||||
}
|
}
|
||||||
newSize -= pborderDragging
|
newSize -= pborderDragging
|
||||||
if newSize < 1 {
|
if newSize < 1 {
|
||||||
@@ -7740,7 +7835,7 @@ func (t *Terminal) Loop() error {
|
|||||||
|
|
||||||
if me.Down {
|
if me.Down {
|
||||||
mxCons := util.Constrain(mx-t.promptLen, 0, len(t.input))
|
mxCons := util.Constrain(mx-t.promptLen, 0, len(t.input))
|
||||||
if !t.inputless && t.inputWindow == nil && my == t.promptLine() && mxCons >= 0 {
|
if !t.inputless && t.inputWindow == nil && my == 0 && mxCons >= 0 {
|
||||||
// Prompt
|
// Prompt
|
||||||
t.cx = mxCons + t.xoffset
|
t.cx = mxCons + t.xoffset
|
||||||
} else if my >= min {
|
} else if my >= min {
|
||||||
|
|||||||
+175
-60
@@ -243,6 +243,90 @@ class TestLayout < TestInteractive
|
|||||||
tmux.until { assert_block(expected, it) }
|
tmux.until { assert_block(expected, it) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_preview_window_next_reverse
|
||||||
|
# https://github.com/junegunn/fzf/issues/4798
|
||||||
|
tmux.send_keys %(seq 5 | #{FZF} --layout=reverse --preview 'echo PREVIEW' --preview-window=next:3 --prompt='line2$ > '), :Enter
|
||||||
|
expected = <<~OUTPUT
|
||||||
|
line2$ >
|
||||||
|
5/5 ───
|
||||||
|
╭────────
|
||||||
|
│ PREVIEW
|
||||||
|
│
|
||||||
|
│
|
||||||
|
╰────────
|
||||||
|
> 1
|
||||||
|
OUTPUT
|
||||||
|
tmux.until { assert_block(expected, it) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_preview_window_next_default
|
||||||
|
tmux.send_keys %(seq 5 | #{FZF} --preview 'echo PREVIEW' --preview-window=next:3), :Enter
|
||||||
|
expected = <<~OUTPUT
|
||||||
|
> 1
|
||||||
|
╭────────
|
||||||
|
│ PREVIEW
|
||||||
|
│
|
||||||
|
│
|
||||||
|
╰────────
|
||||||
|
5/5 ───
|
||||||
|
>
|
||||||
|
OUTPUT
|
||||||
|
tmux.until { assert_block(expected, it) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_preview_window_next_border_line_at_runtime
|
||||||
|
# change-preview-window to next,border-line should resolve BorderLine
|
||||||
|
# to a single horizontal separator, matching the behavior
|
||||||
|
# when next,border-line is the initial spec.
|
||||||
|
tmux.send_keys %(seq 5 | #{FZF} --preview 'echo PREVIEW' --bind 'space:change-preview-window:next:3,border-line'), :Enter
|
||||||
|
tmux.until { |lines| assert_equal 5, lines.match_count }
|
||||||
|
tmux.send_keys :Space
|
||||||
|
expected = <<~OUTPUT
|
||||||
|
> 1
|
||||||
|
───────
|
||||||
|
PREVIEW
|
||||||
|
OUTPUT
|
||||||
|
tmux.until do |lines|
|
||||||
|
cursor = lines.index { it.start_with?('> 1') }
|
||||||
|
assert(cursor)
|
||||||
|
assert_block(expected, lines[cursor..])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_header_first_change_header_at_runtime
|
||||||
|
# --header-first with no initial --header content needs to grow a
|
||||||
|
# header window when change-header adds content at runtime, so the
|
||||||
|
# new header lands below the prompt (not on top of it).
|
||||||
|
tmux.send_keys %(seq 5 | #{FZF} --header-first --bind 'space:change-header:foo'), :Enter
|
||||||
|
tmux.until { |lines| assert_equal 5, lines.match_count }
|
||||||
|
tmux.send_keys :Space
|
||||||
|
expected = <<~OUTPUT
|
||||||
|
>
|
||||||
|
foo
|
||||||
|
OUTPUT
|
||||||
|
tmux.until do |lines|
|
||||||
|
prompt = lines.index { it.start_with?('>') }
|
||||||
|
assert(prompt)
|
||||||
|
assert_block(expected, lines[prompt..])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_preview_window_next_style_full_line
|
||||||
|
tmux.send_keys %(seq 5 | #{FZF} --reverse --preview 'echo PREVIEW' --preview-window=next:3 --header foo --footer bar --style full:line), :Enter
|
||||||
|
expected = <<~OUTPUT
|
||||||
|
>
|
||||||
|
───────
|
||||||
|
PREVIEW
|
||||||
|
|
||||||
|
|
||||||
|
───────
|
||||||
|
foo
|
||||||
|
───────
|
||||||
|
> 1
|
||||||
|
OUTPUT
|
||||||
|
tmux.until { assert_block(expected, it) }
|
||||||
|
end
|
||||||
|
|
||||||
def test_height_range_overflow
|
def test_height_range_overflow
|
||||||
tmux.send_keys 'seq 100 | fzf --height ~5 --info=inline --border rounded', :Enter
|
tmux.send_keys 'seq 100 | fzf --height ~5 --info=inline --border rounded', :Enter
|
||||||
expected = <<~OUTPUT
|
expected = <<~OUTPUT
|
||||||
@@ -1227,75 +1311,106 @@ class TestLayout < TestInteractive
|
|||||||
def test_combinations
|
def test_combinations
|
||||||
skip unless ENV['LONGTEST']
|
skip unless ENV['LONGTEST']
|
||||||
|
|
||||||
base = [
|
begin
|
||||||
'--pointer=@',
|
base = [
|
||||||
'--exact',
|
'--pointer=@',
|
||||||
'--query=123',
|
'--exact',
|
||||||
'--header="$(seq 101 103)"',
|
'--query=123',
|
||||||
'--header-lines=3',
|
'--header="$(seq 101 103)"',
|
||||||
'--footer "$(seq 201 203)"',
|
'--header-lines=3',
|
||||||
'--preview "echo foobar"'
|
'--footer "$(seq 201 203)"',
|
||||||
]
|
'--preview "echo foobar"'
|
||||||
options = [
|
]
|
||||||
['--separator==', '--no-separator'],
|
options = [
|
||||||
['--info=default', '--info=inline', '--info=inline-right'],
|
['--separator==', '--no-separator'],
|
||||||
['--no-input-border', '--input-border'],
|
['--info=default', '--info=inline', '--info=inline-right'],
|
||||||
['--no-header-border', '--header-border=none', '--header-border'],
|
['--no-input-border', '--input-border'],
|
||||||
['--no-header-lines-border', '--header-lines-border'],
|
['--no-header-border', '--header-border=none', '--header-border'],
|
||||||
['--no-footer-border', '--footer-border'],
|
['--no-header-lines-border', '--header-lines-border'],
|
||||||
['--no-list-border', '--list-border'],
|
['--no-footer-border', '--footer-border'],
|
||||||
['--preview-window=right', '--preview-window=up', '--preview-window=down', '--preview-window=left'],
|
['--no-list-border', '--list-border'],
|
||||||
['--header-first', '--no-header-first'],
|
['--preview-window=right', '--preview-window=up', '--preview-window=down', '--preview-window=left', '--preview-window=next'],
|
||||||
['--layout=default', '--layout=reverse', '--layout=reverse-list']
|
['--header-first', '--no-header-first'],
|
||||||
]
|
['--layout=default', '--layout=reverse', '--layout=reverse-list']
|
||||||
# Combination of all options
|
]
|
||||||
combinations = options[0].product(*options.drop(1))
|
# Combination of all options
|
||||||
combinations.each_with_index do |combination, index|
|
combinations = options[0].product(*options.drop(1))
|
||||||
opts = base + combination
|
|
||||||
command = %(seq 1001 2000 | #{FZF} #{opts.join(' ')})
|
|
||||||
puts "# #{index + 1}/#{combinations.length}\n#{command}"
|
|
||||||
tmux.send_keys command, :Enter
|
|
||||||
tmux.until do |lines|
|
|
||||||
layout = combination.find { it.start_with?('--layout=') }.split('=').last
|
|
||||||
header_first = combination.include?('--header-first')
|
|
||||||
|
|
||||||
# Input
|
# Run workers in parallel, each with its own pre-created tmux window.
|
||||||
input = lines.index { it.include?('> 123') }
|
# Tmux setup/teardown is serialized in the main thread to avoid racing
|
||||||
assert(input)
|
# `tmux new-window` and `tmux kill-window` calls on the tmux server.
|
||||||
|
workers = 10
|
||||||
|
tmuxes = Array.new(workers) { Tmux.new }
|
||||||
|
failures = []
|
||||||
|
mutex = Mutex.new
|
||||||
|
queue = Queue.new
|
||||||
|
index = 0
|
||||||
|
threads = tmuxes.map do |local_tmux|
|
||||||
|
Thread.new do
|
||||||
|
command = nil
|
||||||
|
loop do
|
||||||
|
combination = queue.pop or break
|
||||||
|
|
||||||
# Info
|
opts = base + combination
|
||||||
info = lines.index { it.include?('11/997') }
|
command = %(seq 1001 2000 | #{FZF} #{opts.join(' ')})
|
||||||
assert(info)
|
mutex.synchronize do
|
||||||
|
print("\r#{index += 1}/#{combinations.length}")
|
||||||
|
end
|
||||||
|
local_tmux.send_keys command, :Enter
|
||||||
|
local_tmux.until do |lines|
|
||||||
|
layout = combination.find { it.start_with?('--layout=') }.split('=').last
|
||||||
|
header_first = combination.include?('--header-first')
|
||||||
|
|
||||||
assert(layout == 'reverse' ? input <= info : input >= info)
|
# Input
|
||||||
|
input = lines.index { it.include?('> 123') }
|
||||||
|
assert(input)
|
||||||
|
|
||||||
# List
|
# Info
|
||||||
item1 = lines.index { it.include?('1230') }
|
info = lines.index { it.include?('11/997') }
|
||||||
item2 = lines.index { it.include?('1231') }
|
assert(info)
|
||||||
assert_equal(item1, layout == 'default' ? item2 + 1 : item2 - 1)
|
|
||||||
|
|
||||||
# Preview
|
assert(layout == 'reverse' ? input <= info : input >= info)
|
||||||
assert(lines.any? { it.include?('foobar') })
|
|
||||||
|
|
||||||
# Header
|
# List
|
||||||
header1 = lines.index { it.include?('101') }
|
item1 = lines.index { it.include?('1230') }
|
||||||
header2 = lines.index { it.include?('102') }
|
item2 = lines.index { it.include?('1231') }
|
||||||
assert_equal(header2, header1 + 1)
|
assert_equal(item1, layout == 'default' ? item2 + 1 : item2 - 1)
|
||||||
assert((layout == 'reverse') == header_first ? input > header1 : input < header1)
|
|
||||||
|
|
||||||
# Footer
|
# Preview
|
||||||
footer1 = lines.index { it.include?('201') }
|
assert(lines.any? { it.include?('foobar') })
|
||||||
footer2 = lines.index { it.include?('202') }
|
|
||||||
assert_equal(footer2, footer1 + 1)
|
|
||||||
assert(layout == 'reverse' ? footer1 > item2 : footer1 < item2)
|
|
||||||
|
|
||||||
# Header lines
|
# Header
|
||||||
hline1 = lines.index { it.include?('1001') }
|
header1 = lines.index { it.include?('101') }
|
||||||
hline2 = lines.index { it.include?('1002') }
|
header2 = lines.index { it.include?('102') }
|
||||||
assert_equal(hline1, layout == 'default' ? hline2 + 1 : hline2 - 1)
|
assert_equal(header2, header1 + 1)
|
||||||
assert(layout == 'reverse' ? hline1 > header1 : hline1 < header1)
|
assert((layout == 'reverse') == header_first ? input > header1 : input < header1)
|
||||||
|
|
||||||
|
# Footer
|
||||||
|
footer1 = lines.index { it.include?('201') }
|
||||||
|
footer2 = lines.index { it.include?('202') }
|
||||||
|
assert_equal(footer2, footer1 + 1)
|
||||||
|
assert(layout == 'reverse' ? footer1 > item2 : footer1 < item2)
|
||||||
|
|
||||||
|
# Header lines
|
||||||
|
hline1 = lines.index { it.include?('1001') }
|
||||||
|
hline2 = lines.index { it.include?('1002') }
|
||||||
|
assert_equal(hline1, layout == 'default' ? hline2 + 1 : hline2 - 1)
|
||||||
|
assert(layout == 'reverse' ? hline1 > header1 : hline1 < header1)
|
||||||
|
end
|
||||||
|
local_tmux.send_keys :Enter
|
||||||
|
end
|
||||||
|
rescue StandardError, Minitest::Assertion => e
|
||||||
|
mutex.synchronize { failures << [command, e] }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
tmux.send_keys :Enter
|
combinations.each { queue << it }
|
||||||
|
queue.close
|
||||||
|
|
||||||
|
threads.each(&:join)
|
||||||
|
raise failures.inspect unless failures.empty?
|
||||||
|
ensure
|
||||||
|
# Reverse so any tmux window renumbering does not leave stale indices behind.
|
||||||
|
tmuxes&.reverse_each(&:kill)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user