Implement change-preview and change-preview-window actions

The new actions are named with 'change-' prefix to differentiate from
the pre-existing, one-off 'preview(...)' action.

Fix #2360
Fix #2505
Fix #2666

Related #2435
Related #2376
  - Can set up multiple bindings with different change-preview-window actions
  - Not possible to "rotate" through the options with a single binding
  - Enlarge or shrink not possible
This commit is contained in:
Junegunn Choi 2021-11-30 23:37:48 +09:00
parent 7da287e3aa
commit 20b4e6953e
No known key found for this signature in database
GPG Key ID: 254BC280FEF9C627
5 changed files with 367 additions and 221 deletions

View File

@ -1,6 +1,13 @@
CHANGELOG CHANGELOG
========= =========
0.29.0
------
- Added `change-preview(...)` action to change the `--preview` command
- cf. `preview(...)` is a one-off action that doesn't change the default
preview command
- Added `change-preview-window(...)` action
0.28.0 0.28.0
------ ------
- Added `--header-first` option to print header before the prompt line - Added `--header-first` option to print header before the prompt line

View File

@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf 1 "Nov 2021" "fzf 0.28.0" "fzf - a command-line fuzzy finder" .TH fzf 1 "Dec 2021" "fzf 0.29.0" "fzf - a command-line fuzzy finder"
.SH NAME .SH NAME
fzf - a command-line fuzzy finder fzf - a command-line fuzzy finder
@ -810,77 +810,79 @@ e.g.
A key or an event can be bound to one or more of the following actions. A key or an event can be bound to one or more of the following actions.
\fBACTION: DEFAULT BINDINGS (NOTES): \fBACTION: DEFAULT BINDINGS (NOTES):
\fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR \fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR
\fBaccept\fR \fIenter double-click\fR \fBaccept\fR \fIenter double-click\fR
\fBaccept-non-empty\fR (same as \fBaccept\fR except that it prevents fzf from exiting without selection) \fBaccept-non-empty\fR (same as \fBaccept\fR except that it prevents fzf from exiting without selection)
\fBbackward-char\fR \fIctrl-b left\fR \fBbackward-char\fR \fIctrl-b left\fR
\fBbackward-delete-char\fR \fIctrl-h bspace\fR \fBbackward-delete-char\fR \fIctrl-h bspace\fR
\fBbackward-delete-char/eof\fR (same as \fBbackward-delete-char\fR except aborts fzf if query is empty) \fBbackward-delete-char/eof\fR (same as \fBbackward-delete-char\fR except aborts fzf if query is empty)
\fBbackward-kill-word\fR \fIalt-bs\fR \fBbackward-kill-word\fR \fIalt-bs\fR
\fBbackward-word\fR \fIalt-b shift-left\fR \fBbackward-word\fR \fIalt-b shift-left\fR
\fBbeginning-of-line\fR \fIctrl-a home\fR \fBbeginning-of-line\fR \fIctrl-a home\fR
\fBcancel\fR (clear query string if not empty, abort fzf otherwise) \fBcancel\fR (clear query string if not empty, abort fzf otherwise)
\fBchange-prompt(...)\fR (change prompt to the given string) \fBchange-preview(...)\fR (change \fB--preview\fR option)
\fBclear-screen\fR \fIctrl-l\fR \fBchange-preview-window(...)\fR (change \fB--preview-window\fR option)
\fBclear-selection\fR (clear multi-selection) \fBchange-prompt(...)\fR (change prompt to the given string)
\fBclose\fR (close preview window if open, abort fzf otherwise) \fBclear-screen\fR \fIctrl-l\fR
\fBclear-query\fR (clear query string) \fBclear-selection\fR (clear multi-selection)
\fBdelete-char\fR \fIdel\fR \fBclose\fR (close preview window if open, abort fzf otherwise)
\fBdelete-char/eof\fR \fIctrl-d\fR (same as \fBdelete-char\fR except aborts fzf if query is empty) \fBclear-query\fR (clear query string)
\fBdelete-char\fR \fIdel\fR
\fBdelete-char/eof\fR \fIctrl-d\fR (same as \fBdelete-char\fR except aborts fzf if query is empty)
\fBdeselect\fR \fBdeselect\fR
\fBdeselect-all\fR (deselect all matches) \fBdeselect-all\fR (deselect all matches)
\fBdisable-search\fR (disable search functionality) \fBdisable-search\fR (disable search functionality)
\fBdown\fR \fIctrl-j ctrl-n down\fR \fBdown\fR \fIctrl-j ctrl-n down\fR
\fBenable-search\fR (enable search functionality) \fBenable-search\fR (enable search functionality)
\fBend-of-line\fR \fIctrl-e end\fR \fBend-of-line\fR \fIctrl-e end\fR
\fBexecute(...)\fR (see below for the details) \fBexecute(...)\fR (see below for the details)
\fBexecute-silent(...)\fR (see below for the details) \fBexecute-silent(...)\fR (see below for the details)
\fBfirst\fR (move to the first match) \fBfirst\fR (move to the first match)
\fBforward-char\fR \fIctrl-f right\fR \fBforward-char\fR \fIctrl-f right\fR
\fBforward-word\fR \fIalt-f shift-right\fR \fBforward-word\fR \fIalt-f shift-right\fR
\fBignore\fR \fBignore\fR
\fBjump\fR (EasyMotion-like 2-keystroke movement) \fBjump\fR (EasyMotion-like 2-keystroke movement)
\fBjump-accept\fR (jump and accept) \fBjump-accept\fR (jump and accept)
\fBkill-line\fR \fBkill-line\fR
\fBkill-word\fR \fIalt-d\fR \fBkill-word\fR \fIalt-d\fR
\fBlast\fR (move to the last match) \fBlast\fR (move to the last match)
\fBnext-history\fR (\fIctrl-n\fR on \fB--history\fR) \fBnext-history\fR (\fIctrl-n\fR on \fB--history\fR)
\fBpage-down\fR \fIpgdn\fR \fBpage-down\fR \fIpgdn\fR
\fBpage-up\fR \fIpgup\fR \fBpage-up\fR \fIpgup\fR
\fBhalf-page-down\fR \fBhalf-page-down\fR
\fBhalf-page-up\fR \fBhalf-page-up\fR
\fBpreview(...)\fR (see below for the details) \fBpreview(...)\fR (see below for the details)
\fBpreview-down\fR \fIshift-down\fR \fBpreview-down\fR \fIshift-down\fR
\fBpreview-up\fR \fIshift-up\fR \fBpreview-up\fR \fIshift-up\fR
\fBpreview-page-down\fR \fBpreview-page-down\fR
\fBpreview-page-up\fR \fBpreview-page-up\fR
\fBpreview-half-page-down\fR \fBpreview-half-page-down\fR
\fBpreview-half-page-up\fR \fBpreview-half-page-up\fR
\fBpreview-bottom\fR \fBpreview-bottom\fR
\fBpreview-top\fR \fBpreview-top\fR
\fBprevious-history\fR (\fIctrl-p\fR on \fB--history\fR) \fBprevious-history\fR (\fIctrl-p\fR on \fB--history\fR)
\fBprint-query\fR (print query and exit) \fBprint-query\fR (print query and exit)
\fBput\fR (put the character to the prompt) \fBput\fR (put the character to the prompt)
\fBrefresh-preview\fR \fBrefresh-preview\fR
\fBreload(...)\fR (see below for the details) \fBreload(...)\fR (see below for the details)
\fBreplace-query\fR (replace query string with the current selection) \fBreplace-query\fR (replace query string with the current selection)
\fBselect\fR \fBselect\fR
\fBselect-all\fR (select all matches) \fBselect-all\fR (select all matches)
\fBtoggle\fR (\fIright-click\fR) \fBtoggle\fR (\fIright-click\fR)
\fBtoggle-all\fR (toggle all matches) \fBtoggle-all\fR (toggle all matches)
\fBtoggle+down\fR \fIctrl-i (tab)\fR \fBtoggle+down\fR \fIctrl-i (tab)\fR
\fBtoggle-in\fR (\fB--layout=reverse*\fR ? \fBtoggle+up\fR : \fBtoggle+down\fR) \fBtoggle-in\fR (\fB--layout=reverse*\fR ? \fBtoggle+up\fR : \fBtoggle+down\fR)
\fBtoggle-out\fR (\fB--layout=reverse*\fR ? \fBtoggle+down\fR : \fBtoggle+up\fR) \fBtoggle-out\fR (\fB--layout=reverse*\fR ? \fBtoggle+down\fR : \fBtoggle+up\fR)
\fBtoggle-preview\fR \fBtoggle-preview\fR
\fBtoggle-preview-wrap\fR \fBtoggle-preview-wrap\fR
\fBtoggle-search\fR (toggle search functionality) \fBtoggle-search\fR (toggle search functionality)
\fBtoggle-sort\fR \fBtoggle-sort\fR
\fBtoggle+up\fR \fIbtab (shift-tab)\fR \fBtoggle+up\fR \fIbtab (shift-tab)\fR
\fBunbind(...)\fR (unbind bindings) \fBunbind(...)\fR (unbind bindings)
\fBunix-line-discard\fR \fIctrl-u\fR \fBunix-line-discard\fR \fIctrl-u\fR
\fBunix-word-rubout\fR \fIctrl-w\fR \fBunix-word-rubout\fR \fIctrl-w\fR
\fBup\fR \fIctrl-k ctrl-p up\fR \fBup\fR \fIctrl-k ctrl-p up\fR
\fByank\fR \fIctrl-y\fR \fByank\fR \fIctrl-y\fR
.SS ACTION COMPOSITION .SS ACTION COMPOSITION

