summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJunegunn Choi <junegunn.c@gmail.com>2021-11-30 23:37:48 +0900
committerJunegunn Choi <junegunn.c@gmail.com>2021-11-30 23:57:46 +0900
commit20b4e6953ec439f52e9bdcc06ac6ee5bb590d39d (patch)
treed2c2b3543261ef7ec76b0dbc06b2c17d45e1076b
parent7da287e3aae4a9b0e81ebc67701460073afcc0d0 (diff)
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
-rw-r--r--CHANGELOG.md7
-rw-r--r--man/man1/fzf.1114
-rw-r--r--src/options.go45
-rw-r--r--src/terminal.go373
-rwxr-xr-xtest/test_go.rb49
5 files changed, 367 insertions, 221 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2b2d20ce..84754022 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,13 @@
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
------
- Added `--header-first` option to print header before the prompt line
diff --git a/man/man1/fzf.1 b/man/man1/fzf.1
index 5f5eb91e..c87d2c73 100644
--- a/man/man1/fzf.1
+++ b/man/man1/fzf.1
@@ -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
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
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.
\fBACTION: DEFAULT BINDINGS (NOTES):
- \fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR
- \fBaccept\fR \fIenter double-click\fR
- \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-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-kill-word\fR \fIalt-bs\fR
- \fBbackward-word\fR \fIalt-b shift-left\fR
- \fBbeginning-of-line\fR \fIctrl-a home\fR
- \fBcancel\fR (clear query string if not empty, abort fzf otherwise)
- \fBchange-prompt(...)\fR (change prompt to the given string)
- \fBclear-screen\fR \fIctrl-l\fR
- \fBclear-selection\fR (clear multi-selection)
- \fBclose\fR (close preview window if open, abort fzf otherwise)
- \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)
+ \fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR
+ \fBaccept\fR \fIenter double-click\fR
+ \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-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-kill-word\fR \fIalt-bs\fR
+ \fBbackward-word\fR \fIalt-b shift-left\fR
+ \fBbeginning-of-line\fR \fIctrl-a home\fR
+ \fBcancel\fR (clear query string if not empty, abort fzf otherwise)
+ \fBchange-preview(...)\fR (change \fB--preview\fR option)
+ \fBchange-preview-window(...)\fR (change \fB--preview-window\fR option)
+ \fBchange-prompt(...)\fR (change prompt to the given string)
+ \fBclear-screen\fR \fIctrl-l\fR
+ \fBclear-selection\fR (clear multi-selection)
+ \fBclose\fR (close preview window if open, abort fzf otherwise)
+ \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-all\fR (deselect all matches)
- \fBdisable-search\fR (disable search functionality)
- \fBdown\fR \fIctrl-j ctrl-n down\fR
- \fBenable-search\fR (enable search functionality)
- \fBend-of-line\fR \fIctrl-e end\fR
- \fBexecute(...)\fR (see below for the details)
- \fBexecute-silent(...)\fR (see below for the details)
- \fBfirst\fR (move to the first match)
- \fBforward-char\fR \fIctrl-f right\fR
- \fBforward-word\fR \fIalt-f shift-right\fR
+ \fBdeselect-all\fR (deselect all matches)
+ \fBdisable-search\fR (disable search functionality)
+ \fBdown\fR \fIctrl-j ctrl-n down\fR
+ \fBenable-search\fR (enable search functionality)
+ \fBend-of-line\fR \fIctrl-e end\fR
+ \fBexecute(...)\fR (see below for the details)
+ \fBexecute-silent(...)\fR (see below for the details)
+ \fBfirst\fR (move to the first match)
+ \fBforward-char\fR \fIctrl-f right\fR
+ \fBforward-word\fR \fIalt-f shift-right\fR
\fBignore\fR
- \fBjump\fR (EasyMotion-like 2-keystroke movement)
- \fBjump-accept\fR (jump and accept)
+ \fBjump\fR (EasyMotion-like 2-keystroke movement)
+ \fBjump-accept\fR (jump and accept)
\fBkill-line\fR
- \fBkill-word\fR \fIalt-d\fR
- \fBlast\fR (move to the last match)
- \fBnext-history\fR (\fIctrl-n\fR on \fB--history\fR)
- \fBpage-down\fR \fIpgdn\fR
- \fBpage-up\fR \fIpgup\fR
+ \fBkill-word\fR \fIalt-d\fR
+ \fBlast\fR (move to the last match)
+ \fBnext-history\fR (\fIctrl-n\fR on \fB--history\fR)
+ \fBpage-down\fR \fIpgdn\fR
+ \fBpage-up\fR \fIpgup\fR
\fBhalf-page-down\fR
\fBhalf-page-up\fR
- \fBpreview(...)\fR (see below for the details)
- \fBpreview-down\fR \fIshift-down\fR
- \fBpreview-up\fR \fIshift-up\fR
+ \fBpreview(...)\fR (see below for the details)
+ \fBpreview-down\fR \fIshift-down\fR
+ \fBpreview-up\fR \fIshift-up\fR
\fBpreview-page-down\fR
\fBpreview-page-up\fR
\fBpreview-half-page-down\fR
\fBpreview-half-page-up\fR
\fBpreview-bottom\fR
\fBpreview-top\fR
- \fBprevious-history\fR (\fIctrl-p\fR on \fB--history\fR)
- \fBprint-query\fR (print query and exit)
- \fBput\fR (put the character to the prompt)
+ \fBprevious-history\fR (\fIctrl-p\fR on \fB--history\fR)
+ \fBprint-query\fR (print query and exit)
+ \fBput\fR (put the character to the prompt)
\fBrefresh-preview\fR
- \fBreload(...)\fR (see below for the details)
- \fBreplace-query\fR (replace query string with the current selection)
+ \fBreload(...)\fR (see below for the details)
+ \fBreplace-query\fR (replace query string with the current selection)
\fBselect\fR
- \fBselect-all\fR (select all matches)
- \fBtoggle\fR (\fIright-click\fR)
- \fBtoggle-all\fR (toggle all matches)
- \fBtoggle+down\fR \fIctrl-i (tab)\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)
+ \fBselect-all\fR (select all matches)
+ \fBtoggle\fR (\fIright-click\fR)
+ \fBtoggle-all\fR (toggle all matches)
+ \fBtoggle+down\fR \fIctrl-i (tab)\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-preview\fR
\fBtoggle-preview-wrap\fR
- \fBtoggle-search\fR (toggle search functionality)
+ \fBtoggle-search\fR (toggle search functionality)
\fBtoggle-sort\fR
- \fBtoggle+up\fR \fIbtab (shift-tab)\fR
- \fBunbind(...)\fR (unbind bindings)
- \fBunix-line-discard\fR \fIctrl-u\fR
- \fBunix-word-rubout\fR \fIctrl-w\fR
- \fBup\fR \fIctrl-k ctrl-p up\fR
- \fByank\fR \fIctrl-y\fR
+ \fBtoggle+up\fR \fIbtab (shift-tab)\fR
+ \fBunbind(...)\fR (unbind bindings)
+ \fBunix-line-discard\fR \fIctrl-u\fR
+ \fBunix-word-rubout\fR \fIctrl-w\fR
+ \fBup\fR \fIctrl-k ctrl-p up\fR
+ \fByank\fR \fIctrl-y\fR
.SS ACTION COMPOSITION
diff --git a/src/options.go b/src/options.go
index b93fd972..63b385cd 100644
--- a/src/options.go
+++ b/src/options.go
@@ -176,6 +176,14 @@ type previewOpts struct {
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
type Options struct {
Fuzzy bool
@@ -787,7 +795,7 @@ func init() {
// Backreferences are not supported.
// "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|')
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) {
@@ -799,6 +807,10 @@ func parseKeymap(keymap map[tui.Event][]action, str string) {
prefix := symbol + "execute"
if strings.HasPrefix(src[1:], "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") {
prefix = symbol + "preview"
} else if strings.HasPrefix(src[1:], "unbind") {
@@ -1002,6 +1014,10 @@ func parseKeymap(keymap map[tui.Event][]action, str string) {
offset = len("reload")
case actPreview:
offset = len("preview")
+ case actChangePreviewWindow:
+ offset = len("change-preview-window")
+ case actChangePreview:
+ offset = len("change-preview")
case actChangePrompt:
offset = len("change-prompt")
case actUnbind:
@@ -1028,6 +1044,9 @@ func parseKeymap(keymap map[tui.Event][]action, str string) {
}
if t == actUnbind {
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
case "preview":
return actPreview
+ case "change-preview-window":
+ return actChangePreviewWindow
+ case "change-preview":
+ return actChangePreview
case "change-prompt":
return actChangePrompt
case "execute":
@@ -1633,10 +1656,28 @@ func postProcessOptions(opts *Options) {
// Extend the default key map
keymap := defaultKeymap()
for key, actions := range opts.Keymap {
+ lastChangePreviewWindow := action{t: actIgnore}
for _, act := range actions {
- if act.t == actToggleSort {
+ switch act.t {
+ case actToggleSort:
+ // To display "+S"/"-S" on info line
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
}
diff --git a/src/terminal.go b/src/terminal.go
index 2bad5d75..804e2b2e 100644
--- a/src/terminal.go
+++ b/src/terminal.go
@@ -104,88 +104,89 @@ var emptyLine = itemLine{}
// Terminal represents terminal input/output
type Terminal struct {
- initDelay time.Duration
- infoStyle infoStyle
- spinner []string
- prompt func()
- promptLen int
- pointer string
- pointerLen int
- pointerEmpty string
- marker string
- markerLen int
- markerEmpty string
- queryLen [2]int
- layout layoutType
- fullscreen bool
- keepRight bool
- hscroll bool
- hscrollOff int
- scrollOff int
- wordRubout string
- wordNext string
- cx int
- cy int
- offset int
- xoffset int
- yanked []rune
- input []rune
- multi int
- sort bool
- toggleSort bool
- delimiter Delimiter
- expect map[tui.Event]string
- keymap map[tui.Event][]action
- pressed string
- printQuery bool
- history *History
- cycle bool
- headerFirst bool
- headerLines int
- header []string
- header0 []string
- ansi bool
- tabstop int
- margin [4]sizeSpec
- padding [4]sizeSpec
- strong tui.Attr
- unicode bool
- borderShape tui.BorderShape
- cleanExit bool
- paused bool
- border tui.Window
- window tui.Window
- pborder tui.Window
- pwindow tui.Window
- count int
- progress int
- reading bool
- running bool
- failed *string
- jumping jumpMode
- jumpLabels string
- printer func(string)
- printsep string
- merger *Merger
- selected map[int32]selectedItem
- version int64
- reqBox *util.EventBox
- previewOpts previewOpts
- previewer previewer
- previewed previewed
- previewBox *util.EventBox
- eventBox *util.EventBox
- mutex sync.Mutex
- initFunc func()
- prevLines []itemLine
- suppress bool
- sigstop bool
- startChan chan bool
- killChan chan int
- slab *util.Slab
- theme *tui.ColorTheme
- tui tui.Renderer
- executing *util.AtomicBool
+ initDelay time.Duration
+ infoStyle infoStyle
+ spinner []string
+ prompt func()
+ promptLen int
+ pointer string
+ pointerLen int
+ pointerEmpty string
+ marker string
+ markerLen int
+ markerEmpty string
+ queryLen [2]int
+ layout layoutType
+ fullscreen bool
+ keepRight bool
+ hscroll bool
+ hscrollOff int
+ scrollOff int
+ wordRubout string
+ wordNext string
+ cx int
+ cy int
+ offset int
+ xoffset int
+ yanked []rune
+ input []rune
+ multi int
+ sort bool
+ toggleSort bool
+ delimiter Delimiter
+ expect map[tui.Event]string
+ keymap map[tui.Event][]action
+ pressed string
+ printQuery bool
+ history *History
+ cycle bool
+ headerFirst bool
+ headerLines int
+ header []string
+ header0 []string
+ ansi bool
+ tabstop int
+ margin [4]sizeSpec
+ padding [4]sizeSpec
+ strong tui.Attr
+ unicode bool
+ borderShape tui.BorderShape
+ cleanExit bool
+ paused bool
+ border tui.Window
+ window tui.Window
+ pborder tui.Window
+ pwindow tui.Window
+ count int
+ progress int
+ reading bool
+ running bool
+ failed *string
+ jumping jumpMode
+ jumpLabels string
+ printer func(string)
+ printsep string
+ merger *Merger
+ selected map[int32]selectedItem
+ version int64
+ reqBox *util.EventBox
+ initialPreviewOpts previewOpts
+ previewOpts previewOpts
+ previewer previewer
+ previewed previewed
+ previewBox *util.EventBox
+ eventBox *util.EventBox
+ mutex sync.Mutex
+ initFunc func()
+ prevLines []itemLine
+ suppress bool
+ sigstop bool
+ startChan chan bool
+ killChan chan int
+ slab *util.Slab
+ theme *tui.ColorTheme
+ tui tui.Renderer
+ executing *util.AtomicBool
}
type selectedItem struct {
@@ -286,6 +287,8 @@ const (
actTogglePreview
actTogglePreviewWrap
actPreview
+ actChangePreview
+ actChangePreviewWindow
actPreviewTop
actPreviewBottom
actPreviewUp
@@ -324,9 +327,10 @@ type searchRequest struct {
}
type previewRequest struct {
- template string
- pwindow tui.Window
- list []*Item
+ template string
+ pwindow tui.Window
+ scrollOffset int
+ list []*Item
}
type previewResult struct {
@@ -416,7 +420,7 @@ func trimQuery(query string) []rune {
func hasPreviewAction(opts *Options) bool {
for _, actions := range opts.Keymap {
for _, action := range actions {
- if action.t == actPreview {
+ if action.t == actPreview || action.t == actChangePreview {
return true
}
}
@@ -496,72 +500,73 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
wordNext = fmt.Sprintf("[^%s]%s|(.$)", sep, sep)
}
t := Terminal{
- initDelay: delay,
- infoStyle: opts.InfoStyle,
- spinner: makeSpinner(opts.Unicode),
- queryLen: [2]int{0, 0},
- layout: opts.Layout,
- fullscreen: fullscreen,
- keepRight: opts.KeepRight,
- hscroll: opts.Hscroll,
- hscrollOff: opts.HscrollOff,
- scrollOff: opts.ScrollOff,
- wordRubout: wordRubout,
- wordNext: wordNext,
- cx: len(input),
- cy: 0,
- offset: 0,
- xoffset: 0,
- yanked: []rune{},
- input: input,
- multi: opts.Multi,
- sort: opts.Sort > 0,
- toggleSort: opts.ToggleSort,
- delimiter: opts.Delimiter,
- expect: opts.Expect,
- keymap: opts.Keymap,
- pressed: "",
- printQuery: opts.PrintQuery,
- history: opts.History,
- margin: opts.Margin,
- padding: opts.Padding,
- unicode: opts.Unicode,
- borderShape: opts.BorderShape,
- cleanExit: opts.ClearOnExit,
- paused: opts.Phony,
- strong: strongAttr,
- cycle: opts.Cycle,
- headerFirst: opts.HeaderFirst,
- headerLines: opts.HeaderLines,
- header: header,
- header0: header,
- ansi: opts.Ansi,
- tabstop: opts.Tabstop,
- reading: true,
- running: true,
- failed: nil,
- jumping: jumpDisabled,
- jumpLabels: opts.JumpLabels,
- printer: opts.Printer,
- printsep: opts.PrintSep,
- merger: EmptyMerger,
- selected: make(map[int32]selectedItem),
- reqBox: util.NewEventBox(),
- previewOpts: opts.Preview,
- previewer: previewer{0, []string{}, 0, showPreviewWindow, false, true, false, ""},
- previewed: previewed{0, 0, 0, false},
- previewBox: previewBox,
- eventBox: eventBox,
- mutex: sync.Mutex{},
- suppress: true,
- sigstop: false,
- slab: util.MakeSlab(slab16Size, slab32Size),
- theme: opts.Theme,
- startChan: make(chan bool, 1),
- killChan: make(chan int),
- tui: renderer,
- initFunc: func() { renderer.Init() },
- executing: util.NewAtomicBool(false)}
+ initDelay: delay,
+ infoStyle: opts.InfoStyle,
+ spinner: makeSpinner(opts.Unicode),
+ queryLen: [2]int{0, 0},
+ layout: opts.Layout,
+ fullscreen: fullscreen,
+ keepRight: opts.KeepRight,
+ hscroll: opts.Hscroll,
+ hscrollOff: opts.HscrollOff,
+ scrollOff: opts.ScrollOff,
+ wordRubout: wordRubout,
+ wordNext: wordNext,
+ cx: len(input),
+ cy: 0,
+ offset: 0,
+ xoffset: 0,
+ yanked: []rune{},
+ input: input,
+ multi: opts.Multi,
+ sort: opts.Sort > 0,
+ toggleSort: opts.ToggleSort,
+ delimiter: opts.Delimiter,
+ expect: opts.Expect,
+ keymap: opts.Keymap,
+ pressed: "",
+ printQuery: opts.PrintQuery,
+ history: opts.History,
+ margin: opts.Margin,
+ padding: opts.Padding,
+ unicode: opts.Unicode,
+ borderShape: opts.BorderShape,
+ cleanExit: opts.ClearOnExit,
+ paused: opts.Phony,
+ strong: strongAttr,
+ cycle: opts.Cycle,
+ headerFirst: opts.HeaderFirst,
+ headerLines: opts.HeaderLines,
+ header: header,
+ header0: header,
+ ansi: opts.Ansi,
+ tabstop: opts.Tabstop,
+ reading: true,
+ running: true,
+ failed: nil,
+ jumping: jumpDisabled,
+ jumpLabels: opts.JumpLabels,
+ printer: opts.Printer,
+ printsep: opts.PrintSep,
+ merger: EmptyMerger,
+ selected: make(map[int32]selectedItem),
+ reqBox: util.NewEventBox(),
+ initialPreviewOpts: opts.Preview,
+ previewOpts: opts.Preview,
+ previewer: previewer{0, []string{}, 0, showPreviewWindow, false, true, false, ""},
+ previewed: previewed{0, 0, 0, false},
+ previewBox: previewBox,
+ eventBox: eventBox,
+ mutex: sync.Mutex{},
+ suppress: true,
+ sigstop: false,
+ slab: util.MakeSlab(slab16Size, slab32Size),
+ theme: opts.Theme,
+ startChan: make(chan bool, 1),
+ killChan: make(chan int),
+ tui: renderer,
+ initFunc: func() { renderer.Init() },
+ executing: util.NewAtomicBool(false)}
t.prompt, t.promptLen = t.parsePrompt(opts.Prompt)
t.pointer, t.pointerLen = t.processTabs([]rune(opts.Pointer), 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)
}
-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(
- t.replacePlaceholder(t.previewOpts.scroll, false, "", list), "")
+ t.replacePlaceholder(t.previewOpts.scroll, false, "", []*Item{t.currentItem(), nil}), "")
atoi := func(s string) int {
n, e := strconv.Atoi(s)
@@ -1655,20 +1665,21 @@ func (t *Terminal) evaluateScrollOffset(list []*Item, height int) int {
}
base := -1
+ height := util.Max(0, t.pwindow.Height()-t.previewOpts.headerLines)
for _, component := range offsetComponentRegex.FindAllString(offsetExpr, -1) {
if strings.HasPrefix(component, "-/") {
component = component[1:]
}
if component[0] == '/' {
denom := atoi(component[1:])
- if denom == 0 {
- return base
+ if denom != 0 {
+ base -= height / denom
}
- return base - height/denom
+ break
}
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 {
@@ -1972,12 +1983,14 @@ func (t *Terminal) Loop() {
var items []*Item
var commandTemplate string
var pwindow tui.Window
+ initialOffset := 0
t.previewBox.Wait(func(events *util.Events) {
for req, value := range *events {
switch req {
case reqPreviewEnqueue:
request := value.(previewRequest)
commandTemplate = request.template
+ initialOffset = request.scrollOffset
items = request.list
pwindow = request.pwindow
}
@@ -1989,11 +2002,9 @@ func (t *Terminal) Loop() {
if items[0] != nil {
_, query := t.Input()
command := t.replacePlaceholder(commandTemplate, false, string(query), items)
- initialOffset := 0
cmd := util.ExecCommand(command, true)
if pwindow != nil {
height := pwindow.Height()
- initialOffset = util.Max(0, t.evaluateScrollOffset(items, util.Max(0, height-t.previewOpts.headerLines)))
env := os.Environ()
lines := fmt.Sprintf("LINES=%d", height)
columns := fmt.Sprintf("COLUMNS=%d", pwindow.Width())
@@ -2128,7 +2139,7 @@ func (t *Terminal) Loop() {
if len(command) > 0 && t.isPreviewEnabled() {
_, list := t.buildPlusList(command, false)
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 {
t.previewer.enabled = enabled
+ // We need to immediately update t.pwindow so we don't use reqRedraw
t.tui.Clear()
t.resizeWindows()
req(reqPrompt, reqList, reqInfo, reqHeader)
+ return true
}
+ return false
}
toggle := func() bool {
current := t.currentItem()
@@ -2327,7 +2341,7 @@ func (t *Terminal) Loop() {
if valid {
t.cancelPreview()
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 {
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
}
diff --git a/test/test_go.rb b/test/test_go.rb
index 20a4c923..3fc66f94 100755
--- a/test/test_go.rb
+++ b/test/test_go.rb
@@ -2142,6 +2142,55 @@ class TestGoFZF < TestBase
assert_equal expected.chomp, lines.take(6).join("\n")
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
module TestShell