The Day My Own DSL Stayed Gray: Adding a Minimal TextMate Grammar to Antigravity
When a custom config file you use every day renders as flat gray text, misreads pile up. This walkthrough builds a minimal TextMate grammar and language-configuration so Antigravity highlights your own file type, with working code and the pitfalls that trip people up.
One evening I opened a wallpaper-rules file from one of my own apps as an indie developer, and the screen looked oddly quiet. palette: sunset, @when season == winter, comments, strings, all the same shade of gray. Structure I could read at a glance in JavaScript or Python collapsed into flat text the moment the extension became my own .wprules. Every time my eyes tracked a line, I noticed I was redrawing the boundary between key and value in my head.
The cause was not the editor's capability. Antigravity simply does not know the .wprules extension, so it has no cue for where to apply color. The flip side is encouraging: hand it that cue, and the color comes back. Here we will assemble the smallest possible extension that highlights a custom file, using code that actually runs. No flashy features. The goal is the shortest path to "readable."
What Gray Really Means: Highlighting Maps Scopes to Colors
Syntax highlighting does not color text directly. First a grammar assigns each fragment of text a scope name such as keyword.control or string.quoted, and then the theme maps that scope name to a color. Two stages.
A custom file is gray because the first stage is missing. The theme has plenty of colors, but nothing has been tagged with a scope name to paint. So our job is not to pick colors. It is to write a grammar that assigns the right scope names to the text.
This design is not unique to Antigravity. It comes from TextMate and is widely adopted by VS Code-family editors. Antigravity follows the same mechanism, so a TextMate grammar (.tmLanguage.json) drops straight in.
The File We Are Targeting
We will bring color to a .wprules file like the one below. Think of it as a tiny DSL for declaring wallpaper-generation conditions.
There are four semantically distinct elements here: comments (#), directives (@when ...), keys (palette and friends), and values (numbers, booleans, strings). Assign a different scope to each of the four, and the reading cues return all at once.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦Why custom files render gray: there is no language registered, and a single TextMate grammar is enough to bring color back
✦Every file of a minimal extension (grammar, language-configuration, snippets) plus how to verify color with scope inspection
✦Three pitfalls: regex ordering, embedded languages, and theme-independent scope naming, each with a fix
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
The point worth internalizing: languages[].id (the internal identifier wprules) and grammars[].scopeName (the grammar root source.wprules) are different things. The former binds a file to a language; the latter is the top-level scope of the grammar tree. Swap them and you get a confusing state where the language is recognized but no color appears.
Step 2: Teach Comments and Brackets via language-configuration.json
Before coloring, tell the editor about comment markers and bracket pairs. This drives comment toggling with Cmd+/ and auto-closing brackets.
It is a small file, but it alone lifts the editing feel to something usable. Remember the split: the grammar handles color, the configuration handles behavior. Keeping them in separate files keeps you from getting lost.
Step 3: Write the TextMate Grammar
This is the core. In syntaxes/wprules.tmLanguage.json, define a pattern and scope for each of the four elements.
Patterns are factored into repository and referenced from patterns via #name. That lets you read and edit each element in isolation, and adding a new value type later stays a local change.
The scope names are not invented words. They lean on widely used standard names: comment.line, keyword.control, entity.name.tag, string.quoted.double, constant.numeric. That choice is exactly what avoids the third pitfall below.
Step 4: Load It and Verify
Place the extension folder in Antigravity's extensions directory (under ~/.antigravity/extensions/, which varies by environment) or open it in an extension development host and reload. Open a .wprules file, and keys, directives, comments, and values should each rise up in a different color.
When color is missing or wrong, do not guess. Confirm with scope inspection. Open the feature equivalent to "Developer: Inspect Editor Tokens and Scopes" from the command palette and look at which scope is actually assigned to the token under your cursor. If the expected scope is absent, the grammar is at fault; if the scope is right but the color is dull, it is a theme matter. That single step points you at the cause almost instantly.
Pitfall 1: Regex Is Decided by Order
A TextMate grammar tries patterns top to bottom and takes the first match. So putting a too-broad pattern first prevents later, finer patterns from being reached.
For example, placing the numeric match \b\d+...\b before the boolean \b(true|false)\b causes no harm, but if you put a loose "any identifier is a variable" pattern before booleans, true gets swallowed into variable color. Specific first, broad later. Honoring that ordering alone clears up most of the mysterious mis-coloring.
Symptom
Common cause
Where to check
Only part stays uncolored
A broad pattern swallowed it first
Order of patterns
No color at all
scopeName and grammar mismatch
grammars in package.json
Colored but dull
Non-standard scope, theme has no mapping
Scope inspection, move to standard
Pitfall 2: When Another Language Is Embedded in Values
If your DSL allows a JSON or shell fragment as a value, you will want just that part highlighted as the other language. TextMate expresses this with embedding (injection / include).
Point include at another grammar's root scope (here source.json) and that range is painted by JSON's grammar. But embedding raises complexity sharply. Paint your own elements reliably first, and add embedding only once you truly need it. Choosing not to be greedy up front translates directly into easier maintenance.
Pitfall 3: Theme-Independent Scope Naming
This is the most overlooked one. You may name scopes freely, but themes only provide colors for standard scope names. Assign an invented name like my.super.custom.key and, even with a correctly working grammar, the theme has no matching color and you are back to gray.
Keys to entity.name.tag, comments to comment.line, strings to string.quoted.double. By leaning on the nearest standard name, sensible color lands regardless of which theme the user runs. The place to be original is not the naming but the design decision of which meaning maps to which element.
Let the Agent Draft, Let a Human Verify
The step where people stall is the initial velocity of regex and scope naming. Here, handing Antigravity's agent a few lines of sample .wprules and asking it to "draft a tmLanguage.json that assigns standard scopes to these four elements" fills in the skeleton fast.
Just do not trust the output as-is. Agent drafts tend to mix in non-standard scopes and to be careless about pattern order. So the human side commits to verification: look at the actual assignments with scope inspection, and confirm they are standard names in the right order. Generation for speed, verification for correctness. Split the roles, and highlighting a custom file reaches usable quality in a short time. If you want to push editor customization further, the Antigravity Editor advanced customization and the review of conversation width and syntax highlighting pair well with this.
Wrap-Up: Your Next Step
A gray custom file was never an editor limit. It only lacked a language registration. Declare the language in package.json, teach behavior in language-configuration.json, assign standard scopes in tmLanguage.json. With that trio, reading color returns to your own DSL too.
For your next move, pick the one custom file you open most, and scope just its four or five elements. Do not try to paint everything at once. Even coloring only comments and keys visibly cuts down misreads. A single small grammar lightens the daily load on your eyes. I hope you will feel that for yourself, starting with a single file. Thank you for reading.
Share
Thank You for Reading
Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.