Rename the query DSL to the filter DSL.

This commit is contained in:
Qi Xiao 2021-03-28 22:59:58 +01:00
parent 3e512a6dd6
commit b43e5b8793
10 changed files with 126 additions and 125 deletions

View File

@ -198,7 +198,7 @@ func completionStart(app cli.App, bindings tk.Bindings, cfg complete.Config, sma
}
w, err := mode.NewCompletion(app, mode.CompletionSpec{
Name: result.Name, Replace: result.Replace, Items: result.Items,
Filter: queryFilterSpec, Bindings: bindings,
Filter: filterSpec, Bindings: bindings,
})
if w != nil {
app.SetAddon(w, false)

View File

@ -1,9 +1,9 @@
// Package query implements the Elvish query language.
// Package filter implements the Elvish filter DSL.
//
// The Elvish query language is a DSL for filtering data, such as in history
// listing mode and location mode of the interactive editor. It is a subset of
// Elvish's expression syntax, currently a very small one.
package query
// The filter DSL is a subset of Elvish's expression syntax, and is useful for
// filtering a list of items. It is currently used in the listing modes of the
// interactive editor.
package filter
import (
"errors"
@ -15,20 +15,20 @@ import (
"src.elv.sh/pkg/parse/cmpd"
)
// Compile parses and compiles a query.
func Compile(q string) (Query, error) {
qn, errParse := parseQuery(q)
query, errCompile := compileQuery(qn)
return query, diag.Errors(errParse, errCompile)
// Compile parses and compiles a filter.
func Compile(q string) (Filter, error) {
qn, errParse := parseFilter(q)
filter, errCompile := compileFilter(qn)
return filter, diag.Errors(errParse, errCompile)
}
func parseQuery(q string) (*parse.Query, error) {
qn := &parse.Query{}
err := parse.ParseAs(parse.Source{Name: "query", Code: q}, qn, parse.Config{})
func parseFilter(q string) (*parse.Filter, error) {
qn := &parse.Filter{}
err := parse.ParseAs(parse.Source{Name: "filter", Code: q}, qn, parse.Config{})
return qn, err
}
func compileQuery(qn *parse.Query) (Query, error) {
func compileFilter(qn *parse.Filter) (Filter, error) {
if len(qn.Opts) > 0 {
return nil, notSupportedError{"option"}
}
@ -36,11 +36,11 @@ func compileQuery(qn *parse.Query) (Query, error) {
if err != nil {
return nil, err
}
return andQuery{qs}, nil
return andFilter{qs}, nil
}
func compileCompounds(ns []*parse.Compound) ([]Query, error) {
qs := make([]Query, len(ns))
func compileCompounds(ns []*parse.Compound) ([]Filter, error) {
qs := make([]Filter, len(ns))
for i, n := range ns {
q, err := compileCompound(n)
if err != nil {
@ -51,13 +51,13 @@ func compileCompounds(ns []*parse.Compound) ([]Query, error) {
return qs, nil
}
func compileCompound(n *parse.Compound) (Query, error) {
func compileCompound(n *parse.Compound) (Filter, error) {
if pn, ok := cmpd.Primary(n); ok {
switch pn.Type {
case parse.Bareword, parse.SingleQuoted, parse.DoubleQuoted:
s := pn.Value
ignoreCase := s == strings.ToLower(s)
return substringQuery{s, ignoreCase}, nil
return substringFilter{s, ignoreCase}, nil
case parse.List:
return compileList(pn.Elements)
}
@ -65,46 +65,46 @@ func compileCompound(n *parse.Compound) (Query, error) {
return nil, notSupportedError{cmpd.Shape(n)}
}
var errEmptySubquery = errors.New("empty subquery")
var errEmptySubfilter = errors.New("empty subfilter")
func compileList(elems []*parse.Compound) (Query, error) {
func compileList(elems []*parse.Compound) (Filter, error) {
if len(elems) == 0 {
return nil, errEmptySubquery
return nil, errEmptySubfilter
}
head, ok := cmpd.StringLiteral(elems[0])
if !ok {
return nil, notSupportedError{"non-literal subquery head"}
return nil, notSupportedError{"non-literal subfilter head"}
}
switch head {
case "re":
if len(elems) == 1 {
return nil, notSupportedError{"re subquery with no argument"}
return nil, notSupportedError{"re subfilter with no argument"}
}
if len(elems) > 2 {
return nil, notSupportedError{"re subquery with two or more arguments"}
return nil, notSupportedError{"re subfilter with two or more arguments"}
}
arg := elems[1]
s, ok := cmpd.StringLiteral(arg)
if !ok {
return nil, notSupportedError{"re subquery with " + cmpd.Shape(arg)}
return nil, notSupportedError{"re subfilter with " + cmpd.Shape(arg)}
}
p, err := regexp.Compile(s)
if err != nil {
return nil, err
}
return regexpQuery{p}, nil
return regexpFilter{p}, nil
case "and":
qs, err := compileCompounds(elems[1:])
if err != nil {
return nil, err
}
return andQuery{qs}, nil
return andFilter{qs}, nil
case "or":
qs, err := compileCompounds(elems[1:])
if err != nil {
return nil, err
}
return orQuery{qs}, nil
return orFilter{qs}, nil
default:
return nil, notSupportedError{"head " + parse.SourceText(elems[0])}
}

View File

@ -1,86 +1,86 @@
package query_test
package filter_test
import (
"testing"
"src.elv.sh/pkg/edit/query"
"src.elv.sh/pkg/edit/filter"
"src.elv.sh/pkg/parse"
)
func TestCompile(t *testing.T) {
test(t,
That("empty query matches anything").
Query("").Matches("foo", "bar", " ", ""),
That("empty filter matches anything").
Filter("").Matches("foo", "bar", " ", ""),
That("bareword matches any string containing it").
Query("foo").Matches("foobar", "afoo").DoesNotMatch("", "faoo"),
That("bareword is case-insensitive is query is all lower case").
Query("foo").Matches("FOO", "Foo", "FOObar").DoesNotMatch("", "faoo"),
That("bareword is case-sensitive is query is not all lower case").
Query("Foo").Matches("Foobar").DoesNotMatch("foo", "FOO"),
Filter("foo").Matches("foobar", "afoo").DoesNotMatch("", "faoo"),
That("bareword is case-insensitive is filter is all lower case").
Filter("foo").Matches("FOO", "Foo", "FOObar").DoesNotMatch("", "faoo"),
That("bareword is case-sensitive is filter is not all lower case").
Filter("Foo").Matches("Foobar").DoesNotMatch("foo", "FOO"),
That("double quoted string works like bareword").
Query(`"foo"`).Matches("FOO", "Foo", "FOObar").DoesNotMatch("", "faoo"),
Filter(`"foo"`).Matches("FOO", "Foo", "FOObar").DoesNotMatch("", "faoo"),
That("single quoted string works like bareword").
Query(`'foo'`).Matches("FOO", "Foo", "FOObar").DoesNotMatch("", "faoo"),
Filter(`'foo'`).Matches("FOO", "Foo", "FOObar").DoesNotMatch("", "faoo"),
That("space-separated words work like an AND query").
Query("foo bar").
That("space-separated words work like an AND filter").
Filter("foo bar").
Matches("foobar", "bar foo", "foo lorem ipsum bar").
DoesNotMatch("foo", "bar", ""),
That("quoted string can be used when string contains spaces").
Query(`"foo bar"`).
Filter(`"foo bar"`).
Matches("__foo bar xyz").
DoesNotMatch("foobar"),
That("AND query matches if all components match").
Query("[and foo bar]").Matches("foobar", "bar foo").DoesNotMatch("foo"),
That("OR query matches if any component matches").
Query("[or foo bar]").Matches("foo", "bar", "foobar").DoesNotMatch(""),
That("RE query uses component as regular expression to match").
Query("[re f..]").Matches("foo", "f..").DoesNotMatch("fo", ""),
That("AND filter matches if all components match").
Filter("[and foo bar]").Matches("foobar", "bar foo").DoesNotMatch("foo"),
That("OR filter matches if any component matches").
Filter("[or foo bar]").Matches("foo", "bar", "foobar").DoesNotMatch(""),
That("RE filter uses component as regular expression to match").
Filter("[re f..]").Matches("foo", "f..").DoesNotMatch("fo", ""),
// Invalid queries
That("empty list is invalid").
Query("[]").DoesNotCompile("empty subquery"),
Filter("[]").DoesNotCompile("empty subfilter"),
That("starting list with non-literal is invalid").
Query("[[foo] bar]").
DoesNotCompile("non-literal subquery head not supported"),
That("RE query with no argument is invalid").
Query("[re]").
DoesNotCompile("re subquery with no argument not supported"),
That("RE query with two or more arguments is invalid").
Query("[re foo bar]").
DoesNotCompile("re subquery with two or more arguments not supported"),
That("RE query with invalid regular expression is invalid").
Query("[re '[']").
Filter("[[foo] bar]").
DoesNotCompile("non-literal subfilter head not supported"),
That("RE filter with no argument is invalid").
Filter("[re]").
DoesNotCompile("re subfilter with no argument not supported"),
That("RE filter with two or more arguments is invalid").
Filter("[re foo bar]").
DoesNotCompile("re subfilter with two or more arguments not supported"),
That("RE filter with invalid regular expression is invalid").
Filter("[re '[']").
DoesNotCompile("error parsing regexp: missing closing ]: `[`"),
That("invalid syntax results in parse error").
Query("[and").DoesNotParse("parse error: 4-4 in query: should be ']'"),
Filter("[and").DoesNotParse("parse error: 4-4 in filter: should be ']'"),
// Unsupported for now, but may be in future
That("options are not supported yet").
Query("foo &k=v").DoesNotCompile("option not supported"),
Filter("foo &k=v").DoesNotCompile("option not supported"),
That("compound expressions are not supported yet").
Query(`a"foo"`).DoesNotCompile("compound expression not supported"),
Filter(`a"foo"`).DoesNotCompile("compound expression not supported"),
That("indexing expressions are not supported yet").
Query("foo[0]").DoesNotCompile("indexing expression not supported"),
Filter("foo[0]").DoesNotCompile("indexing expression not supported"),
That("variable references are not supported yet").
Query("$a").
Filter("$a").
DoesNotCompile("primary expression of type Variable not supported"),
That("variable references in RE subquery are not supported yet").
Query("[re $a]").
DoesNotCompile("re subquery with primary expression of type Variable not supported"),
That("variable references in AND subquery are not supported yet").
Query("[and $a]").
That("variable references in RE subfilter are not supported yet").
Filter("[re $a]").
DoesNotCompile("re subfilter with primary expression of type Variable not supported"),
That("variable references in AND subfilter are not supported yet").
Filter("[and $a]").
DoesNotCompile("primary expression of type Variable not supported"),
That("variable references in OR subquery are not supported yet").
Query("[or $a]").
That("variable references in OR subfilter are not supported yet").
Filter("[or $a]").
DoesNotCompile("primary expression of type Variable not supported"),
That("other subqueries are not supported yet").
Query("[other foo bar]").
Filter("[other foo bar]").
DoesNotCompile("head other not supported"),
)
}
@ -88,28 +88,28 @@ func TestCompile(t *testing.T) {
func test(t *testing.T, tests ...testCase) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
q, err := query.Compile(test.query)
q, err := filter.Compile(test.filter)
if errType := getErrorType(err); errType != test.errorType {
t.Errorf("%q should have %s, but has %s",
test.query, test.errorType, errType)
test.filter, test.errorType, errType)
}
if err != nil {
if err.Error() != test.errorMessage {
t.Errorf("%q should have error message %q, but is %q",
test.query, test.errorMessage, err)
test.filter, test.errorMessage, err)
}
return
}
for _, s := range test.matches {
ok := q.Match(s)
if !ok {
t.Errorf("%q should match %q, but doesn't", test.query, s)
t.Errorf("%q should match %q, but doesn't", test.filter, s)
}
}
for _, s := range test.doesntMatch {
ok := q.Match(s)
if ok {
t.Errorf("%q shouldn't match %q, but does", test.query, s)
t.Errorf("%q shouldn't match %q, but does", test.filter, s)
}
}
})
@ -118,7 +118,7 @@ func test(t *testing.T, tests ...testCase) {
type testCase struct {
name string
query string
filter string
matches []string
doesntMatch []string
errorType errorType
@ -129,8 +129,8 @@ func That(name string) testCase {
return testCase{name: name}
}
func (t testCase) Query(q string) testCase {
t.query = q
func (t testCase) Filter(q string) testCase {
t.filter = q
return t
}

View File

@ -1,20 +1,20 @@
package query
package filter
import (
"regexp"
"strings"
)
// Query represents a compiled query, which can be used to match text.
type Query interface {
// Filter represents a compiled filter, which can be used to match text.
type Filter interface {
Match(s string) bool
}
type andQuery struct {
queries []Query
type andFilter struct {
queries []Filter
}
func (aq andQuery) Match(s string) bool {
func (aq andFilter) Match(s string) bool {
for _, q := range aq.queries {
if !q.Match(s) {
return false
@ -23,11 +23,11 @@ func (aq andQuery) Match(s string) bool {
return true
}
type orQuery struct {
queries []Query
type orFilter struct {
queries []Filter
}
func (oq orQuery) Match(s string) bool {
func (oq orFilter) Match(s string) bool {
for _, q := range oq.queries {
if q.Match(s) {
return true
@ -36,22 +36,22 @@ func (oq orQuery) Match(s string) bool {
return false
}
type substringQuery struct {
type substringFilter struct {
pattern string
ignoreCase bool
}
func (sq substringQuery) Match(s string) bool {
func (sq substringFilter) Match(s string) bool {
if sq.ignoreCase {
s = strings.ToLower(s)
}
return strings.Contains(s, sq.pattern)
}
type regexpQuery struct {
type regexpFilter struct {
pattern *regexp.Regexp
}
func (rq regexpQuery) Match(s string) bool {
func (rq regexpFilter) Match(s string) bool {
return rq.pattern.MatchString(s)
}

View File

@ -1,4 +1,4 @@
package query
package filter
import (
"strings"
@ -10,7 +10,7 @@ import (
)
func Highlight(q string) (ui.Text, []error) {
n, _ := parseQuery(q)
n, _ := parseFilter(q)
w := walker{}
w.walk(n)
text := ui.StyleRegions(q, w.regions)

View File

@ -1,9 +1,10 @@
package query
package filter_test
import (
"reflect"
"testing"
"src.elv.sh/pkg/edit/filter"
"src.elv.sh/pkg/ui"
)
@ -51,7 +52,7 @@ var highlightTests = []struct {
func TestHighlight(t *testing.T) {
for _, test := range highlightTests {
t.Run(test.name, func(t *testing.T) {
got, _ := Highlight(test.q)
got, _ := filter.Highlight(test.q)
if !reflect.DeepEqual(got, test.want) {
t.Errorf("got %s, want %s", got, test.want)
}

View File

@ -8,7 +8,7 @@ import (
"src.elv.sh/pkg/cli/histutil"
"src.elv.sh/pkg/cli/mode"
"src.elv.sh/pkg/cli/tk"
"src.elv.sh/pkg/edit/query"
"src.elv.sh/pkg/edit/filter"
"src.elv.sh/pkg/eval"
"src.elv.sh/pkg/eval/vals"
"src.elv.sh/pkg/eval/vars"
@ -43,15 +43,15 @@ func initListings(ed *Editor, ev *eval.Evaler, st store.Store, histStore histuti
initLocation(ed, ev, st, bindingVar, nb)
}
var queryFilterSpec = mode.FilterSpec{
var filterSpec = mode.FilterSpec{
Maker: func(f string) func(string) bool {
q, _ := query.Compile(f)
q, _ := filter.Compile(f)
if q == nil {
return func(string) bool { return true }
}
return q.Match
},
Highlighter: query.Highlight,
Highlighter: filter.Highlight,
}
func initHistlist(ed *Editor, ev *eval.Evaler, histStore histutil.Store, commonBindingVar vars.PtrVar, nb eval.NsBuilder) {
@ -69,7 +69,7 @@ func initHistlist(ed *Editor, ev *eval.Evaler, histStore histutil.Store, commonB
Dedup: func() bool {
return dedup.Get().(bool)
},
Filter: queryFilterSpec,
Filter: filterSpec,
})
startMode(ed.app, w, err)
},
@ -117,7 +117,7 @@ func initLocation(ed *Editor, ev *eval.Evaler, st store.Store, commonBindingVar
IteratePinned: adaptToIterateString(pinnedVar),
IterateHidden: adaptToIterateString(hiddenVar),
IterateWorkspaces: workspaceIterator,
Filter: queryFilterSpec,
Filter: filterSpec,
})
startMode(ed.app, w, err)
}).Ns())

View File

@ -124,7 +124,7 @@ func initNavigation(ed *Editor, ev *eval.Evaler, nb eval.NsBuilder) {
WidthRatio: func() [3]int {
return convertNavWidthRatio(widthRatioVar.Get())
},
Filter: queryFilterSpec,
Filter: filterSpec,
})
startMode(app, w, nil)
},

View File

@ -343,15 +343,15 @@ const (
Append
)
// Query represents an Elvish query. It uses the same syntax as arguments and
// Filter is the Elvish filter DSL. It uses the same syntax as arguments and
// options to a command.
type Query struct {
type Filter struct {
node
Args []*Compound
Opts []*MapPair
}
func (qn *Query) parse(ps *parser) {
func (qn *Filter) parse(ps *parser) {
parseSpaces(qn, ps)
for {
r := ps.peek()

View File

@ -146,38 +146,38 @@ var testCases = []struct {
wantErrMsg: "should be a composite term representing fd",
},
// Query
// Filter
{
name: "empty query",
name: "empty filter",
code: "",
node: &Query{},
want: ast{"Query", fs{}},
node: &Filter{},
want: ast{"Filter", fs{}},
},
{
name: "query with arguments",
name: "filter with arguments",
code: "foo bar",
node: &Query{},
want: ast{"Query", fs{"Args": []string{"foo", "bar"}}},
node: &Filter{},
want: ast{"Filter", fs{"Args": []string{"foo", "bar"}}},
},
{
name: "query with options",
name: "filter with options",
code: "&foo=bar &lorem=ipsum",
node: &Query{},
want: ast{"Query", fs{"Opts": []string{"&foo=bar", "&lorem=ipsum"}}},
node: &Filter{},
want: ast{"Filter", fs{"Opts": []string{"&foo=bar", "&lorem=ipsum"}}},
},
{
name: "query mixing arguments and options",
name: "filter mixing arguments and options",
code: "foo &a=b bar &x=y",
node: &Query{},
want: ast{"Query", fs{
node: &Filter{},
want: ast{"Filter", fs{
"Args": []string{"foo", "bar"},
"Opts": []string{"&a=b", "&x=y"}}},
},
{
name: "query with leading and trailing whitespaces",
name: "filter with leading and trailing whitespaces",
code: " foo ",
node: &Query{},
want: ast{"Query", fs{"Args": []string{"foo"}}},
node: &Filter{},
want: ast{"Filter", fs{"Args": []string{"foo"}}},
},
// Compound