mirror of
https://github.com/Llewellynvdm/fzf.git
synced 2024-12-23 03:19:01 +00:00
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:
parent
11962dabba
commit
78da928727
@ -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()
|
||||
|
36
src/core.go
36
src/core.go
@ -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:
|
||||
|
@ -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})
|
||||
}
|
||||
|
@ -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":
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user