"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 }