Skip to content

feat(theme): load a VS Code/Shiki theme JSON for syntax highlighting#478

Open
eduwass wants to merge 2 commits into
modem-dev:mainfrom
eduwass:feat/custom-shiki-theme
Open

feat(theme): load a VS Code/Shiki theme JSON for syntax highlighting#478
eduwass wants to merge 2 commits into
modem-dev:mainfrom
eduwass:feat/custom-shiki-theme

Conversation

@eduwass

@eduwass eduwass commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Custom themes can now point at a full VS Code / Shiki theme JSON for syntax highlighting, instead of being limited to the nine semantic [custom_theme.syntax] tokens.

Previously, defining [custom_theme.syntax] dropped the Shiki theme entirely and fell back to recoloring Pierre's default theme at a few collision points. The result was that a real VS Code theme could never be reproduced fully — only roughly approximated.

With this users should be able to take their VSCode themes and reproduce them 100% in Hunk.

What this adds

A new custom_theme.syntax_theme key — a path to a VS Code / Shiki theme JSON, absolute or relative to the config file that declares it:

[custom_theme]
base = "catppuccin-mocha"
syntax_theme = "shades-of-purple.json"
  • The file is loaded and validated at config time (clear errors for a missing file, invalid JSON, or a theme without a name), so a bad path fails fast rather than silently dropping highlighting.
  • The theme is registered with Pierre's highlighter and used as the active syntax theme, so code is colored exactly as that theme would render it in the editor.
  • The nine [custom_theme.syntax] tokens, when present, stay as the collision-normalization palette so syntax hues never visually merge with diff add/remove backgrounds.

Because Hunk renders through Pierre/Shiki, which consumes VS Code theme JSON natively, this works for any VS Code theme, not just one.

Testing

  • bun run format:check
  • bun run typecheck
  • bun run lint
  • bun test src/core/config.test.ts src/ui/themes.test.ts src/ui/diff
  • Real render smoke test against a full Shades of Purple theme JSON: the output contains the theme's own token colors (e.g. #FF9D00, #FAD000, #FF628C, #9EFFFF) that exist only in the full JSON, confirming the theme drives highlighting rather than the nine-token approximation.

Custom themes could only approximate syntax colors with nine semantic
tokens, so any real VS Code theme (e.g. Shades of Purple) rendered as a
rough remap of Pierre's default theme rather than the theme itself.

Add `custom_theme.syntax_theme`: a path (absolute, or relative to the
config file) to a full VS Code/Shiki theme JSON. The file is loaded and
validated at config time, registered with Pierre's highlighter, and used
as the active syntax theme so code is colored exactly as that theme would
in the editor. The nine `[custom_theme.syntax]` tokens stay as the
collision-normalization palette against diff add/remove backgrounds.
@greptile-apps

greptile-apps Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a custom_theme.syntax_theme config key that lets users point at a full VS Code / Shiki theme JSON for syntax highlighting, replacing the previous nine-token approximation with source-accurate coloring. The theme file is loaded and validated eagerly at config time (missing file, invalid JSON, or absent name all produce clear errors), registered with Pierre's custom theme registry before the first highlight pass, and merged cleanly through the existing config-layer stacking.

  • Config loading (config.ts): readCustomSyntaxTheme resolves the path relative to the declaring config file, parses and validates the JSON, and stores both the raw path and the parsed data in CustomThemeConfig; mergeCustomTheme propagates both fields with correct override semantics.
  • Theme resolution (themes.ts): buildCustomTheme uses a clear three-way precedence — full JSON theme → 9-token semantic remap → inherited base syntax — and exposes syntaxThemeData on AppTheme for downstream registration.
  • Pierre registration (pierre.ts): ensureCustomSyntaxThemeRegistered lazily registers the loaded JSON with a module-level dedup Set before each prepareHighlighter call to avoid duplicate-registration warnings from Pierre.

Confidence Score: 4/5

The feature is well-contained and fails fast on bad config; the main uncertainty is around the module-level registration cache in pierre.ts.

All new code paths are exercised by tests, config-time validation is solid, and the merge logic is correct. The module-level registeredCustomSyntaxThemes Set in pierre.ts only deduplicates by theme name — if Hunk picks up a new AppTheme with the same theme name but different token data in the same process, Pierre would continue serving the original loader silently.

src/ui/diff/pierre.ts — the module-level registration Set and its name-only dedup logic

Important Files Changed

