package rustlang // The literals of Rust: strings ordinary and raw, byte strings, characters — // and lifetimes, which begin with the same rune a character does and are the // one place this scanner has to make a decision rather than read one. import ( "strings" "rickub.com/turbo-editors/turbo-core/syntax" ) // --- strings ---------------------------------------------------------------- // isRawStringStart reports whether a raw string opens here: r", r#", br#" and // so on. func isRawStringStart(s *syntax.LineScanner) bool { at := 0 if s.Peek(at) == 'b' { at++ } if s.Peek(at) != 'r' { return false } at++ for s.Peek(at) == '#' { at++ } return s.Peek(at) == '"' } // startRawString colours a raw string from its opener, counting the hashes that // will have to close it. func startRawString(s *syntax.LineScanner, open *carry) { start := s.Pos() if s.Peek(0) == 'b' { s.Advance(1) } s.Advance(1) // the r hashes := 0 for s.Peek(0) == '#' { hashes++ s.Advance(1) } s.Advance(1) // the opening quote open.rawOpen, open.rawHashes = true, hashes consumeRaw(s, open) s.Emit(start, s.Pos(), syntax.ClassString) } // continueRawString colours the rest of a raw string opened on an earlier line. func continueRawString(s *syntax.LineScanner, open *carry) bool { consumeRaw(s, open) s.Emit(0, s.Pos(), syntax.ClassString) return !open.rawOpen && !s.AtEnd() } // consumeRaw runs to the closing quote-plus-hashes, or to the end of the line. // // A raw string has no escapes at all, which is the whole point of it: the only // thing that ends one is a quote followed by exactly as many hashes as opened // it. func consumeRaw(s *syntax.LineScanner, open *carry) { closer := `"` + strings.Repeat("#", open.rawHashes) for !s.AtEnd() { if s.HasPrefix(0, closer) { s.Advance(len([]rune(closer))) open.rawOpen, open.rawHashes = false, 0 return } s.Advance(1) } } // isByteOrCharStart reports whether a byte string or byte character opens here: // b"…" or b'…'. The raw forms are caught by isRawStringStart first. func isByteOrCharStart(s *syntax.LineScanner) bool { return s.Peek(0) == 'b' && (s.Peek(1) == '"' || s.Peek(1) == '\'') } // takeByteLiteral colours b"…" and b'…'. func takeByteLiteral(s *syntax.LineScanner, open *carry) { if s.Peek(1) == '\'' { start := s.Pos() s.Advance(1) consumeQuoted(s, '\'') s.Emit(start, s.Pos(), syntax.ClassChar) return } start := s.Pos() s.Advance(1) // the b s.Advance(1) // the opening quote open.stringOpen = true consumeString(s, open) s.Emit(start, s.Pos(), syntax.ClassString) } // startString colours an ordinary "…" string. func startString(s *syntax.LineScanner, open *carry) { start := s.Pos() s.Advance(1) open.stringOpen = true consumeString(s, open) s.Emit(start, s.Pos(), syntax.ClassString) } // continueString colours the rest of a string opened on an earlier line. Rust // allows a real newline inside "…", so this is not the error state it would be // in most languages. func continueString(s *syntax.LineScanner, open *carry) bool { consumeString(s, open) s.Emit(0, s.Pos(), syntax.ClassString) return !open.stringOpen && !s.AtEnd() } // consumeString runs to the closing quote or to the end of the line, honouring // backslash escapes. // // A backslash at the very end of a line is Rust's line continuation, which eats // the newline and the indentation that follows it. The string stays open either // way, so nothing here has to tell the two apart. func consumeString(s *syntax.LineScanner, open *carry) { for !s.AtEnd() { if s.Peek(0) == '\\' { s.Advance(2) continue } if s.Peek(0) == '"' { s.Advance(1) open.stringOpen = false return } s.Advance(1) } } // consumeQuoted runs to a closing quote on this line, honouring escapes. func consumeQuoted(s *syntax.LineScanner, quote rune) { s.Advance(1) // the opening quote for !s.AtEnd() { if s.Peek(0) == '\\' { s.Advance(2) continue } if s.Peek(0) == quote { s.Advance(1) return } s.Advance(1) } } // --- characters and lifetimes ----------------------------------------------- // takeQuoteOrLifetime tells a character literal from a lifetime. // // They begin with the same rune, and Rust settles it by what follows: 'a' is a // character and 'a is a lifetime. The rule here is to look for the closing // quote where a character literal would have to put it — one rune along, or two // for an escape — and to read a lifetime when it is not there. That gets // 'static, '\n', 'a', '\u{1F600}' and 'a right, and it is decided entirely from // the line in front of it. func takeQuoteOrLifetime(s *syntax.LineScanner) { if isCharLiteral(s) { start := s.Pos() consumeQuoted(s, '\'') s.Emit(start, s.Pos(), syntax.ClassChar) return } // A lifetime is coloured as a type: it is a generic parameter, declared and // used in the same places one is. Quote and name go out as **one** span — // emitting the name first and the quote after it put the line's spans out // of order, which the editor draws wrongly rather than noticing. start := s.Pos() s.Advance(1) for !s.AtEnd() && syntax.IsWordRune(s.Peek(0)) { s.Advance(1) } s.Emit(start, s.Pos(), syntax.ClassType) } // isCharLiteral reports whether the quote at the scanner's position opens a // character literal rather than a lifetime. func isCharLiteral(s *syntax.LineScanner) bool { if s.Peek(1) == '\\' { // An escape: '\n' closes at 3, '\u{1F600}' further along. Look for the // quote rather than decoding the escape. for at := 2; at < 12; at++ { if s.Peek(at) == '\'' { return true } if s.Peek(at) == 0 { return false } } return false } return s.Peek(1) != 0 && s.Peek(2) == '\'' }