Experimental implementation of "reload" action

# Reload input list with different sources
  seq 10 | fzf --bind 'ctrl-a:reload(seq 100),ctrl-b:reload(seq 1000)'

  # Reload as you type
  seq 10 | fzf --bind 'change:reload:seq {q}' --phony

  # Integration with ripgrep
  RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
  INITIAL_QUERY=""
  FZF_DEFAULT_COMMAND="$RG_PREFIX '$INITIAL_QUERY'" \
    fzf --bind "change:reload:$RG_PREFIX {q} || true" \
        --ansi --phony --query "$INITIAL_QUERY"

Close #751
Close #965
Close #974
Close #1736
Related #1723
This commit is contained in:
Junegunn Choi 2019-11-10 11:36:22 +09:00
parent 11962dabba
commit 78da928727
No known key found for this signature in database
GPG Key ID: 254BC280FEF9C627
7 changed files with 164 additions and 58 deletions

View File

@ -64,6 +64,13 @@ func (cl *ChunkList) Push(data []byte) bool {
return ret
}
// Clear clears the data
func (cl *ChunkList) Clear() {
cl.mutex.Lock()
cl.chunks = nil
cl.mutex.Unlock()
}
// Snapshot returns immutable snapshot of the ChunkList
func (cl *ChunkList) Snapshot() ([]*Chunk, int) {
cl.mutex.Lock()

View File

@ -135,8 +135,9 @@ func Run(opts *Options, revision string) {
// Reader
streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync
var reader *Reader
if !streamingFilter {
reader := NewReader(func(data []byte) bool {
reader = NewReader(func(data []byte) bool {
return chunkList.Push(data)
}, eventBox, opts.ReadZero)
go reader.ReadSource()
@ -223,6 +224,7 @@ func Run(opts *Options, revision string) {
// Event coordination
reading := true
ticks := 0
var nextCommand *string
eventBox.Watch(EvtReadNew)
for {
delay := true
@ -241,21 +243,41 @@ func Run(opts *Options, revision string) {
switch evt {
case EvtReadNew, EvtReadFin:
reading = reading && evt == EvtReadNew
clearCache := false
if evt == EvtReadFin && nextCommand != nil {
chunkList.Clear()
clearCache = true
go reader.restart(*nextCommand)
nextCommand = nil
} else {
reading = reading && evt == EvtReadNew
}
snapshot, count := chunkList.Snapshot()
terminal.UpdateCount(count, !reading, value.(bool))
terminal.UpdateCount(count, !reading, value.(*string))
if opts.Sync {
terminal.UpdateList(PassMerger(&snapshot, opts.Tac))
}
matcher.Reset(snapshot, input(), false, !reading, sort)
matcher.Reset(snapshot, input(), false, !reading, sort, clearCache)
case EvtSearchNew:
var command *string
switch val := value.(type) {
case bool:
sort = val
case searchRequest:
sort = val.sort
command = val.command
}
if command != nil {
if reading {
reader.terminate()
nextCommand = command
} else {
reading = true
chunkList.Clear()
go reader.restart(*command)
}
}
snapshot, _ := chunkList.Snapshot()
matcher.Reset(snapshot, input(), true, !reading, sort)
matcher.Reset(snapshot, input(), true, !reading, sort, command != nil)
delay = false
case EvtSearchProgress:

View File

@ -12,10 +12,11 @@ import (
// MatchRequest represents a search request
type MatchRequest struct {
chunks []*Chunk
pattern *Pattern
final bool
sort bool
chunks []*Chunk
pattern *Pattern
final bool
sort bool
clearCache bool
}
// Matcher is responsible for performing search
@ -69,7 +70,7 @@ func (m *Matcher) Loop() {
events.Clear()
})
if request.sort != m.sort {
if request.sort != m.sort || request.clearCache {
m.sort = request.sort
m.mergerCache = make(map[string]*Merger)
clearChunkCache()
@ -221,7 +222,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
}
// Reset is called to interrupt/signal the ongoing search
func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final bool, sort bool) {
func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final bool, sort bool, clearCache bool) {
pattern := m.patternBuilder(patternRunes)
var event util.EventType
@ -230,5 +231,5 @@ func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final
} else {
event = reqRetry
}
m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort && pattern.sortable})
m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort && pattern.sortable, clearCache})
}

View File

@ -631,13 +631,15 @@ 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)?):.+|:(execute(?:-multi|-silent)?)(\([^)]*\)|\[[^\]]*\]|~[^~]*~|![^!]*!|@[^@]*@|\#[^\#]*\#|\$[^\$]*\$|%[^%]*%|\^[^\^]*\^|&[^&]*&|\*[^\*]*\*|;[^;]*;|/[^/]*/|\|[^\|]*\|)`)
`(?si):(execute(?:-multi|-silent)?|reload):.+|:(execute(?:-multi|-silent)?|reload)(\([^)]*\)|\[[^\]]*\]|~[^~]*~|![^!]*!|@[^@]*@|\#[^\#]*\#|\$[^\$]*\$|%[^%]*%|\^[^\^]*\^|&[^&]*&|\*[^\*]*\*|;[^;]*;|/[^/]*/|\|[^\|]*\|)`)
}
func parseKeymap(keymap map[int][]action, str string) {
masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string {
prefix := ":execute"
if src[len(prefix)] == '-' {
if strings.HasPrefix(src, ":reload") {
prefix = ":reload"
} else if src[len(prefix)] == '-' {
c := src[len(prefix)+1]
if c == 's' || c == 'S' {
prefix += "-silent"
@ -790,6 +792,8 @@ func parseKeymap(keymap map[int][]action, str string) {
} else {
var offset int
switch t {
case actReload:
offset = len("reload")
case actExecuteSilent:
offset = len("execute-silent")
case actExecuteMulti:
@ -825,6 +829,8 @@ func isExecuteAction(str string) actionType {
prefix = matches[0][2]
}
switch prefix {
case "reload":
return actReload
case "execute":
return actExecute
case "execute-silent":

View File

@ -4,6 +4,8 @@ import (
"bufio"
"io"
"os"
"os/exec"
"sync"
"sync/atomic"
"time"
@ -16,11 +18,16 @@ type Reader struct {
eventBox *util.EventBox
delimNil bool
event int32
finChan chan bool
mutex sync.Mutex
exec *exec.Cmd
command *string
killed bool
}
// NewReader returns new Reader object
func NewReader(pusher func([]byte) bool, eventBox *util.EventBox, delimNil bool) *Reader {
return &Reader{pusher, eventBox, delimNil, int32(EvtReady)}
return &Reader{pusher, eventBox, delimNil, int32(EvtReady), make(chan bool, 1), sync.Mutex{}, nil, nil, false}
}
func (r *Reader) startEventPoller() {
@ -29,9 +36,10 @@ func (r *Reader) startEventPoller() {
pollInterval := readerPollIntervalMin
for {
if atomic.CompareAndSwapInt32(ptr, int32(EvtReadNew), int32(EvtReady)) {
r.eventBox.Set(EvtReadNew, true)
r.eventBox.Set(EvtReadNew, (*string)(nil))
pollInterval = readerPollIntervalMin
} else if atomic.LoadInt32(ptr) == int32(EvtReadFin) {
r.finChan <- true
return
} else {
pollInterval += readerPollIntervalStep
@ -46,7 +54,35 @@ func (r *Reader) startEventPoller() {
func (r *Reader) fin(success bool) {
atomic.StoreInt32(&r.event, int32(EvtReadFin))
r.eventBox.Set(EvtReadFin, success)
<-r.finChan
r.mutex.Lock()
ret := r.command
if success || r.killed {
ret = nil
}
r.mutex.Unlock()
r.eventBox.Set(EvtReadFin, ret)
}
func (r *Reader) terminate() {
r.mutex.Lock()
defer func() { r.mutex.Unlock() }()
r.killed = true
if r.exec != nil && r.exec.Process != nil {
util.KillCommand(r.exec)
} else {
os.Stdin.Close()
}
}
func (r *Reader) restart(command string) {
r.event = int32(EvtReady)
r.startEventPoller()
success := r.readFromCommand(nil, command)
r.fin(success)
}
// ReadSource reads data from the default command or from standard input
@ -54,12 +90,13 @@ func (r *Reader) ReadSource() {
r.startEventPoller()
var success bool
if util.IsTty() {
// The default command for *nix requires bash
shell := "bash"
cmd := os.Getenv("FZF_DEFAULT_COMMAND")
if len(cmd) == 0 {
// The default command for *nix requires bash
success = r.readFromCommand("bash", defaultCommand)
success = r.readFromCommand(&shell, defaultCommand)
} else {
success = r.readFromCommand("sh", cmd)
success = r.readFromCommand(nil, cmd)
}
} else {
success = r.readFromStdin()
@ -102,16 +139,25 @@ func (r *Reader) readFromStdin() bool {
return true
}
func (r *Reader) readFromCommand(shell string, cmd string) bool {
listCommand := util.ExecCommandWith(shell, cmd, false)
out, err := listCommand.StdoutPipe()
func (r *Reader) readFromCommand(shell *string, command string) bool {
r.mutex.Lock()
r.killed = false
r.command = &command
if shell != nil {
r.exec = util.ExecCommandWith(*shell, command, true)
} else {
r.exec = util.ExecCommand(command, true)
}
out, err := r.exec.StdoutPipe()
if err != nil {
r.mutex.Unlock()
return false
}
err = listCommand.Start()
err = r.exec.Start()
r.mutex.Unlock()
if err != nil {
return false
}
r.feed(out)
return listCommand.Wait() == nil
return r.exec.Wait() == nil
}

View File

@ -12,6 +12,7 @@ func TestReadFromCommand(t *testing.T) {
eb := util.NewEventBox()
reader := Reader{
pusher: func(s []byte) bool { strs = append(strs, string(s)); return true },
finChan: make(chan bool, 1),
eventBox: eb,
event: int32(EvtReady)}
@ -23,7 +24,7 @@ func TestReadFromCommand(t *testing.T) {
}
// Normal command
reader.fin(reader.readFromCommand("sh", `echo abc && echo def`))
reader.fin(reader.readFromCommand(nil, `echo abc && echo def`))
if len(strs) != 2 || strs[0] != "abc" || strs[1] != "def" {
t.Errorf("%s", strs)
}
@ -48,7 +49,7 @@ func TestReadFromCommand(t *testing.T) {
reader.startEventPoller()
// Failing command
reader.fin(reader.readFromCommand("sh", `no-such-command`))
reader.fin(reader.readFromCommand(nil, `no-such-command`))
strs = []string{}
if len(strs) > 0 {
t.Errorf("%s", strs)

View File

@ -102,7 +102,7 @@ type Terminal struct {
count int
progress int
reading bool
success bool
failed *string
jumping jumpMode
jumpLabels string
printer func(string)
@ -228,6 +228,7 @@ const (
actExecuteMulti // Deprecated
actSigStop
actTop
actReload
)
type placeholderFlags struct {
@ -238,6 +239,11 @@ type placeholderFlags struct {
file bool
}
type searchRequest struct {
sort bool
command *string
}
func toActions(types ...actionType) []action {
actions := make([]action, len(types))
for idx, t := range types {
@ -408,7 +414,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
ansi: opts.Ansi,
tabstop: opts.Tabstop,
reading: true,
success: true,
failed: nil,
jumping: jumpDisabled,
jumpLabels: opts.JumpLabels,
printer: opts.Printer,
@ -440,11 +446,11 @@ func (t *Terminal) Input() []rune {
}
// UpdateCount updates the count information
func (t *Terminal) UpdateCount(cnt int, final bool, success bool) {
func (t *Terminal) UpdateCount(cnt int, final bool, failedCommand *string) {
t.mutex.Lock()
t.count = cnt
t.reading = !final
t.success = success
t.failed = failedCommand
t.mutex.Unlock()
t.reqBox.Set(reqInfo, nil)
if final {
@ -742,7 +748,9 @@ func (t *Terminal) printInfo() {
pos = 2
}
output := fmt.Sprintf("%d/%d", t.merger.Length(), t.count)
found := t.merger.Length()
total := util.Max(found, t.count)
output := fmt.Sprintf("%d/%d", found, total)
if t.toggleSort {
if t.sort {
output += " +S"
@ -760,16 +768,15 @@ func (t *Terminal) printInfo() {
if t.progress > 0 && t.progress < 100 {
output += fmt.Sprintf(" (%d%%)", t.progress)
}
if !t.success && t.count == 0 {
if len(os.Getenv("FZF_DEFAULT_COMMAND")) > 0 {
output = "[$FZF_DEFAULT_COMMAND failed]"
} else {
output = "[default command failed - $FZF_DEFAULT_COMMAND required]"
}
if t.failed != nil && t.count == 0 {
output = fmt.Sprintf("[Command failed: %s]", *t.failed)
}
if pos+len(output) <= t.window.Width() {
t.window.CPrint(tui.ColInfo, 0, output)
maxWidth := t.window.Width() - pos
if len(output) > maxWidth {
outputRunes, _ := t.trimRight([]rune(output), maxWidth-2)
output = string(outputRunes) + ".."
}
t.window.CPrint(tui.ColInfo, 0, output)
}
func (t *Terminal) printHeader() {
@ -1383,7 +1390,7 @@ func (t *Terminal) hasPreviewWindow() bool {
func (t *Terminal) currentItem() *Item {
cnt := t.merger.Length()
if cnt > 0 && cnt > t.cy {
if t.cy >= 0 && cnt > 0 && cnt > t.cy {
return t.merger.Get(t.cy).item
}
return nil
@ -1508,11 +1515,10 @@ func (t *Terminal) Loop() {
t.mutex.Lock()
reading := t.reading
t.mutex.Unlock()
if !reading {
break
}
time.Sleep(spinnerDuration)
t.reqBox.Set(reqInfo, nil)
if reading {
t.reqBox.Set(reqInfo, nil)
}
}
}()
}
@ -1533,7 +1539,7 @@ func (t *Terminal) Loop() {
// We don't display preview window if no match
if request[0] != nil {
command := replacePlaceholder(t.preview.command,
t.ansi, t.delimiter, t.printsep, false, string(t.input), request)
t.ansi, t.delimiter, t.printsep, false, string(t.Input()), request)
cmd := util.ExecCommand(command, true)
if t.pwindow != nil {
env := os.Environ()
@ -1673,6 +1679,10 @@ func (t *Terminal) Loop() {
looping := true
for looping {
var newCommand *string
changed := false
queryChanged := false
event := t.tui.GetChar()
t.mutex.Lock()
@ -1754,9 +1764,7 @@ func (t *Terminal) Loop() {
}
case actToggleSort:
t.sort = !t.sort
t.eventBox.Set(EvtSearchNew, t.sort)
t.mutex.Unlock()
return false
changed = true
case actPreviewUp:
if t.hasPreviewWindow() {
scrollPreview(-1)
@ -2025,10 +2033,24 @@ func (t *Terminal) Loop() {
}
}
}
case actReload:
t.failed = nil
valid, list := t.buildPlusList(a.a, false)
// If the command template has {q}, we run the command even when the
// query string is empty.
if !valid {
_, query := hasPreviewFlags(a.a)
valid = query
}
if valid {
command := replacePlaceholder(a.a,
t.ansi, t.delimiter, t.printsep, false, string(t.input), list)
newCommand = &command
}
}
return true
}
changed := false
mapkey := event.Type
if t.jumping == jumpDisabled {
actions := t.keymap[mapkey]
@ -2042,8 +2064,9 @@ func (t *Terminal) Loop() {
continue
}
t.truncateQuery()
changed = string(previousInput) != string(t.input)
if onChanges, prs := t.keymap[tui.Change]; changed && prs {
queryChanged = string(previousInput) != string(t.input)
changed = changed || queryChanged
if onChanges, prs := t.keymap[tui.Change]; queryChanged && prs {
if !doActions(onChanges, tui.Change) {
continue
}
@ -2061,7 +2084,7 @@ func (t *Terminal) Loop() {
req(reqList)
}
if changed {
if queryChanged {
if t.isPreviewEnabled() {
_, q := hasPreviewFlags(t.preview.command)
if q {
@ -2070,14 +2093,14 @@ func (t *Terminal) Loop() {
}
}
if changed || t.cx != previousCx {
if queryChanged || t.cx != previousCx {
req(reqPrompt)
}
t.mutex.Unlock() // Must be unlocked before touching reqBox
if changed {
t.eventBox.Set(EvtSearchNew, t.sort)
if changed || newCommand != nil {
t.eventBox.Set(EvtSearchNew, searchRequest{sort: t.sort, command: newCommand})
}
for _, event := range events {
t.reqBox.Set(event, nil)