diff --git a/src/chunklist.go b/src/chunklist.go index 510cd73..cd635c2 100644 --- a/src/chunklist.go +++ b/src/chunklist.go @@ -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() diff --git a/src/core.go b/src/core.go index ae8c8eb..d9d98d1 100644 --- a/src/core.go +++ b/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: diff --git a/src/matcher.go b/src/matcher.go index 6925087..22aa819 100644 --- a/src/matcher.go +++ b/src/matcher.go @@ -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}) } diff --git a/src/options.go b/src/options.go index 9e09379..848ce3d 100644 --- a/src/options.go +++ b/src/options.go @@ -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": diff --git a/src/reader.go b/src/reader.go index b418f54..dd68486 100644 --- a/src/reader.go +++ b/src/reader.go @@ -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 } diff --git a/src/reader_test.go b/src/reader_test.go index c29936c..b061068 100644 --- a/src/reader_test.go +++ b/src/reader_test.go @@ -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) diff --git a/src/terminal.go b/src/terminal.go index 3c656cc..2db811c 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -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)