Move stuff around for better code organization

This commit is contained in:
Daisuke Maki 2016-12-14 11:00:22 +09:00
parent b1ea3c36b1
commit 61b5d22fb8
34 changed files with 912 additions and 852 deletions

View file

@ -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

View file

@ -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++ {

View file

@ -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")
}

View file

@ -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,
}
}
}

640
filter.go
View file

@ -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)
}
}
}

170
filter/external.go Normal file
View file

@ -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)
}
}
}

52
filter/filter.go Normal file
View file

@ -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
}

View file

@ -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())
})
}
}

57
filter/fuzzy.go Normal file
View file

@ -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
}

75
filter/interface.go Normal file
View file

@ -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
}

192
filter/regexp.go Normal file
View file

@ -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: &regexpQueryFactory{
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
}

60
filter/set.go Normal file
View file

@ -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]
}

View file

@ -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
}

View file

@ -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

View file

@ -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 {

View file

@ -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
}

28
internal/buffer/line.go Normal file
View file

@ -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
}

View file

@ -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) {

View file

@ -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

View file

@ -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 {

53
line/interface.go Normal file
View file

@ -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
}

12
line/matched.go Normal file
View file

@ -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
}

View file

@ -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
}

30
peco.go
View file

@ -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
})
}()

View file

@ -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()

View file

@ -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

View file

@ -6,7 +6,7 @@ import (
pdebug "github.com/lestrrat/go-pdebug"
"github.com/pkg/errors"
"golang.org/x/net/context"
"context"
)
// EndMark returns true

View file

@ -9,7 +9,7 @@ import (
"testing"
"time"
"golang.org/x/net/context"
"context"
)
type RegexpFilter struct {

View file

@ -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)
}

View file

@ -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())

View file

@ -5,7 +5,7 @@ import (
"os/signal"
"syscall"
"golang.org/x/net/context"
"context"
)
type SigReceivedHandler interface {

View file

@ -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()

View file

@ -7,7 +7,7 @@ import (
"testing"
"time"
"golang.org/x/net/context"
"context"
"github.com/stretchr/testify/assert"
)

View file

@ -4,7 +4,7 @@ import (
"time"
"github.com/peco/peco/hub"
"golang.org/x/net/context"
"context"
)
type statusMsgReq interface {