// lesson: syntax-highlighting

Syntax Highlighting

The last subsystem before assembly. Syntax highlighting sounds like "parse the language" and would be a terrible idea if it were: real parsers demand valid programs, and an editor's buffer is invalid almost always โ€” you're typing, half the file is mid-keystroke. What editors actually run (until you get to tree-sitter-class machinery) is a lexer-shaped state machine: classify every character into a handful of buckets โ€” keyword, string, comment, number, normal โ€” with just enough state to get the annoying cases right.

Per character, because that's what rendering needs: the render loop walks the line emitting SGR color codes (ESC [ 3x m โ€” the same SGR family as your status bar's inverted video) whenever the class changes from one char to the next; a std::vector<Hl> parallel to the line is exactly the right output shape. (Hl is an enum class, and the color mapping lives elsewhere โ€” classification and presentation separated, so the tests can check classification without caring about colors.)

The interesting design decision comes from a nasty fact: /* ... */ comments cross lines. Whether line 500 starts inside a comment depends, in principle, on every line above it. Rescanning the file per keystroke is O(file) per frame; the editor answer is beautiful: per-line highlighting with one bit of carried state. Each line's highlight function takes "did we start inside a comment?" and returns "did we end inside one?" โ€” line n's output feeding line n+1's input, like carry propagation in an adder. Your editor caches per-line results and, after an edit to line n, re-highlights from n downward only while the carry-out changes โ€” type /* at the top of the file and the repaint cascades; fix a typo inside a line and re-highlighting stops after that one line. (Strings, in our dialect as in C's grammar, do not cross lines โ€” an unterminated string dies at the newline, so the string flag is line-local and doesn't join the carry.)

The classification rules, in precedence order โ€” comment state beats string state beats everything, because inside /* */ a quote is just punctuation, and inside "..." a // is just two slashes:

  • Carrying a comment: chars are Comment until */ (both chars Comment), then back to normal scanning.
  • In a string: chars are String; backslash escapes the next char (both String โ€” "a\"b" doesn't end at the escaped quote); the closing " is String; end of line terminates the string without carry.
  • // opens a comment to end of line; /* opens the carrying kind; " opens a string.
  • Numbers: a digit run is Number if it starts after a separator (or at line start); a . between digits stays Number (3.14). A digit glued to a word (x1, return42) is Normal โ€” highlighting 1 in x1 as a number is the kind of half-right that's worse than wrong.
  • Keywords: matched as whole words โ€” separator (or line edge) on both sides. int in printf must not light up.
  • A separator for these purposes: any char that's not a letter, digit, or underscore. (The C-identifier class again โ€” the same definition as the word-motion classes, not by coincidence: both are asking "where do tokens end?")

โ€บ The Highlight State Machine

20 pts

Implement highlight_line(line, keywords, starts_in_comment) โ†’ HlLine{spans, ends_in_comment} per the rules above. spans has exactly one entry per character. An empty line passes the carry through untouched.

This function is your render loop's per-line preprocessor: cache the results, feed each carry-out forward, and re-highlight below an edit only while carry-outs change.

Log in to submit a solution and earn points.