perf(markdown): move code highlighting off the main thread into a Shiki worker
Tokenize closed code blocks in a dedicated Shiki Web Worker instead of calling the shared highlighter synchronously on the UI thread. This removes the one-shot main-thread highlight stall when a code fence closes on a large block. Streaming behavior is unchanged: the open (streaming) fence still renders as plain text and is highlighted once on close. On any worker failure the block keeps its escaped plain code — highlighting never falls back onto the main thread. - Add markdownShikiThemeDefinition (dependency-free CSS-variable theme) so the worker can use the theme without pulling in @pierre/diffs / React. - Add markdown-worker-protocol, markdown-shiki.worker, and the main-thread markdown-worker client. - Route highlightCodeBlocks through the worker; keep the size/VSCode line guard and mermaid skip on the main thread. - Add shiki as a direct dependency (was transitive via @pierre/diffs).
This commit is contained in:
@@ -99,7 +99,7 @@
|
|||||||
},
|
},
|
||||||
"packages/electron": {
|
"packages/electron": {
|
||||||
"name": "@openchamber/electron",
|
"name": "@openchamber/electron",
|
||||||
"version": "1.12.4",
|
"version": "1.13.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@openchamber/web": "workspace:*",
|
"@openchamber/web": "workspace:*",
|
||||||
"electron-context-menu": "^4.1.2",
|
"electron-context-menu": "^4.1.2",
|
||||||
@@ -114,7 +114,7 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@openchamber/ui",
|
"name": "@openchamber/ui",
|
||||||
"version": "1.12.4",
|
"version": "1.13.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@base-ui/react": "^1.4.0",
|
"@base-ui/react": "^1.4.0",
|
||||||
"@codemirror/autocomplete": "^6.20.0",
|
"@codemirror/autocomplete": "^6.20.0",
|
||||||
@@ -178,6 +178,7 @@
|
|||||||
"rehype-katex": "^7.0.1",
|
"rehype-katex": "^7.0.1",
|
||||||
"remark-math": "^6.0.0",
|
"remark-math": "^6.0.0",
|
||||||
"remend": "^1.2.1",
|
"remend": "^1.2.1",
|
||||||
|
"shiki": "^3.23.0",
|
||||||
"simple-git": "^3.28.0",
|
"simple-git": "^3.28.0",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"strip-json-comments": "^5.0.3",
|
"strip-json-comments": "^5.0.3",
|
||||||
@@ -216,7 +217,7 @@
|
|||||||
},
|
},
|
||||||
"packages/vscode": {
|
"packages/vscode": {
|
||||||
"name": "openchamber",
|
"name": "openchamber",
|
||||||
"version": "1.12.4",
|
"version": "1.13.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@openchamber/ui": "workspace:*",
|
"@openchamber/ui": "workspace:*",
|
||||||
"@opencode-ai/sdk": "^1.17.7",
|
"@opencode-ai/sdk": "^1.17.7",
|
||||||
@@ -239,7 +240,7 @@
|
|||||||
},
|
},
|
||||||
"packages/web": {
|
"packages/web": {
|
||||||
"name": "@openchamber/web",
|
"name": "@openchamber/web",
|
||||||
"version": "1.12.4",
|
"version": "1.13.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
"openchamber": "./bin/cli.js",
|
"openchamber": "./bin/cli.js",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -73,6 +73,7 @@
|
|||||||
"rehype-katex": "^7.0.1",
|
"rehype-katex": "^7.0.1",
|
||||||
"remark-math": "^6.0.0",
|
"remark-math": "^6.0.0",
|
||||||
"remend": "^1.2.1",
|
"remend": "^1.2.1",
|
||||||
|
"shiki": "^3.23.0",
|
||||||
"simple-git": "^3.28.0",
|
"simple-git": "^3.28.0",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"strip-json-comments": "^5.0.3",
|
"strip-json-comments": "^5.0.3",
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
/// <reference lib="webworker" />
|
||||||
|
|
||||||
|
import { bundledLanguages, createHighlighter, type BundledLanguage } from 'shiki';
|
||||||
|
import { MARKDOWN_SHIKI_THEME, MARKDOWN_SHIKI_THEME_DEFINITION } from './markdownShikiThemeDefinition';
|
||||||
|
import type { MarkdownWorkerRequest, MarkdownWorkerResponse } from './markdown-worker-protocol';
|
||||||
|
|
||||||
|
// Single shared highlighter for the worker. Languages load lazily on demand.
|
||||||
|
let highlighter: ReturnType<typeof createHighlighter> | undefined;
|
||||||
|
|
||||||
|
// Serialize work so language loading / tokenization never overlaps.
|
||||||
|
let queue = Promise.resolve();
|
||||||
|
|
||||||
|
const ensureHighlighter = (): ReturnType<typeof createHighlighter> => {
|
||||||
|
highlighter ??= createHighlighter({
|
||||||
|
// Cast: the theme is a CSS-variable TextMate theme; Shiki accepts the shape.
|
||||||
|
themes: [MARKDOWN_SHIKI_THEME_DEFINITION as unknown as Parameters<typeof createHighlighter>[0]['themes'][number]],
|
||||||
|
langs: [],
|
||||||
|
});
|
||||||
|
return highlighter;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.onmessage = (event: MessageEvent<MarkdownWorkerRequest>) => {
|
||||||
|
const request = event.data;
|
||||||
|
if (request.type === 'init') {
|
||||||
|
ensureHighlighter();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
queue = queue.then(() => highlight(request)).catch(() => {});
|
||||||
|
};
|
||||||
|
|
||||||
|
async function highlight(request: Extract<MarkdownWorkerRequest, { type: 'highlight' }>): Promise<void> {
|
||||||
|
try {
|
||||||
|
const instance = await ensureHighlighter();
|
||||||
|
let lang = request.lang in bundledLanguages ? request.lang : 'text';
|
||||||
|
if (lang !== 'text' && !instance.getLoadedLanguages().includes(lang)) {
|
||||||
|
try {
|
||||||
|
await instance.loadLanguage(bundledLanguages[lang as BundledLanguage]);
|
||||||
|
} catch {
|
||||||
|
lang = 'text';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const html = instance.codeToHtml(request.code, {
|
||||||
|
lang,
|
||||||
|
theme: MARKDOWN_SHIKI_THEME,
|
||||||
|
tabindex: false,
|
||||||
|
});
|
||||||
|
post({ type: 'highlight', id: request.id, html });
|
||||||
|
} catch (error) {
|
||||||
|
post({ type: 'error', id: request.id, message: error instanceof Error ? error.message : String(error) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function post(response: MarkdownWorkerResponse): void {
|
||||||
|
self.postMessage(response);
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// Message protocol for the markdown Shiki Web Worker.
|
||||||
|
//
|
||||||
|
// The worker tokenizes a complete code block off the main thread and returns
|
||||||
|
// ready-to-splice Shiki HTML. The theme is dependency-free and imported inside
|
||||||
|
// the worker directly, so it is not sent over postMessage.
|
||||||
|
|
||||||
|
export type MarkdownWorkerRequest =
|
||||||
|
| { type: 'init' }
|
||||||
|
| { type: 'highlight'; id: number; code: string; lang: string };
|
||||||
|
|
||||||
|
export type MarkdownWorkerResponse =
|
||||||
|
| { type: 'highlight'; id: number; html: string }
|
||||||
|
| { type: 'error'; id: number; message: string };
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import MarkdownShikiWorkerUrl from './markdown-shiki.worker.ts?worker&url';
|
||||||
|
import type { MarkdownWorkerRequest, MarkdownWorkerResponse } from './markdown-worker-protocol';
|
||||||
|
|
||||||
|
// Main-thread client for the markdown Shiki worker. Moves syntax tokenization
|
||||||
|
// off the UI thread: a closed code block is shipped to the worker, which returns
|
||||||
|
// ready-to-splice Shiki HTML. On any failure (no worker support, worker crash,
|
||||||
|
// tokenization error) the promise resolves to `null` and the caller keeps the
|
||||||
|
// escaped plain-text code — highlighting never falls back onto the main thread.
|
||||||
|
|
||||||
|
let worker: Worker | undefined;
|
||||||
|
let nextId = 0;
|
||||||
|
const pending = new Map<number, (html: string | null) => void>();
|
||||||
|
|
||||||
|
const failAll = (): void => {
|
||||||
|
pending.forEach((resolve) => resolve(null));
|
||||||
|
pending.clear();
|
||||||
|
worker?.terminate();
|
||||||
|
worker = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getWorker = (): Worker | undefined => {
|
||||||
|
if (worker) return worker;
|
||||||
|
if (typeof window === 'undefined' || typeof Worker === 'undefined') return undefined;
|
||||||
|
try {
|
||||||
|
worker = new Worker(MarkdownShikiWorkerUrl, { type: 'module' });
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
worker.onmessage = (event: MessageEvent<MarkdownWorkerResponse>) => {
|
||||||
|
const resolve = pending.get(event.data.id);
|
||||||
|
if (!resolve) return;
|
||||||
|
pending.delete(event.data.id);
|
||||||
|
resolve(event.data.type === 'highlight' ? event.data.html : null);
|
||||||
|
};
|
||||||
|
worker.onerror = failAll;
|
||||||
|
worker.onmessageerror = failAll;
|
||||||
|
worker.postMessage({ type: 'init' } satisfies MarkdownWorkerRequest);
|
||||||
|
return worker;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Highlight a complete code block in the worker. Resolves to Shiki `<pre>` HTML,
|
||||||
|
* or `null` if highlighting is unavailable or failed (caller keeps plain code).
|
||||||
|
*/
|
||||||
|
export const highlightCodeInWorker = (code: string, lang: string): Promise<string | null> => {
|
||||||
|
const instance = getWorker();
|
||||||
|
if (!instance) return Promise.resolve(null);
|
||||||
|
const id = ++nextId;
|
||||||
|
return new Promise<string | null>((resolve) => {
|
||||||
|
pending.set(id, resolve);
|
||||||
|
instance.postMessage({ type: 'highlight', id, code, lang } satisfies MarkdownWorkerRequest);
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -2,14 +2,9 @@ import { marked, type Tokens } from 'marked';
|
|||||||
import remend from 'remend';
|
import remend from 'remend';
|
||||||
import katex from 'katex';
|
import katex from 'katex';
|
||||||
import DOMPurify from 'dompurify';
|
import DOMPurify from 'dompurify';
|
||||||
import {
|
|
||||||
getSharedHighlighter,
|
|
||||||
type DiffsThemeNames,
|
|
||||||
type SupportedLanguages,
|
|
||||||
} from '@pierre/diffs';
|
|
||||||
import { buildAgentMentionUrl, parseAgentHref, parseSkillHref } from '@/lib/messages/inlineMessageLinks';
|
import { buildAgentMentionUrl, parseAgentHref, parseSkillHref } from '@/lib/messages/inlineMessageLinks';
|
||||||
import { isVSCodeRuntime } from '@/lib/desktop';
|
import { isVSCodeRuntime } from '@/lib/desktop';
|
||||||
import { ensureMarkdownShikiTheme, MARKDOWN_SHIKI_THEME } from './markdownTheme';
|
import { highlightCodeInWorker } from './markdown-worker';
|
||||||
|
|
||||||
const escapeAttr = (value: string): string =>
|
const escapeAttr = (value: string): string =>
|
||||||
value.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
value.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
||||||
@@ -195,13 +190,6 @@ const highlightCodeBlocks = async (html: string): Promise<string> => {
|
|||||||
const matches = [...html.matchAll(CODE_BLOCK_RE)];
|
const matches = [...html.matchAll(CODE_BLOCK_RE)];
|
||||||
if (matches.length === 0) return html;
|
if (matches.length === 0) return html;
|
||||||
|
|
||||||
ensureMarkdownShikiTheme();
|
|
||||||
const highlighter = await getSharedHighlighter({
|
|
||||||
themes: [MARKDOWN_SHIKI_THEME as DiffsThemeNames],
|
|
||||||
langs: [],
|
|
||||||
preferredHighlighter: 'shiki-wasm',
|
|
||||||
});
|
|
||||||
|
|
||||||
const lineLimit = isVSCodeRuntime() ? VSCODE_CODE_HIGHLIGHT_LINE_LIMIT : CODE_HIGHLIGHT_LINE_LIMIT;
|
const lineLimit = isVSCodeRuntime() ? VSCODE_CODE_HIGHLIGHT_LINE_LIMIT : CODE_HIGHLIGHT_LINE_LIMIT;
|
||||||
|
|
||||||
let result = html;
|
let result = html;
|
||||||
@@ -220,28 +208,13 @@ const highlightCodeBlocks = async (html: string): Promise<string> => {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let lang = requested;
|
// Tokenize off the main thread. On failure the worker resolves to null and
|
||||||
|
// we keep the original escaped <pre><code> (no main-thread highlight).
|
||||||
if (lang !== 'text' && !highlighter.getLoadedLanguages().includes(lang)) {
|
const highlighted = await highlightCodeInWorker(code, requested);
|
||||||
try {
|
if (highlighted) {
|
||||||
await highlighter.loadLanguage(lang as SupportedLanguages);
|
// Stamp the language so the decorate pass can show a header label.
|
||||||
} catch {
|
const stamped = highlighted.replace(/^<pre/, `<pre data-md-lang="${requested}"`);
|
||||||
lang = 'text';
|
result = result.replace(full, () => stamped);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const highlighted = highlighter
|
|
||||||
.codeToHtml(code, {
|
|
||||||
lang: lang as SupportedLanguages,
|
|
||||||
theme: MARKDOWN_SHIKI_THEME as DiffsThemeNames,
|
|
||||||
tabindex: false,
|
|
||||||
})
|
|
||||||
// Stamp the language so the decorate pass can show a header label.
|
|
||||||
.replace(/^<pre/, `<pre data-md-lang="${requested}"`);
|
|
||||||
result = result.replace(full, () => highlighted);
|
|
||||||
} catch {
|
|
||||||
// Leave the original (escaped, sanitized) <pre><code> in place.
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
// Static, CSS-variable-driven Shiki theme definition.
|
||||||
|
//
|
||||||
|
// Token colors reference CSS custom properties (`--md-syntax-*`) instead of
|
||||||
|
// concrete colors, so a highlighted code block does NOT need to be re-tokenized
|
||||||
|
// when the app theme changes — only the CSS variables on the markdown container
|
||||||
|
// update, and the browser repaints. This mirrors OpenCode's `var(--syntax-*)`
|
||||||
|
// theme approach and keeps highlighting results cacheable across theme switches.
|
||||||
|
//
|
||||||
|
// This module is intentionally dependency-free (no `@pierre/diffs`, no React) so
|
||||||
|
// it can be imported from inside the Shiki Web Worker bundle without dragging in
|
||||||
|
// main-thread-only modules.
|
||||||
|
|
||||||
|
export const MARKDOWN_SHIKI_THEME = 'openchamber-md';
|
||||||
|
|
||||||
|
// Loosely typed on purpose: consumers (`@pierre/diffs` registration and the raw
|
||||||
|
// Shiki worker) each cast to their own theme type. The shape is a standard
|
||||||
|
// TextMate-style theme registration.
|
||||||
|
export const MARKDOWN_SHIKI_THEME_DEFINITION = {
|
||||||
|
name: MARKDOWN_SHIKI_THEME,
|
||||||
|
colors: {
|
||||||
|
'editor.background': 'transparent',
|
||||||
|
'editor.foreground': 'var(--md-syntax-foreground)',
|
||||||
|
},
|
||||||
|
tokenColors: [
|
||||||
|
{
|
||||||
|
scope: ['comment', 'punctuation.definition.comment', 'string.comment'],
|
||||||
|
settings: { foreground: 'var(--md-syntax-comment)', fontStyle: 'italic' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scope: ['string', 'punctuation.definition.string', 'string.template'],
|
||||||
|
settings: { foreground: 'var(--md-syntax-string)' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scope: ['constant.numeric', 'constant.language', 'constant.character', 'constant'],
|
||||||
|
settings: { foreground: 'var(--md-syntax-number)' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scope: ['keyword', 'storage', 'storage.type', 'storage.modifier', 'keyword.control'],
|
||||||
|
settings: { foreground: 'var(--md-syntax-keyword)' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scope: ['keyword.operator', 'punctuation.separator', 'punctuation.terminator'],
|
||||||
|
settings: { foreground: 'var(--md-syntax-operator)' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scope: ['entity.name.function', 'support.function', 'meta.function-call'],
|
||||||
|
settings: { foreground: 'var(--md-syntax-function)' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scope: [
|
||||||
|
'entity.name.type',
|
||||||
|
'entity.name.class',
|
||||||
|
'support.type',
|
||||||
|
'support.class',
|
||||||
|
'entity.other.inherited-class',
|
||||||
|
],
|
||||||
|
settings: { foreground: 'var(--md-syntax-type)' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scope: ['variable', 'variable.other', 'variable.parameter', 'meta.definition.variable'],
|
||||||
|
settings: { foreground: 'var(--md-syntax-variable)' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scope: ['variable.other.property', 'meta.property-name', 'support.type.property-name'],
|
||||||
|
settings: { foreground: 'var(--md-syntax-property)' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scope: ['entity.name.tag', 'punctuation.definition.tag'],
|
||||||
|
settings: { foreground: 'var(--md-syntax-keyword)' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scope: ['entity.other.attribute-name'],
|
||||||
|
settings: { foreground: 'var(--md-syntax-property)' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scope: ['markup.bold', 'punctuation.definition.bold'],
|
||||||
|
settings: { fontStyle: 'bold' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scope: ['markup.italic', 'punctuation.definition.italic'],
|
||||||
|
settings: { fontStyle: 'italic' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scope: ['markup.heading', 'markup.heading entity.name'],
|
||||||
|
settings: { foreground: 'var(--md-syntax-keyword)', fontStyle: 'bold' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scope: ['markup.inserted', 'punctuation.definition.inserted'],
|
||||||
|
settings: { foreground: 'var(--md-syntax-inserted)' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scope: ['markup.deleted', 'punctuation.definition.deleted'],
|
||||||
|
settings: { foreground: 'var(--md-syntax-deleted)' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scope: ['invalid', 'invalid.illegal'],
|
||||||
|
settings: { foreground: 'var(--md-syntax-deleted)' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as const;
|
||||||
@@ -1,108 +1,30 @@
|
|||||||
import { registerCustomTheme, type ThemeRegistrationResolved } from '@pierre/diffs';
|
import { registerCustomTheme, type ThemeRegistrationResolved } from '@pierre/diffs';
|
||||||
import type { Theme } from '@/types/theme';
|
import type { Theme } from '@/types/theme';
|
||||||
|
import { MARKDOWN_SHIKI_THEME, MARKDOWN_SHIKI_THEME_DEFINITION } from './markdownShikiThemeDefinition';
|
||||||
|
|
||||||
// Name of the static Shiki theme we register once. Its token colors reference
|
// The static Shiki theme name. Its definition (token colors referencing
|
||||||
// CSS custom properties (`--md-syntax-*`) instead of concrete colors, so a
|
// `--md-syntax-*` CSS variables) lives in the dependency-free
|
||||||
// highlighted code block does NOT need to be re-tokenized when the app theme
|
// `markdownShikiThemeDefinition` module so it can also be imported inside the
|
||||||
// changes — only the CSS variables on the markdown container update, and the
|
// Shiki Web Worker. See that module for the rationale.
|
||||||
// browser repaints. This mirrors OpenCode's `var(--syntax-*)` theme approach
|
export { MARKDOWN_SHIKI_THEME };
|
||||||
// and keeps highlighting results cacheable across theme switches.
|
|
||||||
export const MARKDOWN_SHIKI_THEME = 'openchamber-md';
|
|
||||||
|
|
||||||
let registered = false;
|
let registered = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register the static, CSS-variable-driven Shiki theme. Safe to call multiple
|
* Register the static, CSS-variable-driven Shiki theme with `@pierre/diffs`.
|
||||||
* times; only the first call registers.
|
* Safe to call multiple times; only the first call registers.
|
||||||
|
*
|
||||||
|
* NOTE: markdown code highlighting now runs through the dedicated Shiki worker
|
||||||
|
* (`markdown-worker`), which uses the raw theme definition directly. This
|
||||||
|
* registration remains only for any `@pierre/diffs`-based consumer of the
|
||||||
|
* `openchamber-md` theme name.
|
||||||
*/
|
*/
|
||||||
export const ensureMarkdownShikiTheme = (): void => {
|
export const ensureMarkdownShikiTheme = (): void => {
|
||||||
if (registered) return;
|
if (registered) return;
|
||||||
registered = true;
|
registered = true;
|
||||||
|
|
||||||
registerCustomTheme(MARKDOWN_SHIKI_THEME, () =>
|
registerCustomTheme(MARKDOWN_SHIKI_THEME, () =>
|
||||||
Promise.resolve({
|
Promise.resolve(MARKDOWN_SHIKI_THEME_DEFINITION as unknown as ThemeRegistrationResolved),
|
||||||
name: MARKDOWN_SHIKI_THEME,
|
|
||||||
colors: {
|
|
||||||
'editor.background': 'transparent',
|
|
||||||
'editor.foreground': 'var(--md-syntax-foreground)',
|
|
||||||
},
|
|
||||||
tokenColors: [
|
|
||||||
{
|
|
||||||
scope: ['comment', 'punctuation.definition.comment', 'string.comment'],
|
|
||||||
settings: { foreground: 'var(--md-syntax-comment)', fontStyle: 'italic' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: ['string', 'punctuation.definition.string', 'string.template'],
|
|
||||||
settings: { foreground: 'var(--md-syntax-string)' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: ['constant.numeric', 'constant.language', 'constant.character', 'constant'],
|
|
||||||
settings: { foreground: 'var(--md-syntax-number)' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: ['keyword', 'storage', 'storage.type', 'storage.modifier', 'keyword.control'],
|
|
||||||
settings: { foreground: 'var(--md-syntax-keyword)' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: ['keyword.operator', 'punctuation.separator', 'punctuation.terminator'],
|
|
||||||
settings: { foreground: 'var(--md-syntax-operator)' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: ['entity.name.function', 'support.function', 'meta.function-call'],
|
|
||||||
settings: { foreground: 'var(--md-syntax-function)' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: [
|
|
||||||
'entity.name.type',
|
|
||||||
'entity.name.class',
|
|
||||||
'support.type',
|
|
||||||
'support.class',
|
|
||||||
'entity.other.inherited-class',
|
|
||||||
],
|
|
||||||
settings: { foreground: 'var(--md-syntax-type)' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: ['variable', 'variable.other', 'variable.parameter', 'meta.definition.variable'],
|
|
||||||
settings: { foreground: 'var(--md-syntax-variable)' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: ['variable.other.property', 'meta.property-name', 'support.type.property-name'],
|
|
||||||
settings: { foreground: 'var(--md-syntax-property)' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: ['entity.name.tag', 'punctuation.definition.tag'],
|
|
||||||
settings: { foreground: 'var(--md-syntax-keyword)' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: ['entity.other.attribute-name'],
|
|
||||||
settings: { foreground: 'var(--md-syntax-property)' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: ['markup.bold', 'punctuation.definition.bold'],
|
|
||||||
settings: { fontStyle: 'bold' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: ['markup.italic', 'punctuation.definition.italic'],
|
|
||||||
settings: { fontStyle: 'italic' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: ['markup.heading', 'markup.heading entity.name'],
|
|
||||||
settings: { foreground: 'var(--md-syntax-keyword)', fontStyle: 'bold' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: ['markup.inserted', 'punctuation.definition.inserted'],
|
|
||||||
settings: { foreground: 'var(--md-syntax-inserted)' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: ['markup.deleted', 'punctuation.definition.deleted'],
|
|
||||||
settings: { foreground: 'var(--md-syntax-deleted)' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: ['invalid', 'invalid.illegal'],
|
|
||||||
settings: { foreground: 'var(--md-syntax-deleted)' },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
} as unknown as ThemeRegistrationResolved),
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user