View File

@ -176,6 +176,14 @@ type previewOpts struct {
headerLines int headerLines int
} }
func (a previewOpts) sameLayout(b previewOpts) bool {
return a.size == b.size && a.position == b.position && a.border == b.border && a.hidden == b.hidden
}
func (a previewOpts) sameContentLayout(b previewOpts) bool {
return a.wrap == b.wrap && a.headerLines == b.headerLines
}
// Options stores the values of command-line options // Options stores the values of command-line options
type Options struct { type Options struct {
Fuzzy bool Fuzzy bool
@ -787,7 +795,7 @@ func init() {
// Backreferences are not supported. // Backreferences are not supported.
// "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|') // "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|')
executeRegexp = regexp.MustCompile( executeRegexp = regexp.MustCompile(
`(?si)[:+](execute(?:-multi|-silent)?|reload|preview|change-prompt|unbind):.+|[:+](execute(?:-multi|-silent)?|reload|preview|change-prompt|unbind)(\([^)]*\)|\[[^\]]*\]|~[^~]*~|![^!]*!|@[^@]*@|\#[^\#]*\#|\$[^\$]*\$|%[^%]*%|\^[^\^]*\^|&[^&]*&|\*[^\*]*\*|;[^;]*;|/[^/]*/|\|[^\|]*\|)`) `(?si)[:+](execute(?:-multi|-silent)?|reload|preview|change-prompt|change-preview-window|change-preview|unbind):.+|[:+](execute(?:-multi|-silent)?|reload|preview|change-prompt|change-preview-window|change-preview|unbind)(\([^)]*\)|\[[^\]]*\]|~[^~]*~|![^!]*!|@[^@]*@|\#[^\#]*\#|\$[^\$]*\$|%[^%]*%|\^[^\^]*\^|&[^&]*&|\*[^\*]*\*|;[^;]*;|/[^/]*/|\|[^\|]*\|)`)
} }
func parseKeymap(keymap map[tui.Event][]action, str string) { func parseKeymap(keymap map[tui.Event][]action, str string) {
@ -799,6 +807,10 @@ func parseKeymap(keymap map[tui.Event][]action, str string) {
prefix := symbol + "execute" prefix := symbol + "execute"
if strings.HasPrefix(src[1:], "reload") { if strings.HasPrefix(src[1:], "reload") {
prefix = symbol + "reload" prefix = symbol + "reload"
} else if strings.HasPrefix(src[1:], "change-preview-window") {
prefix = symbol + "change-preview-window"
} else if strings.HasPrefix(src[1:], "change-preview") {
prefix = symbol + "change-preview"
} else if strings.HasPrefix(src[1:], "preview") { } else if strings.HasPrefix(src[1:], "preview") {
prefix = symbol + "preview" prefix = symbol + "preview"
} else if strings.HasPrefix(src[1:], "unbind") { } else if strings.HasPrefix(src[1:], "unbind") {
@ -1002,6 +1014,10 @@ func parseKeymap(keymap map[tui.Event][]action, str string) {
offset = len("reload") offset = len("reload")
case actPreview: case actPreview:
offset = len("preview") offset = len("preview")
case actChangePreviewWindow:
offset = len("change-preview-window")
case actChangePreview:
offset = len("change-preview")
case actChangePrompt: case actChangePrompt:
offset = len("change-prompt") offset = len("change-prompt")
case actUnbind: case actUnbind:
@ -1028,6 +1044,9 @@ func parseKeymap(keymap map[tui.Event][]action, str string) {
} }
if t == actUnbind { if t == actUnbind {
parseKeyChords(actionArg, "unbind target required") parseKeyChords(actionArg, "unbind target required")
} else if t == actChangePreviewWindow {
opts := previewOpts{}
parsePreviewWindow(&opts, actionArg)
} }
} }
} }
@ -1053,6 +1072,10 @@ func isExecuteAction(str string) actionType {
return actUnbind return actUnbind
case "preview": case "preview":
return actPreview return actPreview
case "change-preview-window":
return actChangePreviewWindow
case "change-preview":
return actChangePreview
case "change-prompt": case "change-prompt":
return actChangePrompt return actChangePrompt
case "execute": case "execute":
@ -1633,11 +1656,29 @@ func postProcessOptions(opts *Options) {
// Extend the default key map // Extend the default key map
keymap := defaultKeymap() keymap := defaultKeymap()
for key, actions := range opts.Keymap { for key, actions := range opts.Keymap {
lastChangePreviewWindow := action{t: actIgnore}
for _, act := range actions { for _, act := range actions {
if act.t == actToggleSort { switch act.t {
case actToggleSort:
// To display "+S"/"-S" on info line
opts.ToggleSort = true opts.ToggleSort = true
case actChangePreviewWindow:
lastChangePreviewWindow = act
} }
} }
// Re-organize actions so that we only keep the last change-preview-window
// and it comes first in the list.
// * change-preview-window(up,+10)+preview(sleep 3; cat {})+change-preview-window(up,+20)
// -> change-preview-window(up,+20)+preview(sleep 3; cat {})
if lastChangePreviewWindow.t == actChangePreviewWindow {
reordered := []action{lastChangePreviewWindow}
for _, act := range actions {
if act.t != actChangePreviewWindow {
reordered = append(reordered, act)
}
}
actions = reordered
}
keymap[key] = actions keymap[key] = actions
} }
opts.Keymap = keymap opts.Keymap = keymap

View File

@ -104,88 +104,89 @@ var emptyLine = itemLine{}
// Terminal represents terminal input/output // Terminal represents terminal input/output
type Terminal struct { type Terminal struct {
initDelay time.Duration initDelay time.Duration
infoStyle infoStyle infoStyle infoStyle
spinner []string spinner []string
prompt func() prompt func()
promptLen int promptLen int
pointer string pointer string
pointerLen int pointerLen int
pointerEmpty string pointerEmpty string
marker string marker string
markerLen int markerLen int
markerEmpty string markerEmpty string
queryLen [2]int queryLen [2]int
layout layoutType layout layoutType
fullscreen bool fullscreen bool
keepRight bool keepRight bool
hscroll bool hscroll bool
hscrollOff int hscrollOff int
scrollOff int scrollOff int
wordRubout string wordRubout string
wordNext string wordNext string
cx int cx int
cy int cy int
offset int offset int
xoffset int xoffset int
yanked []rune yanked []rune
input []rune input []rune
multi int multi int
sort bool sort bool
toggleSort bool toggleSort bool
delimiter Delimiter delimiter Delimiter
expect map[tui.Event]string expect map[tui.Event]string
keymap map[tui.Event][]action keymap map[tui.Event][]action
pressed string pressed string
printQuery bool printQuery bool
history *History history *History
cycle bool cycle bool
headerFirst bool headerFirst bool
headerLines int headerLines int
header []string header []string
header0 []string header0 []string
ansi bool ansi bool
tabstop int tabstop int
margin [4]sizeSpec margin [4]sizeSpec
padding [4]sizeSpec padding [4]sizeSpec
strong tui.Attr strong tui.Attr
unicode bool unicode bool
borderShape tui.BorderShape borderShape tui.BorderShape
cleanExit bool cleanExit bool
paused bool paused bool
border tui.Window border tui.Window
window tui.Window window tui.Window
pborder tui.Window pborder tui.Window
pwindow tui.Window pwindow tui.Window
count int count int
progress int progress int
reading bool reading bool
running bool running bool
failed *string failed *string
jumping jumpMode jumping jumpMode
jumpLabels string jumpLabels string
printer func(string) printer func(string)
printsep string printsep string
merger *Merger merger *Merger
selected map[int32]selectedItem selected map[int32]selectedItem
version int64 version int64
reqBox *util.EventBox reqBox *util.EventBox
previewOpts previewOpts initialPreviewOpts previewOpts
previewer previewer previewOpts previewOpts
previewed previewed previewer previewer
previewBox *util.EventBox previewed previewed
eventBox *util.EventBox previewBox *util.EventBox
mutex sync.Mutex eventBox *util.EventBox
initFunc func() mutex sync.Mutex
prevLines []itemLine initFunc func()
suppress bool prevLines []itemLine
sigstop bool suppress bool
startChan chan bool sigstop bool
killChan chan int startChan chan bool
slab *util.Slab killChan chan int
theme *tui.ColorTheme slab *util.Slab
tui tui.Renderer theme *tui.ColorTheme
executing *util.AtomicBool tui tui.Renderer
executing *util.AtomicBool
} }
type selectedItem struct { type selectedItem struct {
@ -286,6 +287,8 @@ const (
actTogglePreview actTogglePreview
actTogglePreviewWrap actTogglePreviewWrap
actPreview actPreview
actChangePreview
actChangePreviewWindow
actPreviewTop actPreviewTop
actPreviewBottom actPreviewBottom
actPreviewUp actPreviewUp
@ -324,9 +327,10 @@ type searchRequest struct {
} }
type previewRequest struct { type previewRequest struct {
template string template string
pwindow tui.Window pwindow tui.Window
list []*Item scrollOffset int
list []*Item
} }
type previewResult struct { type previewResult struct {
@ -416,7 +420,7 @@ func trimQuery(query string) []rune {
func hasPreviewAction(opts *Options) bool { func hasPreviewAction(opts *Options) bool {
for _, actions := range opts.Keymap { for _, actions := range opts.Keymap {
for _, action := range actions { for _, action := range actions {
if action.t == actPreview { if action.t == actPreview || action.t == actChangePreview {
return true return true
} }
} }
@ -496,72 +500,73 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
wordNext = fmt.Sprintf("[^%s]%s|(.$)", sep, sep) wordNext = fmt.Sprintf("[^%s]%s|(.$)", sep, sep)
} }
t := Terminal{ t := Terminal{
initDelay: delay, initDelay: delay,
infoStyle: opts.InfoStyle, infoStyle: opts.InfoStyle,
spinner: makeSpinner(opts.Unicode), spinner: makeSpinner(opts.Unicode),
queryLen: [2]int{0, 0}, queryLen: [2]int{0, 0},
layout: opts.Layout, layout: opts.Layout,
fullscreen: fullscreen, fullscreen: fullscreen,
keepRight: opts.KeepRight, keepRight: opts.KeepRight,
hscroll: opts.Hscroll, hscroll: opts.Hscroll,
hscrollOff: opts.HscrollOff, hscrollOff: opts.HscrollOff,
scrollOff: opts.ScrollOff, scrollOff: opts.ScrollOff,
wordRubout: wordRubout, wordRubout: wordRubout,
wordNext: wordNext, wordNext: wordNext,
cx: len(input), cx: len(input),
cy: 0, cy: 0,
offset: 0, offset: 0,
xoffset: 0, xoffset: 0,
yanked: []rune{}, yanked: []rune{},
input: input, input: input,
multi: opts.Multi, multi: opts.Multi,
sort: opts.Sort > 0, sort: opts.Sort > 0,
toggleSort: opts.ToggleSort, toggleSort: opts.ToggleSort,
delimiter: opts.Delimiter, delimiter: opts.Delimiter,
expect: opts.Expect, expect: opts.Expect,
keymap: opts.Keymap, keymap: opts.Keymap,
pressed: "", pressed: "",
printQuery: opts.PrintQuery, printQuery: opts.PrintQuery,
history: opts.History, history: opts.History,
margin: opts.Margin, margin: opts.Margin,
padding: opts.Padding, padding: opts.Padding,
unicode: opts.Unicode, unicode: opts.Unicode,
borderShape: opts.BorderShape, borderShape: opts.BorderShape,
cleanExit: opts.ClearOnExit, cleanExit: opts.ClearOnExit,
paused: opts.Phony, paused: opts.Phony,
strong: strongAttr, strong: strongAttr,
cycle: opts.Cycle, cycle: opts.Cycle,
headerFirst: opts.HeaderFirst, headerFirst: opts.HeaderFirst,
headerLines: opts.HeaderLines, headerLines: opts.HeaderLines,
header: header, header: header,
header0: header, header0: header,
ansi: opts.Ansi, ansi: opts.Ansi,
tabstop: opts.Tabstop, tabstop: opts.Tabstop,
reading: true, reading: true,
running: true, running: true,
failed: nil, failed: nil,
jumping: jumpDisabled, jumping: jumpDisabled,
jumpLabels: opts.JumpLabels, jumpLabels: opts.JumpLabels,
printer: opts.Printer, printer: opts.Printer,
printsep: opts.PrintSep, printsep: opts.PrintSep,
merger: EmptyMerger, merger: EmptyMerger,
selected: make(map[int32]selectedItem), selected: make(map[int32]selectedItem),
reqBox: util.NewEventBox(), reqBox: util.NewEventBox(),
previewOpts: opts.Preview, initialPreviewOpts: opts.Preview,
previewer: previewer{0, []string{}, 0, showPreviewWindow, false, true, false, ""}, previewOpts: opts.Preview,
previewed: previewed{0, 0, 0, false}, previewer: previewer{0, []string{}, 0, showPreviewWindow, false, true, false, ""},
previewBox: previewBox, previewed: previewed{0, 0, 0, false},
eventBox: eventBox, previewBox: previewBox,
mutex: sync.Mutex{}, eventBox: eventBox,
suppress: true, mutex: sync.Mutex{},
sigstop: false, suppress: true,
slab: util.MakeSlab(slab16Size, slab32Size), sigstop: false,
theme: opts.Theme, slab: util.MakeSlab(slab16Size, slab32Size),
startChan: make(chan bool, 1), theme: opts.Theme,
killChan: make(chan int), startChan: make(chan bool, 1),
tui: renderer, killChan: make(chan int),
initFunc: func() { renderer.Init() }, tui: renderer,
executing: util.NewAtomicBool(false)} initFunc: func() { renderer.Init() },
executing: util.NewAtomicBool(false)}
t.prompt, t.promptLen = t.parsePrompt(opts.Prompt) t.prompt, t.promptLen = t.parsePrompt(opts.Prompt)
t.pointer, t.pointerLen = t.processTabs([]rune(opts.Pointer), 0) t.pointer, t.pointerLen = t.processTabs([]rune(opts.Pointer), 0)
t.marker, t.markerLen = t.processTabs([]rune(opts.Marker), 0) t.marker, t.markerLen = t.processTabs([]rune(opts.Marker), 0)
@ -1642,9 +1647,14 @@ func (t *Terminal) replacePlaceholder(template string, forcePlus bool, input str
template, t.ansi, t.delimiter, t.printsep, forcePlus, input, list) template, t.ansi, t.delimiter, t.printsep, forcePlus, input, list)
} }
func (t *Terminal) evaluateScrollOffset(list []*Item, height int) int { func (t *Terminal) evaluateScrollOffset() int {
if t.pwindow == nil {
return 0
}
// We only need the current item to calculate the scroll offset
offsetExpr := offsetTrimCharsRegex.ReplaceAllString( offsetExpr := offsetTrimCharsRegex.ReplaceAllString(
t.replacePlaceholder(t.previewOpts.scroll, false, "", list), "") t.replacePlaceholder(t.previewOpts.scroll, false, "", []*Item{t.currentItem(), nil}), "")
atoi := func(s string) int { atoi := func(s string) int {
n, e := strconv.Atoi(s) n, e := strconv.Atoi(s)
@ -1655,20 +1665,21 @@ func (t *Terminal) evaluateScrollOffset(list []*Item, height int) int {
} }
base := -1 base := -1
height := util.Max(0, t.pwindow.Height()-t.previewOpts.headerLines)
for _, component := range offsetComponentRegex.FindAllString(offsetExpr, -1) { for _, component := range offsetComponentRegex.FindAllString(offsetExpr, -1) {
if strings.HasPrefix(component, "-/") { if strings.HasPrefix(component, "-/") {
component = component[1:] component = component[1:]
} }
if component[0] == '/' { if component[0] == '/' {
denom := atoi(component[1:]) denom := atoi(component[1:])
if denom == 0 { if denom != 0 {
return base base -= height / denom
} }
return base - height/denom break
} }
base += atoi(component) base += atoi(component)
} }
return base return util.Max(0, base)
} }
func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, printsep string, forcePlus bool, query string, allItems []*Item) string { func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, printsep string, forcePlus bool, query string, allItems []*Item) string {
@ -1972,12 +1983,14 @@ func (t *Terminal) Loop() {
var items []*Item var items []*Item
var commandTemplate string var commandTemplate string
var pwindow tui.Window var pwindow tui.Window
initialOffset := 0
t.previewBox.Wait(func(events *util.Events) { t.previewBox.Wait(func(events *util.Events) {
for req, value := range *events { for req, value := range *events {
switch req { switch req {
case reqPreviewEnqueue: case reqPreviewEnqueue:
request := value.(previewRequest) request := value.(previewRequest)
commandTemplate = request.template commandTemplate = request.template
initialOffset = request.scrollOffset
items = request.list items = request.list
pwindow = request.pwindow pwindow = request.pwindow
} }
@ -1989,11 +2002,9 @@ func (t *Terminal) Loop() {
if items[0] != nil { if items[0] != nil {
_, query := t.Input() _, query := t.Input()
command := t.replacePlaceholder(commandTemplate, false, string(query), items) command := t.replacePlaceholder(commandTemplate, false, string(query), items)
initialOffset := 0
cmd := util.ExecCommand(command, true) cmd := util.ExecCommand(command, true)
if pwindow != nil { if pwindow != nil {
height := pwindow.Height() height := pwindow.Height()
initialOffset = util.Max(0, t.evaluateScrollOffset(items, util.Max(0, height-t.previewOpts.headerLines)))
env := os.Environ() env := os.Environ()
lines := fmt.Sprintf("LINES=%d", height) lines := fmt.Sprintf("LINES=%d", height)
columns := fmt.Sprintf("COLUMNS=%d", pwindow.Width()) columns := fmt.Sprintf("COLUMNS=%d", pwindow.Width())
@ -2128,7 +2139,7 @@ func (t *Terminal) Loop() {
if len(command) > 0 && t.isPreviewEnabled() { if len(command) > 0 && t.isPreviewEnabled() {
_, list := t.buildPlusList(command, false) _, list := t.buildPlusList(command, false)
t.cancelPreview() t.cancelPreview()
t.previewBox.Set(reqPreviewEnqueue, previewRequest{command, t.pwindow, list}) t.previewBox.Set(reqPreviewEnqueue, previewRequest{command, t.pwindow, t.evaluateScrollOffset(), list})
} }
} }
@ -2253,13 +2264,16 @@ func (t *Terminal) Loop() {
} }
} }
} }
togglePreview := func(enabled bool) { togglePreview := func(enabled bool) bool {
if t.previewer.enabled != enabled { if t.previewer.enabled != enabled {
t.previewer.enabled = enabled t.previewer.enabled = enabled
// We need to immediately update t.pwindow so we don't use reqRedraw
t.tui.Clear() t.tui.Clear()
t.resizeWindows() t.resizeWindows()
req(reqPrompt, reqList, reqInfo, reqHeader) req(reqPrompt, reqList, reqInfo, reqHeader)
return true
} }
return false
} }
toggle := func() bool { toggle := func() bool {
current := t.currentItem() current := t.currentItem()
@ -2327,7 +2341,7 @@ func (t *Terminal) Loop() {
if valid { if valid {
t.cancelPreview() t.cancelPreview()
t.previewBox.Set(reqPreviewEnqueue, t.previewBox.Set(reqPreviewEnqueue,
previewRequest{t.previewOpts.command, t.pwindow, list}) previewRequest{t.previewOpts.command, t.pwindow, t.evaluateScrollOffset(), list})
} }
} }
} }
@ -2707,6 +2721,39 @@ func (t *Terminal) Loop() {
for key := range keys { for key := range keys {
delete(t.keymap, key) delete(t.keymap, key)
} }
case actChangePreview:
if t.previewOpts.command != a.a {
togglePreview(len(a.a) > 0)
t.previewOpts.command = a.a
refreshPreview(t.previewOpts.command)
}
case actChangePreviewWindow:
currentPreviewOpts := t.previewOpts
// Reset preview options and apply the additional options
t.previewOpts = t.initialPreviewOpts
parsePreviewWindow(&t.previewOpts, a.a)
if t.previewOpts.hidden {
togglePreview(false)
} else {
// Full redraw
if !currentPreviewOpts.sameLayout(t.previewOpts) {
if togglePreview(true) {
refreshPreview(t.previewOpts.command)
} else {
req(reqRedraw)
}
} else if !currentPreviewOpts.sameContentLayout(t.previewOpts) {
t.previewed.version = 0
req(reqPreviewRefresh)
}
// Adjust scroll offset
if t.hasPreviewWindow() && currentPreviewOpts.scroll != t.previewOpts.scroll {
scrollPreviewTo(t.evaluateScrollOffset())
}
}
} }
return true return true
} }

View File

@ -2142,6 +2142,55 @@ class TestGoFZF < TestBase
assert_equal expected.chomp, lines.take(6).join("\n") assert_equal expected.chomp, lines.take(6).join("\n")
end end
end end
def test_change_preview_window
tmux.send_keys "seq 1000 | #{FZF} --preview 'echo [[{}]]' --preview-window border-none --bind '" \
'a:change-preview(echo __{}__),' \
'b:change-preview-window(down)+change-preview(echo =={}==)+change-preview-window(up),' \
'c:change-preview(),d:change-preview-window(hidden),' \
"e:preview(printf ::%${FZF_PREVIEW_COLUMNS}s{})+change-preview-window(up),f:change-preview-window(up,wrap)'", :Enter
tmux.until { |lines| assert_equal 1000, lines.item_count }
tmux.until { |lines| assert_includes lines[0], '[[1]]' }
# change-preview action permanently changes the preview command set by --preview
tmux.send_keys 'a'
tmux.until { |lines| assert_includes lines[0], '__1__' }
tmux.send_keys :Up
tmux.until { |lines| assert_includes lines[0], '__2__' }
# When multiple change-preview-window actions are bound to a single key,
# the last one wins and the updated options are immediately applied to the new preview
tmux.send_keys 'b'
tmux.until { |lines| assert_equal '==2==', lines[0] }
tmux.send_keys :Up
tmux.until { |lines| assert_equal '==3==', lines[0] }
# change-preview with an empty preview command closes the preview window
tmux.send_keys 'c'
tmux.until { |lines| refute_includes lines[0], '==' }
# change-preview again to re-open the preview window
tmux.send_keys 'a'
tmux.until { |lines| assert_equal '__3__', lines[0] }
# Hide the preview window with hidden flag
tmux.send_keys 'd'
tmux.until { |lines| refute_includes lines[0], '__3__' }
# One-off preview
tmux.send_keys 'e'
tmux.until do |lines|
assert_equal '::', lines[0]
refute_includes lines[1], '3'
end
# Wrapped
tmux.send_keys 'f'
tmux.until do |lines|
assert_equal '::', lines[0]
assert_equal ' 3', lines[1]
end
end
end end
module TestShell module TestShell