Skip to content

Commit

Permalink
syntax: add tests for the generic syntax map
Browse files Browse the repository at this point in the history
  • Loading branch information
nelsam committed Jan 14, 2020
1 parent db1737a commit be38aa6
Show file tree
Hide file tree
Showing 2 changed files with 190 additions and 2 deletions.
7 changes: 5 additions & 2 deletions syntax/map.go
Expand Up @@ -109,7 +109,7 @@ func (w Wrapped) skip(d []rune) int {
if w.Nested {
open := []rune(w.Open)
if len(d) >= len(open) && match(open, d[:len(open)]) {
return w.end(d[len(open):])
return len(open) + w.end(d[len(open):])
}
}
c := []rune(w.Close)
Expand Down Expand Up @@ -252,12 +252,12 @@ func (g Generic) Parse(d []rune) Map {
next := nextWord(d[j:])
c, ok = g.dynWord(lastWord, next)
}
lastWord = word
if ok {
curr.constructs = append(curr.constructs, input.SyntaxLayer{
Construct: c,
Spans: []input.Span{{Start: wordStart, End: i}},
})
lastWord = word
continue
}
}
Expand Down Expand Up @@ -291,6 +291,9 @@ func isSeparator(r rune) bool {
}

func isNumeric(w []rune) bool {
if len(w) >= 2 && w[0] == '-' {
w = w[1:]
}
decimal := false
for _, r := range w {
if r == '.' && !decimal {
Expand Down
185 changes: 185 additions & 0 deletions syntax/map_test.go
@@ -0,0 +1,185 @@
// This is free and unencumbered software released into the public
// domain. For more information, see <http://unlicense.org> or the
// accompanying UNLICENSE file.

package syntax_test

import (
"fmt"
"strings"
"testing"

"github.com/nelsam/vidar/commander/input"
"github.com/nelsam/vidar/syntax"
"github.com/nelsam/vidar/theme"
"github.com/poy/onpar"
"github.com/poy/onpar/expect"
"github.com/poy/onpar/matchers"
)

var (
equal = matchers.Equal
not = matchers.Not
)

func TestGeneric(t *testing.T) {
o := onpar.New()
defer o.Run(t)

o.BeforeEach(func(t *testing.T) (expect.Expectation, syntax.Generic) {
return expect.New(t), syntax.Generic{
Scopes: []syntax.Scope{{Open: "{", Close: "}"}},
Wrapped: []syntax.Wrapped{
{Open: "//", Close: "\n", Construct: theme.Comment},
{Open: "'", Close: "'", Escapes: []string{`\'`}, Construct: theme.String},
{Open: "/*", Close: "*/", Nested: true, Construct: theme.Comment},
},
StaticWords: map[string]theme.LanguageConstruct{
"package": theme.Keyword,
"make": theme.Builtin,
},
DynamicWords: []syntax.Word{
{Before: "fn", Construct: theme.Func},
{Before: "var", After: "int", Construct: theme.Num},
},
}
})

o.Spec("it understands scope", func(expect expect.Expectation, g syntax.Generic) {
m := g.Parse([]rune(` { { }}`))
expect(m.Depth(0)).To(equal(0))
expect(m.Depth(1)).To(equal(1))
expect(m.Depth(4)).To(equal(2))
expect(m.Depth(8)).To(equal(1))
})

o.Group("syntax layers", func() {
o.BeforeEach(func(expect expect.Expectation, g syntax.Generic) (expect.Expectation, []input.SyntaxLayer, string) {
source := `
// some docs
foo := 'some string\' with escapes'
/* and comment blocks /* can nest */ without closing early */
{ will open a new scope
package is a keyword
numbers like 12.3 and -5 should be highlighted as numeric, always
numbers like 1.2.3 should _not_ be highlighted as numeric, though
{ will open a nested scope and should be rainbow highlighted
make is a builtin
} should match the theme.LanguageConstruct value of the brace it closes
}
fn somefunc is how we define a function
we highlight variables by their type with var somevariable int`
m := g.Parse([]rune(source))
return expect, m.Layers(), source
})

o.Spec("it recognizes single-line comments", func(expect expect.Expectation, layers []input.SyntaxLayer, source string) {
expect(layers).To(haveLayer(theme.Comment, source, "// some docs\n"))
})

o.Spec("it recognizes nested comment blocks", func(expect expect.Expectation, layers []input.SyntaxLayer, source string) {
expect(layers).To(haveLayer(theme.Comment, source, "/* and comment blocks /* can nest */ without closing early */"))
})

o.Spec("it recognizes strings and skips escaped quotes", func(expect expect.Expectation, layers []input.SyntaxLayer, source string) {
expect(layers).To(haveLayer(theme.String, source, `'some string\' with escapes'`))
})

o.Spec("it recognizes positive and negative numbers", func(expect expect.Expectation, layers []input.SyntaxLayer, source string) {
expect(layers).To(haveLayer(theme.Num, source, "12.3"))
expect(layers).To(haveLayer(theme.Num, source, "-5"))
})

o.Spec("it does not highlight semantic versions", func(expect expect.Expectation, layers []input.SyntaxLayer, source string) {
expect(layers).To(not(haveLayer(theme.Num, source, "1.2.3")))
})

o.Spec("it recognizes static words", func(expect expect.Expectation, layers []input.SyntaxLayer, source string) {
expect(layers).To(haveLayer(theme.Keyword, source, "package"))
expect(layers).To(haveLayer(theme.Builtin, source, "make"))
})

o.Spec("it recognizes dynamic words surrounded by static words", func(expect expect.Expectation, layers []input.SyntaxLayer, source string) {
expect(layers).To(haveLayer(theme.Func, source, "somefunc"))
expect(layers).To(haveLayer(theme.Num, source, "somevariable"))
})

o.Spec("it handles rainbow colors for scope pairs", func(expect expect.Expectation, layers []input.SyntaxLayer, source string) {
expect(layers).To(haveLayer(theme.ScopePair, source, "{"))
expect(layers).To(haveLayer(theme.ScopePair+1, source, "{", nth(2)))
expect(layers).To(haveLayer(theme.ScopePair+1, source, "}"))
expect(layers).To(haveLayer(theme.ScopePair, source, "}", nth(2)))
})
})
}

type haveLayerMatcher struct {
construct theme.LanguageConstruct
source, expected string
skip int
}

type layerOpt func(haveLayerMatcher) haveLayerMatcher

// nth returns a layerOpt that tells the haveLayerMatcher to match the nth
// occurrance of the expected string.
func nth(n int) layerOpt {
return func(m haveLayerMatcher) haveLayerMatcher {
m.skip = n - 1
return m
}
}

func haveLayer(construct theme.LanguageConstruct, source, expected string, opts ...layerOpt) haveLayerMatcher {
m := haveLayerMatcher{construct: construct, source: source, expected: expected}
for _, o := range opts {
m = o(m)
}
return m
}

func (m haveLayerMatcher) Match(actual interface{}) (interface{}, error) {
start := indexN(m.source, m.expected, m.skip)
end := start + len(m.expected)
switch l := actual.(type) {
case input.SyntaxLayer:
return actual, m.matchLayer(l, start, end)
case []input.SyntaxLayer:
for _, layer := range l {
if err := m.matchLayer(layer, start, end); err == nil {
return actual, nil
}
}
return actual, fmt.Errorf("expected to find a span from %d to %d in a layer with construct %#v in %#v", start, end, m.construct, l)
default:
return actual, fmt.Errorf("expected actual to be of type input.SyntaxLayer or []input.SyntaxLayer; was %T", actual)
}
}

func (m haveLayerMatcher) matchLayer(l input.SyntaxLayer, start, end int) error {
if l.Construct != m.construct {
return fmt.Errorf("expected construct %#v to equal %#v", l.Construct, m.construct)
}
for _, s := range l.Spans {
if s.Start == start && s.End == end {
return nil
}
}
return fmt.Errorf("expected to find a span from %d to %d in %#v", start, end, l.Spans)
}

func indexN(haystack, needle string, n int) int {
i := strings.Index(haystack, needle)
if i == -1 {
return -1
}
for ; n > 0; n-- {
i += len(needle)
nextIdx := strings.Index(haystack[i:], needle)
if nextIdx == -1 {
return -1
}
i += nextIdx
}
return i
}

0 comments on commit be38aa6

Please sign in to comment.