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 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 // Snapshot returns immutable snapshot of the ChunkList
func (cl *ChunkList) Snapshot() ([]*Chunk, int) { func (cl *ChunkList) Snapshot() ([]*Chunk, int) {
cl.mutex.Lock() cl.mutex.Lock()

View File

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

View File

@ -12,10 +12,11 @@ import (
// MatchRequest represents a search request // MatchRequest represents a search request
type MatchRequest struct { type MatchRequest struct {
chunks []*Chunk chunks []*Chunk
pattern *Pattern pattern *Pattern
final bool final bool
sort bool sort bool
clearCache bool
} }
// Matcher is responsible for performing search // Matcher is responsible for performing search
@ -69,7 +70,7 @@ func (m *Matcher) Loop() {
events.Clear() events.Clear()
}) })
if request.sort != m.sort { if request.sort != m.sort || request.clearCache {
m.sort = request.sort m.sort = request.sort
m.mergerCache = make(map[string]*Merger) m.mergerCache = make(map[string]*Merger)
clearChunkCache() clearChunkCache()
@ -221,7 +222,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
} }
// Reset is called to interrupt/signal the ongoing search // 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) pattern := m.patternBuilder(patternRunes)
var event util.EventType var event util.EventType
@ -230,5 +231,5 @@ func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final
} else { } else {
event = reqRetry 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. // 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)?):.+|:(execute(?:-multi|-silent)?)(\([^)]*\)|\[[^\]]*\]|~[^~]*~|![^!]*!|@[^@]*@|\#[^\#]*\#|\$[^\$]*\$|%[^%]*%|\^[^\^]*\^|&[^&]*&|\*[^\*]*\*|;[^;]*;|/[^/]*/|\|[^\|]*\|)`) `(?si):(execute(?:-multi|-silent)?|reload):.+|:(execute(?:-multi|-silent)?|reload)(\([^)]*\)|\[[^\]]*\]|~[^~]*~|![^!]*!|@[^@]*@|\#[^\#]*\#|\$[^\$]*\$|%[^%]*%|\^[^\^]*\^|&[^&]*&|\*[^\*]*\*|;[^;]*;|/[^/]*/|\|[^\|]*\|)`)
} }
func parseKeymap(keymap map[int][]action, str string) { func parseKeymap(keymap map[int][]action, str string) {
masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string { masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string {
prefix := ":execute" prefix := ":execute"
if src[len(prefix)] == '-' { if strings.HasPrefix(src, ":reload") {
prefix = ":reload"
} else if src[len(prefix)] == '-' {
c := src[len(prefix)+1] c := src[len(prefix)+1]
if c == 's' || c == 'S' { if c == 's' || c == 'S' {
prefix += "-silent" prefix += "-silent"
@ -790,6 +792,8 @@ func parseKeymap(keymap map[int][]action, str string) {
} else { } else {
var offset int var offset int
switch t { switch t {
case actReload:
offset = len("reload")
case actExecuteSilent: case actExecuteSilent:
offset = len("execute-silent") offset = len("execute-silent")
case actExecuteMulti: case actExecuteMulti:
@ -825,6 +829,8 @@ func isExecuteAction(str string) actionType {
prefix = matches[0][2] prefix = matches[0][2]
} }
switch prefix { switch prefix {
case "reload":
return actReload
case "execute": case "execute":
return actExecute return actExecute
case "execute-silent": case "execute-silent":

View File

@ -4,6 +4,8 @@ import (
"bufio" "bufio"
"io" "io"
"os" "os"
"os/exec"
"sync"
"sync/atomic" "sync/atomic"
"time" "time"
@ -16,11 +18,16 @@ type Reader struct {
eventBox *util.EventBox eventBox *util.EventBox
delimNil bool delimNil bool
event int32 event int32
finChan chan bool
mutex sync.Mutex
exec *exec.Cmd
command *string
killed bool
} }
// NewReader returns new Reader object // NewReader returns new Reader object
func NewReader(pusher func([]byte) bool, eventBox *util.EventBox, delimNil bool) *Reader { 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() { func (r *Reader) startEventPoller() {
@ -29,9 +36,10 @@ func (r *Reader) startEventPoller() {
pollInterval := readerPollIntervalMin pollInterval := readerPollIntervalMin
for { for {
if atomic.CompareAndSwapInt32(ptr, int32(EvtReadNew), int32(EvtReady)) { if atomic.CompareAndSwapInt32(ptr, int32(EvtReadNew), int32(EvtReady)) {
r.eventBox.Set(EvtReadNew, true) r.eventBox.Set(EvtReadNew, (*string)(nil))
pollInterval = readerPollIntervalMin pollInterval = readerPollIntervalMin
} else if atomic.LoadInt32(ptr) == int32(EvtReadFin) { } else if atomic.LoadInt32(ptr) == int32(EvtReadFin) {
r.finChan <- true
return return
} else { } else {
pollInterval += readerPollIntervalStep pollInterval += readerPollIntervalStep
@ -46,7 +54,35 @@ func (r *Reader) startEventPoller() {
func (r *Reader) fin(success bool) { func (r *Reader) fin(success bool) {
atomic.StoreInt32(&r.event, int32(EvtReadFin)) 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 // ReadSource reads data from the default command or from standard input
@ -54,12 +90,13 @@ func (r *Reader) ReadSource() {
r.startEventPoller() r.startEventPoller()
var success bool var success bool
if util.IsTty() { if util.IsTty() {
// The default command for *nix requires bash
shell := "bash"
cmd := os.Getenv("FZF_DEFAULT_COMMAND") cmd := os.Getenv("FZF_DEFAULT_COMMAND")
if len(cmd) == 0 { if len(cmd) == 0 {
// The default command for *nix requires bash success = r.readFromCommand(&shell, defaultCommand)
success = r.readFromCommand("bash", defaultCommand)
} else { } else {
success = r.readFromCommand("sh", cmd) success = r.readFromCommand(nil, cmd)
} }
} else { } else {
success = r.readFromStdin() success = r.readFromStdin()
@ -102,16 +139,25 @@ func (r *Reader) readFromStdin() bool {
return true return true
} }
func (r *Reader) readFromCommand(shell string, cmd string) bool { func (r *Reader) readFromCommand(shell *string, command string) bool {
listCommand := util.ExecCommandWith(shell, cmd, false) r.mutex.Lock()
out, err := listCommand.StdoutPipe() 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 { if err != nil {
r.mutex.Unlock()
return false return false
} }
err = listCommand.Start() err = r.exec.Start()
r.mutex.Unlock()
if err != nil { if err != nil {
return false return false
} }
r.feed(out) 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() eb := util.NewEventBox()
reader := Reader{ reader := Reader{
pusher: func(s []byte) bool { strs = append(strs, string(s)); return true }, pusher: func(s []byte) bool { strs = append(strs, string(s)); return true },
finChan: make(chan bool, 1),
eventBox: eb, eventBox: eb,
event: int32(EvtReady)} event: int32(EvtReady)}
@ -23,7 +24,7 @@ func TestReadFromCommand(t *testing.T) {
} }
// Normal command // 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" { if len(strs) != 2 || strs[0] != "abc" || strs[1] != "def" {
t.Errorf("%s", strs) t.Errorf("%s", strs)
} }
@ -48,7 +49,7 @@ func TestReadFromCommand(t *testing.T) {
reader.startEventPoller() reader.startEventPoller()
// Failing command // Failing command
reader.fin(reader.readFromCommand("sh", `no-such-command`)) reader.fin(reader.readFromCommand(nil, `no-such-command`))
strs = []string{} strs = []string{}
if len(strs) > 0 { if len(strs) > 0 {
t.Errorf("%s", strs) t.Errorf("%s", strs)

View File

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