mirror of
https://github.com/peco/peco.git
synced 2026-09-10 07:16:29 -04:00
Move stuff around for better code organization
This commit is contained in:
parent
b1ea3c36b1
commit
61b5d22fb8
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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++ {
|
||||
|
|
|
|||
24
buffer.go
24
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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
640
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
170
filter/external.go
Normal file
170
filter/external.go
Normal 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
52
filter/filter.go
Normal 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
|
||||
}
|
||||
|
|
@ -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
57
filter/fuzzy.go
Normal 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
75
filter/interface.go
Normal 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
192
filter/regexp.go
Normal 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: ®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
|
||||
}
|
||||
60
filter/set.go
Normal file
60
filter/set.go
Normal 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]
|
||||
}
|
||||
103
filterutil.go
103
filterutil.go
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
2
input.go
2
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 {
|
||||
|
|
|
|||
120
interface.go
120
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
|
||||
}
|
||||
|
|
|
|||
28
internal/buffer/line.go
Normal file
28
internal/buffer/line.go
Normal 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
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
53
line/interface.go
Normal 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
12
line/matched.go
Normal 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
|
||||
}
|
||||
|
||||
|
|
@ -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
30
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
|
||||
})
|
||||
}()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import (
|
|||
|
||||
pdebug "github.com/lestrrat/go-pdebug"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/net/context"
|
||||
"context"
|
||||
)
|
||||
|
||||
// EndMark returns true
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
"context"
|
||||
)
|
||||
|
||||
type RegexpFilter struct {
|
||||
|
|
|
|||
12
selection.go
12
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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import (
|
|||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
"context"
|
||||
)
|
||||
|
||||
type SigReceivedHandler interface {
|
||||
|
|
|
|||
12
source.go
12
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()
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
"context"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue