import { useCallback, useEffect, useRef, useState } from 'react' import Editor, { type OnMount, type BeforeMount } from '@monaco-editor/react' import type { editor as MonacoEditor } from 'monaco-editor' import { useTreeEditorStore } from '@/store/treeEditorStore' import { treeMarkdownApi } from '@/api/treeMarkdown' import { resolutionFlowLanguage, LANGUAGE_ID } from './resolutionFlowLanguage' import { resolutionFlowTheme, THEME_ID } from './resolutionFlowTheme' import { createCompletionProvider } from './resolutionFlowCompletions' import { CodeModeToolbar } from './CodeModeToolbar' import { SyntaxHelpPanel } from './SyntaxHelpPanel' import { setMonacoEditor } from './monacoEditorRef' import { Spinner } from '@/components/common/Spinner' export function CodeModeEditor() { const editorRef = useRef(null) const validationTimeoutRef = useRef | null>(null) const abortControllerRef = useRef(null) const { markdownSource, markdownValidationErrors, isMarkdownValid, isValidating, setMarkdownSource, setMarkdownValidationResult, getAvailableTargetNodes, } = useTreeEditorStore() const [syntaxHelpOpen, setSyntaxHelpOpen] = useState(false) // Register language and theme before editor mounts const handleEditorWillMount: BeforeMount = useCallback((monaco) => { // Register language if not already registered const langs = monaco.languages.getLanguages() if (!langs.some((l: { id: string }) => l.id === LANGUAGE_ID)) { monaco.languages.register({ id: LANGUAGE_ID }) monaco.languages.setMonarchTokensProvider(LANGUAGE_ID, resolutionFlowLanguage) } // Register theme monaco.editor.defineTheme(THEME_ID, resolutionFlowTheme) // Register completion provider monaco.languages.registerCompletionItemProvider( LANGUAGE_ID, createCompletionProvider(() => getAvailableTargetNodes().map(n => ({ id: n.id, label: n.label, type: n.type, })) ) ) }, [getAvailableTargetNodes]) // Editor mounted const handleEditorDidMount: OnMount = useCallback((editor) => { editorRef.current = editor setMonacoEditor(editor) editor.focus() }, []) // Debounced validation on change const handleEditorChange = useCallback((value: string | undefined) => { const md = value ?? '' setMarkdownSource(md) // Cancel pending validation if (validationTimeoutRef.current) { clearTimeout(validationTimeoutRef.current) } if (abortControllerRef.current) { abortControllerRef.current.abort() } // Debounce 800ms validationTimeoutRef.current = setTimeout(async () => { abortControllerRef.current = new AbortController() try { const result = await treeMarkdownApi.validateMarkdown(md) setMarkdownValidationResult(result) // Set Monaco markers if (editorRef.current) { const monaco = (await import('monaco-editor')).default ?? await import('monaco-editor') const model = editorRef.current.getModel() if (model && monaco.editor?.setModelMarkers) { const markers = result.errors.map((err) => ({ startLineNumber: err.line, startColumn: err.column, endLineNumber: err.line, endColumn: err.column + 1, message: err.message, severity: err.severity === 'error' ? 8 : 4, // MarkerSeverity.Error : Warning })) monaco.editor.setModelMarkers(model, 'resolutionflow', markers) } } } catch { // Validation cancelled or failed — ignore } }, 800) }, [setMarkdownSource, setMarkdownValidationResult]) // Insert template at cursor const handleInsertTemplate = useCallback((template: string) => { const editor = editorRef.current if (!editor) return const position = editor.getPosition() if (!position) return const model = editor.getModel() if (!model) return // Insert at end of document const lastLine = model.getLineCount() const lastColumn = model.getLineMaxColumn(lastLine) editor.executeEdits('insert-template', [{ range: { startLineNumber: lastLine, startColumn: lastColumn, endLineNumber: lastLine, endColumn: lastColumn, }, text: template, }]) // Move cursor to the inserted template const newLastLine = model.getLineCount() editor.setPosition({ lineNumber: newLastLine, column: 1 }) editor.revealLine(newLastLine) editor.focus() }, []) // Cleanup on unmount useEffect(() => { return () => { setMonacoEditor(null) if (validationTimeoutRef.current) { clearTimeout(validationTimeoutRef.current) } if (abortControllerRef.current) { abortControllerRef.current.abort() } } }, []) return (
setSyntaxHelpOpen(!syntaxHelpOpen)} syntaxHelpOpen={syntaxHelpOpen} />
} options={{ minimap: { enabled: false }, fontSize: 13, lineNumbers: 'on', wordWrap: 'on', scrollBeyondLastLine: false, renderLineHighlight: 'line', tabSize: 2, insertSpaces: true, automaticLayout: true, suggestOnTriggerCharacters: true, quickSuggestions: true, padding: { top: 12, bottom: 12 }, accessibilitySupport: 'on', }} /> {/* Syntax help as absolute overlay on right side */} {syntaxHelpOpen && (
setSyntaxHelpOpen(false)} />
)}
) }