| 📦 Turbo Rust 713ea5c k33g 19h ago | 1 | package rustlang |
| 2 | |
| 3 | // The literals of Rust: strings ordinary and raw, byte strings, characters — |
| 4 | // and lifetimes, which begin with the same rune a character does and are the |
| 5 | // one place this scanner has to make a decision rather than read one. |
| 6 | |
| 7 | import ( |
| 8 | "strings" |
| 9 | |
| 10 | "rickub.com/turbo-editors/turbo-core/syntax" |
| 11 | ) |
| 12 | |
| 13 | // --- strings ---------------------------------------------------------------- |
| 14 | |
| 15 | // isRawStringStart reports whether a raw string opens here: r", r#", br#" and |
| 16 | // so on. |
| 17 | func isRawStringStart(s *syntax.LineScanner) bool { |
| 18 | at := 0 |
| 19 | if s.Peek(at) == 'b' { |
| 20 | at++ |
| 21 | } |
| 22 | if s.Peek(at) != 'r' { |
| 23 | return false |
| 24 | } |
| 25 | at++ |
| 26 | for s.Peek(at) == '#' { |
| 27 | at++ |
| 28 | } |
| 29 | return s.Peek(at) == '"' |
| 30 | } |
| 31 | |
| 32 | // startRawString colours a raw string from its opener, counting the hashes that |
| 33 | // will have to close it. |
| 34 | func startRawString(s *syntax.LineScanner, open *carry) { |
| 35 | start := s.Pos() |
| 36 | if s.Peek(0) == 'b' { |
| 37 | s.Advance(1) |
| 38 | } |
| 39 | s.Advance(1) // the r |
| 40 | |
| 41 | hashes := 0 |
| 42 | for s.Peek(0) == '#' { |
| 43 | hashes++ |
| 44 | s.Advance(1) |
| 45 | } |
| 46 | s.Advance(1) // the opening quote |
| 47 | |
| 48 | open.rawOpen, open.rawHashes = true, hashes |
| 49 | consumeRaw(s, open) |
| 50 | s.Emit(start, s.Pos(), syntax.ClassString) |
| 51 | } |
| 52 | |
| 53 | // continueRawString colours the rest of a raw string opened on an earlier line. |
| 54 | func continueRawString(s *syntax.LineScanner, open *carry) bool { |
| 55 | consumeRaw(s, open) |
| 56 | s.Emit(0, s.Pos(), syntax.ClassString) |
| 57 | return !open.rawOpen && !s.AtEnd() |
| 58 | } |
| 59 | |
| 60 | // consumeRaw runs to the closing quote-plus-hashes, or to the end of the line. |
| 61 | // |
| 62 | // A raw string has no escapes at all, which is the whole point of it: the only |
| 63 | // thing that ends one is a quote followed by exactly as many hashes as opened |
| 64 | // it. |
| 65 | func consumeRaw(s *syntax.LineScanner, open *carry) { |
| 66 | closer := `"` + strings.Repeat("#", open.rawHashes) |
| 67 | |
| 68 | for !s.AtEnd() { |
| 69 | if s.HasPrefix(0, closer) { |
| 70 | s.Advance(len([]rune(closer))) |
| 71 | open.rawOpen, open.rawHashes = false, 0 |
| 72 | return |
| 73 | } |
| 74 | s.Advance(1) |
| 75 | } |
| 76 | } |
| 77 | |
| 78 | // isByteOrCharStart reports whether a byte string or byte character opens here: |
| 79 | // b"…" or b'…'. The raw forms are caught by isRawStringStart first. |
| 80 | func isByteOrCharStart(s *syntax.LineScanner) bool { |
| 81 | return s.Peek(0) == 'b' && (s.Peek(1) == '"' || s.Peek(1) == '\'') |
| 82 | } |
| 83 | |
| 84 | // takeByteLiteral colours b"…" and b'…'. |
| 85 | func takeByteLiteral(s *syntax.LineScanner, open *carry) { |
| 86 | if s.Peek(1) == '\'' { |
| 87 | start := s.Pos() |
| 88 | s.Advance(1) |
| 89 | consumeQuoted(s, '\'') |
| 90 | s.Emit(start, s.Pos(), syntax.ClassChar) |
| 91 | return |
| 92 | } |
| 93 | |
| 94 | start := s.Pos() |
| 95 | s.Advance(1) // the b |
| 96 | s.Advance(1) // the opening quote |
| 97 | open.stringOpen = true |
| 98 | consumeString(s, open) |
| 99 | s.Emit(start, s.Pos(), syntax.ClassString) |
| 100 | } |
| 101 | |
| 102 | // startString colours an ordinary "…" string. |
| 103 | func startString(s *syntax.LineScanner, open *carry) { |
| 104 | start := s.Pos() |
| 105 | s.Advance(1) |
| 106 | open.stringOpen = true |
| 107 | |
| 108 | consumeString(s, open) |
| 109 | s.Emit(start, s.Pos(), syntax.ClassString) |
| 110 | } |
| 111 | |
| 112 | // continueString colours the rest of a string opened on an earlier line. Rust |
| 113 | // allows a real newline inside "…", so this is not the error state it would be |
| 114 | // in most languages. |
| 115 | func continueString(s *syntax.LineScanner, open *carry) bool { |
| 116 | consumeString(s, open) |
| 117 | s.Emit(0, s.Pos(), syntax.ClassString) |
| 118 | return !open.stringOpen && !s.AtEnd() |
| 119 | } |
| 120 | |
| 121 | // consumeString runs to the closing quote or to the end of the line, honouring |
| 122 | // backslash escapes. |
| 123 | // |
| 124 | // A backslash at the very end of a line is Rust's line continuation, which eats |
| 125 | // the newline and the indentation that follows it. The string stays open either |
| 126 | // way, so nothing here has to tell the two apart. |
| 127 | func consumeString(s *syntax.LineScanner, open *carry) { |
| 128 | for !s.AtEnd() { |
| 129 | if s.Peek(0) == '\\' { |
| 130 | s.Advance(2) |
| 131 | continue |
| 132 | } |
| 133 | if s.Peek(0) == '"' { |
| 134 | s.Advance(1) |
| 135 | open.stringOpen = false |
| 136 | return |
| 137 | } |
| 138 | s.Advance(1) |
| 139 | } |
| 140 | } |
| 141 | |
| 142 | // consumeQuoted runs to a closing quote on this line, honouring escapes. |
| 143 | func consumeQuoted(s *syntax.LineScanner, quote rune) { |
| 144 | s.Advance(1) // the opening quote |
| 145 | for !s.AtEnd() { |
| 146 | if s.Peek(0) == '\\' { |
| 147 | s.Advance(2) |
| 148 | continue |
| 149 | } |
| 150 | if s.Peek(0) == quote { |
| 151 | s.Advance(1) |
| 152 | return |
| 153 | } |
| 154 | s.Advance(1) |
| 155 | } |
| 156 | } |
| 157 | |
| 158 | // --- characters and lifetimes ----------------------------------------------- |
| 159 | |
| 160 | // takeQuoteOrLifetime tells a character literal from a lifetime. |
| 161 | // |
| 162 | // They begin with the same rune, and Rust settles it by what follows: 'a' is a |
| 163 | // character and 'a is a lifetime. The rule here is to look for the closing |
| 164 | // quote where a character literal would have to put it — one rune along, or two |
| 165 | // for an escape — and to read a lifetime when it is not there. That gets |
| 166 | // 'static, '\n', 'a', '\u{1F600}' and 'a right, and it is decided entirely from |
| 167 | // the line in front of it. |
| 168 | func takeQuoteOrLifetime(s *syntax.LineScanner) { |
| 169 | if isCharLiteral(s) { |
| 170 | start := s.Pos() |
| 171 | consumeQuoted(s, '\'') |
| 172 | s.Emit(start, s.Pos(), syntax.ClassChar) |
| 173 | return |
| 174 | } |
| 175 | |
| 176 | // A lifetime is coloured as a type: it is a generic parameter, declared and |
| 177 | // used in the same places one is. Quote and name go out as **one** span — |
| 178 | // emitting the name first and the quote after it put the line's spans out |
| 179 | // of order, which the editor draws wrongly rather than noticing. |
| 180 | start := s.Pos() |
| 181 | s.Advance(1) |
| 182 | for !s.AtEnd() && syntax.IsWordRune(s.Peek(0)) { |
| 183 | s.Advance(1) |
| 184 | } |
| 185 | s.Emit(start, s.Pos(), syntax.ClassType) |
| 186 | } |
| 187 | |
| 188 | // isCharLiteral reports whether the quote at the scanner's position opens a |
| 189 | // character literal rather than a lifetime. |
| 190 | func isCharLiteral(s *syntax.LineScanner) bool { |
| 191 | if s.Peek(1) == '\\' { |
| 192 | // An escape: '\n' closes at 3, '\u{1F600}' further along. Look for the |
| 193 | // quote rather than decoding the escape. |
| 194 | for at := 2; at < 12; at++ { |
| 195 | if s.Peek(at) == '\'' { |
| 196 | return true |
| 197 | } |
| 198 | if s.Peek(at) == 0 { |
| 199 | return false |
| 200 | } |
| 201 | } |
| 202 | return false |
| 203 | } |
| 204 | return s.Peek(1) != 0 && s.Peek(2) == '\'' |
| 205 | } |