Skip to content

feat(editor): the built-in themes colour Markdown (#676) - #684

Merged
PathGao merged 3 commits into
masterfrom
feat/editor-markdown-palette
Aug 19, 2026
Merged

feat(editor): the built-in themes colour Markdown (#676)#684
PathGao merged 3 commits into
masterfrom
feat/editor-markdown-palette

Conversation

@PathGao

@PathGao PathGao commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

What this is

The editor's own two themes now colour Markdown. Follow-up to #678, which fixed imported VS Code themes and left the built-ins as they were.

Before, both defineTheme calls shipped rules: [] and inherited Monaco's vs / vs-dark — palettes written for source code, which had never heard of this document. Measured against the base themes, that gave four colours, with the wrong things sharing them:

construct token before (light) after (light)
heading #, list marker, table divider keyword #0000FF #0969da accent
table pipes keyword.table.* #0000FF — same as a heading #656d76 muted
table header cell keyword.table.header #0000FF — the whole row read as a heading #1f2328 text
blockquote > comment #008000 #656d76 muted
fences, indented code string #A31515 #1a7f37 code
inline code variable #001188 #1a7f37 code
fenced block body variable.source inherited inline code's colour #1f2328 text
link string.link none #8250df link
horizontal rule meta.separator none #656d76 muted
bold, italic strong, emphasis font style only #9a6700, style kept

The colours are the app's own six, the ones styles.css exposes as --color-accent-fg, --color-success-fg, --color-attention-fg, --color-done-fg, --color-fg-muted and --color-fg-default, so the editor now reads as part of the same app as the preview rather than as a code editor someone embedded.

Mechanism

Monaco resolves a token against a trie, and that is what made two of the twelve rules necessary. keyword.table.header is the text of a header cell and inherits whatever keyword says — so colouring headings blue painted every table header blue too. variable.source is the body of a fenced block and inherits inline code's colour, which turned whole blocks green. Both rules exist to undo an inheritance rather than to add a colour, and editorTheme.test.ts fails if either is dropped.

Neither can say "leave it alone": a Monaco rule must name a colour, and FontStyle.NotSet has no foreground equivalent. They name the app's own --color-fg-default, which is why a header cell sits a hair off the body cells below it (#1f2328 against the base theme's #000000) — in step with the preview instead of with Monaco.

Bold and italic carry no fontStyle, which Monaco reads as NotSet and merges by leaving the base theme's strong: bold / emphasis: italic standing. Spelling regular would have flattened them.

Headings and list markers still share one colour. The tokenizer emits a bare keyword for #, for setext underlines, for list bullets and for the ---|--- divider alike, so nothing downstream can tell them apart. That ceiling is the grammar, not the theme, and moving it means owning a fork of Monaco's Markdown monarch definition — a separate piece of work, and still only a regex's guess about what will render.

Scope

Imported VS Code themes are untouched: they take their own tokenColors through theme.ts, including the scope renaming #678 added.

The editor's default text colour is left as Monaco's (#000000 / #D4D4D4) rather than moved to --color-fg-default. Repainting every character of every document is a bigger change than colouring Markdown syntax, and the difference is under 3% luminance.

No new setting. The palette follows the light/dark theme the app is already on, the same as every other colour in the app.

Tests

scripts/editorTheme.test.ts, five behaviour tests over markdownTokenRules:

  • Every token is one the grammar emits — checked against monaco-editor's own markdown.js, since a rule keyed on a name nothing emits is silently inert. That is exactly the defect How can I highlight markdown formatting (bold/italics etc.) with different colours? #676 was, one layer over, and it is the failure mode this change is most exposed to.
  • The colours are the app palette — each rule's hex is compared against the --color-* value parsed out of styles.css for that appearance. The duplication is one-way and unavoidable (defineTheme takes hex strings and never sees a stylesheet), so this is what keeps the two from drifting.
  • The two inheritance overrides differ from what they override; bold and italic decide no font style; light and dark differ on every rule.

Revert rules: markdownTokenRules(…) to rules: [] and the suite goes red on the first two.

Verification

npm audit             0 vulnerabilities
npm run check         805 files, 0 errors, 0 warnings
npm test              936 pass, 0 fail
npm run test:vitest   42 files, 376 pass
cargo test            148 pass

The before/after table was measured in Chromium against the dev server with window.__TAURI_INTERNALS__ stubbed, by typing a document that exercises every rule and reading getComputedStyle().color off the rendered token spans — light and dark. What that does not cover is how the palette reads on a real display at length; it is six colours that the rest of the app already uses on the same two backgrounds, but nobody has written a long document in it yet.

Revised after review

Two defects in the first version, both found by looking at a real document side by side with the old build.

The palette was leaking into every other language. Monaco appends the grammar's tokenPostfix to every token (monarchLexer.js), so Markdown emits keyword.md / string.md / comment.md while Python emits keyword.python and so on. The rules were spelled bare, which places them at the root of the token trie — so a six-colour prose palette repainted the Python, Rust and JSON inside fenced code blocks, over Monaco's defaults which are tuned for code. Every rule is now .md-scoped, and a test fails on any rule that is not.

Measured after the fix, in a ```python block: the comment is back to #608B4E, `assert` to `#569CD6`, the string to `#CE9178` — Monaco's own vs-dark values, i.e. exactly what shipped before this PR.

Muting the table frame cost more than it bought. The first version made table pipes and > --color-fg-muted, on the theory that the frame is noise and the cells should read. Against the body text that is a step backwards, which is measurable — CIEDE2000 against the editor's text colour, light theme:

marker before this PR first version now
table pipes #0000FF, ΔE 39.7 #656d76, ΔE 33.0 #0969da, ΔE 42.4
> #008000, ΔE 43.1 #656d76, ΔE 33.0 #0969da, ΔE 42.4
--- rule uncoloured, ΔE 0 #656d76, ΔE 33.0 #0969da, ΔE 42.4

All block markup — #, list markers, the table frame and divider, >, horizontal rules — now takes one colour. Distinguishing kinds of markup matters less than distinguishing markup from prose, and the tokenizer cannot tell a heading from a list marker anyway.

Two constructs still move closer to the text colour, deliberately: a table header cell (ΔE 39.7 → 9.0) and the body of a fenced block (30.0 → 9.0). Both are content that the old theme painted as though it were syntax.

The four semantic families stay apart. Worst pair, CIEDE2000: structure↔link ΔE 17.8 light / 21.2 dark. For comparison the old palette's worst pair was link↔emphasis at ΔE 0.0 — both were simply the body text colour. (This is also why the editor's link colour stays purple rather than moving to the accent: that change would make the tightest pair zero.)

PathGao added 3 commits August 19, 2026 22:20
Both themes shipped `rules: []` and inherited Monaco's vs/vs-dark, which
were written for source code: headings, list markers and table pipes all
arrived as one blue `keyword`, links and horizontal rules got no colour
at all, and bold and italic got a font style and nothing else.

Twelve rules, in the app's own six colours — the ones the preview and
the chrome resolve through --color-*. Two of them exist to undo Monaco's
trie inheritance, which would otherwise paint every table header like a
heading and every fenced block like inline code.

Headings and lists still share a colour: the tokenizer emits one
`keyword` for both, and a theme cannot invent a distinction the grammar
never made.
Monaco appends the grammar's `tokenPostfix` to every token it emits, so
Markdown's are `keyword.md`, `string.md`, `comment.md`. The rules here
were spelled bare, which puts them at the root of the token trie — where
they repainted the Python, Rust and JSON inside fenced code blocks too,
with a six-colour prose palette instead of the defaults tuned for code.

Scoping them to `.md` also means a prefix no longer stands in for its
children (`keyword.table.left.md` walks `keyword` first), so the table
tokens are spelled out — and that is the chance to fix the other half:
the table frame and `>` had gone muted, which put them nearer the body
text than the blue they replaced. All block markup now reads as markup.
Monaco's defaults paint a tag in a Markdown document as source code — a
maroon tag name, a red attribute — which made the one construct that is
not Markdown the loudest thing on the line. It takes the muted colour,
which is what the escape hatch deserves.
@PathGao
PathGao merged commit 6ddd24d into master Aug 19, 2026
4 checks passed
@PathGao
PathGao deleted the feat/editor-markdown-palette branch August 19, 2026 16:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant