Files
codeg/src/lib/monaco-themes.ts
2026-04-08 23:42:25 +08:00

399 lines
11 KiB
TypeScript

"use client"
import { useEffect, useState } from "react"
import type { BeforeMount } from "@monaco-editor/react"
export const MONACO_LIGHT_THEME = "codeg-light"
export const MONACO_DARK_THEME = "codeg-dark"
export const monacoTokenRules = {
light: [
{ token: "diff.header", foreground: "52525B", fontStyle: "bold" },
{ token: "diff.meta", foreground: "71717A" },
{ token: "diff.range", foreground: "0369A1", fontStyle: "bold" },
{ token: "diff.file", foreground: "334155" },
{ token: "diff.inserted", foreground: "166534" },
{ token: "diff.deleted", foreground: "991B1B" },
{ token: "diff.context", foreground: "3F3F46" },
],
dark: [
{ token: "diff.header", foreground: "D4D4D8", fontStyle: "bold" },
{ token: "diff.meta", foreground: "A1A1AA" },
{ token: "diff.range", foreground: "7DD3FC", fontStyle: "bold" },
{ token: "diff.file", foreground: "D4D4D8" },
{ token: "diff.inserted", foreground: "86EFAC" },
{ token: "diff.deleted", foreground: "FDA4AF" },
{ token: "diff.context", foreground: "E4E4E7" },
],
}
export const monacoThemeColors = {
light: {
focusBorder: "#a1a1aa",
"editor.background": "#ffffff",
"editor.foreground": "#09090b",
"editorGutter.background": "#ffffff",
"editorLineNumber.foreground": "#a1a1aa",
"editorLineNumber.activeForeground": "#18181b",
"editor.lineHighlightBackground": "#f4f4f5",
"editor.selectionBackground": "#e4e4e7",
"editor.inactiveSelectionBackground": "#f4f4f5",
"editorWidget.background": "#ffffff",
"editorWidget.foreground": "#09090b",
"editorWidget.border": "#e4e4e7",
"editorHoverWidget.background": "#ffffff",
"editorHoverWidget.foreground": "#09090b",
"editorHoverWidget.border": "#e4e4e7",
"editorHoverWidget.statusBarBackground": "#f4f4f5",
"editorSuggestWidget.background": "#ffffff",
"editorSuggestWidget.border": "#e4e4e7",
"editorSuggestWidget.foreground": "#09090b",
"editorSuggestWidget.highlightForeground": "#18181b",
"editorSuggestWidget.selectedBackground": "#f4f4f5",
"menu.background": "#ffffff",
"menu.foreground": "#09090b",
"menu.selectionBackground": "#f4f4f5",
"menu.selectionForeground": "#09090b",
"menu.separatorBackground": "#e4e4e7",
"menu.border": "#e4e4e7",
"input.background": "#ffffff",
"input.foreground": "#09090b",
"input.border": "#e4e4e7",
"dropdown.background": "#ffffff",
"dropdown.foreground": "#09090b",
"dropdown.border": "#e4e4e7",
"list.hoverBackground": "#f4f4f5",
"list.activeSelectionBackground": "#f4f4f5",
"list.activeSelectionForeground": "#09090b",
"list.inactiveSelectionBackground": "#f4f4f5",
"list.inactiveSelectionForeground": "#09090b",
"list.focusOutline": "#a1a1aa",
"peekView.border": "#e4e4e7",
"peekViewEditor.background": "#ffffff",
"peekViewEditor.matchHighlightBackground": "#e4e4e7",
"peekViewEditorGutter.background": "#ffffff",
"peekViewResult.background": "#ffffff",
"peekViewResult.fileForeground": "#09090b",
"peekViewResult.lineForeground": "#71717a",
"peekViewResult.matchHighlightBackground": "#e4e4e7",
"peekViewResult.selectionBackground": "#f4f4f5",
"peekViewResult.selectionForeground": "#09090b",
"peekViewTitle.background": "#f4f4f5",
"peekViewTitleLabel.foreground": "#09090b",
"peekViewTitleDescription.foreground": "#71717a",
},
dark: {
focusBorder: "#71717a",
"editor.background": "#171717",
"editor.foreground": "#fafafa",
"editorGutter.background": "#171717",
"editorLineNumber.foreground": "#71717a",
"editorLineNumber.activeForeground": "#fafafa",
"editor.lineHighlightBackground": "#27272a",
"editor.selectionBackground": "#3f3f46",
"editor.inactiveSelectionBackground": "#27272a",
"editorWidget.background": "#18181b",
"editorWidget.foreground": "#fafafa",
"editorWidget.border": "#27272a",
"editorHoverWidget.background": "#18181b",
"editorHoverWidget.foreground": "#fafafa",
"editorHoverWidget.border": "#27272a",
"editorHoverWidget.statusBarBackground": "#27272a",
"editorSuggestWidget.background": "#18181b",
"editorSuggestWidget.border": "#27272a",
"editorSuggestWidget.foreground": "#fafafa",
"editorSuggestWidget.highlightForeground": "#ffffff",
"editorSuggestWidget.selectedBackground": "#27272a",
"menu.background": "#18181b",
"menu.foreground": "#fafafa",
"menu.selectionBackground": "#27272a",
"menu.selectionForeground": "#fafafa",
"menu.separatorBackground": "#3f3f46",
"menu.border": "#27272a",
"input.background": "#18181b",
"input.foreground": "#fafafa",
"input.border": "#27272a",
"dropdown.background": "#18181b",
"dropdown.foreground": "#fafafa",
"dropdown.border": "#27272a",
"list.hoverBackground": "#27272a",
"list.activeSelectionBackground": "#27272a",
"list.activeSelectionForeground": "#fafafa",
"list.inactiveSelectionBackground": "#27272a",
"list.inactiveSelectionForeground": "#fafafa",
"list.focusOutline": "#71717a",
"peekView.border": "#27272a",
"peekViewEditor.background": "#171717",
"peekViewEditor.matchHighlightBackground": "#3f3f46",
"peekViewEditorGutter.background": "#171717",
"peekViewResult.background": "#18181b",
"peekViewResult.fileForeground": "#fafafa",
"peekViewResult.lineForeground": "#a1a1aa",
"peekViewResult.matchHighlightBackground": "#3f3f46",
"peekViewResult.selectionBackground": "#27272a",
"peekViewResult.selectionForeground": "#fafafa",
"peekViewTitle.background": "#27272a",
"peekViewTitleLabel.foreground": "#fafafa",
"peekViewTitleDescription.foreground": "#a1a1aa",
},
}
export const defineDiffLanguage: BeforeMount = (monaco) => {
const hasDiffLanguage = monaco.languages
.getLanguages()
.some((language: { id: string }) => language.id === "diff")
if (!hasDiffLanguage) {
monaco.languages.register({ id: "diff" })
}
monaco.languages.setMonarchTokensProvider("diff", {
defaultToken: "diff.context",
tokenizer: {
root: [
[/^diff --git .*$/, "diff.header"],
[/^index .*$/, "diff.meta"],
[/^@@ .*@@.*$/, "diff.range"],
[/^(?:\+\+\+|---) .*$/, "diff.file"],
[/^\+.*$/, "diff.inserted"],
[/^-.*$/, "diff.deleted"],
[/^\\ No newline at end of file$/, "diff.meta"],
[/^Binary files .* differ$/, "diff.meta"],
[/^.*$/, "diff.context"],
],
},
})
}
/**
* Override Monaco's built-in Python tokenizer to fix triple-quoted string
* handling. The default monarch tokenizer doesn't correctly parse `f"""..."""`
* or `"""..."""`, causing everything after the closing quotes to be highlighted
* as a string.
*/
const fixPythonTripleQuotes: BeforeMount = (monaco) => {
monaco.languages.setMonarchTokensProvider("python", {
defaultToken: "",
keywords: [
"False",
"None",
"True",
"and",
"as",
"assert",
"async",
"await",
"break",
"class",
"continue",
"def",
"del",
"elif",
"else",
"except",
"finally",
"for",
"from",
"global",
"if",
"import",
"in",
"is",
"lambda",
"nonlocal",
"not",
"or",
"pass",
"raise",
"return",
"try",
"while",
"with",
"yield",
],
builtins: [
"abs",
"all",
"any",
"bin",
"bool",
"breakpoint",
"bytearray",
"bytes",
"callable",
"chr",
"classmethod",
"compile",
"complex",
"delattr",
"dict",
"dir",
"divmod",
"enumerate",
"eval",
"exec",
"filter",
"float",
"format",
"frozenset",
"getattr",
"globals",
"hasattr",
"hash",
"help",
"hex",
"id",
"input",
"int",
"isinstance",
"issubclass",
"iter",
"len",
"list",
"locals",
"map",
"max",
"memoryview",
"min",
"next",
"object",
"oct",
"open",
"ord",
"pow",
"print",
"property",
"range",
"repr",
"reversed",
"round",
"set",
"setattr",
"slice",
"sorted",
"staticmethod",
"str",
"sum",
"super",
"tuple",
"type",
"vars",
"zip",
],
brackets: [
{ open: "{", close: "}", token: "delimiter.curly" },
{ open: "[", close: "]", token: "delimiter.bracket" },
{ open: "(", close: ")", token: "delimiter.parenthesis" },
],
tokenizer: {
root: [
// decorators
[/^(\s*)(@\w+)/, ["white", "tag"]],
// triple-quoted strings (must come before single-quoted)
[/(?:[fFrRbBuU]{1,2})?"""/, "string", "@tdqs"],
[/(?:[fFrRbBuU]{1,2})?'''/, "string", "@tsqs"],
// single-line strings
[/(?:[fFrRbBuU]{1,2})?"([^"\\]|\\.)*$/, "string.invalid"],
[/(?:[fFrRbBuU]{1,2})?'([^'\\]|\\.)*$/, "string.invalid"],
[/(?:[fFrRbBuU]{1,2})?"/, "string", "@dqs"],
[/(?:[fFrRbBuU]{1,2})?'/, "string", "@sqs"],
// comments
[/#.*$/, "comment"],
// identifiers and keywords
[
/[a-zA-Z_]\w*/,
{
cases: {
"@keywords": "keyword",
"@builtins": "type.identifier",
"@default": "identifier",
},
},
],
// numbers
[/0[xX][0-9a-fA-F](_?[0-9a-fA-F])*/, "number.hex"],
[/0[oO][0-7](_?[0-7])*/, "number.octal"],
[/0[bB][01](_?[01])*/, "number.binary"],
[/\d[\d_]*(\.\d[\d_]*)?([eE][+-]?\d[\d_]*)?[jJ]?/, "number"],
// operators
[/[+\-*/%&|^~<>!=]=?/, "operator"],
[/[{}()[\]]/, "@brackets"],
[/[;,.]/, "delimiter"],
],
// triple-double-quoted string
tdqs: [
[/[^"\\]+/, "string"],
[/\\./, "string.escape"],
[/"""/, "string", "@pop"],
[/"/, "string"],
],
// triple-single-quoted string
tsqs: [
[/[^'\\]+/, "string"],
[/\\./, "string.escape"],
[/'''/, "string", "@pop"],
[/'/, "string"],
],
// double-quoted string
dqs: [
[/[^"\\]+/, "string"],
[/\\./, "string.escape"],
[/"/, "string", "@pop"],
],
// single-quoted string
sqs: [
[/[^'\\]+/, "string"],
[/\\./, "string.escape"],
[/'/, "string", "@pop"],
],
},
})
}
export const defineMonacoThemes: BeforeMount = (monaco) => {
defineDiffLanguage(monaco)
fixPythonTripleQuotes(monaco)
monaco.editor.defineTheme(MONACO_LIGHT_THEME, {
base: "vs",
inherit: true,
rules: monacoTokenRules.light,
colors: monacoThemeColors.light,
})
monaco.editor.defineTheme(MONACO_DARK_THEME, {
base: "vs-dark",
inherit: true,
rules: monacoTokenRules.dark,
colors: monacoThemeColors.dark,
})
}
export function useMonacoThemeSync() {
const [theme, setTheme] = useState(MONACO_LIGHT_THEME)
useEffect(() => {
if (typeof window === "undefined") return
const root = document.documentElement
const syncTheme = () => {
setTheme(
root.classList.contains("dark") ? MONACO_DARK_THEME : MONACO_LIGHT_THEME
)
}
syncTheme()
const observer = new MutationObserver(syncTheme)
observer.observe(root, {
attributes: true,
attributeFilter: ["class"],
})
return () => {
observer.disconnect()
}
}, [])
return theme
}