turbo-editors/turbo-golopublic Fork 0
main
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-golo.git
git clone ssh://git@rickub.com/turbo-editors/turbo-golo.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

scan.go · 165 lines · 6.3 KBGo Blame HistoryRaw
📦 Turbo Golo d710c1b k33g 12h ago1package gololang
2
3import "rickub.com/turbo-editors/turbo-core/syntax"
4
5// carry is what a line of Golo leaves open for the next one: which construct,
6// if any, the line ended inside.
7//
8// Four constructs may reach the next line, and the interpreter's own lexer is
9// the authority on each. A block comment runs from one ---- to the next
10// wherever that is. A "string", a """multi-line string""" and a 'character'
11// are all read to their closing quote, and that quote may be on a later line:
12// lexer.go reads a plain string with `for l.ch != '"' && l.ch != 0`, which
13// stops at the quote or the end of the file and at nothing in between. So an
14// unterminated string paints the rest of the file — and that is what the
15// interpreter does with it too, which is the reason to carry rather than to
16// stop at the line the way Turbo MoonBit does for a language whose grammar
17// forbids the newline.
18//
19// None of the four nests, so a value saying which one is open is the whole of
20// the state. A depth would be a claim the language does not make.
21type carry int
22
23const (
24 // nothingOpen is the state between constructs, and the zero value
25 // ScanLines starts the first line in.
26 nothingOpen carry = iota
27 // blockCommentOpen means a ---- was seen and its closing ---- was not.
28 blockCommentOpen
29 // stringOpen means a " was seen and its closing " was not.
30 stringOpen
31 // multilineStringOpen means a """ was seen and its closing """ was not.
32 multilineStringOpen
33 // charOpen means a ' was seen and its closing ' was not.
34 charOpen
35)
36
37// Highlight colours Golo source.
38//
39// It is written against syntax.LineScanner, a line at a time, with the
40// interpreter's lexer/lexer.go as its specification. GoloScript's own lexer is
41// a Go package, but its module is named `golo` and is not importable from
42// another module, so the rules are carried here rather than called — and what
43// the lexer reads as one token, this scanner colours as one span.
44//
45// It is deliberately tolerant of broken input: source under the cursor is
46// invalid most of the time it is being typed, and a highlighter that gives up
47// is a highlighter that flickers off.
48//
49// spans := gololang.Highlight("function main = |args| {\n println(\"hi\")\n}\n")
50// // spans[0][0] covers "function" with syntax.ClassKeyword
51func Highlight(src string) [][]syntax.Span {
52 return syntax.ScanLines(src, scanLine)
53}
54
55// scanLine colours one line, finishing whatever the previous line left open
56// before looking at anything new, and reports what this line leaves open.
57func scanLine(line []rune, open carry) ([]syntax.Span, carry) {
58 s := syntax.NewLineScanner(line)
59
60 open = finishOpen(s, open)
61 for !s.AtEnd() {
62 open = scanToken(s)
63 }
64 return s.Spans(), open
65}
66
67// finishOpen colours the continuation of a construct opened on an earlier
68// line, and says whether it is still open at the end of this one. With nothing
69// open it does nothing.
70func finishOpen(s *syntax.LineScanner, open carry) carry {
71 switch open {
72 case blockCommentOpen:
73 return stillOpen(syntax.FinishBlockComment(s, blockCommentMarker, syntax.ClassComment), open)
74 case multilineStringOpen:
75 return stillOpen(syntax.FinishBlockComment(s, multilineStringQuote, syntax.ClassString), open)
76 case stringOpen:
77 return finishQuoted(s, '"', syntax.ClassString, open)
78 case charOpen:
79 return finishQuoted(s, '\'', syntax.ClassChar, open)
80 default:
81 return nothingOpen
82 }
83}
84
85// stillOpen turns "did it close?" into what the next line should be told.
86func stillOpen(closed bool, open carry) carry {
87 if closed {
88 return nothingOpen
89 }
90 return open
91}
92
93// blockCommentMarker opens and closes a block comment. The lexer asks for four
94// dashes exactly: three are an operator run, and a fifth is part of the text.
95const blockCommentMarker = "----"
96
97// scanToken colours whatever starts at the scanner's position, and reports
98// which construct, if any, ran off the end of the line.
99//
100// The order of the cases is the design, and three of them are load-bearing:
101//
102// - ---- is tested before the operators, because - is an operator rune and
103// the run would otherwise be coloured as one.
104// - """ is tested before ", because both begin with a quote and what tells
105// them apart is the two runes after it. The lexer makes the same check in
106// the same order.
107// - A digit is tested before a word, and a word before a dot, so that 1.5 is
108// one number and hello.World is a name, a dot and a name.
109func scanToken(s *syntax.LineScanner) carry {
110 r := s.Peek(0)
111
112 switch {
113 case r == ' ' || r == '\t':
114 s.SkipSpaces()
115 case r == '#':
116 // # opens a comment that runs to the end of the line. A shebang is
117 // one too: #!/usr/bin/env golo is how a script is run as a command,
118 // and the interpreter reads it as a comment because that is what it
119 // is.
120 s.TakeRest(syntax.ClassComment)
121 case s.HasPrefix(0, blockCommentMarker):
122 return stillOpen(syntax.OpenBlockComment(s, blockCommentMarker, blockCommentMarker, syntax.ClassComment), blockCommentOpen)
123 case s.HasPrefix(0, multilineStringQuote):
124 return takeMultilineString(s)
125 case r == '"':
126 return takeQuoted(s, '"', syntax.ClassString, stringOpen)
127 case r == '\'':
128 return takeQuoted(s, '\'', syntax.ClassChar, charOpen)
129 case syntax.IsDigit(r):
130 takeNumber(s)
131 case isIdentifierStart(r):
132 takeWord(s)
133 case r == '.':
134 takeDot(s)
135 case r == '$':
136 // $ joins a union to one of its variants in `augment Shape$Circle`.
137 // It is structure rather than computation, so it is punctuation like
138 // the dot in a module path.
139 s.Take(1, syntax.ClassPunctuation)
140 case syntax.IsOperatorRune(r):
141 s.TakeWhile(syntax.ClassOperator, syntax.IsOperatorRune)
142 case syntax.IsPunctuationRune(r):
143 s.Take(1, syntax.ClassPunctuation)
144 default:
145 // A rune nothing here claims — a stray control character, a symbol
146 // outside the ranges the lexer accepts in a name — is stepped over
147 // uncoloured rather than guessed at.
148 s.Advance(1)
149 }
150 return nothingOpen
151}
152
153// takeDot colours a dot and whatever the language says belongs with it.
154//
155// A run of dots is an operator — .. is a range and ... marks a variadic
156// parameter — and a dot on its own is punctuation: the separator in a module
157// path such as hello.World, or in a decimal that takeNumber did not claim
158// because no digit came before it.
159func takeDot(s *syntax.LineScanner) {
160 if s.Peek(1) == '.' {
161 s.TakeWhile(syntax.ClassOperator, func(r rune) bool { return r == '.' })
162 return
163 }
164 s.Take(1, syntax.ClassPunctuation)
165}