import React, { useRef, useEffect, forwardRef, useImperativeHandle } from 'react'; import Editor from '@monaco-editor/react'; import type { ThemeConfig } from '@/lib/themes'; interface CodeEditorProps { value: string; onChange: (value: string) => void; onSelectionChange?: (lineStart: number, lineEnd: number) => void; language?: string; className?: string; themeConfig?: ThemeConfig; } export interface CodeEditorRef { selectLines: (startLine: number, endLine?: number) => void; focus: () => void; } export const CodeEditor = forwardRef(({ value, onChange, onSelectionChange, language = 'plaintext', className = '', themeConfig }, ref) => { const editorRef = useRef(null); const monacoRef = useRef(null); const [editorReady, setEditorReady] = React.useState(false); // Expose methods to parent component useImperativeHandle(ref, () => ({ selectLines: (startLine: number, endLine?: number) => { if (editorRef.current) { const actualEndLine = endLine || startLine; const selection = { startLineNumber: startLine, startColumn: 1, endLineNumber: actualEndLine, endColumn: editorRef.current.getModel()?.getLineMaxColumn(actualEndLine) || 1, }; editorRef.current.setSelection(selection); editorRef.current.revealLineInCenter(startLine); editorRef.current.focus(); } }, focus: () => { if (editorRef.current) { editorRef.current.focus(); } }, }), []); const handleEditorDidMount = (editor: any, monaco: any) => { editorRef.current = editor; monacoRef.current = monaco; setEditorReady(true); // Add selection change listener if (onSelectionChange) { editor.onDidChangeCursorSelection((e: any) => { const selection = e.selection; const startLine = selection.startLineNumber; const endLine = selection.endLineNumber; onSelectionChange(startLine, endLine); }); } }; const handleEditorChange = (value: string | undefined) => { onChange(value || ''); }; // Ensure Monaco theme is always set after both editor and themeConfig are ready useEffect(() => { if (!editorReady || !themeConfig || !monacoRef.current) return; // Convert HSL to hex for Monaco editor const hslToHex = (hsl: string): string => { const match = hsl.match(/hsl\((\d+),\s*(\d+)%\,\s*(\d+)%\)/); if (!match) return '#000000'; const h = parseInt(match[1]) / 360; const s = parseInt(match[2]) / 100; const l = parseInt(match[3]) / 100; const hue2rgb = (p: number, q: number, t: number) => { if (t < 0) t += 1; if (t > 1) t -= 1; if (t < 1/6) return p + (q - p) * 6 * t; if (t < 1/2) return q; if (t < 2/3) return p + (q - p) * (2/3 - t) * 6; return p; }; let r, g, b; if (s === 0) { r = g = b = l; } else { const q = l < 0.5 ? l * (1 + s) : l + s - l * s; const p = 2 * l - q; r = hue2rgb(p, q, h + 1/3); g = hue2rgb(p, q, h); b = hue2rgb(p, q, h - 1/3); } const toHex = (c: number) => { const hex = Math.round(c * 255).toString(16); return hex.length === 1 ? '0' + hex : hex; }; return `#${toHex(r)}${toHex(g)}${toHex(b)}`; }; const defineThemeFromConfig = (config: ThemeConfig) => { const themeName = `osborne-${config.id}`; const backgroundColor = hslToHex(config.colors.background); const foregroundColor = hslToHex(config.colors.foreground); const cardColor = hslToHex(config.colors.card); const borderColor = hslToHex(config.colors.border); const mutedColor = hslToHex(config.colors.muted); const primaryColor = hslToHex(config.colors.primary); monacoRef.current.editor.defineTheme(themeName, { base: config.type === 'dark' ? 'vs-dark' : 'vs', inherit: true, rules: [ { token: '', foreground: foregroundColor.substring(1) }, { token: 'comment', foreground: config.type === 'dark' ? '6A9955' : '008000', fontStyle: 'italic' }, { token: 'string', foreground: config.type === 'dark' ? 'CE9178' : 'A31515' }, { token: 'number', foreground: config.type === 'dark' ? 'B5CEA8' : '098658' }, { token: 'keyword', foreground: primaryColor.substring(1), fontStyle: 'bold' }, { token: 'type', foreground: config.type === 'dark' ? '4EC9B0' : '267F99' }, { token: 'function', foreground: config.type === 'dark' ? 'DCDCAA' : '795E26' }, { token: 'variable', foreground: config.type === 'dark' ? '9CDCFE' : '001080' }, ], colors: { 'editor.background': backgroundColor, 'editor.foreground': foregroundColor, 'editor.lineHighlightBackground': cardColor, 'editor.selectionBackground': primaryColor + '40', 'editorLineNumber.foreground': hslToHex(config.colors.mutedForeground), 'editorLineNumber.activeForeground': foregroundColor, 'editorGutter.background': backgroundColor, 'editor.inactiveSelectionBackground': mutedColor, 'editorWhitespace.foreground': borderColor, 'editorCursor.foreground': foregroundColor, 'editorIndentGuide.background': borderColor, 'editorIndentGuide.activeBackground': primaryColor, 'editor.findMatchBackground': primaryColor + '60', 'editor.findMatchHighlightBackground': primaryColor + '30', } }); return themeName; }; const themeName = defineThemeFromConfig(themeConfig); monacoRef.current.editor.setTheme(themeName); }, [themeConfig, editorReady]); return (
Loading editor...
} /> ); }); CodeEditor.displayName = 'CodeEditor';