Reference: languages coloured
Neutral description of which files Turbo JS colours, how it decides, and what each scanner recognises.
Recognition
A file's extension decides whenever it is one of these:
| Extension | Language |
|---|---|
.js, .mjs, .cjs |
JavaScript |
.json, .jsonc |
JSON |
.toml |
TOML |
.yaml, .yml |
YAML |
.md, .markdown |
Markdown |
.html, .htm |
HTML |
.xml, .xsd, .xsl, .xslt, .svg, .plist, .csproj, .pom |
XML |
.sh, .bash, .zsh |
Shell |
.dockerfile, .containerfile |
Dockerfile |
Extensions are matched case-insensitively, and only the last one counts: notes.js.md is Markdown, main.js.backup is not JavaScript, and main.test.js is JavaScript.
A file whose extension decides nothing is looked up by name next. Only files that carry no useful extension need this:
| Name | Language |
|---|---|
Dockerfile, Containerfile |
Dockerfile |
A name matches on the whole of it or on the part before the first dot, ignoring case — so Dockerfile, dockerfile and Dockerfile.dev are all recognised, while Dockerfile.md is Markdown, because the extension is consulted first.
A file that neither table claims is read by its first line. A shebang naming node makes it JavaScript: a command-line tool written in JavaScript is a file with no extension whose first line is #!/usr/bin/env node, and Node strips that line before parsing. A shebang naming a shell — sh, bash, zsh, dash or ksh — makes it a shell script. The interpreter is recognised as a path element or as the argument to env.
| First line | Result |
|---|---|
#!/usr/bin/env node |
JavaScript |
#!/usr/local/bin/node |
JavaScript |
#!/usr/bin/env -S node --no-warnings |
JavaScript |
#!/bin/sh |
Shell |
#!/usr/bin/env bash |
Shell |
#!/usr/bin/env python3 |
Not coloured |
Anything not starting #! |
Not coloured |
The order is fixed — extension, then name, then first line — and the first to decide wins.
Everything else is shown in plain text. That is not an error — opening a PNG in the editor is not a mistake, it is just not coloured. In particular TypeScript (.ts, .tsx), JSX (.jsx) and CSS are not coloured: the language server serves .ts files all the same, but the scanner here is for JavaScript.
Classes
Every scanner produces the same vocabulary of classes, and each maps to one theme key.
| Class | Theme key | Produced by |
|---|---|---|
identifier |
syntax.identifier |
JavaScript, JSON (a bare word, which is not JSON), TOML, shell, YAML, Dockerfile |
keyword |
syntax.keyword |
JavaScript, shell, HTML (doctype), XML, Dockerfile |
type |
syntax.type |
JavaScript (class names and capitalised names), TOML (table headers), YAML (tags) |
builtin |
syntax.builtin |
JavaScript (the standard library's and Node's globals), shell (builtins and expansions), YAML (anchors and aliases), Dockerfile (variables) |
constant |
syntax.constant |
JavaScript, JSON, TOML, shell, YAML, HTML and XML (entities) |
function |
syntax.function |
JavaScript, shell (the command) |
string |
syntax.string |
all |
char |
syntax.char |
JavaScript (regular-expression literals) |
number |
syntax.number |
JavaScript, JSON, TOML, shell, YAML, Dockerfile |
comment |
syntax.comment |
JavaScript, JSON, TOML, shell, HTML, YAML, XML, Dockerfile |
operator |
syntax.operator |
JavaScript, TOML, shell, HTML, YAML (block scalar headers), XML, Dockerfile |
punctuation |
syntax.punctuation |
JavaScript, JSON, TOML, shell, Markdown, YAML, Dockerfile |
heading |
syntax.heading |
Markdown |
tag |
syntax.tag |
HTML, XML |
attribute |
syntax.attribute |
JSON (keys), JavaScript (decorators), HTML, XML, Dockerfile (flags) |
emphasis |
syntax.emphasis |
Markdown |
link |
syntax.link |
Markdown |
JavaScript produces no heading, tag, emphasis or link span. In turbo-classic, syntax.number and syntax.constant are both magenta and syntax.char is the green of syntax.string, so 42 and true share a colour there and so do /re/ and "re"; other themes separate them. See how to write your own theme if you want to change it.
JavaScript
Hand-written, in internal/jslang, and registered under the same name as the scanner turbo-core ships for JavaScript, which it replaces. What it adds over the library's: regular-expression literals, the hashbang line, Node's globals, the name after function and class, private names, decorators, and a leading capital read as a class name.
Two constructs cross a line break, and are carried to the next line: a /* … */ block comment to its closing */, and a `…` template literal to its closing backtick. Neither nests. An ordinary '…' or "…" string does not cross a line: an unterminated one is coloured to the end of its line, and the next line is code again.
| Recognised | As |
|---|---|
as, async, await, break, case, catch, class, const, continue, debugger, default, delete, do, else, export, extends, finally, for, from, function, get, if, import, in, instanceof, let, new, of, return, set, static, super, switch, throw, try, typeof, var, void, while, with, yield, and the reserved enum, implements, interface, package, private, protected, public |
keyword |
true, false, null, undefined, NaN, Infinity, this |
constant |
the standard library's globals — Array, Object, Promise, Math, JSON, Map, Set, Symbol, BigInt, Error, TypeError, parseInt, setTimeout, structuredClone, fetch, URL, … |
builtin |
Node's globals — process, Buffer, console, require, module, exports, __dirname, __filename, setImmediate, performance, crypto — and the browser's document and window |
builtin |
the name after function, function* or async function — parse in function parse(input) {} |
function |
the name after class — widget in class widget extends Base {} |
type |
any other name starting with an ASCII capital — Greeter, EventEmitter, MyError — even before a ( |
type |
any other name immediately before ( — compute(3), obj.method(), this.#reset() |
function |
any word after . or ?., whatever it is spelt like — map.get(k) a function, options.default a name, user?.class a name |
property, never a keyword |
get, set, static, of, from and as before a ( — get(key) |
function |
#count, a private member, the # included |
identifier, or function before ( |
@decorator, @observable.ref, the dotted path included |
attribute |
any other name: a Unicode letter, _ or $, then letters, digits and the same — x, $el, _, café, 名前 |
identifier |
"…", '…' with backslash escapes, on one line |
string |
`…`, ${…} interpolations included, across lines |
string |
/…/gi, a regular-expression literal, flags included, where the previous token allows one and a closing / exists on the line |
char |
42, 1_000, 0xFF, 0o17, 0b1010, 1.5e-3, 2E+10, .5, 10n, 0xFFn |
number |
// to the end of the line; /* … */ and /** … */ across lines |
comment |
#! on the first line of the file, to its end |
comment |
... |
punctuation, as one span |
?. |
operator, as one span |
runs of +-*/%=<>!&|^~?: — including =>, ??, ===, **, >>>= |
operator |
()[]{},; and a lone . |
punctuation |
A slash divides after a value and opens a regular expression everywhere else. After a name, a number, a string, a template, a regular expression, ) or ], a / is division. After an operator, (, [, {, }, ,, ;, :, a keyword, or at the start of a line, it opens a regular expression — provided a closing / exists on the same line; otherwise it divides, whatever came before it. A regular expression cannot cross a line, so that bound keeps a wrong guess to one line.
A point joins a number only when a digit follows it, so 1.toString() is the number 1, a dot and a method. The sign after an e is part of the number, except in a hexadecimal literal, where 0xE+1 is a sum.
A capitalised name is a class by convention, not by rule. JavaScript lets you write const Count = 1, and it is coloured as a class all the same, as is MAX_SIZE. Classes and constructors are what people capitalise, and the colour follows the people.
Not recognised, each for a stated reason:
| Not recognised | Because |
|---|---|
The code inside ${…} in a template |
Colouring it means the scanner reaching back into itself with a nesting depth to carry, for a construct that is usually one short expression; the whole literal is a string |
A backtick inside ${…} |
For the same reason: it ends the template early, and the code after it is coloured as code until the next backtick |
| A regular expression with no closing slash on its line | It cannot be one — the language forbids a newline in the literal — so the slash divides |
| JSX | <div> is read as an operator, a name and an operator; there is no HTML inside JavaScript here |
| TypeScript's annotations and types | A different language; .ts files are not coloured at all |
| Nested block comments | JavaScript ends a block comment at the first */; a depth would be a claim about a different language |
| A string continued with a trailing backslash | The string stops at its line and the next line is code; the continuation is rare enough not to be worth carrying |
Whether a SCREAMING_SNAKE_CASE name is a constant |
The leading capital says class, and nothing in the spelling separates the two conventions |
# as a comment |
It is one only in the hashbang, on the first line; anywhere else # begins a private name or is stepped over |
| Whether a name is bound in this scope | Nothing here reads more than one line at a time; that is the language server's question, and F1 answers it |
JSON
Hand-written, in internal/jslang/json.go, thirty lines, because the language is thirty lines: strings, numbers, three words, six punctuation marks. It is the language every Node project's manifest is written in, and turbo-core does not colour it.
| Recognised | As |
|---|---|
"name" followed by a colon — a key |
attribute |
"demo" anywhere else — a value |
string |
-12.5e+3, 42, 0.5 |
number |
true, false, null |
constant |
{, }, [, ], ,, : |
punctuation |
// to the end of the line; /* … */ across lines |
comment |
any bare word — undefined, NaN, a key without quotes |
identifier |
A string is a key when a colon follows it, past any spaces, and a value otherwise. {"a" 1} colours "a" as a value: it is not a key yet.
Comments are tolerated, not endorsed. Strict JSON has none; tsconfig.json, .jsonc files and every editor's own settings file do, and a scanner that painted them as broken would be wrong in exactly the files most likely to hold one.
| Not recognised | Because |
|---|---|
| Whether the document is valid JSON | The scanner colours tokens; a bare word comes out as an identifier rather than stopping the line, and a duplicated key looks like any other |
| Single-quoted strings, trailing commas and the rest of JSON5 | Not JSON; a ' is stepped over uncoloured |
| An unterminated string stopping anywhere but its line | It is coloured to the end of the line, as a value, and the next line is JSON again |
TOML
| Recognised | As |
|---|---|
# comment |
comment |
[table], [[array]] |
the name as a type, the brackets as punctuation |
key = |
identifier, then operator |
"basic", 'literal', """multi-line""", '''multi-line''' |
string |
true, false |
constant |
numbers, dates, times, inf, nan |
number |
YAML
A compose file, a Kubernetes manifest and a CI workflow are all this: there is no separate dialect, because a dialect would be somebody else's schema to keep in step with.
| Recognised | As |
|---|---|
# comment |
comment |
key: before a space or the end of the line |
the key as an identifier, the colon as punctuation |
"quoted": 1, 'quoted': 1 |
the quoted key as an identifier |
- opening a sequence entry |
punctuation |
"…", '…' |
string |
true, false, yes, no, on, off, null |
constant, whatever their case |
| numbers, dates and times written without quotes | number |
&anchor, *alias |
builtin |
!!str, !Custom |
type |
---, ... |
the whole line as punctuation |
{, }, [, ], , |
punctuation |
|, >, with their chomping and indentation indicators |
the header as an operator, the body as a string |
A colon is a separator only when a space or the end of the line follows it. image: node:24-alpine is a key and one value, and url: http://example.com/x is a key and one URL — colouring the inner colons as separators would put every image tag and every URL in three colours.
A block scalar's extent is decided by indentation, not by a delimiter. The first content line after | or > fixes the block's indentation; every line indented at least that far belongs to it, and the first line that is not ends it. A blank line inside a block stays inside it: a literal scalar keeps its empty lines, and ending the block at the first paragraph break would cut a shell script in a CI file in half.
A # needs a space before it to start a comment, so colour: ff#00aa is one scalar.
| Not recognised | Because |
|---|---|
| The schema of a compose file, a manifest or a workflow | Colouring services: differently from any other key means carrying somebody else's schema, and it goes stale the day they add a key |
| Multi-document streams as separate documents | --- is coloured, but nothing is reset at it; nothing in the colouring depends on document boundaries |
| Whether a bare word is a string or a number to a parser | 1.2.3 is a version to a reader and a string to YAML; the scanner colours what it looks like |
Markdown
| Recognised | As |
|---|---|
# Heading … ###### Heading |
the whole line as a heading |
**bold**, __bold__, *italic*, _italic_ |
emphasis |
`code` |
string |
[text](target),  |
the whole thing as a link |
- , * , + , 1. , 1) |
the marker as punctuation |
> |
punctuation |
---, ***, ___ |
punctuation |
``` and ~~~ fences |
the whole block, opening and closing lines included, as a string |
A fenced block is one colour whatever language it announces: ```javascript does not colour its contents as JavaScript in a Markdown file. The run of markers that opens a block must be matched by the same character to close it, so a backtick fence is not closed by a tilde one. An unclosed fence colours to the end of the file.
The run of markers opening emphasis must be matched by a run of the same length, so **bold** is one span rather than two italics.
HTML
| Recognised | As |
|---|---|
<tag, </tag, >, /> |
tag |
attribute names, including data-*, xlink:href, @click, v-bind.prop |
attribute |
= |
operator |
"…", '…' |
string |
<!-- … -->, across lines |
comment |
&, © |
constant |
<!DOCTYPE …> and other declarations |
keyword |
Text between tags is not coloured. A bare & with no ; within 32 characters is left alone, because it is legal text.
The contents of <script> and <style> are not coloured as JavaScript and CSS, even in this editor: the HTML scanner is turbo-core's, and it does not hand a region of a line to another scanner.
XML
Its own scanner rather than HTML's, for one reason that matters: CDATA. The whole point of <![CDATA[ … ]]> is that its contents are not markup, and colouring the tags inside one as tags is exactly backwards.
| Recognised | As |
|---|---|
<?xml version="1.0"?> and other processing instructions |
the target and ?> as keyword, the pairs between as attributes and strings |
<!DOCTYPE …> and the other <! forms |
keyword |
<!-- … -->, across lines |
comment |
<![CDATA[ … ]]>, across lines |
string |
<tag, </tag, >, /> |
tag |
<ns:tag>, xsi:type |
the prefix and the local name as one span |
| attribute names | attribute |
= |
operator |
"…", '…' |
string |
&, © |
constant |
A comment and a CDATA section close on different delimiters, and are carried separately: a --> inside a CDATA section does not end it.
A bare & with no semicolon within 32 characters is left alone, because it is legal text in plenty of documents and swallowing the rest of the line would be the bigger mistake.
Text between tags is not coloured.
Shell
Applies to sh, bash and zsh alike: the keywords recognised are the ones they share.
| Recognised | As |
|---|---|
if, then, fi, for, while, case, esac, function, return, … |
keyword |
true, false |
constant |
echo, printf, export, local, read, cd, set, source, … |
builtin |
$NAME, ${…}, $(…), $1, $?, $@ |
builtin |
| the first bare word on a line | function |
every later bare word, and NAME in NAME=value |
identifier |
'…', with nothing escaped or expanded inside |
string |
"…", with the expansions inside it coloured as expansions |
string |
# to end of line |
comment |
$(a $(b) c) is one span: nesting is counted. An option such as -euo is one word, not a minus and a word.
Heredocs are not recognised. <<EOF and the text after it are coloured as ordinary shell.
Dockerfile
| Recognised | As |
|---|---|
FROM, RUN, COPY, ADD, ARG, ENV, CMD, ENTRYPOINT, EXPOSE, LABEL, USER, VOLUME, WORKDIR, HEALTHCHECK, ONBUILD, SHELL, STOPSIGNAL, MAINTAINER |
keyword, in any case |
AS, NONE |
keyword |
# comment, including the # syntax= and # escape= directives |
comment |
--from=builder, --chown=me:me |
the flag name as an attribute |
$NAME, ${NAME}, ${NAME:-default} |
builtin, as one span to the closing brace |
"…", '…' |
string |
a trailing \ |
operator |
| numbers | number |
paths and image references — /usr/local/bin, node:24-alpine |
identifier, as one span |
Only the first word of a line can be an instruction, and a word that is not one is an argument — which is what keeps a continuation line's first word out of the keyword colour.
Nothing crosses a line break. A \ joins two lines for Docker, but each half still reads as a command and is coloured on its own.
| Not recognised | Because |
|---|---|
The shell inside a RUN |
It would mean running the shell scanner over part of a line and mapping its columns out, and RUN may hold any language |
Heredocs in a RUN |
The same reason the shell scanner does not recognise them |
Which stage a --from names |
Nothing here reads the rest of the file |
See also
- Theme file format — every key these classes resolve to
- Colouring and completion — why the scanners are written this way
- How to write your own theme
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 |
|