turbo-editors/turbo-rustpublic Fork 0
v1.0.1
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-rust.git
git clone ssh://git@rickub.com/turbo-editors/turbo-rust.git

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

scan.go · 187 lines · 5.9 KBGo Blame HistoryRaw
📦 Turbo Rust 713ea5c k33g 12h ago1package rustlang
2
3import "rickub.com/turbo-editors/turbo-core/syntax"
4
5// carry is what a line of Rust leaves open for the next one.
6//
7// Three constructs in Rust can cross a line break, and each of them has to be
8// remembered exactly rather than guessed at: a block comment (which nests, so a
9// depth and not a flag), a raw string (whose terminator is a quote followed by
10// as many hashes as its opener had), and an ordinary string (which may contain
11// a real newline). Anything else is decided from the line in front of you.
12type carry struct {
13 // commentDepth is how many /* are still open. Rust nests block comments,
14 // so /* /* */ */ is one comment and a flag would end it one level early.
15 commentDepth int
16 // rawHashes is the number of # a raw string was opened with, and rawOpen
17 // says whether one is open at all — a raw string may be opened with none,
18 // so the count alone cannot say.
19 rawOpen bool
20 rawHashes int
21 // stringOpen says an ordinary "…" string ran past the end of a line.
22 stringOpen bool
23}
24
25// Highlight colours Rust source.
26//
27// It is written against syntax.LineScanner, a line at a time, with the three
28// multi-line constructs above threaded through carry. Rust has no tokeniser in
29// the Go standard library the way Go does, so this is a scanner in the same
30// style as the ones turbo-core ships for TOML, Markdown and shell.
31//
32// It is deliberately tolerant of broken input: source under the cursor is
33// invalid most of the time it is being typed, and a highlighter that gives up
34// is a highlighter that flickers off.
35func Highlight(src string) [][]syntax.Span {
36 return syntax.ScanLines(src, scanLine)
37}
38
39// scanLine colours one line and returns what it leaves open.
40func scanLine(line []rune, open carry) ([]syntax.Span, carry) {
41 s := syntax.NewLineScanner(line)
42
43 // Whatever ran past the end of the previous line is finished first: until
44 // it closes, nothing on this line is code.
45 if !finishCarried(s, &open) {
46 return s.Spans(), open
47 }
48
49 for !s.AtEnd() {
50 scanToken(s, &open)
51 }
52 return s.Spans(), open
53}
54
55// finishCarried closes whatever the previous line left open, and reports
56// whether the rest of this line is code.
57func finishCarried(s *syntax.LineScanner, open *carry) bool {
58 switch {
59 case open.commentDepth > 0:
60 return continueBlockComment(s, open)
61 case open.rawOpen:
62 return continueRawString(s, open)
63 case open.stringOpen:
64 return continueString(s, open)
65 }
66 return true
67}
68
69// scanToken colours whatever starts at the scanner's position.
70func scanToken(s *syntax.LineScanner, open *carry) {
71 r := s.Peek(0)
72
73 switch {
74 case r == ' ' || r == '\t':
75 s.SkipSpaces()
76 case s.HasPrefix(0, "//"):
77 s.TakeRest(syntax.ClassComment)
78 case s.HasPrefix(0, "/*"):
79 startBlockComment(s, open)
80 case r == '#' && (s.Peek(1) == '[' || (s.Peek(1) == '!' && s.Peek(2) == '[')):
81 takeAttribute(s)
82 case isRawStringStart(s):
83 startRawString(s, open)
84 case isByteOrCharStart(s):
85 takeByteLiteral(s, open)
86 case r == '"':
87 startString(s, open)
88 case r == '\'':
89 takeQuoteOrLifetime(s)
90 case syntax.IsDigit(r):
91 takeNumber(s)
92 case syntax.IsLetter(r) || r == '_':
93 takeWord(s)
94 case r == ':':
95 // A path separator and a type annotation are both structure rather
96 // than computation, so they go with the brackets and the commas. ":"
97 // is an operator rune, so this has to come first.
98 s.TakeWhile(syntax.ClassPunctuation, func(c rune) bool { return c == ':' })
99 case s.HasPrefix(0, ".."):
100 // A range really is an operation, and "." is a punctuation rune, so
101 // this has to come first too.
102 s.Take(rangeWidth(s), syntax.ClassOperator)
103 case syntax.IsOperatorRune(r):
104 s.TakeWhile(syntax.ClassOperator, syntax.IsOperatorRune)
105 case syntax.IsPunctuationRune(r) || r == '#' || r == '@' || r == '$':
106 s.Take(1, syntax.ClassPunctuation)
107 default:
108 // A rune nothing here claims — an emoji in an identifier, say — is
109 // stepped over uncoloured rather than guessed at.
110 s.Advance(1)
111 }
112}
113
114// --- comments ---------------------------------------------------------------
115
116// startBlockComment colours a /* that opens on this line, counting the nesting
117// Rust allows.
118func startBlockComment(s *syntax.LineScanner, open *carry) {
119 start := s.Pos()
120 s.Advance(2)
121 open.commentDepth = 1
122
123 consumeComment(s, open)
124 s.Emit(start, s.Pos(), syntax.ClassComment)
125}
126
127// continueBlockComment colours the rest of a comment opened on an earlier line,
128// and reports whether the line has code after it.
129func continueBlockComment(s *syntax.LineScanner, open *carry) bool {
130 consumeComment(s, open)
131 s.Emit(0, s.Pos(), syntax.ClassComment)
132 return open.commentDepth == 0 && !s.AtEnd()
133}
134
135// consumeComment runs to the end of the comment or to the end of the line,
136// keeping the nesting depth in step.
137func consumeComment(s *syntax.LineScanner, open *carry) {
138 for !s.AtEnd() {
139 switch {
140 case s.HasPrefix(0, "/*"):
141 open.commentDepth++
142 s.Advance(2)
143 case s.HasPrefix(0, "*/"):
144 open.commentDepth--
145 s.Advance(2)
146 if open.commentDepth == 0 {
147 return
148 }
149 default:
150 s.Advance(1)
151 }
152 }
153}
154
155// --- attributes -------------------------------------------------------------
156
157// takeAttribute colours #[derive(Debug)] and its inner form #![no_std].
158//
159// An attribute that runs past the end of its line is coloured to the end and
160// not carried: unlike a comment or a string, an unclosed attribute is nearly
161// always a half-typed one, and carrying it would paint the rest of the file.
162func takeAttribute(s *syntax.LineScanner) {
163 start := s.Pos()
164
165 // Step over the # and the ! of the inner form, so that the bracket
166 // matching below starts where the brackets actually are. Counting from the
167 // # instead ends #![no_std] at its second rune, which is a bug this had.
168 s.Advance(1)
169 if s.Peek(0) == '!' {
170 s.Advance(1)
171 }
172
173 depth := 0
174 for !s.AtEnd() {
175 switch s.Peek(0) {
176 case '[':
177 depth++
178 case ']':
179 depth--
180 }
181 s.Advance(1)
182 if depth == 0 {
183 break
184 }
185 }
186 s.Emit(start, s.Pos(), syntax.ClassAttribute)
187}