Skip to content

Commit d2ebcd5

Browse files
committed
Replace CodeMirror/Lezer syntax highlighting with Shiki
Replace the custom CodeMirror/Lezer-based syntax highlighting in the diff view with Shiki, which uses TextMate grammars (same as VS Code) for better language coverage and highlighting quality. Key changes: - Add shikiHighlighter.ts module that manages a singleton Shiki highlighter with lazy async initialization and DOM-based theme detection (github-light/github-dark matched to the app's light/dark mode). - Refactor diffParsing.ts to use Shiki's tokenizer instead of CodeMirror parsers, with language ID string caches replacing the old WeakMap<Parser> caches. - Observe <html> class mutations so highlighting caches are cleared and components re-render when the app theme toggles. - Map .svelte and .vue to TypeScript for line-by-line highlighting, since their SFC grammars require full-file context. - Remove 24 @codemirror/* and @lezer/* dependencies in favor of the single shiki package.
1 parent 17771b0 commit d2ebcd5

File tree

9 files changed

+884
-706
lines changed

9 files changed

+884
-706
lines changed

apps/desktop/src/components/projectSettings/AppearanceSettings.svelte

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,15 @@
1111
Textbox,
1212
Toggle,
1313
} from "@gitbutler/ui";
14+
import { LIGHT_THEMES, DARK_THEMES, setSyntaxThemes } from "@gitbutler/ui/utils/shikiHighlighter";
1415
import type { ScrollbarVisilitySettings } from "@gitbutler/ui/components/scroll/Scrollbar.svelte";
1516
1617
const userSettings = inject(SETTINGS);
18+
19+
// Sync persisted syntax theme settings to the shiki highlighter.
20+
$effect(() => {
21+
setSyntaxThemes($userSettings.syntaxThemeLight, $userSettings.syntaxThemeDark);
22+
});
1723
const diff = `@@ -56,10 +56,10 @@
1824
// Diff example
1925
projectName={project.title}
@@ -164,6 +170,62 @@
164170
/>
165171
</CardGroup.Item>
166172

173+
<CardGroup.Item alignment="center">
174+
{#snippet title()}
175+
Syntax theme (light)
176+
{/snippet}
177+
{#snippet caption()}
178+
Color scheme used for syntax highlighting when the app is in light mode.
179+
{/snippet}
180+
{#snippet actions()}
181+
<Select
182+
maxWidth={200}
183+
value={$userSettings.syntaxThemeLight}
184+
options={LIGHT_THEMES}
185+
onselect={(value) => {
186+
userSettings.update((s) => ({
187+
...s,
188+
syntaxThemeLight: value,
189+
}));
190+
}}
191+
>
192+
{#snippet itemSnippet({ item, highlighted })}
193+
<SelectItem selected={item.value === $userSettings.syntaxThemeLight} {highlighted}>
194+
{item.label}
195+
</SelectItem>
196+
{/snippet}
197+
</Select>
198+
{/snippet}
199+
</CardGroup.Item>
200+
201+
<CardGroup.Item alignment="center">
202+
{#snippet title()}
203+
Syntax theme (dark)
204+
{/snippet}
205+
{#snippet caption()}
206+
Color scheme used for syntax highlighting when the app is in dark mode.
207+
{/snippet}
208+
{#snippet actions()}
209+
<Select
210+
maxWidth={200}
211+
value={$userSettings.syntaxThemeDark}
212+
options={DARK_THEMES}
213+
onselect={(value) => {
214+
userSettings.update((s) => ({
215+
...s,
216+
syntaxThemeDark: value,
217+
}));
218+
}}
219+
>
220+
{#snippet itemSnippet({ item, highlighted })}
221+
<SelectItem selected={item.value === $userSettings.syntaxThemeDark} {highlighted}>
222+
{item.label}
223+
</SelectItem>
224+
{/snippet}
225+
</Select>
226+
{/snippet}
227+
</CardGroup.Item>
228+
167229
<CardGroup.Item>
168230
{#snippet title()}
169231
Font family

apps/desktop/src/lib/settings/userSettings.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ export interface Settings {
5454
allInOneDiff: boolean;
5555
highlightDiffs: boolean;
5656
svgAsImage: boolean;
57+
syntaxThemeLight: string;
58+
syntaxThemeDark: string;
5759
}
5860

5961
const defaults: Settings = {
@@ -82,6 +84,8 @@ const defaults: Settings = {
8284
allInOneDiff: false,
8385
highlightDiffs: false,
8486
svgAsImage: true,
87+
syntaxThemeLight: "github-light",
88+
syntaxThemeDark: "github-dark",
8589
};
8690

8791
export function loadUserSettings(platformName: string): Writable<Settings> {

apps/web/src/lib/components/chat/MessageCode.svelte

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
<script lang="ts">
2-
import { codeContentToTokens, parserFromExtension } from "@gitbutler/ui/utils/diffParsing";
2+
import {
3+
clearHighlightingCaches,
4+
codeContentToTokens,
5+
parserFromExtension,
6+
} from "@gitbutler/ui/utils/diffParsing";
7+
import { onHighlighterChange } from "@gitbutler/ui/utils/shikiHighlighter";
38
49
interface Props {
510
text: string;
@@ -8,8 +13,21 @@
813
914
const { text, lang }: Props = $props();
1015
11-
const parser = $derived(parserFromExtension(lang));
12-
const lines = $derived(codeContentToTokens(text, parser));
16+
// Reactive trigger: re-derive when shiki highlighter becomes ready
17+
// or the app theme (light/dark) changes.
18+
let highlighterVersion = $state(0);
19+
$effect(() => {
20+
return onHighlighterChange(() => {
21+
clearHighlightingCaches();
22+
highlighterVersion += 1;
23+
});
24+
});
25+
26+
const langId = $derived(parserFromExtension(lang));
27+
const lines = $derived.by(() => {
28+
void highlighterVersion;
29+
return codeContentToTokens(text, langId);
30+
});
1331
</script>
1432

1533
<div class="code-wrapper scrollbar">

packages/ui/package.json

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -33,30 +33,9 @@
3333
"playwright:install:ct": "playwright install --with-deps chromium webkit"
3434
},
3535
"devDependencies": {
36-
"@codemirror/lang-cpp": "^6.0.3",
37-
"@codemirror/lang-css": "^6.3.1",
38-
"@codemirror/lang-go": "^6.0.1",
39-
"@codemirror/lang-html": "^6.4.9",
40-
"@codemirror/lang-java": "^6.0.2",
41-
"@codemirror/lang-javascript": "^6.2.4",
42-
"@codemirror/lang-json": "^6.0.2",
43-
"@codemirror/lang-markdown": "^6.3.4",
44-
"@codemirror/lang-php": "^6.0.2",
45-
"@codemirror/lang-python": "^6.2.1",
46-
"@codemirror/lang-rust": "^6.0.2",
47-
"@codemirror/lang-vue": "^0.1.3",
48-
"@codemirror/lang-wast": "^6.0.2",
49-
"@codemirror/lang-xml": "^6.1.0",
50-
"@codemirror/lang-yaml": "^6.1.2",
51-
"@codemirror/language": "^6.12.1",
52-
"@codemirror/legacy-modes": "^6.5.2",
5336
"@csstools/postcss-bundler": "^2.0.8",
5437
"@gitbutler/design-core": "1.8.3",
55-
"@lezer/common": "^1.5.1",
56-
"@lezer/highlight": "^1.2.1",
5738
"@playwright/experimental-ct-svelte": "^1.58.2",
58-
"@replit/codemirror-lang-nix": "^6.0.1",
59-
"@replit/codemirror-lang-svelte": "^6.0.0",
6039
"@storybook/addon-docs": "^10.2.12",
6140
"@storybook/addon-links": "^10.2.12",
6241
"@storybook/addon-svelte-csf": "5.0.11",
@@ -69,8 +48,6 @@
6948
"@types/postcss-pxtorem": "^6.1.0",
7049
"@vitest/browser": "catalog:",
7150
"autoprefixer": "^10.4.24",
72-
"codemirror-lang-elixir": "^4.0.0",
73-
"codemirror-lang-hcl": "^0.1.0",
7451
"cpy-cli": "^5.0.0",
7552
"cssnano": "^7.1.0",
7653
"dayjs": "^1.11.13",
@@ -83,9 +60,10 @@
8360
"postcss-nesting": "catalog:postcss",
8461
"postcss-pxtorem": "catalog:postcss",
8562
"rimraf": "^6.1.3",
86-
"svgo": "^3.3.3",
63+
"shiki": "^4.0.2",
8764
"storybook": "^10.2.12",
8865
"svelte-check": "catalog:svelte",
66+
"svgo": "^3.3.3",
8967
"vite": "catalog:",
9068
"vitest": "catalog:"
9169
},

packages/ui/src/lib/components/hunkDiff/HunkDiffBody.svelte

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import HunkDiffRow, { type ContextMenuParams } from "$components/hunkDiff/HunkDiffRow.svelte";
33
import LineSelection from "$components/hunkDiff/lineSelection.svelte";
44
import {
5+
clearHighlightingCaches,
56
type ContentSection,
67
type DependencyLock,
78
generateRows,
@@ -12,6 +13,7 @@
1213
type Row,
1314
SectionType,
1415
} from "$lib/utils/diffParsing";
16+
import { onHighlighterChange } from "$lib/utils/shikiHighlighter";
1517
import type { LineSelectionParams } from "$components/hunkDiff/lineSelection.svelte";
1618
import type { Snippet } from "svelte";
1719
@@ -54,10 +56,23 @@
5456
}: Props = $props();
5557
5658
const lineSelection = new LineSelection();
59+
60+
// Reactive trigger: re-compute rows when shiki highlighter becomes ready
61+
// or the app theme (light/dark) changes.
62+
let highlighterVersion = $state(0);
63+
$effect(() => {
64+
return onHighlighterChange(() => {
65+
clearHighlightingCaches();
66+
highlighterVersion += 1;
67+
});
68+
});
69+
5770
const parser = $derived(parserFromFilename(filePath));
58-
const renderRows = $derived(
59-
generateRows(filePath, content, inlineUnifiedDiffs, parser, undefined, lineLocks),
60-
);
71+
const renderRows = $derived.by(() => {
72+
// Access highlighterVersion so rows recompute when shiki finishes loading
73+
void highlighterVersion;
74+
return generateRows(filePath, content, inlineUnifiedDiffs, parser, undefined, lineLocks);
75+
});
6176
const clickable = $derived(!!isSelectable);
6277
const maxLineNumber = $derived.by(() => {
6378
if (renderRows.length === 0) return 0;

0 commit comments

Comments
 (0)