feat(theme): load a VS Code/Shiki theme JSON for syntax highlighting#478
feat(theme): load a VS Code/Shiki theme JSON for syntax highlighting#478eduwass wants to merge 2 commits into
Conversation
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 SummaryThis PR adds a
Confidence Score: 4/5The 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
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]
%%{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]
Prompt To Fix All With AIFix 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 |
…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).
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_themekey — a path to a VS Code / Shiki theme JSON, absolute or relative to the config file that declares it:name), so a bad path fails fast rather than silently dropping highlighting.[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:checkbun run typecheckbun run lintbun test src/core/config.test.ts src/ui/themes.test.ts src/ui/diff#FF9D00,#FAD000,#FF628C,#9EFFFF) that exist only in the full JSON, confirming the theme drives highlighting rather than the nine-token approximation.