diff --git a/src/main/frontend/app/routes/editor/editor.tsx b/src/main/frontend/app/routes/editor/editor.tsx index e106a567..b657439a 100644 --- a/src/main/frontend/app/routes/editor/editor.tsx +++ b/src/main/frontend/app/routes/editor/editor.tsx @@ -1,4 +1,4 @@ -import Editor, { type Monaco, type OnMount } from '@monaco-editor/react' +import Editor, { type Monaco, type OnMount, type BeforeMount } from '@monaco-editor/react' import XsdManager from 'monaco-xsd-code-completion/esm/XsdManager' import XsdFeatures from 'monaco-xsd-code-completion/esm/XsdFeatures' import 'monaco-xsd-code-completion/src/style.css' @@ -47,6 +47,54 @@ const SAVED_DISPLAY_DURATION = 2000 const ELEMENT_ERROR_RE = /[Ee]lement [\u2018\u2019'"'{]?([\w:.-]+)[\u2018\u2019'"'}]?/ const ATTRIBUTE_ERROR_RE = /[Aa]ttribute [\u2018\u2019'"'{]?([\w:.-]+)[\u2018\u2019'"'}]?/ +const XML_MONARCH_GRAMMAR = { + defaultToken: '', + tokenPostfix: '.xml', + tokenizer: { + root: [ + [/[^<&]+/, ''], + [/&\w+;/, 'string.escape'], + [//, { token: 'delimiter.cdata', next: '@pop' }], + [/\]/, ''], + ], + xmlDecl: [ + [/\s+/, ''], + [/[\w:-]+/, 'attribute.name'], + [/=/, 'delimiter'], + [/"[^"]*"/, 'attribute.value'], + [/'[^']*'/, 'attribute.value'], + [/\?>/, { token: 'delimiter', next: '@pop' }], + ], + tag: [ + [/\s+/, ''], + // flow: namespace attributes — literal regexes, matched before generic rules + [/((?:xmlns:flow|flow:[\w-]+))(\s*=\s*(?:"[^"]*"|'[^']*'))/, ['flow-attribute', 'flow-attribute-value']], + [/(?:xmlns:flow|flow:[\w-]+)/, 'flow-attribute'], + // Regular attributes + [/([\w.:_-]+)(\s*=\s*(?:"[^"]*"|'[^']*'))/, ['attribute.name', 'attribute.value']], + [/[\w.:_-]+/, 'attribute.name'], + [/=/, 'delimiter'], + [/\/>/, { token: 'delimiter', next: '@pop' }], + [/>/, { token: 'delimiter', next: '@pop' }], + ], + comment: [ + [/-->/, { token: 'comment', next: '@pop' }], + [/[^-]+/, 'comment'], + [/--/, 'comment'], + [/./, 'comment'], + ], + }, +} + function extractLocalName(name: string): string { return name.includes(':') ? name.split(':').pop()! : name } @@ -314,11 +362,40 @@ export default function CodeEditor() { .catch(console.error) }, [editorMounted]) + const handleEditorBeforeMount: BeforeMount = (monacoInstance) => { + monacoInstance.languages.setMonarchTokensProvider( + 'xml', + XML_MONARCH_GRAMMAR as Parameters[1], + ) + + // Use non-conflicting names — 'vs-dark' is a Monaco built-in and can't safely be overridden + monacoInstance.editor.defineTheme('flow-vs-light', { + base: 'vs', + inherit: true, + rules: [ + { token: 'flow-attribute.xml', foreground: 'a8a8a8', fontStyle: 'italic' }, + { token: 'flow-attribute-value.xml', foreground: 'a8a8a8', fontStyle: 'italic' }, + ], + colors: {}, + }) + monacoInstance.editor.defineTheme('flow-vs-dark', { + base: 'vs-dark', + inherit: true, + rules: [ + { token: 'flow-attribute.xml', foreground: '808080', fontStyle: 'italic' }, + { token: 'flow-attribute-value.xml', foreground: '808080', fontStyle: 'italic' }, + ], + colors: {}, + }) + } + const handleEditorMount: OnMount = (editor, monacoInstance) => { editorReference.current = editor monacoReference.current = monacoInstance setEditorMounted(true) + setTimeout(() => monacoInstance.editor.setTheme(`flow-vs-${theme}`), 0) + editor.addAction({ id: 'save-file', label: 'Save File', @@ -545,8 +622,9 @@ export default function CodeEditor() {
{ scheduleSave()