| 📦 Turbo MoonBit cc1f595 k33g yesterday | 1 | # Reference: languages coloured |
| 2 | |
| 3 | > Neutral description of which files Turbo MoonBit colours, how it decides, and what each scanner recognises. |
| 4 | |
| 5 | ## Recognition |
| 6 | |
| 7 | A file's **extension** decides whenever it is one of these: |
| 8 | |
| 9 | | Extension | Language | |
| 10 | | --- | --- | |
| 11 | | `.mbt`, `.mbti`, `.mbtx` | MoonBit | |
| 12 | | `.toml` | TOML | |
| 13 | | `.yaml`, `.yml` | YAML | |
| 14 | | `.md`, `.markdown` | Markdown | |
| 15 | | `.js`, `.mjs`, `.cjs` | JavaScript | |
| 16 | | `.html`, `.htm` | HTML | |
| 17 | | `.xml`, `.xsd`, `.xsl`, `.xslt`, `.svg`, `.plist`, `.csproj`, `.pom` | XML | |
| 18 | | `.sh`, `.bash`, `.zsh` | Shell | |
| 19 | | `.dockerfile`, `.containerfile` | Dockerfile | |
| 20 | |
| 21 | Extensions are matched case-insensitively, and only the last one counts: `README.mbt.md` is Markdown, and `main.mbt.backup` is not MoonBit. |
| 22 | |
| 23 | A file whose extension decides nothing is looked up by **name** next. Only files that carry no useful extension need this: |
| 24 | |
| 25 | | Name | Language | |
| 26 | | --- | --- | |
| 27 | | `Dockerfile`, `Containerfile` | Dockerfile | |
| 28 | |
| 29 | 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. |
| 30 | |
| 31 | `moon.mod`, `moon.pkg` and `moon.work` are **not** in that table. They are MoonBit's own configuration DSL rather than MoonBit, and their legacy JSON forms — `moon.mod.json`, `moon.pkg.json` — are not JSON that this editor colours either. All five open in plain text. |
| 32 | |
| 33 | A file that neither table claims is read by its **first line**. A shebang naming a shell — `sh`, `bash`, `zsh`, `dash` or `ksh` — makes it a shell script, and the interpreter is recognised as a path element or as the argument to `env`. That is what colours a script in a `bin` directory, a git hook, or `configure`. |
| 34 | |
| 35 | **No shebang makes a file MoonBit.** The language has no interpreter line: a file opening with `#!` would lex as an attribute named `!` and fail. A file with no extension is not MoonBit, and claiming otherwise would take a shell script away from the scanner that can actually colour it. |
| 36 | |
| 37 | | First line | Result | |
| 38 | | --- | --- | |
| 39 | | `#!/bin/sh` | Shell | |
| 40 | | `#!/usr/bin/env bash` | Shell | |
| 41 | | `#!/usr/bin/env -S bash -e` | Shell | |
| 42 | | `#!/usr/bin/env moon` | Not coloured | |
| 43 | | `#!/usr/bin/env node` | Not coloured | |
| 44 | | Anything not starting `#!` | Not coloured | |
| 45 | |
| 46 | The order is fixed — extension, then name, then first line — and the first to decide wins. |
| 47 | |
| 48 | 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. |
| 49 | |
| 50 | ## Classes |
| 51 | |
| 52 | Every scanner produces the same vocabulary of classes, and each maps to one theme key. |
| 53 | |
| 54 | | Class | Theme key | Produced by | |
| 55 | | --- | --- | --- | |
| 56 | | `identifier` | `syntax.identifier` | MoonBit, TOML, JavaScript, shell, YAML, Dockerfile | |
| 57 | | `keyword` | `syntax.keyword` | MoonBit, JavaScript, shell, HTML (doctype), XML, Dockerfile | |
| 58 | | `type` | `syntax.type` | MoonBit (every capitalised name, and package qualifiers), TOML (table headers), YAML (tags) | |
| 59 | | `builtin` | `syntax.builtin` | MoonBit (the prelude), JavaScript, shell (builtins and expansions), YAML (anchors and aliases), Dockerfile (variables) | |
| 60 | | `constant` | `syntax.constant` | MoonBit, TOML, JavaScript, shell, YAML, HTML and XML (entities) | |
| 61 | | `function` | `syntax.function` | MoonBit, JavaScript, shell (the command) | |
| 62 | | `string` | `syntax.string` | all | |
| 63 | | `char` | `syntax.char` | MoonBit (`'c'` and `b'c'`) | |
| 64 | | `number` | `syntax.number` | MoonBit, TOML, JavaScript, shell, YAML, Dockerfile | |
| 65 | | `comment` | `syntax.comment` | MoonBit, TOML, JavaScript, shell, HTML, YAML, XML, Dockerfile | |
| 66 | | `operator` | `syntax.operator` | MoonBit, TOML, JavaScript, shell, HTML, YAML (block scalar headers), XML, Dockerfile | |
| 67 | | `punctuation` | `syntax.punctuation` | MoonBit, TOML, JavaScript, shell, Markdown, YAML, Dockerfile | |
| 68 | | `heading` | `syntax.heading` | Markdown | |
| 69 | | `tag` | `syntax.tag` | HTML, XML | |
| 70 | | `attribute` | `syntax.attribute` | MoonBit (attributes and labelled arguments), HTML, XML, Dockerfile (flags) | |
| 71 | | `emphasis` | `syntax.emphasis` | Markdown | |
| 72 | | `link` | `syntax.link` | Markdown | |
| 73 | |
| 74 | In `turbo-classic` alone, `syntax.attribute` and `syntax.identifier` are both plain yellow, so a MoonBit attribute or label is not told apart from an ordinary name in that one theme. The other seven give them different colours. See [how to write your own theme](../how-to/write-a-theme.md) if you want to change it. |
| 75 | |
| 76 | ## MoonBit |
| 77 | |
| 78 | Hand-written, in `internal/moonbitlang`. **Nothing crosses a line break**, and that is a property of the language rather than a simplification: MoonBit has no block comment, a newline before a closing quote is an *unterminated literal* error, a multi-line string is a run of self-contained `#|` or `$|` lines, and an attribute is explicitly one line. So a stray quote colours to the end of its line and the next line is code again. |
| 79 | |
| 80 | | Recognised | As | |
| 81 | | --- | --- | |
| 82 | | `and`, `as`, `async`, `break`, `catch`, `const`, `continue`, `declare`, `defer`, `derive`, `else`, `enum`, `enumview`, `extend`, `extenum`, `extern`, `fn`, `for`, `guard`, `if`, `impl`, `import`, `in`, `is`, `let`, `letrec`, `lexscan`, `loop`, `match`, `mut`, `nobreak`, `nocancel`, `noraise`, `package`, `priv`, `proof_assert`, `proof_let`, `pub`, `raise`, `readonly`, `return`, `struct`, `suberror`, `test`, `throw`, `trait`, `try`, `type`, `using`, `where`, `while`, `with` | keyword | |
| 83 | | `try!` and `guard!`, mark included | keyword | |
| 84 | | `true`, `false`, `None`, `Some`, `Ok`, `Err` | constant | |
| 85 | | any name starting with an ASCII capital — `Int`, `StringBuilder`, `Shape`, `Circle` | type | |
| 86 | | `println`, `abort`, `panic`, `fail`, `ignore`, `inspect`, `debug`, `repr`, `hash`, `compare`, `null`, `assert_eq`, `assert_not_eq`, `assert_true`, `assert_false`, `debug_assert`, `debug_inspect`, `json_inspect`, `physical_equal` | builtin | |
| 87 | | any other lower-case name immediately before `(` | function | |
| 88 | | `"…"`, `b"…"`, `re"…"` | string | |
| 89 | | `'c'`, `b'c'` | char | |
| 90 | | `#\|` and `$\|` | the two-rune prefix as punctuation, the rest of the line as string | |
| 91 | | `42`, `1_000`, `0xFF_FF`, `0o17`, `0b1010`, `1.5`, `1.`, `1.5e-3`, `0x1.8p3F`, `42U`, `42L`, `42UL`, `42N`, `1.0F` | number | |
| 92 | | `//` and `///` to the end of the line | comment | |
| 93 | | `#deprecated("…")`, `#external`, `#custom.attribute(key="v")` — the whole line | attribute | |
| 94 | | `name~` in a labelled argument, tilde included | attribute | |
| 95 | | `@json`, `@moonbitlang/core/builtin`, `@my-pkg` — the `@` included, as one span | type | |
| 96 | | `.0` in a tuple accessor | the dot as punctuation, the digits as number | |
| 97 | | `..`, `..=`, `..<`, `...` | operator | |
| 98 | | runs of `+-*/%=<>!&\|^~?:` | operator | |
| 99 | | `()[]{},;.` | punctuation | |
| 100 | |
| 101 | **Nothing here is a table of built-in types, and nothing needs to be.** MoonBit's identifier case is a *lexical* rule rather than a convention: the grammar says a `uident` "begins with an ASCII uppercase letter", and only a type, a trait or an enum constructor may be spelt that way. `Int`, `StringBuilder` and a type somebody wrote this morning are all coloured by the same line. Every other scanner in this family needs a table here; this one does not. |
| 102 | |
| 103 | **An integer ends before `..`.** The grammar is explicit — "before `..`, an integer ends first, so `1..=2` begins with `1` and `..=`" — so a dot is only part of a number when a second one does not follow it. Without that rule `1..=2` reads as the double `1.` and then `.=2`, and every range in the file is miscoloured. |
| 104 | |
| 105 | **A number's suffix is upper case or it is not a suffix.** `42UL` is one number; `42u` is the number `42` followed by the name `u`, which is what the compiler sees too. |
| 106 | |
| 107 | **An attribute takes the whole line.** The grammar hands it everything after the dotted name: "everything through the next newline is the raw payload". Colouring less than the line would be inventing a structure the lexer does not have. |
| 108 | |
| 109 | **`#|` and `#deprecated` are told apart by the rune after the `#`.** An attribute's name must start with a letter or an underscore; a multi-line string line has a bar there. |
| 110 | |
| 111 | **A doc comment is coloured like any other comment.** `///`, `///|` and `//` all reach `syntax.comment`, because turbo-core's set of classes is closed on purpose — that is what lets one theme colour every language an editor will ever learn. |
| 112 | |
| 113 | **A name after a dot is never a keyword.** MoonBit's dot-identifiers "use the identifier case rules without consulting the keyword table, so `.if` is valid" — a record with a field called `type` is ordinary MoonBit. |
| 114 | |
| 115 | **`package` is coloured as a keyword in a `.mbt` file too**, although it is only a *reserved* word there. It is a real keyword in the `.mbti` interface files this editor also colours, and in a `.mbt` file the colour says exactly what the compiler is about to: this word is not yours to use. The rest of the reserved list — `move`, `ref`, `static`, `unsafe`, `await` and the forty others — is deliberately left alone, because those really are names you may use. |
| 116 | |
| 117 | **A tilde against the end of a lower-case name is a label**, and against anything else it is not: the grammar says "ASCII-uppercase identifiers and keywords cannot form labels", so `Foo~` is a type and a tilde. |
| 118 | |
| 119 | **Not recognised**, each for a stated reason: |
| 120 | |
| 121 | | Not recognised | Because | |
| 122 | | --- | --- | |
| 123 | | The expression inside `\{…}` | The grammar matches it to "the matching `}`", with braces inside nested literals not counting — finding the end needs the parser. `"a \{b} c"` is therefore one string span, brace to brace. **A string nested inside an interpolation is where that stops**: the scanner takes the first unescaped quote as the closer, so `"a \{f("x")} c"` scans as string, then `x` as an identifier, then string. The spans stay in order and never overlap; the cost is a wrong colour inside a nested literal, which is rarer than the brace-counting bugs the alternative would cause | |
| 124 | | An enum constructor of your own, as anything but a type | Nothing in the syntax separates `Circle(1.0)` from a type applied to arguments; inventing a separation means being wrong in both directions instead of one | |
| 125 | | `.5` as a number | MoonBit requires a digit before the point, so a leading dot is a tuple accessor or a dot-identifier and never a literal | |
| 126 | | A reserved word as a keyword | `move`, `ref` and the rest are identifiers the compiler merely warns about, and colouring them would tell a reader they cannot write `let ref = 1` when they can | |
| 127 | | An identifier holding non-ASCII letters | MoonBit allows CJK and several other ranges in a name; the rune predicates this scanner is built on are ASCII, so such a name is stepped over uncoloured rather than guessed at | |
| 128 | | `.mbt.md` as MoonBit | It is a Markdown document with MoonBit in its fences. Its extension is `.md`, and Markdown is what colours it | |
| 129 | | 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](../how-to/ask-about-code.md) | |
| 130 | |
| 131 | ## TOML |
| 132 | |
| 133 | | Recognised | As | |
| 134 | | --- | --- | |
| 135 | | `# comment` | comment | |
| 136 | | `[table]`, `[[array]]` | the name as a type, the brackets as punctuation | |
| 137 | | `key =` | identifier, then operator | |
| 138 | | `"basic"`, `'literal'`, `"""multi-line"""`, `'''multi-line'''` | string | |
| 139 | | `true`, `false` | constant | |
| 140 | | numbers, dates, times, `inf`, `nan` | number | |
| 141 | |
| 142 | ## YAML |
| 143 | |
| 144 | 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. |
| 145 | |
| 146 | | Recognised | As | |
| 147 | | --- | --- | |
| 148 | | `# comment` | comment | |
| 149 | | `key:` before a space or the end of the line | the key as an identifier, the colon as punctuation | |
| 150 | | `"quoted": 1`, `'quoted': 1` | the quoted key as an identifier | |
| 151 | | `- ` opening a sequence entry | punctuation | |
| 152 | | `"…"`, `'…'` | string | |
| 153 | | `true`, `false`, `yes`, `no`, `on`, `off`, `null` | constant, whatever their case | |
| 154 | | numbers, dates and times written without quotes | number | |
| 155 | | `&anchor`, `*alias` | builtin | |
| 156 | | `!!str`, `!Custom` | type | |
| 157 | | `---`, `...` | the whole line as punctuation | |
| 158 | | `{`, `}`, `[`, `]`, `,` | punctuation | |
| 159 | | `\|`, `>`, with their chomping and indentation indicators | the header as an operator, the body as a string | |
| 160 | |
| 161 | **A colon is a separator only when a space or the end of the line follows it.** `image: nginx:1.27` 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. |
| 162 | |
| 163 | **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. |
| 164 | |
| 165 | **A `#` needs a space before it to start a comment**, so `colour: ff#00aa` is one scalar. |
| 166 | |
| 167 | | Not recognised | Because | |
| 168 | | --- | --- | |
| 169 | | 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 | |
| 170 | | Multi-document streams as separate documents | `---` is coloured, but nothing is reset at it; nothing in the colouring depends on document boundaries | |
| 171 | | 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 | |
| 172 | |
| 173 | ## Markdown |
| 174 | |
| 175 | | Recognised | As | |
| 176 | | --- | --- | |
| 177 | | `# Heading` … `###### Heading` | the whole line as a heading | |
| 178 | | `**bold**`, `__bold__`, `*italic*`, `_italic_` | emphasis | |
| 179 | | `` `code` `` | string | |
| 180 | | `[text](target)`, `` | the whole thing as a link | |
| 181 | | `- `, `* `, `+ `, `1. `, `1) ` | the marker as punctuation | |
| 182 | | `>` | punctuation | |
| 183 | | `---`, `***`, `___` | punctuation | |
| 184 | | ` ``` ` and `~~~` fences | the whole block, opening and closing lines included, as a string | |
| 185 | |
| 186 | A fenced block is **one colour whatever language it announces**: ```` ```moonbit ```` does not colour its contents as MoonBit. 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. |
| 187 | |
| 188 | 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. |
| 189 | |
| 190 | ## JavaScript |
| 191 | |
| 192 | | Recognised | As | |
| 193 | | --- | --- | |
| 194 | | `const`, `let`, `function`, `class`, `async`, `await`, `import`, `export`, … | keyword | |
| 195 | | `true`, `false`, `null`, `undefined`, `NaN`, `Infinity`, `this` | constant | |
| 196 | | `console`, `document`, `window`, `Array`, `Object`, `Promise`, `Math`, `JSON`, … | builtin | |
| 197 | | a name immediately before `(` | function | |
| 198 | | `"…"`, `'…'` | string | |
| 199 | | `` `…` ``, interpolations included, across lines | string | |
| 200 | | `//` to end of line, `/* … */` across lines | comment | |
| 201 | | `42`, `3.14`, `0x1f`, `0b1010`, `0o777`, `1_000_000`, `1e6`, `10n` | number | |
| 202 | | runs of `+-*/%=<>!&|^~?:` | operator | |
| 203 | | `()[]{},;.` | punctuation | |
| 204 | |
| 205 | **Regular-expression literals are not recognised.** Telling `/x/g` from a division needs to know whether the previous token could end an expression; a wrong guess colours the rest of a line as a string, which is worse than leaving a regex the colour of an operator. |
| 206 | |
| 207 | Globals are recognised by name, so a file that shadows `Math` still has it coloured as a builtin — the same rule MoonBit's builtins follow here. |
| 208 | |
| 209 | ## HTML |
| 210 | |
| 211 | | Recognised | As | |
| 212 | | --- | --- | |
| 213 | | `<tag`, `</tag`, `>`, `/>` | tag | |
| 214 | | attribute names, including `data-*`, `xlink:href`, `@click`, `v-bind.prop` | attribute | |
| 215 | | `=` | operator | |
| 216 | | `"…"`, `'…'` | string | |
| 217 | | `<!-- … -->`, across lines | comment | |
| 218 | | `&`, `©` | constant | |
| 219 | | `<!DOCTYPE …>` and other declarations | keyword | |
| 220 | |
| 221 | Text between tags is not coloured. A bare `&` with no `;` within 32 characters is left alone, because it is legal text. |
| 222 | |
| 223 | **The contents of `<script>` and `<style>` are not coloured** as JavaScript and CSS. |
| 224 | |
| 225 | ## XML |
| 226 | |
| 227 | 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. |
| 228 | |
| 229 | | Recognised | As | |
| 230 | | --- | --- | |
| 231 | | `<?xml version="1.0"?>` and other processing instructions | the target and `?>` as keyword, the pairs between as attributes and strings | |
| 232 | | `<!DOCTYPE …>` and the other `<!` forms | keyword | |
| 233 | | `<!-- … -->`, across lines | comment | |
| 234 | | `<![CDATA[ … ]]>`, across lines | string | |
| 235 | | `<tag`, `</tag`, `>`, `/>` | tag | |
| 236 | | `<ns:tag>`, `xsi:type` | the prefix and the local name as **one** span | |
| 237 | | attribute names | attribute | |
| 238 | | `=` | operator | |
| 239 | | `"…"`, `'…'` | string | |
| 240 | | `&`, `©` | constant | |
| 241 | |
| 242 | **A comment and a CDATA section close on different delimiters**, and are carried separately: a `-->` inside a CDATA section does not end it. |
| 243 | |
| 244 | **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. |
| 245 | |
| 246 | Text between tags is not coloured. |
| 247 | |
| 248 | ## Shell |
| 249 | |
| 250 | Applies to `sh`, `bash` and `zsh` alike: the keywords recognised are the ones they share. |
| 251 | |
| 252 | | Recognised | As | |
| 253 | | --- | --- | |
| 254 | | `if`, `then`, `fi`, `for`, `while`, `case`, `esac`, `function`, `return`, … | keyword | |
| 255 | | `true`, `false` | constant | |
| 256 | | `echo`, `printf`, `export`, `local`, `read`, `cd`, `set`, `source`, … | builtin | |
| 257 | | `$NAME`, `${…}`, `$(…)`, `$1`, `$?`, `$@` | builtin | |
| 258 | | the **first bare word on a line** | function | |
| 259 | | every later bare word, and `NAME` in `NAME=value` | identifier | |
| 260 | | `'…'`, with nothing escaped or expanded inside | string | |
| 261 | | `"…"`, with the expansions inside it coloured as expansions | string | |
| 262 | | `#` to end of line | comment | |
| 263 | |
| 264 | `$(a $(b) c)` is one span: nesting is counted. An option such as `-euo` is one word, not a minus and a word. |
| 265 | |
| 266 | **Heredocs are not recognised.** `<<EOF` and the text after it are coloured as ordinary shell. |
| 267 | |
| 268 | ## Dockerfile |
| 269 | |
| 270 | | Recognised | As | |
| 271 | | --- | --- | |
| 272 | | `FROM`, `RUN`, `COPY`, `ADD`, `ARG`, `ENV`, `CMD`, `ENTRYPOINT`, `EXPOSE`, `LABEL`, `USER`, `VOLUME`, `WORKDIR`, `HEALTHCHECK`, `ONBUILD`, `SHELL`, `STOPSIGNAL`, `MAINTAINER` | keyword, in any case | |
| 273 | | `AS`, `NONE` | keyword | |
| 274 | | `# comment`, including the `# syntax=` and `# escape=` directives | comment | |
| 275 | | `--from=builder`, `--chown=me:me` | the flag name as an attribute | |
| 276 | | `$NAME`, `${NAME}`, `${NAME:-default}` | builtin, as one span to the closing brace | |
| 277 | | `"…"`, `'…'` | string | |
| 278 | | a trailing `\` | operator | |
| 279 | | numbers | number | |
| 280 | | paths and image references — `/usr/local/bin`, `golang:1.26-alpine` | identifier, as **one** span | |
| 281 | |
| 282 | **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. |
| 283 | |
| 284 | **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. |
| 285 | |
| 286 | | Not recognised | Because | |
| 287 | | --- | --- | |
| 288 | | 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 | |
| 289 | | Heredocs in a `RUN` | The same reason the shell scanner does not recognise them | |
| 290 | | Which stage a `--from` names | Nothing here reads the rest of the file | |
| 291 | |
| 292 | ## See also |
| 293 | |
| 294 | - [Theme file format](themes.md) — every key these classes resolve to |
| 295 | - [Colouring and completion](../explanation/colouring-and-completion.md) — why the scanners are written this way |
| 296 | - [How to write your own theme](../how-to/write-a-theme.md) |