package rustlang import "rickub.com/turbo-editors/turbo-core/syntax" // carry is what a line of Rust leaves open for the next one. // // Three constructs in Rust can cross a line break, and each of them has to be // remembered exactly rather than guessed at: a block comment (which nests, so a // depth and not a flag), a raw string (whose terminator is a quote followed by // as many hashes as its opener had), and an ordinary string (which may contain // a real newline). Anything else is decided from the line in front of you. type carry struct { // commentDepth is how many /* are still open. Rust nests block comments, // so /* /* */ */ is one comment and a flag would end it one level early. commentDepth int // rawHashes is the number of # a raw string was opened with, and rawOpen // says whether one is open at all — a raw string may be opened with none, // so the count alone cannot say. rawOpen bool rawHashes int // stringOpen says an ordinary "…" string ran past the end of a line. stringOpen bool } // Highlight colours Rust source. // // It is written against syntax.LineScanner, a line at a time, with the three // multi-line constructs above threaded through carry. Rust has no tokeniser in // the Go standard library the way Go does, so this is a scanner in the same // style as the ones turbo-core ships for TOML, Markdown and shell. // // It is deliberately tolerant of broken input: source under the cursor is // invalid most of the time it is being typed, and a highlighter that gives up // is a highlighter that flickers off. func Highlight(src string) [][]syntax.Span { return syntax.ScanLines(src, scanLine) } // scanLine colours one line and returns what it leaves open. func scanLine(line []rune, open carry) ([]syntax.Span, carry) { s := syntax.NewLineScanner(line) // Whatever ran past the end of the previous line is finished first: until // it closes, nothing on this line is code. if !finishCarried(s, &open) { return s.Spans(), open } for !s.AtEnd() { scanToken(s, &open) } return s.Spans(), open } // finishCarried closes whatever the previous line left open, and reports // whether the rest of this line is code. func finishCarried(s *syntax.LineScanner, open *carry) bool { switch { case open.commentDepth > 0: return continueBlockComment(s, open) case open.rawOpen: return continueRawString(s, open) case open.stringOpen: return continueString(s, open) } return true } // scanToken colours whatever starts at the scanner's position. func scanToken(s *syntax.LineScanner, open *carry) { r := s.Peek(0) switch { case r == ' ' || r == '\t': s.SkipSpaces() case s.HasPrefix(0, "//"): s.TakeRest(syntax.ClassComment) case s.HasPrefix(0, "/*"): startBlockComment(s, open) case r == '#' && (s.Peek(1) == '[' || (s.Peek(1) == '!' && s.Peek(2) == '[')): takeAttribute(s) case isRawStringStart(s): startRawString(s, open) case isByteOrCharStart(s): takeByteLiteral(s, open) case r == '"': startString(s, open) case r == '\'': takeQuoteOrLifetime(s) case syntax.IsDigit(r): takeNumber(s) case syntax.IsLetter(r) || r == '_': takeWord(s) case r == ':': // A path separator and a type annotation are both structure rather // than computation, so they go with the brackets and the commas. ":" // is an operator rune, so this has to come first. s.TakeWhile(syntax.ClassPunctuation, func(c rune) bool { return c == ':' }) case s.HasPrefix(0, ".."): // A range really is an operation, and "." is a punctuation rune, so // this has to come first too. s.Take(rangeWidth(s), syntax.ClassOperator) case syntax.IsOperatorRune(r): s.TakeWhile(syntax.ClassOperator, syntax.IsOperatorRune) case syntax.IsPunctuationRune(r) || r == '#' || r == '@' || r == '$': s.Take(1, syntax.ClassPunctuation) default: // A rune nothing here claims — an emoji in an identifier, say — is // stepped over uncoloured rather than guessed at. s.Advance(1) } } // --- comments --------------------------------------------------------------- // startBlockComment colours a /* that opens on this line, counting the nesting // Rust allows. func startBlockComment(s *syntax.LineScanner, open *carry) { start := s.Pos() s.Advance(2) open.commentDepth = 1 consumeComment(s, open) s.Emit(start, s.Pos(), syntax.ClassComment) } // continueBlockComment colours the rest of a comment opened on an earlier line, // and reports whether the line has code after it. func continueBlockComment(s *syntax.LineScanner, open *carry) bool { consumeComment(s, open) s.Emit(0, s.Pos(), syntax.ClassComment) return open.commentDepth == 0 && !s.AtEnd() } // consumeComment runs to the end of the comment or to the end of the line, // keeping the nesting depth in step. func consumeComment(s *syntax.LineScanner, open *carry) { for !s.AtEnd() { switch { case s.HasPrefix(0, "/*"): open.commentDepth++ s.Advance(2) case s.HasPrefix(0, "*/"): open.commentDepth-- s.Advance(2) if open.commentDepth == 0 { return } default: s.Advance(1) } } } // --- attributes ------------------------------------------------------------- // takeAttribute colours #[derive(Debug)] and its inner form #![no_std]. // // An attribute that runs past the end of its line is coloured to the end and // not carried: unlike a comment or a string, an unclosed attribute is nearly // always a half-typed one, and carrying it would paint the rest of the file. func takeAttribute(s *syntax.LineScanner) { start := s.Pos() // Step over the # and the ! of the inner form, so that the bracket // matching below starts where the brackets actually are. Counting from the // # instead ends #![no_std] at its second rune, which is a bug this had. s.Advance(1) if s.Peek(0) == '!' { s.Advance(1) } depth := 0 for !s.AtEnd() { switch s.Peek(0) { case '[': depth++ case ']': depth-- } s.Advance(1) if depth == 0 { break } } s.Emit(start, s.Pos(), syntax.ClassAttribute) }