import React, { useEffect, useRef } from 'react'; import MonacoEditor from 'react-monaco-editor'; import * as monaco from 'monaco-editor'; import type * as monacoTypes from 'monaco-editor'; import { v4 as uuidv4 } from 'uuid'; import { css } from '@emotion/css'; import { installAutoHeightLayout } from '../shared/autoHeightLayout'; import { hideMirroredInputAreaClassName } from '../shared/hideMirroredInputArea'; import { language, languageConfiguration } from './yaml'; import { getYamlCompletionProvider } from './completion/getCompletionProvider'; import { validateYaml } from './validation'; import type { YamlEditorSchema } from './types'; interface YamlEditorProps { size?: 'small' | 'middle' | 'large'; theme?: 'light' | 'dark'; value?: string; placeholder?: string; enableAutocomplete?: boolean; readOnly?: boolean; disabled?: boolean; schemas?: YamlEditorSchema[]; onChange?: (value: string) => void; onEnter?: (value: string) => void; onBlur?: (value: string) => void; editorDidMount?: (editor: monacoTypes.editor.IStandaloneCodeEditor) => void; } const YAML_LANG_ID = 'yaml'; const SIZE_MAP: Record< string, { className: string; top: number; bottom: number; } > = { small: { className: 'ant-input-sm', top: 1, bottom: 1, }, middle: { className: 'ant-input-md', top: 1, bottom: 1, }, large: { className: 'ant-input-lg', top: 3, bottom: 2, }, }; const themeMap: Record = { light: 'yaml-light', dark: 'yaml-dark', }; const containerDisabledClassName = css` .monaco-editor { user-select: none; pointer-events: none; } `; const containerReadOnlyClassName = css` .monaco-editor .cursors-layer > .cursor { opacity: 0 !important; } `; const getStyles = (placeholder?: string) => { return { placeholder: css({ '::after': { content: `'${placeholder}'`, opacity: 0.6, }, }), }; }; export default function YamlEditor(props: YamlEditorProps) { const id = uuidv4(); const { size = 'middle', theme = 'light', value, placeholder, enableAutocomplete = true, readOnly = false, disabled = false, schemas = [], onChange, onEnter, onBlur, editorDidMount, } = props; const autocompleteDisposeFun = useRef<(() => void) | null>(null); const containerRef = useRef(null); const editorRef = useRef(null); const styles = getStyles(placeholder); const handleEditorDidMount = (editor: monacoTypes.editor.IStandaloneCodeEditor) => { editorRef.current = editor; // 定义主题 monaco.editor.defineTheme('yaml-light', { base: 'vs', inherit: true, rules: [ { token: 'key', foreground: '0000FF' }, { token: 'string', foreground: '008000' }, { token: 'number', foreground: '098658' }, { token: 'comment', foreground: '008000', fontStyle: 'italic' }, { token: 'delimiter', foreground: '000000' }, { token: 'tag', foreground: '800080' }, { token: 'keyword', foreground: '0000FF', fontStyle: 'bold' }, ], colors: { 'editor.background': '#00000000', focusBorder: '#00000000', 'editor.lineHighlightBackground': '#00000000', 'editor.lineHighlightBorder': '#00000000', }, }); monaco.editor.defineTheme('yaml-dark', { base: 'vs-dark', inherit: true, rules: [ { token: 'key', foreground: '9CDCFE' }, { token: 'string', foreground: 'CE9178' }, { token: 'number', foreground: 'B5CEA8' }, { token: 'comment', foreground: '6A9955', fontStyle: 'italic' }, { token: 'delimiter', foreground: 'D4D4D4' }, { token: 'tag', foreground: 'C586C0' }, { token: 'keyword', foreground: '569CD6', fontStyle: 'bold' }, ], colors: { 'editor.background': '#00000000', focusBorder: '#00000000', 'editor.lineHighlightBackground': '#00000000', 'editor.lineHighlightBorder': '#00000000', }, }); const isEditorFocused = editor.createContextKey('isEditorFocused' + id, false); // 设置焦点状态 editor.onDidBlurEditorWidget(() => { isEditorFocused.set(false); onBlur?.(editor.getValue()); const position = editor.getPosition(); if (position) { const newSelection = new monaco.Selection(position.lineNumber, position.column, position.lineNumber, position.column); editor.setSelection(newSelection); } }); editor.onDidFocusEditorText(() => { isEditorFocused.set(true); }); if (containerRef.current) { installAutoHeightLayout(editor, containerRef.current, { minHeight: 20 }); } // 禁用默认的搜索快捷键 monaco.editor.addKeybindingRule({ keybinding: monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyF, command: null, }); // Shift + Enter 换行 editor.addCommand( monaco.KeyMod.Shift | monaco.KeyCode.Enter, () => { const position = editor.getPosition(); if (position) { editor.executeEdits('shift-enter', [ { range: new monaco.Range(position.lineNumber, position.column, position.lineNumber, position.column), text: '\n', }, ]); editor.setPosition({ lineNumber: position.lineNumber + 1, column: 1, }); } }, 'isEditorFocused' + id, ); // Enter 键处理 - 移除阻止默认行为的规则,允许正常换行 // 只有当提供了 onEnter 回调且没有建议窗口时才触发回调 if (onEnter) { editor.addCommand( monaco.KeyCode.Enter, () => { onEnter(editor.getValue()); }, '!suggestWidgetVisible && isEditorFocused' + id, ); } // 内容变化时进行验证 editor.onDidChangeModelContent(() => { const model = editor.getModel(); if (model) { const content = model.getValue(); const errors = validateYaml(content, schemas); const markers = errors.map((error) => ({ message: error.message, severity: error.severity === 'error' ? monaco.MarkerSeverity.Error : error.severity === 'warning' ? monaco.MarkerSeverity.Warning : monaco.MarkerSeverity.Info, startLineNumber: error.startLineNumber, endLineNumber: error.endLineNumber, startColumn: error.startColumn, endColumn: error.endColumn, })); monaco.editor.setModelMarkers(model, 'yaml', markers); } }); editorDidMount?.(editor); }; useEffect(() => { // 注册 YAML 语言 monaco.languages.register({ id: YAML_LANG_ID, aliases: ['YAML', 'yaml'], extensions: ['.yaml', '.yml'], mimetypes: ['application/x-yaml', 'text/x-yaml'], }); // 设置语法高亮 monaco.languages.setMonarchTokensProvider(YAML_LANG_ID, language as any); // 设置语言配置 monaco.languages.setLanguageConfiguration(YAML_LANG_ID, languageConfiguration as any); return () => { autocompleteDisposeFun.current?.(); }; }, []); useEffect(() => { const editor = editorRef.current; if (!editor) return; // 清理之前的自动补全提供器 if (autocompleteDisposeFun.current) { autocompleteDisposeFun.current(); autocompleteDisposeFun.current = null; } // 如果启用自动补全,设置新的补全提供器 if (enableAutocomplete) { const completionProvider = getYamlCompletionProvider(monaco, schemas); const filteringCompletionProvider: monacoTypes.languages.CompletionItemProvider = { ...completionProvider, provideCompletionItems: (model, position, context, token) => { if (editor.getModel()?.id !== model.id) { return { suggestions: [] }; } return completionProvider.provideCompletionItems(model, position, context, token); }, }; const disposable = monaco.languages.registerCompletionItemProvider(YAML_LANG_ID, filteringCompletionProvider); autocompleteDisposeFun.current = () => disposable.dispose(); } // 处理占位符 const model = editor.getModel(); if (model) { model.deltaDecorations( model.getAllDecorations().map((d) => d.id), [], ); } if (placeholder) { const placeholderDecorators = [ { range: new monaco.Range(1, 1, 1, 1), options: { className: styles.placeholder, isWholeLine: true, }, }, ]; let decorators: string[] = []; const checkDecorators = () => { const model = editor.getModel(); if (!model) return; const newDecorators = model.getValueLength() === 0 ? placeholderDecorators : []; decorators = model.deltaDecorations(decorators, newDecorators); }; checkDecorators(); editor.onDidChangeModelContent(checkDecorators); } }, [enableAutocomplete, JSON.stringify(schemas), placeholder]); return (
); }