m/fzf
1
0
mirror of https://github.com/junegunn/fzf.git synced 2025-11-17 07:43:39 -05:00

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
5 changed files with 367 additions and 221 deletions

View File

@@ -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,11 +1656,29 @@ 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
}
opts.Keymap = keymap

View File

@@ -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
}