Filename Overview
src/core/config.ts Adds readCustomSyntaxTheme — loads, JSON-parses, and validates a VS Code theme file at config time; integrates path resolution (absolute or relative to config dir) and error messaging; threads syntaxThemePath/syntaxThemeData through mergeCustomTheme correctly.
src/ui/diff/pierre.ts Adds ensureCustomSyntaxThemeRegistered with a module-level Set to gate Pierre custom-theme registration; registration is lazy (prepareHighlighter) and guarded against duplicates, but same-name detection could prevent re-registration when theme data changes within the same process.
src/ui/themes.ts buildCustomTheme now resolves syntaxTheme name and propagates syntaxThemeData; three-way ternary (full JSON > 9-token > base inherit) is correct and clearly commented.
src/core/types.ts Introduces CustomSyntaxThemeData (name + open index) and adds syntaxThemePath/syntaxThemeData to CustomThemeConfig; clean additions with doc comments.
src/ui/themes/types.ts Adds optional syntaxThemeData field to AppTheme with a doc comment; straightforward.
src/core/config.test.ts Three new test cases cover the happy path, missing file, and missing name; the invalid-JSON error path is untested despite having a specific try/catch branch and error message.
src/ui/themes.test.ts Adds a test confirming full syntax theme JSON sets syntaxTheme by name, preserves syntaxThemeData, and keeps the 9-token palette alongside it.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[TOML config file] -->|syntax_theme = path| B[readCustomTheme]
    B --> C[normalizeString]
    C -->|undefined| D[No custom syntax theme]
    C -->|path string| E[readCustomSyntaxTheme]
    E --> F{File exists?}
    F -->|No| G[throw: missing file]
    F -->|Yes| H[JSON.parse]
    H -->|parse error| I[throw: invalid JSON]
    H -->|success| J{has non-empty name?}
    J -->|No| K[throw: no name]
    J -->|Yes| L[CustomSyntaxThemeData]
    L --> M[CustomThemeConfig]
    M --> N[mergeCustomTheme]
    N --> O[buildCustomTheme]
    O --> P{syntaxThemeData present?}
    P -->|Yes| Q[syntaxTheme = data.name]
    P -->|No syntax override| R[syntaxTheme = undefined]
    P -->|No override| S[inherit base syntaxTheme]
    Q --> T[AppTheme]
    R --> T
    S --> T
    T --> U[prepareHighlighter]
    U --> V[ensureCustomSyntaxThemeRegistered]
    V --> W{name in Set?}
    W -->|Yes| X[Skip]
    W -->|No| Y[registerCustomTheme]
    Y --> Z[Pierre/Shiki highlights with custom tokens]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[TOML config file] -->|syntax_theme = path| B[readCustomTheme]
    B --> C[normalizeString]
    C -->|undefined| D[No custom syntax theme]
    C -->|path string| E[readCustomSyntaxTheme]
    E --> F{File exists?}
    F -->|No| G[throw: missing file]
    F -->|Yes| H[JSON.parse]
    H -->|parse error| I[throw: invalid JSON]
    H -->|success| J{has non-empty name?}
    J -->|No| K[throw: no name]
    J -->|Yes| L[CustomSyntaxThemeData]
    L --> M[CustomThemeConfig]
    M --> N[mergeCustomTheme]
    N --> O[buildCustomTheme]
    O --> P{syntaxThemeData present?}
    P -->|Yes| Q[syntaxTheme = data.name]
    P -->|No syntax override| R[syntaxTheme = undefined]
    P -->|No override| S[inherit base syntaxTheme]
    Q --> T[AppTheme]
    R --> T
    S --> T
    T --> U[prepareHighlighter]
    U --> V[ensureCustomSyntaxThemeRegistered]
    V --> W{name in Set?}
    W -->|Yes| X[Skip]
    W -->|No| Y[registerCustomTheme]
    Y --> Z[Pierre/Shiki highlights with custom tokens]
Loading
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
src/ui/diff/pierre.ts:590-596
**Stale registration on same-name theme reload**

`registeredCustomSyntaxThemes` is a module-level `Set` that only gates re-registration by theme `name`. If a user updates the theme JSON on disk and Hunk picks up a new `AppTheme` in the same process (e.g., via any live config-reload path), the Set would prevent re-registration even though `syntaxThemeData` has changed. The old loader closure would remain in Pierre's registry, silently serving stale token colors without any warning.

### Issue 2 of 2
src/core/config.test.ts:253-268
**No test for the invalid-JSON error path**

The `readCustomSyntaxTheme` function wraps `JSON.parse` in a try/catch and produces a specific error message (`Expected custom_theme.syntax_theme … to be valid JSON: …`). There are tests for missing file and missing `name`, but the invalid-JSON branch has no coverage. A simple fixture with a file containing `{ not valid json }` would close the gap and guard the error message wording.

Reviews (1): Last reviewed commit: "feat(theme): load a VS Code/Shiki theme ..." | Re-trigger Greptile

Comment thread src/ui/diff/pierre.ts
Comment thread src/core/config.test.ts
…eiling

Add a config test for the JSON.parse failure branch of
custom_theme.syntax_theme, and document why the Pierre theme registration
dedups by name only (no in-process JSON-reload path exists).
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