mirror of
https://github.com/peco/peco.git
synced 2026-09-10 07:16:29 -04:00
227 lines
5.1 KiB
Go
227 lines
5.1 KiB
Go
package peco
|
|
|
|
import (
|
|
"sync"
|
|
"time"
|
|
|
|
"context"
|
|
|
|
"github.com/lestrrat/go-pdebug"
|
|
"github.com/peco/peco/filter"
|
|
"github.com/peco/peco/hub"
|
|
"github.com/peco/peco/internal/buffer"
|
|
"github.com/peco/peco/line"
|
|
"github.com/peco/peco/pipeline"
|
|
)
|
|
|
|
func newFilterProcessor(f filter.Filter, q string) *filterProcessor {
|
|
return &filterProcessor{
|
|
filter: f,
|
|
query: q,
|
|
}
|
|
}
|
|
|
|
func (fp *filterProcessor) Accept(ctx context.Context, in chan interface{}, out pipeline.ChanOutput) {
|
|
acceptAndFilter(ctx, fp.filter, in, out)
|
|
}
|
|
|
|
// This flusher is run in a separate goroutine so that the filter can
|
|
// run separately from accepting incoming messages
|
|
func flusher(ctx context.Context, f filter.Filter, incoming chan []line.Line, done chan struct{}, out pipeline.ChanOutput) {
|
|
if pdebug.Enabled {
|
|
g := pdebug.Marker("flusher goroutine")
|
|
defer g.End()
|
|
}
|
|
|
|
defer close(done)
|
|
defer out.SendEndMark("end of filter")
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case buf, ok := <-incoming:
|
|
if !ok {
|
|
return
|
|
}
|
|
pdebug.Printf("flusher: %#v", buf)
|
|
f.Apply(ctx, buf, out)
|
|
buffer.ReleaseLineListBuf(buf)
|
|
}
|
|
}
|
|
}
|
|
|
|
func acceptAndFilter(ctx context.Context, f filter.Filter, in chan interface{}, out pipeline.ChanOutput) {
|
|
flush := make(chan []line.Line)
|
|
flushDone := make(chan struct{})
|
|
go flusher(ctx, f, flush, flushDone, out)
|
|
|
|
buf := buffer.GetLineListBuf()
|
|
bufsiz := f.BufSize()
|
|
if bufsiz <= 0 {
|
|
bufsiz = cap(buf)
|
|
}
|
|
defer func() { <-flushDone }() // Wait till the flush goroutine is done
|
|
defer close(flush) // Kill the flush goroutine
|
|
|
|
flushTicker := time.NewTicker(50 * time.Millisecond)
|
|
defer flushTicker.Stop()
|
|
|
|
start := time.Now()
|
|
lines := 0
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
if pdebug.Enabled {
|
|
pdebug.Printf("filter received done")
|
|
}
|
|
return
|
|
case <-flushTicker.C:
|
|
if len(buf) > 0 {
|
|
flush <- buf
|
|
buf = buffer.GetLineListBuf()
|
|
}
|
|
case v := <-in:
|
|
switch v.(type) {
|
|
case error:
|
|
if pipeline.IsEndMark(v.(error)) {
|
|
if pdebug.Enabled {
|
|
pdebug.Printf("filter received end mark (read %d lines, %s since starting accept loop)", lines+len(buf), time.Since(start).String())
|
|
}
|
|
if len(buf) > 0 {
|
|
flush <- buf
|
|
buf = nil
|
|
}
|
|
}
|
|
return
|
|
case line.Line:
|
|
if pdebug.Enabled {
|
|
pdebug.Printf("incoming line")
|
|
lines++
|
|
}
|
|
// We buffer the lines so that we can receive more lines to
|
|
// process while we filter what we already have. The buffer
|
|
// size is fairly big, because this really only makes a
|
|
// difference if we have a lot of lines to process.
|
|
buf = append(buf, v.(line.Line))
|
|
if len(buf) >= bufsiz {
|
|
flush <- buf
|
|
buf = buffer.GetLineListBuf()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func NewFilter(state *Peco) *Filter {
|
|
return &Filter{
|
|
state: state,
|
|
}
|
|
}
|
|
|
|
// Work is the actual work horse that that does the matching
|
|
// in a goroutine of its own. It wraps Matcher.Match().
|
|
func (f *Filter) Work(ctx context.Context, q hub.Payload) {
|
|
defer q.Done()
|
|
|
|
query, ok := q.Data().(string)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
if pdebug.Enabled {
|
|
g := pdebug.Marker("Filter.Work query '%s'", query)
|
|
defer g.End()
|
|
}
|
|
|
|
state := f.state
|
|
if query == "" {
|
|
state.ResetCurrentLineBuffer()
|
|
if !state.config.StickySelection {
|
|
state.Selection().Reset()
|
|
}
|
|
return
|
|
}
|
|
|
|
// Create a new pipeline
|
|
p := pipeline.New()
|
|
p.SetSource(state.Source())
|
|
|
|
// Wraps the actual filter
|
|
selectedFilter := state.Filters().Current()
|
|
ctx = selectedFilter.NewContext(ctx, query)
|
|
p.Add(newFilterProcessor(selectedFilter, query))
|
|
|
|
buf := NewMemoryBuffer()
|
|
p.SetDestination(buf)
|
|
state.SetCurrentLineBuffer(buf)
|
|
|
|
go func() {
|
|
defer state.Hub().SendDraw(&DrawOptions{RunningQuery: true})
|
|
if err := p.Run(ctx); err != nil {
|
|
state.Hub().SendStatusMsg(err.Error())
|
|
}
|
|
}()
|
|
|
|
go func() {
|
|
if pdebug.Enabled {
|
|
g := pdebug.Marker("Periodic draw request for '%s'", query)
|
|
defer g.End()
|
|
}
|
|
t := time.NewTicker(5 * time.Millisecond)
|
|
defer t.Stop()
|
|
defer state.Hub().SendStatusMsg("")
|
|
defer state.Hub().SendDraw(&DrawOptions{RunningQuery: true})
|
|
for {
|
|
select {
|
|
case <-p.Done():
|
|
return
|
|
case <-t.C:
|
|
state.Hub().SendDraw(&DrawOptions{RunningQuery: true})
|
|
}
|
|
}
|
|
}()
|
|
|
|
<-p.Done()
|
|
|
|
if !state.config.StickySelection {
|
|
state.Selection().Reset()
|
|
}
|
|
}
|
|
|
|
// Loop keeps watching for incoming queries, and upon receiving
|
|
// a query, spawns a goroutine to do the heavy work. It also
|
|
// checks for previously running queries, so we can avoid
|
|
// running many goroutines doing the grep at the same time
|
|
func (f *Filter) Loop(ctx context.Context, cancel func()) error {
|
|
defer cancel()
|
|
|
|
// previous holds the function that can cancel the previous
|
|
// query. This is used when multiple queries come in succession
|
|
// and the previous query is discarded anyway
|
|
var mutex sync.Mutex
|
|
var previous func()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil
|
|
case q := <-f.state.Hub().QueryCh():
|
|
workctx, workcancel := context.WithCancel(ctx)
|
|
|
|
mutex.Lock()
|
|
if previous != nil {
|
|
if pdebug.Enabled {
|
|
pdebug.Printf("Canceling previous query")
|
|
}
|
|
previous()
|
|
}
|
|
previous = workcancel
|
|
mutex.Unlock()
|
|
|
|
f.state.Hub().SendStatusMsg("Running query...")
|
|
|
|
go f.Work(workctx, q)
|
|
}
|
|
}
|
|
}
|