import React, { useState, useRef, useEffect } from 'react' import Editor, { EditorProps, loader } from '@monaco-editor/react' import { TextWidget, IconWidget, usePrefix, useTheme, } from '@pind/designable-react' import * as monaco from 'monaco-editor/esm/vs/editor/editor.api' import { Tooltip } from 'antd' import { parseExpression, parse } from '@babel/parser' import { globalThisPolyfill, uid } from '@pind/designable-shared' import { format } from './format' import cls from 'classnames' import './styles.less' import './config' import { initMonaco } from './config' export type Monaco = typeof monaco export interface MonacoInputProps extends EditorProps { helpLink?: string | boolean helpCode?: string helpCodeViewWidth?: number | string extraLib?: string onChange?: (value: string) => void } const InternalMonacoInput: React.FC = ({ className, language, defaultLanguage, width, helpLink, helpCode, helpCodeViewWidth, height, onMount, onChange, ...props }) => { const [loaded, setLoaded] = useState(false) const theme = useTheme() const valueRef = useRef('') const validateRef = useRef(0) const submitRef = useRef(0) const declarationRef = useRef([]) const extraLibRef = useRef() const monacoRef = useRef() const editorRef = useRef() const computedLanguage = useRef( (language || defaultLanguage) as string ) const realLanguage = useRef('') const unmountedRef = useRef(false) const changedRef = useRef(false) const uidRef = useRef(uid()) const prefix = usePrefix('monaco-input') const input = props.value || props.defaultValue useEffect(() => { unmountedRef.current = false initMonaco() return () => { if (extraLibRef.current) { extraLibRef.current.dispose() } unmountedRef.current = true } }, []) useEffect(() => { if (monacoRef.current && props.extraLib) { updateExtraLib() } }, [props.extraLib]) const updateExtraLib = () => { if (extraLibRef.current) { extraLibRef.current.dispose() } if (!monacoRef.current || !props.extraLib) return extraLibRef.current = monacoRef.current.languages.typescript.typescriptDefaults.addExtraLib( props.extraLib, `${uidRef.current}.d.ts` ) } const isFileLanguage = () => { const lang = computedLanguage.current return lang === 'javascript' || lang === 'typescript' } const isExpLanguage = () => { const lang = computedLanguage.current return lang === 'javascript.expression' || lang === 'typescript.expression' } const renderHelper = () => { const getHref = () => { if (typeof helpLink === 'string') return helpLink if (isFileLanguage()) { return 'https://developer.mozilla.org/zh-CN/docs/Web/JavaScript' } if (isExpLanguage()) { return 'https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators' } } if (helpLink === false) return null const href = getHref() return ( href && ( } > ) ) } const onMountHandler = ( editor: monaco.editor.IStandaloneCodeEditor, monaco: Monaco ) => { editorRef.current = editor monacoRef.current = monaco onMount?.(editor, monaco) const model = editor.getModel() const currentValue = editor.getValue() if (model) { model['getDesignerLanguage'] = () => computedLanguage.current } if (currentValue) { format(computedLanguage.current, currentValue) .then((content) => { editor.setValue(content) setLoaded(true) }) .catch(() => { setLoaded(true) }) } else { setLoaded(true) } if (props.extraLib) { updateExtraLib() } editor.onDidChangeModelContent(() => { onChangeHandler(editor.getValue()) }) } const submit = () => { clearTimeout(submitRef.current) submitRef.current = globalThisPolyfill.setTimeout(() => { onChange?.(valueRef.current) }, 1000) } const validate = () => { if (realLanguage.current === 'typescript') { globalThisPolyfill.clearTimeout(validateRef.current) validateRef.current = globalThisPolyfill.setTimeout(() => { try { if (valueRef.current) { if (isFileLanguage()) { parse(valueRef.current, { sourceType: 'module', plugins: ['typescript', 'jsx'], }) } else if (isExpLanguage()) { parseExpression(valueRef.current, { plugins: ['typescript', 'jsx'], }) } } if (!monacoRef.current || !editorRef.current) return const model = editorRef.current.getModel() if (model) { monacoRef.current.editor.setModelMarkers( model, computedLanguage.current, [] ) } declarationRef.current = editorRef.current.deltaDecorations( declarationRef.current, [ { range: new monacoRef.current.Range(1, 1, 1, 1), options: {}, }, ] ) submit() } catch (e) { if (!monacoRef.current || !editorRef.current) return declarationRef.current = editorRef.current.deltaDecorations( declarationRef.current, [ { range: new monacoRef.current.Range( e.loc.line, e.loc.column, e.loc.line, e.loc.column ), options: { isWholeLine: true, glyphMarginClassName: 'monaco-error-highline', }, }, ] ) const model = editorRef.current.getModel() if (!model) return monacoRef.current.editor.setModelMarkers( model, computedLanguage.current, [ { code: '1003', severity: 8, startLineNumber: e.loc.line, startColumn: e.loc.column, endLineNumber: e.loc.line, endColumn: e.loc.column, message: e.message, }, ] ) } }, 240) } else { submit() if (!editorRef.current || !monacoRef.current) return declarationRef.current = editorRef.current.deltaDecorations( declarationRef.current, [ { range: new monacoRef.current.Range(1, 1, 1, 1), options: {}, }, ] ) } } const onChangeHandler = (value: string) => { changedRef.current = true valueRef.current = value validate() } computedLanguage.current = (language || defaultLanguage) as string realLanguage.current = /(?:javascript|typescript)/gi.test( computedLanguage.current ) ? 'typescript' : computedLanguage.current const renderHelpCode = () => { if (!helpCode) return null return (
) } return (
{renderHelper()}
{renderHelpCode()}
) } export const MonacoInput = Object.assign(InternalMonacoInput, { loader, })