import React, { useCallback, useEffect, useMemo, useState } from 'react'; import Editor, { loader, type BeforeMount, type EditorProps } from '@monaco-editor/react'; import { useStore } from '../store'; import { sanitizeDataTableFontSize } from '../utils/dataGridDisplay'; import { DEFAULT_MONO_FONT_FAMILY } from '../utils/fontFamilies'; export type { BeforeMount, OnMount } from '@monaco-editor/react'; export type GonaviMonacoTypography = 'code' | 'data'; const DEFAULT_FONT_SIZE = 14; const MIN_FONT_SIZE = 12; const MAX_FONT_SIZE = 20; let monacoConfiguredPromise: Promise | null = null; let transparentThemesRegistered = false; const isTestRuntime = (): boolean => { const env = (import.meta as unknown as { env?: Record }).env || {}; return env.MODE === 'test' || env.VITEST === true || env.VITEST === 'true'; }; export const registerGonaviMonacoThemes: BeforeMount = (monaco) => { if (transparentThemesRegistered) { return; } monaco.editor.defineTheme('transparent-dark', { base: 'vs-dark', inherit: true, rules: [], colors: { 'editor.background': '#00000000', 'editor.lineHighlightBackground': '#ffffff10', 'editorGutter.background': '#00000000', 'editorStickyScroll.background': '#1e1e1e', 'editorStickyScrollHover.background': '#2a2a2a', }, }); monaco.editor.defineTheme('transparent-light', { base: 'vs', inherit: true, rules: [], colors: { 'editor.background': '#00000000', 'editor.lineHighlightBackground': '#00000010', 'editorGutter.background': '#00000000', 'editorStickyScroll.background': '#ffffff', 'editorStickyScrollHover.background': '#f5f5f5', }, }); transparentThemesRegistered = true; }; const ensureMonacoConfigured = (): Promise => { if (isTestRuntime()) { return Promise.resolve(); } if (!monacoConfiguredPromise) { monacoConfiguredPromise = import('monaco-editor/esm/nls.messages.zh-cn') .then(() => import('monaco-editor')) .then((monaco) => { loader.config({ monaco }); }); } return monacoConfiguredPromise; }; interface MonacoEditorProps extends EditorProps { gonaviTypography?: GonaviMonacoTypography; } const MonacoEditor: React.FC = ({ beforeMount, gonaviTypography = 'code', loading, options, ...props }) => { const [ready, setReady] = useState(isTestRuntime); const uiVersion = useStore((state) => state.appearance.uiVersion); const dataTableFontSize = useStore((state) => state.appearance.dataTableFontSize); const dataTableFontSizeFollowGlobal = useStore((state) => state.appearance.dataTableFontSizeFollowGlobal); const monoFontFamily = useStore((state) => state.appearance.customMonoFontFamily); const globalFontSize = useStore((state) => state.fontSize); useEffect(() => { let cancelled = false; void ensureMonacoConfigured() .then(() => { if (!cancelled) { setReady(true); } }) .catch((error) => { console.error('Failed to configure Monaco Editor', error); if (!cancelled) { setReady(true); } }); return () => { cancelled = true; }; }, []); const handleBeforeMount: BeforeMount = useCallback((monaco) => { registerGonaviMonacoThemes(monaco); beforeMount?.(monaco); }, [beforeMount]); const resolvedOptions = useMemo(() => { if (uiVersion !== 'v2') { return options; } const effectiveGlobalFontSize = Math.min( MAX_FONT_SIZE, Math.max(MIN_FONT_SIZE, Math.round(Number(globalFontSize) || DEFAULT_FONT_SIZE)), ); const effectiveDataTableFontSize = dataTableFontSizeFollowGlobal !== false ? effectiveGlobalFontSize : (sanitizeDataTableFontSize(dataTableFontSize) ?? effectiveGlobalFontSize); const resolvedFontSize = gonaviTypography === 'data' ? effectiveDataTableFontSize : Math.max(10, Math.round(effectiveDataTableFontSize * 0.92)); return { ...options, fontFamily: options?.fontFamily ?? monoFontFamily ?? DEFAULT_MONO_FONT_FAMILY, fontSize: options?.fontSize ?? resolvedFontSize, lineHeight: options?.lineHeight ?? Math.max(18, Math.round(resolvedFontSize * 1.62)), }; }, [ dataTableFontSize, dataTableFontSizeFollowGlobal, globalFontSize, gonaviTypography, monoFontFamily, options, uiVersion, ]); if (!ready) { return (
{loading || null}
); } return ( ); }; export default MonacoEditor;