import type { Extension } from "@codemirror/state"; import type { EditorTheme, CustomThemeColors } from "@/stores/settingsStore"; import type { AppThemeAppearance } from "@/lib/appTheme"; import { HighlightStyle, syntaxHighlighting } from "@codemirror/language"; import { tags } from "@lezer/highlight"; type CodeMirrorStyleSpec = Parameters[0]; type LucideIconNode = Array<[string, Record]>; export const EDITOR_FONT_SIZE_CSS_VAR = "--dbx-editor-font-size"; export const EDITOR_FONT_FAMILY_CSS_VAR = "--dbx-editor-font-family"; const EDITOR_SELECTION_BACKGROUND_CSS_VAR = "--dbx-editor-selection-background"; const SUPPORTS_COLOR_MIX = typeof CSS !== "undefined" && typeof CSS.supports === "function" && CSS.supports("color", "color-mix(in oklch, black 50%, white)"); const SUPPORTS_OKLCH = typeof CSS !== "undefined" && typeof CSS.supports === "function" && CSS.supports("color", "oklch(0.62 0.19 255)"); // ==================== 自定义主题配置 ==================== // 在这里修改你喜欢的颜色! const customThemeColors = { lineNumber: "#6c7086", // 行号颜色 lineNumberActive: "#cdd6f4", // 当前行号颜色 selection: "#313244", // 选中文本背景 cursor: "#f5e0dc", // 光标颜色 // 语法高亮颜色 keyword: "#cba6f7", // 关键字 (SELECT, FROM, WHERE 等) string: "#a6e3a1", // 字符串 number: "#fab387", // 数字 comment: "#6c7086", // 注释 type: "#89b4fa", // 类型 (INTEGER, TEXT 等) variable: "#f38ba8", // 变量 function: "#89dceb", // 函数 operator: "#89b4fa", // 运算符 punctuation: "#9399b2", // 标点符号 property: "#f9e2af", // 属性/字段名 tag: "#cba6f7", // XML/HTML 标签 attribute: "#fab387", // 属性名 className: "#f9e2af", // 类名 // UI 元素 gutterBackground: "#181825", // 侧边栏背景 activeLine: "#313244", // 当前行高亮 matchingBracket: "#45475a", // 匹配括号背景 // 特殊 builtin: "#89dceb", // 内置函数 meta: "#cdd6f4", // 元信息 invalid: "#f38ba8", // 无效字符 }; /** 创建自定义 CodeMirror 主题 */ function createCustomTheme( EditorView: typeof import("@codemirror/view").EditorView, colors?: CustomThemeColors, isDark: boolean = true, ): Extension { // 根据系统主题设置默认背景色和前景色 const defaultColors = isDark ? { background: "#1e1e2e", foreground: "#cdd6f4" } : { background: "#fafafa", foreground: "#242424" }; const c = { ...defaultColors, ...customThemeColors, ...(colors || {}) }; // 映射用户自定义属性名到 CodeMirror 内部属性名 if (colors) { if (colors.field) { c.variable = colors.field; c.property = colors.field; } if (colors.table) { // 表名通常被识别为 propertyName,如果单独设置了表名颜色则覆盖 c.property = colors.table; } } const theme = EditorView.theme( { "&": { backgroundColor: c.background, color: c.foreground, [EDITOR_SELECTION_BACKGROUND_CSS_VAR]: c.selection, }, ".cm-content": { caretColor: c.cursor, }, ".cm-cursor": { borderLeftColor: c.cursor, }, "&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": { backgroundColor: c.selection, }, ".cm-activeLine": { backgroundColor: c.activeLine, }, ".cm-gutters": { backgroundColor: c.gutterBackground, color: c.lineNumber, borderRight: "1px solid #313244", }, ".cm-activeLineGutter": { backgroundColor: c.activeLine, color: c.lineNumberActive, }, ".cm-matchingBracket": { backgroundColor: c.matchingBracket, outline: "none", }, }, { dark: isDark }, ); const highlightStyle = HighlightStyle.define([ { tag: tags.keyword, color: c.keyword }, { tag: tags.controlKeyword, color: c.keyword }, { tag: tags.definitionKeyword, color: c.keyword }, { tag: tags.moduleKeyword, color: c.keyword }, { tag: tags.operatorKeyword, color: c.keyword }, { tag: tags.string, color: c.string }, { tag: tags.special(tags.string), color: c.string }, { tag: tags.number, color: c.number }, { tag: tags.integer, color: c.number }, { tag: tags.float, color: c.number }, { tag: tags.comment, color: c.comment, fontStyle: "italic" }, { tag: tags.lineComment, color: c.comment, fontStyle: "italic" }, { tag: tags.blockComment, color: c.comment, fontStyle: "italic" }, { tag: tags.typeName, color: c.type }, { tag: tags.typeOperator, color: c.type }, { tag: tags.name, color: c.variable }, // ← 添加:普通标识符(字段名、表名等) { tag: tags.variableName, color: c.variable }, { tag: tags.definition(tags.variableName), color: c.variable }, { tag: tags.function(tags.variableName), color: c.function }, { tag: tags.function(tags.propertyName), color: c.function }, { tag: tags.standard(tags.variableName), color: c.builtin }, { tag: tags.propertyName, color: c.property }, { tag: tags.operator, color: c.operator }, { tag: tags.compareOperator, color: c.operator }, { tag: tags.logicOperator, color: c.operator }, { tag: tags.arithmeticOperator, color: c.operator }, { tag: tags.punctuation, color: c.punctuation }, { tag: tags.paren, color: c.punctuation }, { tag: tags.brace, color: c.punctuation }, { tag: tags.bracket, color: c.punctuation }, { tag: tags.tagName, color: c.tag }, { tag: tags.attributeName, color: c.attribute }, { tag: tags.attributeValue, color: c.string }, { tag: tags.className, color: c.className }, { tag: tags.bool, color: c.keyword }, { tag: tags.null, color: c.keyword }, { tag: tags.meta, color: c.meta }, { tag: tags.invalid, color: c.invalid }, { tag: tags.heading, color: c.keyword, fontWeight: "bold" }, { tag: tags.heading1, color: c.keyword, fontWeight: "bold" }, { tag: tags.heading2, color: c.keyword, fontWeight: "bold" }, { tag: tags.heading3, color: c.keyword, fontWeight: "bold" }, { tag: tags.strong, color: c.foreground, fontWeight: "bold" }, { tag: tags.emphasis, color: c.foreground, fontStyle: "italic" }, { tag: tags.link, color: c.type, textDecoration: "underline" }, { tag: tags.url, color: c.type, textDecoration: "underline" }, { tag: tags.labelName, color: c.property }, { tag: tags.namespace, color: c.className }, { tag: tags.macroName, color: c.function }, { tag: tags.literal, color: c.string }, { tag: tags.special(tags.string), color: c.string }, { tag: tags.regexp, color: c.string }, { tag: tags.escape, color: c.string }, { tag: tags.processingInstruction, color: c.keyword }, { tag: tags.inserted, color: c.string }, { tag: tags.deleted, color: c.invalid }, { tag: tags.changed, color: c.property }, { tag: tags.self, color: c.keyword }, { tag: tags.derefOperator, color: c.operator }, { tag: tags.unit, color: c.type }, { tag: tags.angleBracket, color: c.punctuation }, { tag: tags.annotation, color: c.property }, { tag: tags.modifier, color: c.keyword }, { tag: tags.list, color: c.foreground }, { tag: tags.quote, color: c.string, fontStyle: "italic" }, { tag: tags.monospace, color: c.foreground }, { tag: tags.strikethrough, color: c.invalid, textDecoration: "line-through" }, { tag: tags.contentSeparator, color: c.operator }, { tag: tags.special(tags.name), color: c.builtin }, ]); return [theme, syntaxHighlighting(highlightStyle)]; } // ====================================================== const TABLE_ICON: LucideIconNode = [ ["path", { d: "M12 3v18" }], ["rect", { width: "18", height: "18", x: "3", y: "3", rx: "2" }], ["path", { d: "M3 9h18" }], ["path", { d: "M3 15h18" }], ]; const COLUMNS_ICON: LucideIconNode = [ ["rect", { width: "18", height: "18", x: "3", y: "3", rx: "2" }], ["path", { d: "M12 3v18" }], ]; const KEYWORD_ICON: LucideIconNode = [ ["path", { d: "m16 18 6-6-6-6" }], ["path", { d: "m8 6-6 6 6 6" }], ]; const SNIPPET_ICON: LucideIconNode = [ ["path", { d: "M8 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h1" }], ["path", { d: "M16 3h1a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-1" }], ]; const FUNCTION_ICON: LucideIconNode = [ ["path", { d: "m15 10 5 5-5 5" }], ["path", { d: "M4 4v7a4 4 0 0 0 4 4h12" }], ]; const SCHEMA_ICON: LucideIconNode = [ ["path", { d: "M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2v11z" }], ]; function encodeSvgIcon(iconNode: LucideIconNode): string { const body = iconNode .map( ([tag, attrs]) => `<${tag} ${Object.entries(attrs) .map(([key, value]) => `${key}="${value}"`) .join(" ")} />`, ) .join(""); const svg = `${body}`; return `url("data:image/svg+xml,${encodeURIComponent(svg)}")`; } function lucideCompletionIconMask(iconNode: LucideIconNode) { const mask = encodeSvgIcon(iconNode); return { "--dbx-completion-icon-mask": mask, }; } function colorMixValue(fallback: string, preferred: string): string { return SUPPORTS_COLOR_MIX ? preferred : fallback; } function oklchValue(fallback: string, preferred: string): string { return SUPPORTS_OKLCH ? preferred : fallback; } export function cellDetailActiveLineColor(): string { return colorMixValue("var(--accent)", "color-mix(in oklch, var(--foreground) 4%, transparent)"); } /** Load a CodeMirror theme extension by theme name. */ export function resolveEditorTheme(theme: EditorTheme, appAppearance: AppThemeAppearance): Exclude { if (theme === "app") return appAppearance === "dark" ? "one-dark" : "vscode-light"; return theme; } /** Load a CodeMirror theme extension by theme name. */ export async function loadEditorTheme( theme: EditorTheme, appAppearance: AppThemeAppearance = "dark", customColors?: CustomThemeColors, ): Promise { const resolvedTheme = resolveEditorTheme(theme, appAppearance); switch (resolvedTheme) { case "one-dark": return (await import("@codemirror/theme-one-dark")).oneDark; case "vscode-dark": return (await import("@uiw/codemirror-theme-vscode")).vscodeDark; case "vscode-light": return (await import("@uiw/codemirror-theme-vscode")).vscodeLight; case "nord": return (await import("@uiw/codemirror-theme-nord")).nord; case "okaidia": return (await import("@uiw/codemirror-theme-okaidia")).okaidia; case "material": return (await import("@uiw/codemirror-theme-material")).materialDark; case "duotone-light": return (await import("@uiw/codemirror-theme-duotone")).duotoneLight; case "duotone-dark": return (await import("@uiw/codemirror-theme-duotone")).duotoneDark; case "xcode": return (await import("@uiw/codemirror-theme-xcode")).xcodeLight; case "custom": return createCustomTheme((await import("@codemirror/view")).EditorView, customColors, appAppearance === "dark"); default: return (await import("@codemirror/theme-one-dark")).oneDark; } } export function buildEditorFontThemeRules( opts?: { fixedHeight?: boolean; scrollable?: boolean }, defaults?: { size?: number; family?: string }, ): CodeMirrorStyleSpec { return { "&": { ...(opts?.fixedHeight ? { height: "100%" } : {}), fontSize: `var(${EDITOR_FONT_SIZE_CSS_VAR}, ${defaults?.size ?? 13}px)`, }, ...(opts?.scrollable ? { ".cm-scroller": { overflow: "auto" } } : {}), ".cm-content": { fontFamily: `var(${EDITOR_FONT_FAMILY_CSS_VAR}, ${defaults?.family ?? "monospace"})`, lineHeight: "1.6", padding: "0", }, ".cm-line": { padding: "0 2px !important", }, ".cm-selectionLayer .cm-selectionBackground": { display: "none", }, ".cm-cursor": { height: "1.6em !important", transform: "translateY(-0.3em)", }, ".cm-trimmedSelection": { backgroundColor: `var(${EDITOR_SELECTION_BACKGROUND_CSS_VAR}, rgb(148 163 184 / 38%))`, borderRadius: "0", }, ".cm-trimmedSelection-topLeft": { borderTopLeftRadius: "3px", }, ".cm-trimmedSelection-topRight": { borderTopRightRadius: "3px", }, ".cm-trimmedSelection-bottomLeft": { borderBottomLeftRadius: "3px", }, ".cm-trimmedSelection-bottomRight": { borderBottomRightRadius: "3px", }, ".cm-gutters": { borderRight: "0 !important", fontSize: `var(${EDITOR_FONT_SIZE_CSS_VAR}, ${defaults?.size ?? 13}px)`, fontFamily: `var(${EDITOR_FONT_FAMILY_CSS_VAR}, ${defaults?.family ?? "monospace"})`, position: "relative", userSelect: "none", }, ".cm-gutters:after": { background: "rgba(148, 163, 184, 0.38)", bottom: "0", content: "''", pointerEvents: "none", position: "absolute", right: "0", top: "0", width: "1px", zIndex: "10", }, ".cm-lineNumbers .cm-gutterElement": { cursor: "pointer", paddingRight: "16px", userSelect: "none", }, }; } /** Build a CodeMirror theme extension for font size + font family. */ export function editorFontTheme( EditorView: typeof import("@codemirror/view").EditorView, size: number, family: string, opts?: { fixedHeight?: boolean; scrollable?: boolean }, ): Extension { return EditorView.theme(buildEditorFontThemeRules(opts, { size, family })); } export function buildSqlCompletionThemeRules(): CodeMirrorStyleSpec { return { ".cm-tooltip.cm-tooltip-autocomplete": { background: "var(--popover)", border: colorMixValue( "1px solid var(--border)", "1px solid color-mix(in oklch, var(--border) 82%, var(--foreground) 18%)", ), borderRadius: "8px", boxShadow: "0 8px 18px rgb(0 0 0 / 0.14)", color: "var(--popover-foreground)", fontFamily: `var(${EDITOR_FONT_FAMILY_CSS_VAR}, var(--font-mono, monospace))`, maxWidth: "min(520px, calc(100vw - 24px))", minWidth: "min(280px, calc(100vw - 24px))", overflow: "hidden", padding: "4px 0", }, ".cm-tooltip.cm-tooltip-autocomplete > ul": { maxHeight: "min(280px, calc(100vh - 32px))", minWidth: "min(280px, calc(100vw - 24px))", padding: "0 4px 0 !important", scrollbarColor: colorMixValue( "var(--muted-foreground) transparent", "color-mix(in oklch, var(--muted-foreground) 44%, transparent) transparent", ), scrollbarWidth: "thin", }, ".cm-tooltip.cm-tooltip-autocomplete > ul > li": { alignItems: "center", borderRadius: "6px", color: "var(--popover-foreground)", display: "flex", fontSize: `clamp(12px, var(${EDITOR_FONT_SIZE_CSS_VAR}, 13px), 14px)`, fontWeight: "520", height: "28px", letterSpacing: "0", lineHeight: "28px", padding: "0 10px !important", transition: "background-color 90ms ease, color 90ms ease", }, ".cm-tooltip.cm-tooltip-autocomplete > ul > li[aria-selected]": { background: `${colorMixValue("var(--accent)", "color-mix(in oklch, var(--primary) 14%, var(--popover))")} !important`, color: "var(--popover-foreground) !important", outline: colorMixValue( "1px solid var(--border)", "1px solid color-mix(in oklch, var(--primary) 22%, transparent)", ), }, ".cm-completionIcon": { alignItems: "center", display: "inline-flex", flex: "0 0 15px", height: "15px", justifyContent: "center", marginRight: "0.65em", opacity: "1", position: "relative", overflow: "hidden", width: "15px", }, ".cm-completionIcon:before": { backgroundColor: "currentColor", content: "''", display: "block", height: "14px", position: "absolute", WebkitMaskImage: "var(--dbx-completion-icon-mask)", WebkitMaskPosition: "center", WebkitMaskRepeat: "no-repeat", WebkitMaskSize: "14px 14px", maskImage: "var(--dbx-completion-icon-mask)", maskPosition: "center", maskRepeat: "no-repeat", maskSize: "14px 14px", width: "14px", }, ".cm-completionIcon:after": { content: "'none'", display: "none", }, ".cm-completionIcon-table": { color: colorMixValue("var(--primary)", "color-mix(in oklch, var(--primary) 92%, var(--popover-foreground))"), ...lucideCompletionIconMask(TABLE_ICON), }, ".cm-completionIcon-column": { color: colorMixValue( "var(--blue-500, #3b82f6)", "color-mix(in oklch, var(--blue-500, #3b82f6) 92%, var(--popover-foreground))", ), ...lucideCompletionIconMask(COLUMNS_ICON), }, ".cm-completionIcon-keyword": { color: colorMixValue( "var(--orange-500, #f97316)", "color-mix(in oklch, var(--orange-500, #f97316) 92%, var(--popover-foreground))", ), ...lucideCompletionIconMask(KEYWORD_ICON), }, ".cm-completionIcon-snippet": { color: colorMixValue( "var(--violet-500, #8b5cf6)", "color-mix(in oklch, var(--violet-500, #8b5cf6) 92%, var(--popover-foreground))", ), ...lucideCompletionIconMask(SNIPPET_ICON), }, ".cm-completionIcon-function": { color: colorMixValue( "var(--emerald-500, #10b981)", "color-mix(in oklch, var(--emerald-500, #10b981) 92%, var(--popover-foreground))", ), ...lucideCompletionIconMask(FUNCTION_ICON), }, ".cm-completionIcon-schema": { color: colorMixValue( "var(--amber-500, #f59e0b)", "color-mix(in oklch, var(--amber-500, #f59e0b) 92%, var(--popover-foreground))", ), ...lucideCompletionIconMask(SCHEMA_ICON), }, ".cm-completionLabel": { color: "inherit", fontFamily: `var(${EDITOR_FONT_FAMILY_CSS_VAR}, var(--font-mono, monospace))`, fontSize: `clamp(12px, var(${EDITOR_FONT_SIZE_CSS_VAR}, 13px), 14px)`, fontWeight: "520", letterSpacing: "0", }, ".cm-completionMatchedText": { color: oklchValue("rgb(29 132 245)", "oklch(0.62 0.19 255)"), fontWeight: "700", textDecoration: "none", }, ".cm-completionDetail": { color: colorMixValue( "var(--muted-foreground)", "color-mix(in oklch, var(--popover-foreground) 68%, var(--popover))", ), fontSize: `clamp(11px, calc(var(${EDITOR_FONT_SIZE_CSS_VAR}, 13px) - 1px), 13px)`, fontWeight: "500", fontStyle: "normal", marginLeft: "10px", opacity: "1", }, }; } export function sqlCompletionTheme(EditorView: typeof import("@codemirror/view").EditorView): Extension { return EditorView.theme(buildSqlCompletionThemeRules()); }