diff --git a/action.go b/action.go index 09eb023..f28d72a 100644 --- a/action.go +++ b/action.go @@ -7,12 +7,14 @@ import ( "os/exec" "unicode" + "context" + "github.com/google/btree" "github.com/lestrrat/go-pdebug" "github.com/nsf/termbox-go" "github.com/peco/peco/internal/keyseq" + "github.com/peco/peco/line" "github.com/pkg/errors" - "golang.org/x/net/context" ) // This is the global map of canonical action name to actions @@ -734,7 +736,7 @@ func makeCommandAction(state *Peco, cc *CommandConfig) ActionFunc { } sel.Ascend(func(it btree.Item) bool { - line := it.(Line) + line := it.(line.Line) var f *os.File var err error diff --git a/action_test.go b/action_test.go index 5dd2649..8f9d7a4 100644 --- a/action_test.go +++ b/action_test.go @@ -5,9 +5,11 @@ import ( "time" "unicode/utf8" + "context" + "github.com/nsf/termbox-go" + "github.com/peco/peco/filter" "github.com/stretchr/testify/assert" - "golang.org/x/net/context" ) func TestActionFunc(t *testing.T) { @@ -291,7 +293,7 @@ func TestRotateFilter(t *testing.T) { return } - var prev LineFilter + var prev filter.Filter first := state.Filters().Current() prev = first for i := 0; i < size; i++ { diff --git a/buffer.go b/buffer.go index f67a9eb..9d7e463 100644 --- a/buffer.go +++ b/buffer.go @@ -3,10 +3,12 @@ package peco import ( "time" + "context" + "github.com/lestrrat/go-pdebug" + "github.com/peco/peco/line" "github.com/peco/peco/pipeline" "github.com/pkg/errors" - "golang.org/x/net/context" ) func NewFilteredBuffer(src Buffer, page, perPage int) *FilteredBuffer { @@ -33,14 +35,14 @@ func NewFilteredBuffer(src Buffer, page, perPage int) *FilteredBuffer { return &fb } -func (flb *FilteredBuffer) Append(l Line) (Line, error) { +func (flb *FilteredBuffer) Append(l line.Line) (line.Line, error) { return l, nil } // LineAt returns the line at index `i`. Note that the i-th element // in this filtered buffer may actually correspond to a totally // different line number in the source buffer. -func (flb FilteredBuffer) LineAt(i int) (Line, error) { +func (flb FilteredBuffer) LineAt(i int) (line.Line, error) { if i >= len(flb.selection) { return nil, errors.Errorf("specified index %d is out of range", len(flb.selection)) } @@ -58,13 +60,13 @@ func NewMemoryBuffer() *MemoryBuffer { return mb } -func (mb *MemoryBuffer) Append(l Line) { +func (mb *MemoryBuffer) Append(l line.Line) { mb.mutex.Lock() defer mb.mutex.Unlock() bufferAppend(&mb.lines, l) } -func bufferAppend(lines *[]Line, l Line) { +func bufferAppend(lines *[]line.Line, l line.Line) { *lines = append(*lines, l) } @@ -74,7 +76,7 @@ func (mb *MemoryBuffer) Size() int { return bufferSize(mb.lines) } -func bufferSize(lines []Line) int { +func bufferSize(lines []line.Line) int { return len(lines) } @@ -86,7 +88,7 @@ func (mb *MemoryBuffer) Reset() { defer g.End() } mb.done = make(chan struct{}) - mb.lines = []Line(nil) + mb.lines = []line.Line(nil) } func (mb *MemoryBuffer) Done() <-chan struct{} { @@ -123,22 +125,22 @@ func (mb *MemoryBuffer) Accept(ctx context.Context, in chan interface{}, _ pipel } return } - case Line: + case line.Line: mb.mutex.Lock() - mb.lines = append(mb.lines, v.(Line)) + mb.lines = append(mb.lines, v.(line.Line)) mb.mutex.Unlock() } } } } -func (mb *MemoryBuffer) LineAt(n int) (Line, error) { +func (mb *MemoryBuffer) LineAt(n int) (line.Line, error) { mb.mutex.RLock() defer mb.mutex.RUnlock() return bufferLineAt(mb.lines, n) } -func bufferLineAt(lines []Line, n int) (Line, error) { +func bufferLineAt(lines []line.Line, n int) (line.Line, error) { if s := len(lines); s <= 0 || n >= s { return nil, errors.New("empty buffer") } diff --git a/config.go b/config.go index 586ab35..4eaf305 100644 --- a/config.go +++ b/config.go @@ -8,14 +8,11 @@ import ( "strings" "github.com/nsf/termbox-go" + "github.com/peco/peco/filter" "github.com/peco/peco/internal/util" "github.com/pkg/errors" ) -// DefaultCustomFilterBufferThreshold is the default value -// for BufferThreshold setting on CustomFilters. -const DefaultCustomFilterBufferThreshold = 100 - var homedirFunc = util.Homedir // NewConfig creates a new Config @@ -57,7 +54,7 @@ func (c *Config) ReadFilename(filename string) error { c.CustomFilter[n] = CustomFilterConfig{ Cmd: cfg[0], Args: cfg[1:], - BufferThreshold: DefaultCustomFilterBufferThreshold, + BufferThreshold: filter.DefaultCustomFilterBufferThreshold, } } } diff --git a/filter.go b/filter.go index a94f1ff..92ac431 100644 --- a/filter.go +++ b/filter.go @@ -1,77 +1,107 @@ package peco import ( - "bufio" - "bytes" - "os/exec" - "regexp" - "sort" - "strings" "sync" "time" - "unicode/utf8" + + "context" "github.com/lestrrat/go-pdebug" + "github.com/peco/peco/filter" "github.com/peco/peco/hub" - "github.com/peco/peco/internal/util" + "github.com/peco/peco/internal/buffer" + "github.com/peco/peco/line" "github.com/peco/peco/pipeline" - "github.com/pkg/errors" - "golang.org/x/net/context" ) -func (fs *FilterSet) Reset() { - fs.mutex.Lock() - defer fs.mutex.Unlock() - fs.current = 0 -} - -func (fs *FilterSet) Size() int { - fs.mutex.Lock() - defer fs.mutex.Unlock() - return len(fs.filters) -} - -func (fs *FilterSet) Add(lf LineFilter) error { - fs.mutex.Lock() - defer fs.mutex.Unlock() - fs.filters = append(fs.filters, lf) - return nil -} - -func (fs *FilterSet) Rotate() { - fs.mutex.Lock() - defer fs.mutex.Unlock() - fs.current++ - if fs.current >= len(fs.filters) { - fs.current = 0 +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.OutputChannel) { + 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.OutputChannel) { if pdebug.Enabled { - pdebug.Printf("FilterSet.Rotate: now filter in effect is %s", fs.filters[fs.current]) + g := pdebug.Marker("flusher goroutine") + defer g.End() + } + + defer close(done) + defer out.SendEndMark("end of filter") + for buf := range incoming { + for _, in := range buf { + if l, err := f.Apply(ctx, in); err == nil { + out.Send(l) + } + } + buffer.ReleaseLineListBuf(buf) } } -func (fs *FilterSet) SetCurrentByName(name string) error { - fs.mutex.Lock() - defer fs.mutex.Unlock() - for i, f := range fs.filters { - if f.String() == name { - fs.current = i - return nil +func acceptAndFilter(ctx context.Context, f filter.Filter, in chan interface{}, out pipeline.OutputChannel) { + flush := make(chan []line.Line) + flushDone := make(chan struct{}) + go flusher(ctx, f, flush, flushDone, out) + + buf := buffer.GetLineListBuf() + defer buffer.ReleaseLineListBuf(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 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 { + 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)) + select { + case <-flushTicker.C: + flush <- buf + buf = buffer.GetLineListBuf() + default: + if len(buf) >= cap(buf) { + flush <- buf + buf = buffer.GetLineListBuf() + } + } + } } } - return ErrFilterNotFound -} - -func (fs *FilterSet) Index() int { - fs.mutex.Lock() - defer fs.mutex.Unlock() - return fs.current -} - -func (fs *FilterSet) Current() LineFilter { - fs.mutex.Lock() - defer fs.mutex.Unlock() - return fs.filters[fs.current] } func NewFilter(state *Peco) *Filter { @@ -107,9 +137,9 @@ func (f *Filter) Work(ctx context.Context, q hub.Payload) { // Create a new pipeline p := pipeline.New() p.SetSource(state.Source()) - thisf := state.Filters().Current().Clone() - thisf.SetQuery(query) - p.Add(thisf) + + // Wraps the actual filter + p.Add(newFilterProcessor(state.Filters().Current(), query)) buf := NewMemoryBuffer() p.SetDestination(buf) @@ -117,7 +147,7 @@ func (f *Filter) Work(ctx context.Context, q hub.Payload) { go func() { defer state.Hub().SendDraw(&DrawOptions{RunningQuery: true}) - ctx = context.WithValue(ctx, "query", query) + ctx = filter.NewContext(ctx, query) if err := p.Run(ctx); err != nil { state.Hub().SendStatusMsg(err.Error()) } @@ -184,493 +214,3 @@ func (f *Filter) Loop(ctx context.Context, cancel func()) error { } } } - -func NewRegexpFilter() *RegexpFilter { - return &RegexpFilter{ - flags: regexpFlagList(defaultFlags), - name: "Regexp", - outCh: pipeline.OutputChannel(make(chan interface{})), - } -} - -func (rf *RegexpFilter) OutCh() <-chan interface{} { - rf.mutex.Lock() - defer rf.mutex.Unlock() - return rf.outCh -} - -func (rf RegexpFilter) Clone() LineFilter { - return &RegexpFilter{ - flags: rf.flags, - quotemeta: rf.quotemeta, - query: rf.query, - name: rf.name, - outCh: pipeline.OutputChannel(make(chan interface{})), - } -} - -const filterBufSize = 1000 - -var filterBufPool = sync.Pool{ - New: func() interface{} { - return make([]Line, 0, filterBufSize) - }, -} - -func releaseFilterLineBuf(l []Line) { - if l == nil { - return - } - l = l[0:0] - filterBufPool.Put(l) -} - -func getFilterLineBuf() []Line { - l := filterBufPool.Get().([]Line) - return l -} - -type filter interface { - filter(Line) (Line, error) -} - -// This flusher is run in a separate goroutine so that the filter can -// run separately from accepting incoming messages -func flusher(f filter, incoming chan []Line, done chan struct{}, out pipeline.OutputChannel) { - if pdebug.Enabled { - g := pdebug.Marker("flusher goroutine") - defer g.End() - } - - defer close(done) - defer out.SendEndMark("end of filter") - for buf := range incoming { - for _, in := range buf { - if l, err := f.filter(in); err == nil { - out.Send(l) - } - } - releaseFilterLineBuf(buf) - } -} - -func (rf *RegexpFilter) Accept(ctx context.Context, in chan interface{}, out pipeline.OutputChannel) { - if pdebug.Enabled { - g := pdebug.Marker("RegexpFilter.Accept") - defer g.End() - } - - filterAcceptAndFilter(ctx, rf, in, out) -} - -func filterAcceptAndFilter(ctx context.Context, f filter, in chan interface{}, out pipeline.OutputChannel) { - flush := make(chan []Line) - flushDone := make(chan struct{}) - go flusher(f, flush, flushDone, out) - - buf := getFilterLineBuf() - defer releaseFilterLineBuf(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 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: - if pdebug.Enabled { - 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)) - select { - case <-flushTicker.C: - flush <- buf - buf = getFilterLineBuf() - default: - if len(buf) >= cap(buf) { - flush <- buf - buf = getFilterLineBuf() - } - } - } - } - } -} - -func (rf *RegexpFilter) filter(l Line) (Line, error) { - regexps, err := rf.getQueryAsRegexps() - if err != nil { - return nil, errors.Wrap(err, "failed to compile queries as regular expression") - } - v := l.DisplayString() - allMatched := true - matches := [][]int{} -TryRegexps: - for _, rx := range regexps { - match := rx.FindAllStringSubmatchIndex(v, -1) - if match == nil { - allMatched = false - break TryRegexps - } - matches = append(matches, match...) - } - - if !allMatched { - return nil, errors.New("filter did not match against given line") - } - - sort.Sort(byMatchStart(matches)) - - // We need to "dedupe" the results. For example, if we matched the - // same region twice, we don't want that to be drawn - - deduped := make([][]int, 0, len(matches)) - - for i, m := range matches { - // Always push the first one - if i == 0 { - deduped = append(deduped, m) - continue - } - - prev := deduped[len(deduped)-1] - switch { - case matchContains(prev, m): - // If the previous match contains this one, then - // don't do anything - continue - case matchOverlaps(prev, m): - // If the previous match overlaps with this one, - // merge the results and make it a bigger one - deduped[len(deduped)-1] = mergeMatches(prev, m) - default: - deduped = append(deduped, m) - } - } - return NewMatchedLine(l, deduped), nil -} - -func (rf *RegexpFilter) getQueryAsRegexps() ([]*regexp.Regexp, error) { - rf.mutex.Lock() - defer rf.mutex.Unlock() - - if q := rf.compiledQuery; q != nil { - return q, nil - } - q, err := queryToRegexps(rf.flags, rf.quotemeta, rf.query) - if err != nil { - return nil, errors.Wrap(err, "failed to compile queries as regular expression") - } - - rf.compiledQuery = q - return q, nil -} - -func (rf *RegexpFilter) SetQuery(q string) { - rf.mutex.Lock() - defer rf.mutex.Unlock() - - rf.query = q - rf.compiledQuery = nil -} - -func (rf RegexpFilter) String() string { - return rf.name -} - -var ErrFilterNotFound = errors.New("specified filter was not found") - -func NewIgnoreCaseFilter() *RegexpFilter { - rf := NewRegexpFilter() - rf.flags = ignoreCaseFlags - rf.quotemeta = true - rf.name = "IgnoreCase" - return rf -} - -func NewCaseSensitiveFilter() *RegexpFilter { - rf := NewRegexpFilter() - rf.quotemeta = true - rf.name = "CaseSensitive" - return rf -} - -// SmartCaseFilter turns ON the ignore-case flag in the regexp -// if the query contains a upper-case character -func NewSmartCaseFilter() *RegexpFilter { - rf := NewRegexpFilter() - rf.quotemeta = true - rf.name = "SmartCase" - rf.flags = regexpFlagFunc(func(q string) []string { - if util.ContainsUpper(q) { - return defaultFlags - } - return []string{"i"} - }) - return rf -} - -// NewFuzzyFilter builds a fuzzy-finder type of filter. -// In effect, this uses a smart case filter, and for q query -// like "ABC" it matches the equivalent of "A(.*)B(.*)C(.*)" -func NewFuzzyFilter() *FuzzyFilter { - return &FuzzyFilter{} -} - -func (ff FuzzyFilter) Clone() LineFilter { - return &FuzzyFilter{ - query: ff.query, - } -} - -func (ff *FuzzyFilter) SetQuery(q string) { - ff.mutex.Lock() - defer ff.mutex.Unlock() - - ff.query = q -} - -func (ff FuzzyFilter) String() string { - return "Fuzzy" -} - -func (ff *FuzzyFilter) Accept(ctx context.Context, in chan interface{}, out pipeline.OutputChannel) { - if pdebug.Enabled { - g := pdebug.Marker("FuzzyFilter.Accept") - defer g.End() - } - - filterAcceptAndFilter(ctx, ff, in, out) -} - -func (ff *FuzzyFilter) filter(l Line) (Line, error) { - query := "" - ff.mutex.Lock() - query = ff.query - ff.mutex.Unlock() - - base := 0 - txt := l.DisplayString() - matches := [][]int{} - - hasUpper := util.ContainsUpper(query) - - for len(query) > 0 { - r, n := utf8.DecodeRuneInString(query) - if r == utf8.RuneError { - // "Silently" ignore (just return a no match) - return nil, errors.New("failed to decode input string") - } - query = query[n:] - - var i int - if hasUpper { // explicit match - i = strings.IndexRune(txt, r) - } else { - i = strings.IndexFunc(txt, util.CaseInsensitiveIndexFunc(r)) - } - if i == -1 { - return nil, errors.New("filter did not match against given line") - } - - // otherwise we have a match, but the next match must match against - // something AFTER the current match - txt = txt[i+n:] - matches = append(matches, []int{base + i, base + i + n}) - base = base + i + n - } - return NewMatchedLine(l, matches), nil -} - -func NewExternalCmdFilter(name string, cmd string, args []string, threshold int, idgen lineIDGenerator, enableSep bool) *ExternalCmdFilter { - if len(args) == 0 { - args = []string{"$QUERY"} - } - - if threshold <= 0 { - threshold = DefaultCustomFilterBufferThreshold - } - - return &ExternalCmdFilter{ - args: args, - cmd: cmd, - enableSep: enableSep, - idgen: idgen, - name: name, - outCh: pipeline.OutputChannel(make(chan interface{})), - thresholdBufsiz: threshold, - } -} - -func (ecf ExternalCmdFilter) Clone() LineFilter { - return &ExternalCmdFilter{ - args: ecf.args, - cmd: ecf.cmd, - enableSep: ecf.enableSep, - idgen: ecf.idgen, - name: ecf.name, - outCh: pipeline.OutputChannel(make(chan interface{})), - thresholdBufsiz: ecf.thresholdBufsiz, - } -} - -func (ecf *ExternalCmdFilter) Verify() error { - if ecf.cmd == "" { - return errors.Errorf("no executable specified for custom matcher '%s'", ecf.name) - } - - if _, err := exec.LookPath(ecf.cmd); err != nil { - return errors.Wrap(err, "failed to locate command") - } - return nil -} - -func (ecf *ExternalCmdFilter) Accept(ctx context.Context, in chan interface{}, out pipeline.OutputChannel) { - if pdebug.Enabled { - g := pdebug.Marker("ExternalCmdFilter.Accept") - defer g.End() - } - defer out.SendEndMark("end of ExternalCmdFilter") - - buf := make([]Line, 0, ecf.thresholdBufsiz) - for { - select { - case <-ctx.Done(): - if pdebug.Enabled { - pdebug.Printf("ExternalCmdFilter received done") - } - return - case v := <-in: - switch v.(type) { - case error: - if pipeline.IsEndMark(v.(error)) { - if pdebug.Enabled { - pdebug.Printf("ExternalCmdFilter received end mark") - } - if len(buf) > 0 { - ecf.launchExternalCmd(ctx, buf, out) - } - } - return - case Line: - if pdebug.Enabled { - pdebug.Printf("ExternalCmdFilter received new line") - } - buf = append(buf, v.(Line)) - if len(buf) < ecf.thresholdBufsiz { - continue - } - - ecf.launchExternalCmd(ctx, buf, out) - buf = buf[0:0] - } - } - } -} - -func (ecf *ExternalCmdFilter) SetQuery(q string) { - ecf.query = q -} - -func (ecf ExternalCmdFilter) String() string { - return ecf.name -} - -func (ecf *ExternalCmdFilter) launchExternalCmd(ctx context.Context, buf []Line, out pipeline.OutputChannel) { - defer func() { recover() }() // ignore errors - if pdebug.Enabled { - g := pdebug.Marker("ExternalCmdFilter.launchExternalCmd") - defer g.End() - } - - args := append([]string(nil), ecf.args...) - for i, v := range args { - if v == "$QUERY" { - args[i] = ecf.query - } - } - cmd := exec.Command(ecf.cmd, args...) - if pdebug.Enabled { - pdebug.Printf("Executing command %s %v", cmd.Path, cmd.Args) - } - - inbuf := &bytes.Buffer{} - for _, l := range buf { - inbuf.WriteString(l.DisplayString() + "\n") - } - - cmd.Stdin = inbuf - r, err := cmd.StdoutPipe() - if err != nil { - return - } - - err = cmd.Start() - if err != nil { - return - } - - go cmd.Wait() - - cmdCh := make(chan Line) - go func(cmdCh chan Line, rdr *bufio.Reader) { - defer func() { recover() }() - defer close(cmdCh) - for { - b, _, err := rdr.ReadLine() - if len(b) > 0 { - // TODO: need to redo the spec for custom matchers - // This is the ONLY location where we need to actually - // RECREATE a RawLine, and thus the only place where - // ctx.enableSep is required. - cmdCh <- NewMatchedLine(NewRawLine(ecf.idgen.next(), string(b), ecf.enableSep), nil) - } - if err != nil { - break - } - } - }(cmdCh, bufio.NewReader(r)) - - defer func() { - if p := cmd.Process; p != nil { - p.Kill() - } - }() - - for { - select { - case <-ctx.Done(): - return - case l, ok := <-cmdCh: - if l == nil || !ok { - return - } - out.Send(l) - } - } -} diff --git a/filter/external.go b/filter/external.go new file mode 100644 index 0000000..fef0681 --- /dev/null +++ b/filter/external.go @@ -0,0 +1,170 @@ +package filter + +import ( + "bufio" + "bytes" + "context" + "os/exec" + + pdebug "github.com/lestrrat/go-pdebug" + "github.com/peco/peco/line" + "github.com/peco/peco/pipeline" + "github.com/pkg/errors" +) + +func NewExternalCmd(name string, cmd string, args []string, threshold int, idgen line.IDGenerator, enableSep bool) *ExternalCmd { + if len(args) == 0 { + args = []string{"$QUERY"} + } + + if threshold <= 0 { + threshold = DefaultCustomFilterBufferThreshold + } + + return &ExternalCmd{ + args: args, + cmd: cmd, + enableSep: enableSep, + idgen: idgen, + name: name, + outCh: pipeline.OutputChannel(make(chan interface{})), + thresholdBufsiz: threshold, + } +} + +func (ecf *ExternalCmd) Verify() error { + if ecf.cmd == "" { + return errors.Errorf("no executable specified for custom matcher '%s'", ecf.name) + } + + if _, err := exec.LookPath(ecf.cmd); err != nil { + return errors.Wrap(err, "failed to locate command") + } + return nil +} + +func (ecf *ExternalCmd) Apply(ctx context.Context, l line.Line) (line.Line, error) { + return nil, nil +} + +func (ecf *ExternalCmd) Accept(ctx context.Context, in chan interface{}, out pipeline.OutputChannel) { + if pdebug.Enabled { + g := pdebug.Marker("ExternalCmd.Accept") + defer g.End() + } + defer out.SendEndMark("end of ExternalCmd") + + buf := make([]line.Line, 0, ecf.thresholdBufsiz) + for { + select { + case <-ctx.Done(): + if pdebug.Enabled { + pdebug.Printf("ExternalCmd received done") + } + return + case v := <-in: + switch v.(type) { + case error: + if pipeline.IsEndMark(v.(error)) { + if pdebug.Enabled { + pdebug.Printf("ExternalCmd received end mark") + } + if len(buf) > 0 { + ecf.launchExternalCmd(ctx, buf, out) + } + } + return + case line.Line: + if pdebug.Enabled { + pdebug.Printf("ExternalCmd received new line") + } + buf = append(buf, v.(line.Line)) + if len(buf) < ecf.thresholdBufsiz { + continue + } + + ecf.launchExternalCmd(ctx, buf, out) + buf = buf[0:0] + } + } + } +} + +func (ecf ExternalCmd) String() string { + return ecf.name +} + +func (ecf *ExternalCmd) launchExternalCmd(ctx context.Context, buf []line.Line, out pipeline.OutputChannel) { + defer func() { recover() }() // ignore errors + if pdebug.Enabled { + g := pdebug.Marker("ExternalCmd.launchExternalCmd") + defer g.End() + } + + query := ctx.Value(queryKey).(string) + args := append([]string(nil), ecf.args...) + for i, v := range args { + if v == "$QUERY" { + args[i] = query + } + } + cmd := exec.Command(ecf.cmd, args...) + if pdebug.Enabled { + pdebug.Printf("Executing command %s %v", cmd.Path, cmd.Args) + } + + inbuf := &bytes.Buffer{} + for _, l := range buf { + inbuf.WriteString(l.DisplayString() + "\n") + } + + cmd.Stdin = inbuf + r, err := cmd.StdoutPipe() + if err != nil { + return + } + + err = cmd.Start() + if err != nil { + return + } + + go cmd.Wait() + + cmdCh := make(chan line.Line) + go func(cmdCh chan line.Line, rdr *bufio.Reader) { + defer func() { recover() }() + defer close(cmdCh) + for { + b, _, err := rdr.ReadLine() + if len(b) > 0 { + // TODO: need to redo the spec for custom matchers + // This is the ONLY location where we need to actually + // RECREATE a Raw, and thus the only place where + // ctx.enableSep is required. + cmdCh <- line.NewMatched(line.NewRaw(ecf.idgen.Next(), string(b), ecf.enableSep), nil) + } + if err != nil { + break + } + } + }(cmdCh, bufio.NewReader(r)) + + defer func() { + if p := cmd.Process; p != nil { + p.Kill() + } + }() + + for { + select { + case <-ctx.Done(): + return + case l, ok := <-cmdCh: + if l == nil || !ok { + return + } + out.Send(l) + } + } +} diff --git a/filter/filter.go b/filter/filter.go new file mode 100644 index 0000000..8ce82ca --- /dev/null +++ b/filter/filter.go @@ -0,0 +1,52 @@ +package filter + +// sort related stuff +type byMatchStart [][]int + +func (m byMatchStart) Len() int { + return len(m) +} + +func (m byMatchStart) Swap(i, j int) { + m[i], m[j] = m[j], m[i] +} + +func (m byMatchStart) Less(i, j int) bool { + if m[i][0] < m[j][0] { + return true + } + + if m[i][0] == m[j][0] { + return m[i][1]-m[i][0] < m[j][1]-m[j][0] + } + + return false +} +func matchContains(a []int, b []int) bool { + return a[0] <= b[0] && a[1] >= b[1] +} + +func matchOverlaps(a []int, b []int) bool { + return a[0] <= b[0] && a[1] >= b[0] || + a[0] <= b[1] && a[1] >= b[1] +} + +func mergeMatches(a []int, b []int) []int { + ret := make([]int, 2) + + // Note: In practice this should never happen + // because we're sorting by N[0] before calling + // this routine, but for completeness' sake... + if a[0] < b[0] { + ret[0] = a[0] + } else { + ret[0] = b[0] + } + + if a[1] < b[1] { + ret[1] = b[1] + } else { + ret[1] = a[1] + } + return ret +} diff --git a/filter_test.go b/filter/filter_test.go similarity index 76% rename from filter_test.go rename to filter/filter_test.go index e5f566c..303f821 100644 --- a/filter_test.go +++ b/filter/filter_test.go @@ -1,14 +1,22 @@ -package peco +package filter import ( + "context" "fmt" "testing" + "github.com/peco/peco/line" "github.com/stretchr/testify/assert" ) -// TestFuzzyFilter tests a fuzzy filter against various inputs -func TestFuzzyFilter(t *testing.T) { +type indexer interface { + Indices() [][]int +} + +// TestFuzzy tests a fuzzy filter against various inputs +func TestFuzzy(t *testing.T) { + ctx := context.Background() + testValues := []struct { input string query string @@ -26,12 +34,12 @@ func TestFuzzyFilter(t *testing.T) { {"パソコンは遅いですネ", "ソネ", true}, // katakana {"🚴🏻 abcd efgh", "🚴🏻e", true}, // unicode } - filter := NewFuzzyFilter() + filter := NewFuzzy() for i, v := range testValues { t.Run(fmt.Sprintf(`"%s" against "%s", expect "%t"`, v.input, v.query, v.selected), func(t *testing.T) { - filter.SetQuery(v.query) - l := NewRawLine(uint64(i), v.input, false) - res, err := filter.filter(l) + ctx = NewContext(ctx, v.query) + l := line.NewRaw(uint64(i), v.input, false) + res, err := filter.Apply(ctx, l) if !v.selected { if !assert.Error(t, err, "filter should fail") { @@ -50,10 +58,11 @@ func TestFuzzyFilter(t *testing.T) { if !assert.NotNil(t, res, "return value should NOT be nil") { return } - if !assert.Implements(t, (*MatchIndexer)(nil), res, "can call Indices()") { + + if !assert.Implements(t, (*indexer)(nil), res, "can call Indices()") { return } - t.Logf("%#v", res.(MatchIndexer).Indices()) + t.Logf("%#v", res.(indexer).Indices()) }) } } diff --git a/filter/fuzzy.go b/filter/fuzzy.go new file mode 100644 index 0000000..cba2ab6 --- /dev/null +++ b/filter/fuzzy.go @@ -0,0 +1,57 @@ +package filter + +import ( + "context" + "errors" + "strings" + "unicode/utf8" + + "github.com/peco/peco/internal/util" + "github.com/peco/peco/line" +) + +// NewFuzzy builds a fuzzy-finder type of filter. +// In effect, this uses a smart case filter, and for q query +// like "ABC" it matches the equivalent of "A(.*)B(.*)C(.*)" +func NewFuzzy() *Fuzzy { + return &Fuzzy{} +} + +func (ff Fuzzy) String() string { + return "Fuzzy" +} + +func (ff *Fuzzy) Apply(ctx context.Context, l line.Line) (line.Line, error) { + query := ctx.Value(queryKey).(string) + base := 0 + txt := l.DisplayString() + matches := [][]int{} + + hasUpper := util.ContainsUpper(query) + + for len(query) > 0 { + r, n := utf8.DecodeRuneInString(query) + if r == utf8.RuneError { + // "Silently" ignore (just return a no match) + return nil, errors.New("failed to decode input string") + } + query = query[n:] + + var i int + if hasUpper { // explicit match + i = strings.IndexRune(txt, r) + } else { + i = strings.IndexFunc(txt, util.CaseInsensitiveIndexFunc(r)) + } + if i == -1 { + return nil, errors.New("filter did not match against given line") + } + + // otherwise we have a match, but the next match must match against + // something AFTER the current match + txt = txt[i+n:] + matches = append(matches, []int{base + i, base + i + n}) + base = base + i + n + } + return line.NewMatched(l, matches), nil +} diff --git a/filter/interface.go b/filter/interface.go new file mode 100644 index 0000000..576f46b --- /dev/null +++ b/filter/interface.go @@ -0,0 +1,75 @@ +package filter + +import ( + "context" + "errors" + "regexp" + "sync" + "time" + + "github.com/peco/peco/line" + "github.com/peco/peco/pipeline" +) + +var ErrFilterNotFound = errors.New("specified filter was not found") + +var ignoreCaseFlags = regexpFlagList([]string{"i"}) +var defaultFlags = regexpFlagList{} +var queryKey = struct{}{} + +// DefaultCustomFilterBufferThreshold is the default value +// for BufferThreshold setting on CustomFilters. +const DefaultCustomFilterBufferThreshold = 100 + +type Set struct { + current int + filters []Filter + mutex sync.Mutex +} + +// internal stuff +type regexpFlags interface { + flags(string) []string +} +type regexpFlagList []string + +type regexpFlagFunc func(string) []string + +type regexpQueryFactory struct { + compiled map[string]regexpQuery + mutex sync.Mutex + threshold time.Duration +} + +type regexpQuery struct { + rx []*regexp.Regexp + lastUsed time.Time +} + +type Fuzzy struct { +} + +type Regexp struct { + factory *regexpQueryFactory + flags regexpFlags + quotemeta bool + mutex sync.Mutex + name string + onEnd func() + outCh pipeline.OutputChannel +} + +type ExternalCmd struct { + args []string + cmd string + enableSep bool + idgen line.IDGenerator + outCh pipeline.OutputChannel + name string + thresholdBufsiz int +} + +type Filter interface { + Apply(context.Context, line.Line) (line.Line, error) + String() string +} diff --git a/filter/regexp.go b/filter/regexp.go new file mode 100644 index 0000000..ae9e3d2 --- /dev/null +++ b/filter/regexp.go @@ -0,0 +1,192 @@ +package filter + +import ( + "context" + "fmt" + "regexp" + "sort" + "strings" + "time" + + "github.com/peco/peco/internal/util" + "github.com/peco/peco/line" + "github.com/peco/peco/pipeline" + "github.com/pkg/errors" +) + +func (r regexpFlagList) flags(_ string) []string { + return []string(r) +} + +func (r regexpFlagFunc) flags(s string) []string { + return r(s) +} + +func regexpFor(q string, flags []string, quotemeta bool) (*regexp.Regexp, error) { + reTxt := q + if quotemeta { + reTxt = regexp.QuoteMeta(q) + } + + if flags != nil && len(flags) > 0 { + reTxt = fmt.Sprintf("(?%s)%s", strings.Join(flags, ""), reTxt) + } + + re, err := regexp.Compile(reTxt) + if err != nil { + return nil, errors.Wrapf(err, "failed to compile regular expression '%s'", reTxt) + } + return re, nil +} + +func queryToRegexps(query string, flags regexpFlags, quotemeta bool) ([]*regexp.Regexp, error) { + queries := strings.Split(strings.TrimSpace(query), " ") + regexps := make([]*regexp.Regexp, 0) + + for _, q := range queries { + re, err := regexpFor(q, flags.flags(query), quotemeta) + if err != nil { + return nil, errors.Wrapf(err, "failed to compile regular expression '%s'", q) + } + regexps = append(regexps, re) + } + + return regexps, nil +} + +// NewContext initializes the context so that it is suitable +// to be passed to `Run()` +func NewContext(ctx context.Context, query string) context.Context { + return context.WithValue(ctx, queryKey, query) +} + +// NewRegexp creates a new regexp based filter +func NewRegexp() *Regexp { + return &Regexp{ + factory: ®expQueryFactory{ + compiled: make(map[string]regexpQuery), + threshold: time.Minute, + }, + flags: regexpFlagList(defaultFlags), + quotemeta: false, + name: "Regexp", + outCh: pipeline.OutputChannel(make(chan interface{})), + } +} + +func (rf *Regexp) OutCh() <-chan interface{} { + rf.mutex.Lock() + defer rf.mutex.Unlock() + return rf.outCh +} + +func (f *regexpQueryFactory) Compile(s string, flags regexpFlags, quotemeta bool) ([]*regexp.Regexp, error) { + f.mutex.Lock() + defer f.mutex.Unlock() + + rq, ok := f.compiled[s] + if ok { + if time.Since(rq.lastUsed) < f.threshold { + return rq.rx, nil + } + delete(f.compiled, s) + } + + rxs, err := queryToRegexps(s, flags, quotemeta) + if err != nil { + return nil, errors.Wrap(err, `failed to compile regular expression`) + } + + rq.lastUsed = time.Now() + rq.rx = rxs + f.compiled[s] = rq + return rxs, nil +} + +func (rf *Regexp) Apply(ctx context.Context, l line.Line) (line.Line, error) { + query := ctx.Value(queryKey).(string) + regexps, err := rf.factory.Compile(query, rf.flags, rf.quotemeta) + if err != nil { + return nil, errors.Wrap(err, "failed to compile queries as regular expression") + } + v := l.DisplayString() + allMatched := true + matches := [][]int{} +TryRegexps: + for _, rx := range regexps { + match := rx.FindAllStringSubmatchIndex(v, -1) + if match == nil { + allMatched = false + break TryRegexps + } + matches = append(matches, match...) + } + + if !allMatched { + return nil, errors.New("filter did not match against given line") + } + + sort.Sort(byMatchStart(matches)) + + // We need to "dedupe" the results. For example, if we matched the + // same region twice, we don't want that to be drawn + + deduped := make([][]int, 0, len(matches)) + + for i, m := range matches { + // Always push the first one + if i == 0 { + deduped = append(deduped, m) + continue + } + + prev := deduped[len(deduped)-1] + switch { + case matchContains(prev, m): + // If the previous match contains this one, then + // don't do anything + continue + case matchOverlaps(prev, m): + // If the previous match overlaps with this one, + // merge the results and make it a bigger one + deduped[len(deduped)-1] = mergeMatches(prev, m) + default: + deduped = append(deduped, m) + } + } + return line.NewMatched(l, deduped), nil +} + +func (rf Regexp) String() string { + return rf.name +} + +func NewIgnoreCase() *Regexp { + rf := NewRegexp() + rf.flags = ignoreCaseFlags + rf.quotemeta = true + rf.name = "IgnoreCase" + return rf +} + +func NewCaseSensitive() *Regexp { + rf := NewRegexp() + rf.quotemeta = true + rf.name = "CaseSensitive" + return rf +} + +// SmartCase turns ON the ignore-case flag in the regexp +// if the query contains a upper-case character +func NewSmartCase() *Regexp { + rf := NewRegexp() + rf.quotemeta = true + rf.name = "SmartCase" + rf.flags = regexpFlagFunc(func(q string) []string { + if util.ContainsUpper(q) { + return defaultFlags + } + return []string{"i"} + }) + return rf +} diff --git a/filter/set.go b/filter/set.go new file mode 100644 index 0000000..0628019 --- /dev/null +++ b/filter/set.go @@ -0,0 +1,60 @@ +package filter + +import ( + pdebug "github.com/lestrrat/go-pdebug" +) + +func (fs *Set) Reset() { + fs.mutex.Lock() + defer fs.mutex.Unlock() + fs.current = 0 +} + +func (fs *Set) Size() int { + fs.mutex.Lock() + defer fs.mutex.Unlock() + return len(fs.filters) +} + +func (fs *Set) Add(lf Filter) error { + fs.mutex.Lock() + defer fs.mutex.Unlock() + fs.filters = append(fs.filters, lf) + return nil +} + +func (fs *Set) Rotate() { + fs.mutex.Lock() + defer fs.mutex.Unlock() + fs.current++ + if fs.current >= len(fs.filters) { + fs.current = 0 + } + if pdebug.Enabled { + pdebug.Printf("Set.Rotate: now filter in effect is %s", fs.filters[fs.current]) + } +} + +func (fs *Set) SetCurrentByName(name string) error { + fs.mutex.Lock() + defer fs.mutex.Unlock() + for i, f := range fs.filters { + if f.String() == name { + fs.current = i + return nil + } + } + return ErrFilterNotFound +} + +func (fs *Set) Index() int { + fs.mutex.Lock() + defer fs.mutex.Unlock() + return fs.current +} + +func (fs *Set) Current() Filter { + fs.mutex.Lock() + defer fs.mutex.Unlock() + return fs.filters[fs.current] +} diff --git a/filterutil.go b/filterutil.go deleted file mode 100644 index 8082d81..0000000 --- a/filterutil.go +++ /dev/null @@ -1,103 +0,0 @@ -package peco - -import ( - "fmt" - "regexp" - "strings" - - "github.com/pkg/errors" -) - -var ignoreCaseFlags = regexpFlagList([]string{"i"}) -var defaultFlags = regexpFlagList{} - -func (r regexpFlagList) flags(_ string) []string { - return []string(r) -} - -func (r regexpFlagFunc) flags(s string) []string { - return r(s) -} - -func regexpFor(q string, flags []string, quotemeta bool) (*regexp.Regexp, error) { - reTxt := q - if quotemeta { - reTxt = regexp.QuoteMeta(q) - } - - if flags != nil && len(flags) > 0 { - reTxt = fmt.Sprintf("(?%s)%s", strings.Join(flags, ""), reTxt) - } - - re, err := regexp.Compile(reTxt) - if err != nil { - return nil, errors.Wrapf(err, "failed to compile regular expression '%s'", reTxt) - } - return re, nil -} - -func queryToRegexps(flags regexpFlags, quotemeta bool, query string) ([]*regexp.Regexp, error) { - queries := strings.Split(strings.TrimSpace(query), " ") - regexps := make([]*regexp.Regexp, 0) - - for _, q := range queries { - re, err := regexpFor(q, flags.flags(query), quotemeta) - if err != nil { - return nil, errors.Wrapf(err, "failed to compile regular expression '%s'", q) - } - regexps = append(regexps, re) - } - - return regexps, nil -} - -// sort related stuff -type byMatchStart [][]int - -func (m byMatchStart) Len() int { - return len(m) -} - -func (m byMatchStart) Swap(i, j int) { - m[i], m[j] = m[j], m[i] -} - -func (m byMatchStart) Less(i, j int) bool { - if m[i][0] < m[j][0] { - return true - } - - if m[i][0] == m[j][0] { - return m[i][1]-m[i][0] < m[j][1]-m[j][0] - } - - return false -} -func matchContains(a []int, b []int) bool { - return a[0] <= b[0] && a[1] >= b[1] -} - -func matchOverlaps(a []int, b []int) bool { - return a[0] <= b[0] && a[1] >= b[0] || - a[0] <= b[1] && a[1] >= b[1] -} - -func mergeMatches(a []int, b []int) []int { - ret := make([]int, 2) - - // Note: In practice this should never happen - // because we're sorting by N[0] before calling - // this routine, but for completeness' sake... - if a[0] < b[0] { - ret[0] = a[0] - } else { - ret[0] = b[0] - } - - if a[1] < b[1] { - ret[1] = b[1] - } else { - ret[1] = a[1] - } - return ret -} diff --git a/glide.yaml b/glide.yaml index e30eb4b..1d0ac99 100644 --- a/glide.yaml +++ b/glide.yaml @@ -5,9 +5,6 @@ import: - package: github.com/mattn/go-runewidth - package: github.com/nsf/termbox-go - package: github.com/pkg/errors -- package: golang.org/x/net - subpackages: - - context - package: github.com/stretchr/testify subpackages: - assert diff --git a/input.go b/input.go index c221626..c5bb227 100644 --- a/input.go +++ b/input.go @@ -4,7 +4,7 @@ import ( "time" "github.com/nsf/termbox-go" - "golang.org/x/net/context" + "context" ) func NewInput(state *Peco, am ActionMap, src chan termbox.Event) *Input { diff --git a/interface.go b/interface.go index 7a28f4b..dab696b 100644 --- a/interface.go +++ b/interface.go @@ -2,16 +2,17 @@ package peco import ( "io" - "regexp" "sync" "time" - "golang.org/x/net/context" + "context" "github.com/google/btree" "github.com/nsf/termbox-go" + "github.com/peco/peco/filter" "github.com/peco/peco/hub" "github.com/peco/peco/internal/keyseq" + "github.com/peco/peco/line" "github.com/peco/peco/pipeline" ) @@ -46,12 +47,6 @@ const ( RegexpMatch = "Regexp" ) -// lineIDGenerator defines an interface for things that generate -// unique IDs for lines used within peco. -type lineIDGenerator interface { - next() uint64 -} - type idgen struct { ch chan uint64 } @@ -72,7 +67,7 @@ type Peco struct { config Config currentLineBuffer Buffer enableSep bool // Enable parsing on separators - filters FilterSet + filters filter.Set idgen *idgen initialFilter string initialQuery string // populated if --query is specified @@ -87,7 +82,7 @@ type Peco struct { queryExecMutex sync.Mutex queryExecTimer *time.Timer readyCh chan struct{} - resultCh chan Line + resultCh chan line.Line screen Screen selection *Selection selectionRangeStart RangeStart @@ -109,33 +104,6 @@ type Peco struct { err error } -// Line represents each of the line that peco uses to display -// and match against queries. -type Line interface { - btree.Item - - ID() uint64 - - // Buffer returns the raw buffer - Buffer() string - - // DisplayString returns the string to be displayed. This means if you have - // a null separator, the contents after the separator are not included - // in this string - DisplayString() string - - // Output returns the string to be display as peco finishes up doing its - // thing. This means if you have null separator, the contents before the - // separator are not included in this string - Output() string - - // IsDirty returns true if this line should be forcefully redrawn - IsDirty() bool - - // SetDirty sets the dirty flag on or off - SetDirty(bool) -} - type MatchIndexer interface { // Indices return the matched portion(s) of a string after filtering. // Note that while Indices may return nil, that just means that there are @@ -143,21 +111,6 @@ type MatchIndexer interface { Indices() [][]int } -// RawLine is the input line as sent to peco, before filtering and what not. -type RawLine struct { - id uint64 - buf string - sepLoc int - displayString string - dirty bool -} - -// MatchedLine contains the indices to the matches -type MatchedLine struct { - Line - indices [][]int -} - type Keyseq interface { Add(keyseq.KeyList, interface{}) AcceptKey(keyseq.Key) (interface{}, error) @@ -268,7 +221,7 @@ type StatusBar struct { type ListArea struct { *AnchorSettings sortTopDown bool - displayCache []Line + displayCache []line.Line dirty bool styles *StyleSet } @@ -291,14 +244,6 @@ type Keymap struct { state *Peco } -// internal stuff -type regexpFlags interface { - flags(string) []string -} -type regexpFlagList []string - -type regexpFlagFunc func(string) []string - // Filter is responsible for the actual "grep" part of peco type Filter struct { state *Peco @@ -422,12 +367,6 @@ type Query struct { type FilterQuery Query -type FilterSet struct { - current int - filters []LineFilter - mutex sync.Mutex -} - // Source implements pipeline.Source, and is the buffer for the input type Source struct { pipeline.OutputChannel @@ -435,9 +374,9 @@ type Source struct { done chan struct{} capacity int enableSep bool - idgen lineIDGenerator + idgen line.IDGenerator in io.Reader - lines []Line + lines []line.Line mutex sync.RWMutex ready chan struct{} setupDone chan struct{} @@ -484,14 +423,14 @@ type RangeStart struct { // Buffer interface is used for containers for lines to be // processed by peco. type Buffer interface { - LineAt(int) (Line, error) + LineAt(int) (line.Line, error) Size() int } // MemoryBuffer is an implementation of Buffer type MemoryBuffer struct { done chan struct{} - lines []Line + lines []line.Line mutex sync.RWMutex PeriodicFunc func() } @@ -508,40 +447,6 @@ type Input struct { state *Peco } -type LineFilter interface { - pipeline.Acceptor - SetQuery(string) - Clone() LineFilter - String() string -} - -type FuzzyFilter struct { - mutex sync.Mutex - query string -} - -type RegexpFilter struct { - compiledQuery []*regexp.Regexp - flags regexpFlags - quotemeta bool - query string - mutex sync.Mutex - name string - onEnd func() - outCh pipeline.OutputChannel -} - -type ExternalCmdFilter struct { - args []string - cmd string - enableSep bool - idgen lineIDGenerator - outCh pipeline.OutputChannel - name string - query string - thresholdBufsiz int -} - // MessageHub is the interface that must be satisfied by the // message hub component. Unless we're in testing, github.com/peco/peco/hub.Hub // is used. @@ -558,3 +463,8 @@ type MessageHub interface { SendStatusMsgAndClear(string, time.Duration) StatusMsgCh() chan hub.Payload } + +type filterProcessor struct { + filter filter.Filter + query string +} diff --git a/internal/buffer/line.go b/internal/buffer/line.go new file mode 100644 index 0000000..e9170c1 --- /dev/null +++ b/internal/buffer/line.go @@ -0,0 +1,28 @@ +package buffer + +import ( + "sync" + + "github.com/peco/peco/line" +) + +const filterBufSize = 1000 + +var lineListPool = sync.Pool{ + New: func() interface{} { + return make([]line.Line, 0, filterBufSize) + }, +} + +func ReleaseLineListBuf(l []line.Line) { + if l == nil { + return + } + l = l[0:0] + lineListPool.Put(l) +} + +func GetLineListBuf() []line.Line { + l := lineListPool.Get().([]line.Line) + return l +} diff --git a/issues_test.go b/issues_test.go index 5b45c61..8b858d0 100644 --- a/issues_test.go +++ b/issues_test.go @@ -9,7 +9,7 @@ import ( termbox "github.com/nsf/termbox-go" "github.com/stretchr/testify/assert" - "golang.org/x/net/context" + "context" ) func TestIssue212_SanityCheck(t *testing.T) { diff --git a/keymap.go b/keymap.go index b159cb6..2f9d783 100644 --- a/keymap.go +++ b/keymap.go @@ -9,7 +9,7 @@ import ( "github.com/nsf/termbox-go" "github.com/peco/peco/internal/keyseq" "github.com/pkg/errors" - "golang.org/x/net/context" + "context" ) // NewKeymap creates a new Keymap struct diff --git a/layout.go b/layout.go index de5896a..525a7a4 100644 --- a/layout.go +++ b/layout.go @@ -9,6 +9,7 @@ import ( "github.com/lestrrat/go-pdebug" "github.com/mattn/go-runewidth" "github.com/nsf/termbox-go" + "github.com/peco/peco/line" "github.com/pkg/errors" ) @@ -280,7 +281,7 @@ func (s *StatusBar) PrintStatus(msg string, clearDelay time.Duration) { func NewListArea(screen Screen, anchor VerticalAnchor, anchorOffset int, sortTopDown bool, styles *StyleSet) *ListArea { return &ListArea{ AnchorSettings: NewAnchorSettings(screen, anchor, anchorOffset), - displayCache: []Line{}, + displayCache: []line.Line{}, dirty: false, sortTopDown: sortTopDown, styles: styles, @@ -288,7 +289,7 @@ func NewListArea(screen Screen, anchor VerticalAnchor, anchorOffset int, sortTop } func (l *ListArea) purgeDisplayCache() { - l.displayCache = []Line{} + l.displayCache = []line.Line{} } func (l *ListArea) IsDirty() bool { @@ -310,6 +311,7 @@ type DrawOptions struct { RunningQuery bool DisableCache bool } + // Draw displays the ListArea on the screen func (l *ListArea) Draw(state *Peco, parent Layout, perPage int, options *DrawOptions) { if pdebug.Enabled { @@ -364,7 +366,7 @@ func (l *ListArea) Draw(state *Peco, parent Layout, perPage int, options *DrawOp // previously drawn lines are cached. first, truncate the cache // to current size of the drawable area if ldc := int(len(l.displayCache)); ldc != perPage { - newCache := make([]Line, perPage) + newCache := make([]line.Line, perPage) copy(newCache, l.displayCache) l.displayCache = newCache } else if perPage > bufsiz { diff --git a/line/interface.go b/line/interface.go new file mode 100644 index 0000000..cc7d42f --- /dev/null +++ b/line/interface.go @@ -0,0 +1,53 @@ +package line + +import "github.com/google/btree" + +// IDGenerator defines an interface for things that generate +// unique IDs for lines used within peco. +type IDGenerator interface { + Next() uint64 +} + +// Line represents each of the line that peco uses to display +// and match against queries. +type Line interface { + btree.Item + + ID() uint64 + + // Buffer returns the raw buffer + Buffer() string + + // DisplayString returns the string to be displayed. This means if you have + // a null separator, the contents after the separator are not included + // in this string + DisplayString() string + + // Output returns the string to be display as peco finishes up doing its + // thing. This means if you have null separator, the contents before the + // separator are not included in this string + Output() string + + // IsDirty returns true if this line should be forcefully redrawn + IsDirty() bool + + // SetDirty sets the dirty flag on or off + SetDirty(bool) +} + +// Raw is the input line as sent to peco, before filtering and what not. +type Raw struct { + id uint64 + buf string + sepLoc int + displayString string + dirty bool +} + +// Matched contains the indices to the matches +type Matched struct { + Line + indices [][]int +} + + diff --git a/line/matched.go b/line/matched.go new file mode 100644 index 0000000..6bcd747 --- /dev/null +++ b/line/matched.go @@ -0,0 +1,12 @@ +package line + +// NewMatched creates a new Matched +func NewMatched(rl Line, matches [][]int) *Matched { + return &Matched{rl, matches} +} + +// Indices returns the indices in the buffer that matched +func (ml Matched) Indices() [][]int { + return ml.indices +} + diff --git a/line.go b/line/raw.go similarity index 64% rename from line.go rename to line/raw.go index 9b94d09..65c5538 100644 --- a/line.go +++ b/line/raw.go @@ -1,4 +1,4 @@ -package peco +package line import ( "strings" @@ -7,12 +7,12 @@ import ( "github.com/peco/peco/internal/util" ) -// NewRawLine creates a new RawLine. The `enableSep` flag tells +// NewRaw creates a new Raw. The `enableSep` flag tells // it if we should search for a null character to split the // string to display and the string to emit upon selection of // of said line -func NewRawLine(id uint64, v string, enableSep bool) *RawLine { - rl := &RawLine{ +func NewRaw(id uint64, v string, enableSep bool) *Raw { + rl := &Raw{ id: id, buf: v, sepLoc: -1, @@ -31,32 +31,32 @@ func NewRawLine(id uint64, v string, enableSep bool) *RawLine { } // Less implements the btree.Item interface -func (rl *RawLine) Less(b btree.Item) bool { +func (rl *Raw) Less(b btree.Item) bool { return rl.id < b.(Line).ID() } // ID returns the unique ID of this line -func (rl *RawLine) ID() uint64 { +func (rl *Raw) ID() uint64 { return rl.id } // IsDirty returns true if this line must be redrawn on the terminal -func (rl RawLine) IsDirty() bool { +func (rl Raw) IsDirty() bool { return rl.dirty } // SetDirty sets the dirty flag -func (rl *RawLine) SetDirty(b bool) { +func (rl *Raw) SetDirty(b bool) { rl.dirty = b } // Buffer returns the raw buffer. May contain null -func (rl RawLine) Buffer() string { +func (rl Raw) Buffer() string { return rl.buf } // DisplayString returns the string to be displayed -func (rl RawLine) DisplayString() string { +func (rl Raw) DisplayString() string { if rl.displayString != "" { return rl.displayString } @@ -70,19 +70,10 @@ func (rl RawLine) DisplayString() string { } // Output returns the string to be displayed *after peco is done -func (rl RawLine) Output() string { +func (rl Raw) Output() string { if i := rl.sepLoc; i > -1 { return rl.buf[i+1:] } return rl.buf } -// NewMatchedLine creates a new MatchedLine -func NewMatchedLine(rl Line, matches [][]int) *MatchedLine { - return &MatchedLine{rl, matches} -} - -// Indices returns the indices in the buffer that matched -func (ml MatchedLine) Indices() [][]int { - return ml.indices -} diff --git a/peco.go b/peco.go index 1c7b874..57aab3d 100644 --- a/peco.go +++ b/peco.go @@ -9,12 +9,14 @@ import ( "time" "unicode/utf8" - "golang.org/x/net/context" + "context" "github.com/google/btree" "github.com/lestrrat/go-pdebug" + "github.com/peco/peco/filter" "github.com/peco/peco/hub" "github.com/peco/peco/internal/util" + "github.com/peco/peco/line" "github.com/peco/peco/pipeline" "github.com/peco/peco/sig" "github.com/pkg/errors" @@ -78,7 +80,7 @@ func (ig *idgen) Run(ctx context.Context) { } } -func (ig *idgen) next() uint64 { +func (ig *idgen) Next() uint64 { return <-ig.ch } @@ -127,13 +129,13 @@ func (p *Peco) Location() *Location { return &p.location } -func (p *Peco) ResultCh() chan Line { +func (p *Peco) ResultCh() chan line.Line { p.mutex.Lock() defer p.mutex.Unlock() return p.resultCh } -func (p *Peco) SetResultCh(ch chan Line) { +func (p *Peco) SetResultCh(ch chan line.Line) { p.mutex.Lock() defer p.mutex.Unlock() p.resultCh = ch @@ -194,7 +196,7 @@ func (p *Peco) Source() pipeline.Source { return p.source } -func (p *Peco) Filters() *FilterSet { +func (p *Peco) Filters() *filter.Set { return &p.filters } @@ -347,7 +349,7 @@ func (p *Peco) Run(ctx context.Context) (err error) { // printing that one line as the result if b := p.CurrentLineBuffer(); b.Size() == 1 { if l, err := b.LineAt(0); err == nil { - p.resultCh = make(chan Line) + p.resultCh = make(chan line.Line) p.Exit(nil) p.resultCh <- l close(p.resultCh) @@ -562,14 +564,14 @@ func (p *Peco) populateSingleKeyJump() error { } func (p *Peco) populateFilters() error { - p.filters.Add(NewIgnoreCaseFilter()) - p.filters.Add(NewCaseSensitiveFilter()) - p.filters.Add(NewSmartCaseFilter()) - p.filters.Add(NewRegexpFilter()) - p.filters.Add(NewFuzzyFilter()) + p.filters.Add(filter.NewIgnoreCase()) + p.filters.Add(filter.NewCaseSensitive()) + p.filters.Add(filter.NewSmartCase()) + p.filters.Add(filter.NewRegexp()) + p.filters.Add(filter.NewFuzzy()) for name, c := range p.config.CustomFilter { - f := NewExternalCmdFilter(name, c.Cmd, c.Args, c.BufferThreshold, p.idgen, p.enableSep) + f := filter.NewExternalCmd(name, c.Cmd, c.Args, c.BufferThreshold, p.idgen, p.enableSep) p.filters.Add(f) } @@ -689,11 +691,11 @@ func (p *Peco) PrintResults() { selection.Add(l) } } - p.SetResultCh(make(chan Line)) + p.SetResultCh(make(chan line.Line)) go func() { defer close(p.resultCh) p.selection.Ascend(func(it btree.Item) bool { - p.ResultCh() <- it.(Line) + p.ResultCh() <- it.(line.Line) return true }) }() diff --git a/peco_test.go b/peco_test.go index bd7f186..b6f7c75 100644 --- a/peco_test.go +++ b/peco_test.go @@ -10,11 +10,13 @@ import ( "testing" "time" + "context" + "github.com/nsf/termbox-go" "github.com/peco/peco/hub" "github.com/peco/peco/internal/util" + "github.com/peco/peco/line" "github.com/stretchr/testify/assert" - "golang.org/x/net/context" ) type nullHub struct{} @@ -135,9 +137,9 @@ func TestIDGen(t *testing.T) { defer cancel() go idgen.Run(ctx) - lines := []*RawLine{} + lines := []*line.Raw{} for i := 0; i < 1000000; i++ { - lines = append(lines, NewRawLine(idgen.next(), fmt.Sprintf("%d", i), false)) + lines = append(lines, line.NewRaw(idgen.Next(), fmt.Sprintf("%d", i), false)) } sel := NewSelection() diff --git a/pipeline/interface.go b/pipeline/interface.go index 30e49c4..1f1d603 100644 --- a/pipeline/interface.go +++ b/pipeline/interface.go @@ -3,7 +3,7 @@ package pipeline import ( "sync" - "golang.org/x/net/context" + "context" ) // EndMarker is an interface for things that tell us the input diff --git a/pipeline/pipeline.go b/pipeline/pipeline.go index e1151b3..6c5e7f4 100644 --- a/pipeline/pipeline.go +++ b/pipeline/pipeline.go @@ -6,7 +6,7 @@ import ( pdebug "github.com/lestrrat/go-pdebug" "github.com/pkg/errors" - "golang.org/x/net/context" + "context" ) // EndMark returns true diff --git a/pipeline/pipeline_test.go b/pipeline/pipeline_test.go index effded7..79b00a8 100644 --- a/pipeline/pipeline_test.go +++ b/pipeline/pipeline_test.go @@ -9,7 +9,7 @@ import ( "testing" "time" - "golang.org/x/net/context" + "context" ) type RegexpFilter struct { diff --git a/selection.go b/selection.go index 93f81d1..6f6f1ea 100644 --- a/selection.go +++ b/selection.go @@ -1,6 +1,9 @@ package peco -import "github.com/google/btree" +import ( + "github.com/google/btree" + "github.com/peco/peco/line" +) // NewSelection creates a new empty Selection func NewSelection() *Selection { @@ -11,14 +14,14 @@ func NewSelection() *Selection { // Add adds a new line to the selection. If the line already // exists in the selection, it is silently ignored -func (s *Selection) Add(l Line) { +func (s *Selection) Add(l line.Line) { s.mutex.Lock() defer s.mutex.Unlock() s.tree.ReplaceOrInsert(l) } // Remove removes the specified line from the selection -func (s *Selection) Remove(l Line) { +func (s *Selection) Remove(l line.Line) { s.mutex.Lock() defer s.mutex.Unlock() s.tree.Delete(l) @@ -30,7 +33,7 @@ func (s *Selection) Reset() { s.tree = btree.New(32) } -func (s *Selection) Has(x Line) bool { +func (s *Selection) Has(x line.Line) bool { s.mutex.Lock() defer s.mutex.Unlock() return s.tree.Has(x) @@ -47,4 +50,3 @@ func (s *Selection) Ascend(i btree.ItemIterator) { defer s.mutex.Unlock() s.tree.Ascend(i) } - diff --git a/selection_test.go b/selection_test.go index 0d75808..5a2feba 100644 --- a/selection_test.go +++ b/selection_test.go @@ -1,18 +1,22 @@ package peco -import "testing" +import ( + "testing" + + "github.com/peco/peco/line" +) func TestSelection(t *testing.T) { s := NewSelection() var i uint64 = 0 - alice := NewRawLine(i, "Alice", false) + alice := line.NewRaw(i, "Alice", false) i++ s.Add(alice) if s.Len() != 1 { t.Errorf("expected Len = 1, got %d", s.Len()) } - s.Add(NewRawLine(i, "Bob", false)) + s.Add(line.NewRaw(i, "Bob", false)) i++ if s.Len() != 2 { t.Errorf("expected Len = 2, got %d", s.Len()) diff --git a/sig/sig.go b/sig/sig.go index 457828e..f9ec272 100644 --- a/sig/sig.go +++ b/sig/sig.go @@ -5,7 +5,7 @@ import ( "os/signal" "syscall" - "golang.org/x/net/context" + "context" ) type SigReceivedHandler interface { diff --git a/source.go b/source.go index 237b665..da16dde 100644 --- a/source.go +++ b/source.go @@ -6,15 +6,17 @@ import ( "sync" "time" + "context" + "github.com/lestrrat/go-pdebug" "github.com/peco/peco/internal/util" + "github.com/peco/peco/line" "github.com/peco/peco/pipeline" - "golang.org/x/net/context" ) // Creates a new Source. Does not start processing the input until you // call Setup() -func NewSource(in io.Reader, idgen lineIDGenerator, capacity int, enableSep bool) *Source { +func NewSource(in io.Reader, idgen line.IDGenerator, capacity int, enableSep bool) *Source { s := &Source{ in: in, // Note that this may be closed, so do not rely on it capacity: capacity, @@ -114,7 +116,7 @@ func (s *Source) Setup(ctx context.Context, state *Peco) { } readCount++ - s.Append(NewRawLine(s.idgen.next(), l, s.enableSep)) + s.Append(line.NewRaw(s.idgen.Next(), l, s.enableSep)) notify.Do(notifycb) } } @@ -170,7 +172,7 @@ func (s *Source) SetupDone() <-chan struct{} { return s.setupDone } -func (s *Source) LineAt(n int) (Line, error) { +func (s *Source) LineAt(n int) (line.Line, error) { s.mutex.RLock() defer s.mutex.RUnlock() return bufferLineAt(s.lines, n) @@ -182,7 +184,7 @@ func (s *Source) Size() int { return bufferSize(s.lines) } -func (s *Source) Append(l Line) { +func (s *Source) Append(l line.Line) { s.mutex.Lock() defer s.mutex.Unlock() diff --git a/source_test.go b/source_test.go index 506e231..0e45b79 100644 --- a/source_test.go +++ b/source_test.go @@ -7,7 +7,7 @@ import ( "testing" "time" - "golang.org/x/net/context" + "context" "github.com/stretchr/testify/assert" ) diff --git a/view.go b/view.go index 2b27f85..74ae1d0 100644 --- a/view.go +++ b/view.go @@ -4,7 +4,7 @@ import ( "time" "github.com/peco/peco/hub" - "golang.org/x/net/context" + "context" ) type statusMsgReq interface {