"use client"; import { EditorView } from "@codemirror/view"; import { tags as t } from "@lezer/highlight"; import { type LanguageName, loadLanguage, } from "@uiw/codemirror-extensions-langs"; import { createTheme } from "@uiw/codemirror-themes"; import { Loader2 } from "lucide-react"; import dynamic from "next/dynamic"; import { useTheme } from "next-themes"; import { useMemo } from "react"; import { cn } from "@/lib/utils"; /* ── Dynamic import (no SSR — CodeMirror needs browser DOM) ── */ const ReactCodeMirror = dynamic(() => import("@uiw/react-codemirror"), { ssr: false, loading: () => (
), }); /* ── Syntax highlighting styles ── */ const darkStyles = [ { tag: [t.comment, t.lineComment, t.blockComment], color: "#6a737d" }, { tag: [t.keyword], color: "#ff7b72" }, { tag: [t.operator], color: "#79c0ff" }, { tag: [t.string, t.special(t.string)], color: "#a5d6ff" }, { tag: [t.number, t.bool, t.null], color: "#79c0ff" }, { tag: [t.variableName], color: "#c9d1d9" }, { tag: [t.definition(t.variableName)], color: "#d2a8ff" }, { tag: [t.function(t.variableName)], color: "#d2a8ff" }, { tag: [t.typeName, t.className], color: "#ffa657" }, { tag: [t.propertyName], color: "#79c0ff" }, { tag: [t.definition(t.propertyName)], color: "#c9d1d9" }, { tag: [t.bracket], color: "#8b949e" }, { tag: [t.punctuation], color: "#8b949e" }, { tag: [t.tagName], color: "#7ee787" }, { tag: [t.attributeName], color: "#79c0ff" }, { tag: [t.attributeValue], color: "#a5d6ff" }, { tag: [t.regexp], color: "#7ee787" }, { tag: [t.escape], color: "#79c0ff" }, { tag: [t.meta], color: "#8b949e" }, ]; const lightStyles = [ { tag: [t.comment, t.lineComment, t.blockComment], color: "#6a737d" }, { tag: [t.keyword], color: "#cf222e" }, { tag: [t.operator], color: "#0550ae" }, { tag: [t.string, t.special(t.string)], color: "#0a3069" }, { tag: [t.number, t.bool, t.null], color: "#0550ae" }, { tag: [t.variableName], color: "#24292f" }, { tag: [t.definition(t.variableName)], color: "#8250df" }, { tag: [t.function(t.variableName)], color: "#8250df" }, { tag: [t.typeName, t.className], color: "#953800" }, { tag: [t.propertyName], color: "#0550ae" }, { tag: [t.definition(t.propertyName)], color: "#24292f" }, { tag: [t.bracket], color: "#57606a" }, { tag: [t.punctuation], color: "#57606a" }, { tag: [t.tagName], color: "#116329" }, { tag: [t.attributeName], color: "#0550ae" }, { tag: [t.attributeValue], color: "#0a3069" }, { tag: [t.regexp], color: "#116329" }, { tag: [t.escape], color: "#0550ae" }, { tag: [t.meta], color: "#57606a" }, ]; /* ── Static theme objects (module-level, never recreated on render) ── */ const darkTheme = createTheme({ theme: "dark", settings: { background: "oklch(0.09 0 0)", foreground: "oklch(0.9 0 0)", caret: "oklch(0.9 0 0)", selection: "oklch(0.2 0 0)", lineHighlight: "oklch(0.12 0 0)", gutterBackground: "oklch(0.09 0 0)", gutterForeground: "oklch(0.42 0 0)", gutterBorder: "transparent", }, styles: darkStyles, }); const lightTheme = createTheme({ theme: "light", settings: { background: "oklch(0.98 0 0)", foreground: "oklch(0.15 0 0)", caret: "oklch(0.15 0 0)", selection: "oklch(0.9 0 0)", lineHighlight: "oklch(0.96 0 0)", gutterBackground: "oklch(0.98 0 0)", gutterForeground: "oklch(0.55 0 0)", gutterBorder: "transparent", }, styles: lightStyles, }); /* ── Language mapping (shiki lang names → CodeMirror loadLanguage names) ── */ const CM_LANG_MAP: Record = { // TypeScript / JavaScript family typescript: "ts", tsx: "tsx", javascript: "js", jsx: "jsx", // Shell variants bash: "bash", sh: "sh", zsh: "sh", // Data formats json: "json", jsonc: "json", yaml: "yaml", toml: "toml", // Markup markdown: "markdown", mdx: "markdown", // CM has no mdx — use markdown html: "html", xml: "xml", // Styles css: "css", scss: "scss", less: "less", // Systems python: "py", ruby: "rb", rust: "rs", go: "go", java: "java", kotlin: "kt", swift: "swift", c: "c", cpp: "cpp", csharp: "cs", // Other php: "php", sql: "sql", graphql: null, // CM has no graphql support dockerfile: null, // CM has no dockerfile support makefile: null, // CM has no makefile support lua: "lua", r: "r", latex: "tex", diff: "diff", // No CM equivalent → plain text viml: null, dotenv: null, fish: null, ini: "ini", }; /* ── Component ── */ interface CodeEditorProps { value: string; onChange: (value: string) => void; language: string | null; fontSize: number; className?: string; } export function CodeEditor({ value, onChange, language, fontSize, className, }: CodeEditorProps) { const { resolvedTheme } = useTheme(); const theme = resolvedTheme !== "light" ? darkTheme : lightTheme; // Resolve and cache language extension const langExtension = useMemo(() => { if (!language) return null; const cmName = CM_LANG_MAP[language]; if (cmName === undefined || cmName === null) return null; return loadLanguage(cmName); }, [language]); // Font size extension const fontSizeExt = useMemo( () => EditorView.theme({ "&": { fontSize: `${fontSize}px` }, ".cm-gutters": { fontSize: `${fontSize}px` }, }), [fontSize], ); // Combined extensions (memoized to avoid re-initialization) const extensions = useMemo(() => { const exts = [fontSizeExt]; if (langExtension) exts.push(langExtension); return exts; }, [fontSizeExt, langExtension]); return ( ); }