mirror of
https://github.com/peco/peco.git
synced 2026-09-10 07:16:29 -04:00
199 lines
4.3 KiB
Go
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: ®expQueryFactory{
|
|
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
|
|
}
|