421 lines
7.6 KiB
Go
421 lines
7.6 KiB
Go
package highlight
|
|
|
|
import (
|
|
"sort"
|
|
"strings"
|
|
|
|
"aego/script/token"
|
|
)
|
|
|
|
type Kind uint8
|
|
|
|
const (
|
|
Plain Kind = iota
|
|
Keyword
|
|
Number
|
|
String
|
|
Comment
|
|
Operator
|
|
Punctuation
|
|
Attribute
|
|
Namespace
|
|
Type
|
|
BuiltinType
|
|
Function
|
|
Method
|
|
Param
|
|
Variable
|
|
Field
|
|
Property
|
|
Constant
|
|
EnumMember
|
|
Builtin
|
|
Script
|
|
Error
|
|
Signal
|
|
Coroutine
|
|
Bracket
|
|
KindCount
|
|
)
|
|
|
|
type State uint32
|
|
|
|
const (
|
|
Normal State = 0
|
|
InComment State = 1
|
|
)
|
|
|
|
func MakeState(depth int, comment bool) State {
|
|
s := State(max(depth, 0)) << 1
|
|
if comment {
|
|
s |= InComment
|
|
}
|
|
return s
|
|
}
|
|
|
|
func (s State) Comment() bool {
|
|
return s&InComment != 0
|
|
}
|
|
|
|
func (s State) Depth() int {
|
|
return int(s >> 1)
|
|
}
|
|
|
|
type Span struct {
|
|
Start int
|
|
End int
|
|
Kind Kind
|
|
}
|
|
|
|
type Sem struct {
|
|
Start int
|
|
End int
|
|
Kind Kind
|
|
}
|
|
|
|
type BracketPos struct {
|
|
Off int
|
|
Ch byte
|
|
Open bool
|
|
Depth int
|
|
}
|
|
|
|
var builtinTypes = map[string]bool{
|
|
"bool": true, "int": true, "float": true, "string": true,
|
|
"Vec2": true, "Vec3": true, "Color": true, "Quat": true, "Rect": true,
|
|
"Transform2D": true, "Transform3D": true,
|
|
"NodeRef": true, "AssetRef": true, "ComponentRef": true,
|
|
}
|
|
|
|
var builtinFuncs = map[string]bool{"len": true, "append": true}
|
|
|
|
const punctuation = ",;:"
|
|
|
|
func letter(c byte) bool {
|
|
return c == '_' || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
|
|
}
|
|
|
|
func digit(c byte) bool {
|
|
return c >= '0' && c <= '9'
|
|
}
|
|
|
|
func ident(c byte) bool {
|
|
return letter(c) || digit(c)
|
|
}
|
|
|
|
func closeComment(text []byte, from int) int {
|
|
for i := from; i+1 < len(text); i++ {
|
|
if text[i] == '*' && text[i+1] == '/' {
|
|
return i + 2
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
func stringEnd(text []byte, i int) int {
|
|
j := i + 1
|
|
for j < len(text) && text[j] != '"' {
|
|
if text[j] == '\\' {
|
|
j++
|
|
}
|
|
j++
|
|
}
|
|
return min(j+1, len(text))
|
|
}
|
|
|
|
func skipSpace(text []byte, i int) int {
|
|
for i < len(text) && (text[i] == ' ' || text[i] == '\t') {
|
|
i++
|
|
}
|
|
return i
|
|
}
|
|
|
|
func prevNonSpace(text []byte, i int) byte {
|
|
for i--; i >= 0; i-- {
|
|
if c := text[i]; c != ' ' && c != '\t' {
|
|
return c
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func typeArgEnd(text []byte, i int) int {
|
|
if i == 0 || !ident(text[i-1]) {
|
|
return -1
|
|
}
|
|
j := i + 1
|
|
for j < len(text) && (ident(text[j]) || text[j] == '.' || text[j] == ',' || text[j] == ' ') {
|
|
j++
|
|
}
|
|
if j == i+1 || j >= len(text) || text[j] != '>' {
|
|
return -1
|
|
}
|
|
if k := skipSpace(text, j+1); k >= len(text) || text[k] != '(' {
|
|
return -1
|
|
}
|
|
return j
|
|
}
|
|
|
|
func bracketAt(text []byte, i, shut int) int {
|
|
switch text[i] {
|
|
case '(', '[', '{':
|
|
return 1
|
|
case ')', ']', '}':
|
|
return -1
|
|
case '<':
|
|
if typeArgEnd(text, i) > 0 {
|
|
return 1
|
|
}
|
|
case '>':
|
|
if i == shut {
|
|
return -1
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func Opens(c byte) bool {
|
|
return c == '(' || c == '[' || c == '{' || c == '<'
|
|
}
|
|
|
|
func walk(text []byte, in State, dst []BracketPos, keep bool) ([]BracketPos, State) {
|
|
n := len(text)
|
|
depth := in.Depth()
|
|
i := 0
|
|
if in.Comment() {
|
|
end := closeComment(text, 0)
|
|
if end < 0 {
|
|
return dst, in
|
|
}
|
|
i = end
|
|
}
|
|
shut := -1
|
|
for i < n {
|
|
c := text[i]
|
|
switch {
|
|
case c == '"':
|
|
i = stringEnd(text, i)
|
|
continue
|
|
case c == '/' && i+1 < n && text[i+1] == '/':
|
|
return dst, MakeState(depth, false)
|
|
case c == '/' && i+1 < n && text[i+1] == '*':
|
|
end := closeComment(text, i+2)
|
|
if end < 0 {
|
|
return dst, MakeState(depth, true)
|
|
}
|
|
i = end
|
|
continue
|
|
}
|
|
switch bracketAt(text, i, shut) {
|
|
case 1:
|
|
if keep {
|
|
dst = append(dst, BracketPos{Off: i, Ch: c, Open: true, Depth: depth})
|
|
}
|
|
depth++
|
|
if c == '<' {
|
|
shut = typeArgEnd(text, i)
|
|
}
|
|
case -1:
|
|
depth = max(depth-1, 0)
|
|
if keep {
|
|
dst = append(dst, BracketPos{Off: i, Ch: c, Depth: depth})
|
|
}
|
|
if c == '>' {
|
|
shut = -1
|
|
}
|
|
}
|
|
i++
|
|
}
|
|
return dst, MakeState(depth, false)
|
|
}
|
|
|
|
func Scan(text []byte, in State) State {
|
|
_, s := walk(text, in, nil, false)
|
|
return s
|
|
}
|
|
|
|
func Brackets(text []byte, in State, dst []BracketPos) ([]BracketPos, State) {
|
|
return walk(text, in, dst, true)
|
|
}
|
|
|
|
func BracketLevels(spans []Span, text []byte, depth int, dst []int) []int {
|
|
for _, s := range spans {
|
|
lv := -1
|
|
if s.Kind == Bracket && s.Start < len(text) {
|
|
if Opens(text[s.Start]) {
|
|
lv = depth
|
|
depth++
|
|
} else {
|
|
depth = max(depth-1, 0)
|
|
lv = depth
|
|
}
|
|
}
|
|
dst = append(dst, lv)
|
|
}
|
|
return dst
|
|
}
|
|
|
|
var yieldForms = map[string]bool{"wait": true, "frames": true, "fixed": true}
|
|
|
|
func classify(text []byte, i, j int, word, prev string) Kind {
|
|
if t := token.Lookup(word); t.IsKeyword() || t.IsReserved() {
|
|
return Keyword
|
|
}
|
|
switch prev {
|
|
case "script":
|
|
return Script
|
|
case "type", "enum":
|
|
return Type
|
|
case "func":
|
|
return Function
|
|
case "package":
|
|
return Namespace
|
|
case "signal":
|
|
return Signal
|
|
case "start":
|
|
return Coroutine
|
|
case "yield":
|
|
if yieldForms[word] {
|
|
return Keyword
|
|
}
|
|
}
|
|
if builtinTypes[word] || word == "Coroutine" || word == "Connection" || word == "ScriptRef" {
|
|
return BuiltinType
|
|
}
|
|
next := skipSpace(text, j)
|
|
call := next < len(text) && text[next] == '('
|
|
if prevNonSpace(text, i) == '.' {
|
|
if call || (next < len(text) && text[next] == '<') {
|
|
return Method
|
|
}
|
|
return Field
|
|
}
|
|
if word == "node" {
|
|
return Builtin
|
|
}
|
|
if word == "start" && next < len(text) && letter(text[next]) {
|
|
return Keyword
|
|
}
|
|
if call {
|
|
if builtinFuncs[word] {
|
|
return Builtin
|
|
}
|
|
return Function
|
|
}
|
|
return Plain
|
|
}
|
|
|
|
func Line(text []byte, in State, dst []Span) ([]Span, State) {
|
|
dst = dst[:0]
|
|
n := len(text)
|
|
depth := in.Depth()
|
|
i := 0
|
|
if in.Comment() {
|
|
end := closeComment(text, 0)
|
|
if end < 0 {
|
|
return append(dst, Span{0, n, Comment}), in
|
|
}
|
|
dst = append(dst, Span{0, end, Comment})
|
|
i = end
|
|
}
|
|
prev := ""
|
|
shut := -1
|
|
for i < n {
|
|
c := text[i]
|
|
switch {
|
|
case c == ' ' || c == '\t' || c == '\r':
|
|
i++
|
|
case c == '/' && i+1 < n && text[i+1] == '/':
|
|
return append(dst, Span{i, n, Comment}), MakeState(depth, false)
|
|
case c == '/' && i+1 < n && text[i+1] == '*':
|
|
end := closeComment(text, i+2)
|
|
if end < 0 {
|
|
return append(dst, Span{i, n, Comment}), MakeState(depth, true)
|
|
}
|
|
dst = append(dst, Span{i, end, Comment})
|
|
i, prev = end, ""
|
|
case c == '"':
|
|
j := stringEnd(text, i)
|
|
dst = append(dst, Span{i, j, String})
|
|
i, prev = j, ""
|
|
case digit(c) || (c == '.' && i+1 < n && digit(text[i+1])):
|
|
j := i + 1
|
|
for j < n && (ident(text[j]) || text[j] == '.') {
|
|
j++
|
|
}
|
|
dst = append(dst, Span{i, j, Number})
|
|
i, prev = j, ""
|
|
case c == '@':
|
|
j := i + 1
|
|
for j < n && ident(text[j]) {
|
|
j++
|
|
}
|
|
dst = append(dst, Span{i, j, Attribute})
|
|
i, prev = j, ""
|
|
case letter(c):
|
|
j := i + 1
|
|
for j < n && ident(text[j]) {
|
|
j++
|
|
}
|
|
word := string(text[i:j])
|
|
dst = append(dst, Span{i, j, classify(text, i, j, word, prev)})
|
|
i, prev = j, word
|
|
case c >= 0x80:
|
|
j := i + 1
|
|
for j < n && text[j] >= 0x80 {
|
|
j++
|
|
}
|
|
dst = append(dst, Span{i, j, Error})
|
|
i, prev = j, ""
|
|
default:
|
|
k := Operator
|
|
switch bracketAt(text, i, shut) {
|
|
case 1:
|
|
k = Bracket
|
|
depth++
|
|
if c == '<' {
|
|
shut = typeArgEnd(text, i)
|
|
}
|
|
case -1:
|
|
k = Bracket
|
|
depth = max(depth-1, 0)
|
|
if c == '>' {
|
|
shut = -1
|
|
}
|
|
default:
|
|
if strings.IndexByte(punctuation, c) >= 0 {
|
|
k = Punctuation
|
|
}
|
|
}
|
|
dst = append(dst, Span{i, i + 1, k})
|
|
i++
|
|
prev = ""
|
|
}
|
|
}
|
|
return dst, MakeState(depth, false)
|
|
}
|
|
|
|
func overridable(k Kind) bool {
|
|
switch k {
|
|
case Plain, Function, Method, Field:
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func Overlay(spans []Span, lineStart int, sem []Sem) {
|
|
if len(sem) == 0 {
|
|
return
|
|
}
|
|
for i := range spans {
|
|
if !overridable(spans[i].Kind) {
|
|
continue
|
|
}
|
|
abs := lineStart + spans[i].Start
|
|
j := sort.Search(len(sem), func(k int) bool { return sem[k].Start >= abs })
|
|
if j < len(sem) && sem[j].Start == abs && sem[j].End == lineStart+spans[i].End {
|
|
spans[i].Kind = sem[j].Kind
|
|
}
|
|
}
|
|
}
|