peco.peco/filter/regexp.go
2016-12-14 14:59:38 +09:00

199 lines
4.3 KiB
Go

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
}
func (rf *Regexp) NewContext(ctx context.Context, query string) context.Context {
return newContext(ctx, 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.ChanOutput(make(chan interface{})),
}
}
func (rf Regexp) BufSize() int {
return 0
}
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, lines []line.Line, out pipeline.ChanOutput) error {
query := ctx.Value(queryKey).(string)
regexps, err := rf.factory.Compile(query, rf.flags, rf.quotemeta)
if err != nil {
return errors.Wrap(err, "failed to compile queries as regular expression")
}
for _, l := range lines {
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 {
continue
}
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)
}
}
out.Send(line.NewMatched(l, deduped))
}
return 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
}