diff options
| author | Junegunn Choi <junegunn.c@gmail.com> | 2021-11-30 23:37:48 +0900 |
|---|---|---|
| committer | Junegunn Choi <junegunn.c@gmail.com> | 2021-11-30 23:57:46 +0900 |
| commit | 20b4e6953ec439f52e9bdcc06ac6ee5bb590d39d (patch) | |
| tree | d2c2b3543261ef7ec76b0dbc06b2c17d45e1076b /src | |
| parent | 7da287e3aae4a9b0e81ebc67701460073afcc0d0 (diff) | |
| download | fzf-20b4e6953ec439f52e9bdcc06ac6ee5bb590d39d.tar.gz | |
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
Diffstat (limited to 'src')
| -rw-r--r-- | src/options.go | 45 | ||||
| -rw-r--r-- | src/terminal.go | 373 |
2 files changed, 253 insertions, 165 deletions
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 } |
