UNPKG

@finos/legend-studio

Version:
892 lines 44.9 kB
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; /** * Copyright (c) 2020-present, Goldman Sachs * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { useEffect, useState, useRef, useCallback, forwardRef } from 'react'; import { observer } from 'mobx-react-lite'; import { editor as monacoEditorAPI, languages as monacoLanguagesAPI, KeyCode, } from 'monaco-editor'; import { ContextMenu, revealError, setErrorMarkers, disposeEditor, baseTextEditorSettings, disableEditorHotKeys, resetLineNumberGutterWidth, clsx, WordWrapIcon, getEditorValue, normalizeLineEnding, MoreHorizontalIcon, HackerIcon, } from '@finos/legend-art'; import { TAB_SIZE, EDITOR_THEME, EDITOR_LANGUAGE, useApplicationStore, useApplicationNavigationContext, } from '@finos/legend-application'; import { useResizeDetector } from 'react-resize-detector'; import { CORE_DND_TYPE, } from '../../../stores/shared/DnDUtil.js'; import { useDrop } from 'react-dnd'; import { flowResult } from 'mobx'; import { useEditorStore } from '../EditorStoreProvider.js'; import { guaranteeNonNullable, hasWhiteSpace, isNonNullable, } from '@finos/legend-shared'; import { PARSER_SECTION_MARKER, PURE_CONNECTION_NAME, PURE_ELEMENT_NAME, PURE_PARSER, } from '@finos/legend-graph'; import { LEGEND_STUDIO_DOCUMENTATION_KEY } from '../../../stores/LegendStudioDocumentation.js'; import { BLANK_CLASS_SNIPPET, CLASS_WITH_CONSTRAINT_SNIPPET, CLASS_WITH_INHERITANCE_SNIPPET, CLASS_WITH_PROPERTY_SNIPPET, DATA_WITH_EXTERNAL_FORMAT_SNIPPET, DATA_WITH_MODEL_STORE_SNIPPET, createDataElementSnippetWithEmbeddedDataSuggestionSnippet, SIMPLE_PROFILE_SNIPPET, SIMPLE_ENUMERATION_SNIPPET, SIMPLE_ASSOCIATION_SNIPPET, SIMPLE_MEASURE_SNIPPET, BLANK_FUNCTION_SNIPPET, SIMPLE_FUNCTION_SNIPPET, SIMPLE_RUNTIME_SNIPPET, JSON_MODEL_CONNECTION_SNIPPET, XML_MODEL_CONNECTION_SNIPPET, MODEL_CHAIN_CONNECTION_SNIPPET, RELATIONAL_DATABASE_CONNECTION_SNIPPET, BLANK_RELATIONAL_DATABASE_SNIPPET, SIMPLE_GENERATION_SPECIFICATION_SNIPPET, BLANK_SERVICE_SNIPPET, SERVICE_WITH_SINGLE_EXECUTION_SNIPPET, SERVICE_WITH_MULTI_EXECUTION_SNIPPET, BLANK_MAPPING_SNIPPET, MAPPING_WITH_M2M_CLASS_MAPPING_SNIPPET, MAPPING_WITH_ENUMERATION_MAPPING_SNIPPET, MAPPING_WITH_RELATIONAL_CLASS_MAPPING_SNIPPET, } from '../../../stores/LegendStudioCodeSnippets.js'; import { LEGEND_STUDIO_APPLICATION_NAVIGATION_CONTEXT_KEY } from '../../../stores/LegendStudioApplicationNavigationContext.js'; const getSectionParserNameFromLineText = (lineText) => { if (lineText.startsWith(PARSER_SECTION_MARKER)) { return lineText.substring(PARSER_SECTION_MARKER.length).split(' ')[0]; } // NOTE: since leading whitespace to parser name is considered invalid, we will return `undefined` return undefined; }; export const GrammarTextEditorHeaderTabContextMenu = observer(forwardRef(function GrammarTextEditorHeaderTabContextMenu(props, ref) { const editorStore = useEditorStore(); const applicationStore = useApplicationStore(); const leaveTextMode = applicationStore.guardUnhandledError(() => flowResult(editorStore.toggleTextMode())); return (_jsx("div", { ref: ref, className: "edit-panel__header__tab__context-menu", children: _jsx("button", { className: "edit-panel__header__tab__context-menu__item", onClick: leaveTextMode, children: "Leave Text Mode" }) })); })); const getParserDocumetation = (editorStore, parserKeyword) => { switch (parserKeyword) { case PURE_PARSER.PURE: { return editorStore.applicationStore.documentationService.getDocEntry(LEGEND_STUDIO_DOCUMENTATION_KEY.GRAMMAR_PARSER_PURE); } case PURE_PARSER.MAPPING: { return editorStore.applicationStore.documentationService.getDocEntry(LEGEND_STUDIO_DOCUMENTATION_KEY.GRAMMAR_PARSER_MAPPING); } case PURE_PARSER.CONNECTION: { return editorStore.applicationStore.documentationService.getDocEntry(LEGEND_STUDIO_DOCUMENTATION_KEY.GRAMMAR_PARSER_CONNECTION); } case PURE_PARSER.RUNTIME: { return editorStore.applicationStore.documentationService.getDocEntry(LEGEND_STUDIO_DOCUMENTATION_KEY.GRAMMAR_PARSER_RUNTIME); } case PURE_PARSER.SERVICE: { return editorStore.applicationStore.documentationService.getDocEntry(LEGEND_STUDIO_DOCUMENTATION_KEY.GRAMMAR_PARSER_SERVICE); } case PURE_PARSER.RELATIONAL: { return editorStore.applicationStore.documentationService.getDocEntry(LEGEND_STUDIO_DOCUMENTATION_KEY.GRAMMAR_PARSER_RELATIONAL); } case PURE_PARSER.FILE_GENERATION_SPECIFICATION: { return editorStore.applicationStore.documentationService.getDocEntry(LEGEND_STUDIO_DOCUMENTATION_KEY.GRAMMAR_PARSER_FILE_GENERATION); } case PURE_PARSER.GENERATION_SPECIFICATION: { return editorStore.applicationStore.documentationService.getDocEntry(LEGEND_STUDIO_DOCUMENTATION_KEY.GRAMMAR_PARSER_GENERATION_SPECIFICATION); } case PURE_PARSER.DATA: { return editorStore.applicationStore.documentationService.getDocEntry(LEGEND_STUDIO_DOCUMENTATION_KEY.GRAMMAR_PARSER_DATA); } default: { const parserDocumentationGetters = editorStore.pluginManager .getApplicationPlugins() .flatMap((plugin) => plugin.getExtraPureGrammarParserDocumentationGetters?.() ?? []); for (const docGetter of parserDocumentationGetters) { const doc = docGetter(editorStore, parserKeyword); if (doc) { return doc; } } } } return undefined; }; const getParserElementDocumentation = (editorStore, parserKeyword, elementKeyword) => { switch (parserKeyword) { case PURE_PARSER.PURE: { if (elementKeyword === PURE_ELEMENT_NAME.CLASS) { return editorStore.applicationStore.documentationService.getDocEntry(LEGEND_STUDIO_DOCUMENTATION_KEY.GRAMMAR_ELEMENT_CLASS); } else if (elementKeyword === PURE_ELEMENT_NAME.PROFILE) { return editorStore.applicationStore.documentationService.getDocEntry(LEGEND_STUDIO_DOCUMENTATION_KEY.GRAMMAR_ELEMENT_PROFILE); } else if (elementKeyword === PURE_ELEMENT_NAME.ENUMERATION) { return editorStore.applicationStore.documentationService.getDocEntry(LEGEND_STUDIO_DOCUMENTATION_KEY.GRAMMAR_ELEMENT_ENUMERATION); } else if (elementKeyword === PURE_ELEMENT_NAME.MEASURE) { return editorStore.applicationStore.documentationService.getDocEntry(LEGEND_STUDIO_DOCUMENTATION_KEY.GRAMMAR_ELEMENT_MEASURE); } else if (elementKeyword === PURE_ELEMENT_NAME.ASSOCIATION) { return editorStore.applicationStore.documentationService.getDocEntry(LEGEND_STUDIO_DOCUMENTATION_KEY.GRAMMAR_ELEMENT_ASSOCIATION); } else if (elementKeyword === PURE_ELEMENT_NAME.FUNCTION) { return editorStore.applicationStore.documentationService.getDocEntry(LEGEND_STUDIO_DOCUMENTATION_KEY.GRAMMAR_ELEMENT_FUNCTION); } return undefined; } case PURE_PARSER.MAPPING: { if (elementKeyword === PURE_ELEMENT_NAME.MAPPING) { return editorStore.applicationStore.documentationService.getDocEntry(LEGEND_STUDIO_DOCUMENTATION_KEY.GRAMMAR_ELEMENT_MAPPING); } return undefined; } case PURE_PARSER.CONNECTION: { if (elementKeyword === PURE_CONNECTION_NAME.JSON_MODEL_CONNECTION) { return editorStore.applicationStore.documentationService.getDocEntry(LEGEND_STUDIO_DOCUMENTATION_KEY.GRAMMAR_CONNECTION_JSON_MODEL_CONNECTION); } else if (elementKeyword === PURE_CONNECTION_NAME.XML_MODEL_CONNECTION) { return editorStore.applicationStore.documentationService.getDocEntry(LEGEND_STUDIO_DOCUMENTATION_KEY.GRAMMAR_CONNECTION_XML_MODEL_CONNECTION); } else if (elementKeyword === PURE_CONNECTION_NAME.MODEL_CHAIN_CONNECTION) { return editorStore.applicationStore.documentationService.getDocEntry(LEGEND_STUDIO_DOCUMENTATION_KEY.GRAMMAR_CONNECTION_MODEL_CHAIN_CONNECTION); } else if (elementKeyword === PURE_CONNECTION_NAME.RELATIONAL_DATABASE_CONNECTION) { return editorStore.applicationStore.documentationService.getDocEntry(LEGEND_STUDIO_DOCUMENTATION_KEY.GRAMMAR_CONNECTION_RELATIONAL_DATABASE_CONNECTION); } // TODO: introduce extension mechanism return undefined; } case PURE_PARSER.RUNTIME: { if (elementKeyword === PURE_ELEMENT_NAME.RUNTIME) { return editorStore.applicationStore.documentationService.getDocEntry(LEGEND_STUDIO_DOCUMENTATION_KEY.GRAMMAR_ELEMENT_RUNTIME); } return undefined; } case PURE_PARSER.SERVICE: { if (elementKeyword === PURE_ELEMENT_NAME.SERVICE) { return editorStore.applicationStore.documentationService.getDocEntry(LEGEND_STUDIO_DOCUMENTATION_KEY.GRAMMAR_ELEMENT_SERVICE); } return undefined; } case PURE_PARSER.RELATIONAL: { if (elementKeyword === PURE_ELEMENT_NAME.DATABASE) { return editorStore.applicationStore.documentationService.getDocEntry(LEGEND_STUDIO_DOCUMENTATION_KEY.GRAMMAR_ELEMENT_DATABASE); } return undefined; } case PURE_PARSER.FILE_GENERATION_SPECIFICATION: { if (elementKeyword === PURE_ELEMENT_NAME.FILE_GENERATION) { return editorStore.applicationStore.documentationService.getDocEntry(LEGEND_STUDIO_DOCUMENTATION_KEY.GRAMMAR_ELEMENT_FILE_GENERATION_SPECIFICATION); } return undefined; } case PURE_PARSER.GENERATION_SPECIFICATION: { if (elementKeyword === PURE_ELEMENT_NAME.GENERATION_SPECIFICATION) { return editorStore.applicationStore.documentationService.getDocEntry(LEGEND_STUDIO_DOCUMENTATION_KEY.GRAMMAR_ELEMENT_GENERATION_SPECIFICATION); } return undefined; } case PURE_PARSER.DATA: { if (elementKeyword === PURE_ELEMENT_NAME.DATA_ELEMENT) { return editorStore.applicationStore.documentationService.getDocEntry(LEGEND_STUDIO_DOCUMENTATION_KEY.GRAMMAR_PARSER_DATA); } return undefined; } default: { const parserElementDocumentationGetters = editorStore.pluginManager .getApplicationPlugins() .flatMap((plugin) => plugin.getExtraPureGrammarParserElementDocumentationGetters?.() ?? []); for (const docGetter of parserElementDocumentationGetters) { const doc = docGetter(editorStore, parserKeyword, elementKeyword); if (doc) { return doc; } } } } return undefined; }; const getParserKeywordSuggestions = (editorStore) => { const parserKeywordSuggestions = editorStore.pluginManager .getApplicationPlugins() .flatMap((plugin) => plugin.getExtraPureGrammarParserKeywordSuggestionGetters?.() ?? []) .flatMap((suggestionGetter) => suggestionGetter(editorStore)); return [ { text: PURE_PARSER.PURE, description: `(core Pure)`, documentation: editorStore.applicationStore.documentationService.getDocEntry(LEGEND_STUDIO_DOCUMENTATION_KEY.GRAMMAR_PARSER_PURE), insertText: PURE_PARSER.PURE, }, { text: PURE_PARSER.MAPPING, description: `(dsl)`, documentation: editorStore.applicationStore.documentationService.getDocEntry(LEGEND_STUDIO_DOCUMENTATION_KEY.GRAMMAR_PARSER_MAPPING), insertText: PURE_PARSER.MAPPING, }, { text: PURE_PARSER.CONNECTION, description: `(dsl)`, documentation: editorStore.applicationStore.documentationService.getDocEntry(LEGEND_STUDIO_DOCUMENTATION_KEY.GRAMMAR_PARSER_CONNECTION), insertText: PURE_PARSER.CONNECTION, }, { text: PURE_PARSER.RUNTIME, description: `(dsl)`, documentation: editorStore.applicationStore.documentationService.getDocEntry(LEGEND_STUDIO_DOCUMENTATION_KEY.GRAMMAR_PARSER_RUNTIME), insertText: PURE_PARSER.RUNTIME, }, { text: PURE_PARSER.RELATIONAL, description: `(external store)`, documentation: editorStore.applicationStore.documentationService.getDocEntry(LEGEND_STUDIO_DOCUMENTATION_KEY.GRAMMAR_PARSER_RELATIONAL), insertText: PURE_PARSER.RELATIONAL, }, { text: PURE_PARSER.SERVICE, description: `(dsl)`, documentation: editorStore.applicationStore.documentationService.getDocEntry(LEGEND_STUDIO_DOCUMENTATION_KEY.GRAMMAR_PARSER_SERVICE), insertText: PURE_PARSER.SERVICE, }, { text: PURE_PARSER.GENERATION_SPECIFICATION, description: `(dsl)`, documentation: editorStore.applicationStore.documentationService.getDocEntry(LEGEND_STUDIO_DOCUMENTATION_KEY.GRAMMAR_PARSER_GENERATION_SPECIFICATION), insertText: PURE_PARSER.GENERATION_SPECIFICATION, }, { text: PURE_PARSER.FILE_GENERATION_SPECIFICATION, description: `(dsl)`, documentation: editorStore.applicationStore.documentationService.getDocEntry(LEGEND_STUDIO_DOCUMENTATION_KEY.GRAMMAR_PARSER_FILE_GENERATION), insertText: PURE_PARSER.FILE_GENERATION_SPECIFICATION, }, { text: PURE_PARSER.DATA, description: `(dsl)`, documentation: editorStore.applicationStore.documentationService.getDocEntry(LEGEND_STUDIO_DOCUMENTATION_KEY.GRAMMAR_PARSER_DATA), insertText: PURE_PARSER.DATA, }, ...parserKeywordSuggestions, ]; }; const getParserElementSnippetSuggestions = (editorStore, parserKeyword) => { switch (parserKeyword) { case PURE_PARSER.PURE: { return [ // class { text: PURE_ELEMENT_NAME.CLASS, description: '(blank)', insertText: BLANK_CLASS_SNIPPET, }, { text: PURE_ELEMENT_NAME.CLASS, description: 'with property', insertText: CLASS_WITH_PROPERTY_SNIPPET, }, { text: PURE_ELEMENT_NAME.CLASS, description: 'with inheritance', insertText: CLASS_WITH_INHERITANCE_SNIPPET, }, { text: PURE_ELEMENT_NAME.CLASS, description: 'with constraint', insertText: CLASS_WITH_CONSTRAINT_SNIPPET, }, // profile { text: PURE_ELEMENT_NAME.PROFILE, insertText: SIMPLE_PROFILE_SNIPPET, }, // enumeration { text: PURE_ELEMENT_NAME.ENUMERATION, insertText: SIMPLE_ENUMERATION_SNIPPET, }, // association { text: PURE_ELEMENT_NAME.ASSOCIATION, insertText: SIMPLE_ASSOCIATION_SNIPPET, }, // measure { text: PURE_ELEMENT_NAME.MEASURE, insertText: SIMPLE_MEASURE_SNIPPET, }, // function { text: PURE_ELEMENT_NAME.FUNCTION, description: '(blank)', insertText: BLANK_FUNCTION_SNIPPET, }, { text: PURE_ELEMENT_NAME.FUNCTION, insertText: SIMPLE_FUNCTION_SNIPPET, }, ]; } case PURE_PARSER.MAPPING: { return [ { text: PURE_ELEMENT_NAME.MAPPING, description: '(blank)', insertText: BLANK_MAPPING_SNIPPET, }, { text: PURE_ELEMENT_NAME.MAPPING, description: 'with model-to-model mapping', insertText: MAPPING_WITH_M2M_CLASS_MAPPING_SNIPPET, }, { text: PURE_ELEMENT_NAME.MAPPING, description: 'with relational mapping', insertText: MAPPING_WITH_RELATIONAL_CLASS_MAPPING_SNIPPET, }, { text: PURE_ELEMENT_NAME.MAPPING, description: 'with enumeration mapping', insertText: MAPPING_WITH_ENUMERATION_MAPPING_SNIPPET, }, ]; } case PURE_PARSER.CONNECTION: { return [ { text: PURE_CONNECTION_NAME.JSON_MODEL_CONNECTION, description: 'JSON model connection', insertText: JSON_MODEL_CONNECTION_SNIPPET, }, { text: PURE_CONNECTION_NAME.XML_MODEL_CONNECTION, description: 'XML model connection', insertText: XML_MODEL_CONNECTION_SNIPPET, }, { text: PURE_CONNECTION_NAME.MODEL_CHAIN_CONNECTION, description: 'model chain connection', insertText: MODEL_CHAIN_CONNECTION_SNIPPET, }, { text: PURE_CONNECTION_NAME.RELATIONAL_DATABASE_CONNECTION, description: 'relational database connection', insertText: RELATIONAL_DATABASE_CONNECTION_SNIPPET, }, // TODO: extension mehcanism for connection and relational database connection ]; } case PURE_PARSER.RUNTIME: { return [ { text: PURE_ELEMENT_NAME.RUNTIME, insertText: SIMPLE_RUNTIME_SNIPPET, }, ]; } case PURE_PARSER.SERVICE: { return [ { text: PURE_ELEMENT_NAME.SERVICE, description: '(blank)', insertText: BLANK_SERVICE_SNIPPET, }, { text: PURE_ELEMENT_NAME.SERVICE, description: 'with single execution', insertText: SERVICE_WITH_SINGLE_EXECUTION_SNIPPET, }, { text: PURE_ELEMENT_NAME.SERVICE, description: 'with multi execution', insertText: SERVICE_WITH_MULTI_EXECUTION_SNIPPET, }, ]; } case PURE_PARSER.RELATIONAL: { return [ { text: PURE_ELEMENT_NAME.DATABASE, description: '(blank)', insertText: BLANK_RELATIONAL_DATABASE_SNIPPET, }, ]; } case PURE_PARSER.FILE_GENERATION_SPECIFICATION: { return [ // TODO?: add extension mechanism for suggestion for different file generations ]; } case PURE_PARSER.GENERATION_SPECIFICATION: { return [ { text: PURE_ELEMENT_NAME.GENERATION_SPECIFICATION, description: '(blank)', insertText: SIMPLE_GENERATION_SPECIFICATION_SNIPPET, }, ]; } case PURE_PARSER.DATA: { const embeddedDateSnippetSuggestions = editorStore.pluginManager .getApplicationPlugins() .flatMap((plugin) => plugin.getExtraEmbeddedDataSnippetSuggestions?.() ?? []); return [ { text: PURE_ELEMENT_NAME.DATA_ELEMENT, description: 'with external format', insertText: DATA_WITH_EXTERNAL_FORMAT_SNIPPET, }, { text: PURE_ELEMENT_NAME.DATA_ELEMENT, description: 'using model store', insertText: DATA_WITH_MODEL_STORE_SNIPPET, }, ...embeddedDateSnippetSuggestions.map((suggestion) => ({ text: PURE_ELEMENT_NAME.DATA_ELEMENT, description: suggestion.description, insertText: createDataElementSnippetWithEmbeddedDataSuggestionSnippet(suggestion.text), })), ]; } default: { const parserElementSnippetSuggestionsGetters = editorStore.pluginManager .getApplicationPlugins() .flatMap((plugin) => plugin.getExtraPureGrammarParserElementSnippetSuggestionsGetters?.() ?? []); for (const snippetSuggestionsGetter of parserElementSnippetSuggestionsGetters) { const snippetSuggestions = snippetSuggestionsGetter(editorStore, parserKeyword); if (snippetSuggestions) { return snippetSuggestions; } } } } return []; }; const getInlineSnippetSuggestions = (editorStore) => [ { text: 'let', description: 'new variable', insertText: `let \${1:} = \${2:};`, }, { text: 'let', description: 'new collection', insertText: `let \${1:} = [\${2:}];`, }, { text: 'cast', description: 'type casting', insertText: `cast(@\${1:model::SomeClass})`, }, // conditionals { text: 'if', description: '(conditional)', insertText: `if(\${1:'true'}, | \${2:/* if true do this */}, | \${3:/* if false do this */})`, }, { text: 'case', description: '(conditional)', insertText: `case(\${1:}, \${2:'true'}, \${3:'false'})`, }, { text: 'match', description: '(conditional)', insertText: `match([x:\${1:String[1]}, \${2:''}])`, }, // collection { text: 'map', description: '(collection)', insertText: `map(x|\${1:})`, }, { text: 'filter', description: '(collection)', insertText: `filter(x|\${1:})`, }, { text: 'fold', description: '(collection)', insertText: `fold({a, b| \${1:$a + $b}}, \${2:0})`, }, { text: 'filter', description: '(collection)', insertText: `filter(x|\${1:})`, }, { text: 'sort', description: '(collection)', insertText: `sort()`, }, { text: 'in', description: '(collection)', insertText: `in()`, }, { text: 'slice', description: '(collection)', insertText: `slice(\${1:1},$\{2:2})`, }, { text: 'removeDuplicates', description: '(collection)', insertText: `removeDuplicates()`, }, { text: 'toOne', description: '(collection)', insertText: `toOne()`, }, { text: 'isEmpty', description: '(collection)', insertText: `isEmpty()`, }, // string { text: 'endsWith', description: '(string)', insertText: `endsWith()`, }, { text: 'startsWith', description: '(string)', insertText: `startsWith()`, }, ]; export const GrammarTextEditor = observer(() => { const [editor, setEditor] = useState(); const editorStore = useEditorStore(); const applicationStore = useApplicationStore(); const grammarTextEditorState = editorStore.grammarTextEditorState; const currentElementLabelRegexString = grammarTextEditorState.currentElementLabelRegexString; const error = grammarTextEditorState.error; const value = normalizeLineEnding(grammarTextEditorState.graphGrammarText); const textEditorRef = useRef(null); const hoverProviderDisposer = useRef(undefined); const suggestionProviderDisposer = useRef(undefined); const leaveTextMode = applicationStore.guardUnhandledError(() => flowResult(editorStore.toggleTextMode())); const toggleWordWrap = () => grammarTextEditorState.setWrapText(!grammarTextEditorState.wrapText); const { ref, width, height } = useResizeDetector(); useEffect(() => { if (width !== undefined && height !== undefined) { editor?.layout({ width, height }); } }, [editor, width, height]); useEffect(() => { if (!editor && textEditorRef.current) { const element = textEditorRef.current; const _editor = monacoEditorAPI.create(element, { ...baseTextEditorSettings, language: EDITOR_LANGUAGE.PURE, theme: EDITOR_THEME.LEGEND, renderValidationDecorations: 'on', }); _editor.onDidChangeModelContent(() => { grammarTextEditorState.setGraphGrammarText(getEditorValue(_editor)); editorStore.graphState.clearCompilationError(); // we can technically can reset the current element label regex string here // but if we do that on first load, the cursor will not jump to the current element // also, it's better to place that logic in an effect that watches for the regex string }); _editor.onKeyDown((event) => { if (event.keyCode === KeyCode.F9) { event.preventDefault(); event.stopPropagation(); flowResult(editorStore.graphState.globalCompileInTextMode()).catch(applicationStore.alertUnhandledError); } else if (event.keyCode === KeyCode.F8) { event.preventDefault(); event.stopPropagation(); flowResult(editorStore.toggleTextMode()).catch(applicationStore.alertUnhandledError); } }); disableEditorHotKeys(_editor); _editor.focus(); // focus on the editor initially setEditor(_editor); } }, [editorStore, applicationStore, editor, grammarTextEditorState]); // Drag and Drop const extraDnDTypes = editorStore.pluginManager .getApplicationPlugins() .flatMap((plugin) => plugin.getExtraPureGrammarTextEditorDnDTypes?.() ?? []); const handleDrop = useCallback((item, monitor) => { if (editor) { editor.trigger('keyboard', 'type', { text: item.data.packageableElement.path, }); } }, [editor]); const [, dropConnector] = useDrop(() => ({ accept: [ ...extraDnDTypes, CORE_DND_TYPE.PROJECT_EXPLORER_PACKAGE, CORE_DND_TYPE.PROJECT_EXPLORER_CLASS, CORE_DND_TYPE.PROJECT_EXPLORER_ASSOCIATION, CORE_DND_TYPE.PROJECT_EXPLORER_MEASURE, CORE_DND_TYPE.PROJECT_EXPLORER_ENUMERATION, CORE_DND_TYPE.PROJECT_EXPLORER_PROFILE, CORE_DND_TYPE.PROJECT_EXPLORER_FUNCTION, CORE_DND_TYPE.PROJECT_EXPLORER_FLAT_DATA, CORE_DND_TYPE.PROJECT_EXPLORER_DATABASE, CORE_DND_TYPE.PROJECT_EXPLORER_MAPPING, CORE_DND_TYPE.PROJECT_EXPLORER_SERVICE, CORE_DND_TYPE.PROJECT_EXPLORER_CONNECTION, CORE_DND_TYPE.PROJECT_EXPLORER_RUNTIME, CORE_DND_TYPE.PROJECT_EXPLORER_FILE_GENERATION, CORE_DND_TYPE.PROJECT_EXPLORER_GENERATION_TREE, CORE_DND_TYPE.PROJECT_EXPLORER_DATA, ], drop: (item, monitor) => handleDrop(item, monitor), }), [extraDnDTypes, handleDrop]); dropConnector(textEditorRef); if (editor) { // Set the value of the editor const currentValue = getEditorValue(editor); if (currentValue !== value) { editor.setValue(value); } editor.updateOptions({ wordWrap: grammarTextEditorState.wrapText ? 'on' : 'off', }); resetLineNumberGutterWidth(editor); const editorModel = editor.getModel(); if (editorModel) { editorModel.updateOptions({ tabSize: TAB_SIZE }); if (error?.sourceInformation) { setErrorMarkers(editorModel, error.message, error.sourceInformation.startLine, error.sourceInformation.startColumn, error.sourceInformation.endLine, error.sourceInformation.endColumn); } else { monacoEditorAPI.setModelMarkers(editorModel, 'Error', []); } } // Disable editing if user is in viewer mode editor.updateOptions({ readOnly: editorStore.isInViewerMode }); } // hover hoverProviderDisposer.current?.dispose(); hoverProviderDisposer.current = monacoLanguagesAPI.registerHoverProvider(EDITOR_LANGUAGE.PURE, { provideHover: (model, position) => { const currentWord = model.getWordAtPosition(position); if (!currentWord) { return { contents: [] }; } // show documention for parser section const lineTextIncludingWordRange = { startLineNumber: position.lineNumber, startColumn: 1, endLineNumber: position.lineNumber, endColumn: currentWord.endColumn, }; const lineTextIncludingWord = model.getValueInRange(lineTextIncludingWordRange); // NOTE: we don't need to trim here since the leading whitespace in front of // the section header is considered invalid syntax in the grammar if (!hasWhiteSpace(lineTextIncludingWord) && lineTextIncludingWord.startsWith(PARSER_SECTION_MARKER)) { const parserKeyword = lineTextIncludingWord.substring(PARSER_SECTION_MARKER.length); const doc = getParserDocumetation(editorStore, parserKeyword); if (doc) { return { range: lineTextIncludingWordRange, contents: [ doc.markdownText ? { value: doc.markdownText.value, } : undefined, doc.url ? { value: `[See documentation](${doc.url})`, } : undefined, ].filter(isNonNullable), }; } } // show documentation for parser element const textUntilPosition = model.getValueInRange({ startLineNumber: 1, startColumn: 1, endLineNumber: position.lineNumber, endColumn: position.column, }); const allParserSectionHeaders = // NOTE: since `###Pure` is implicitly considered as the first section, we prepend it to the text `${PARSER_SECTION_MARKER}${PURE_PARSER.PURE}\n${textUntilPosition}` .split('\n') .filter((line) => line.startsWith(PARSER_SECTION_MARKER)); const currentSectionParserKeyword = getSectionParserNameFromLineText(allParserSectionHeaders[allParserSectionHeaders.length - 1] ?? ''); if (currentSectionParserKeyword) { const doc = getParserElementDocumentation(editorStore, currentSectionParserKeyword, currentWord.word); if (doc) { return { range: { startLineNumber: position.lineNumber, startColumn: currentWord.startColumn, endLineNumber: position.lineNumber, endColumn: currentWord.endColumn, }, contents: [ doc.markdownText ? { value: doc.markdownText.value, } : undefined, doc.url ? { value: `[See documentation](${doc.url})`, } : undefined, ].filter(isNonNullable), }; } } return { contents: [] }; }, }); // suggestion const parserKeywordSuggestions = getParserKeywordSuggestions(editorStore); suggestionProviderDisposer.current?.dispose(); suggestionProviderDisposer.current = monacoLanguagesAPI.registerCompletionItemProvider(EDITOR_LANGUAGE.PURE, { // NOTE: we need to specify this to show suggestions for section // because by default, only alphanumeric characters trigger completion item provider // See https://microsoft.github.io/monaco-editor/api/interfaces/monaco.languages.CompletionContext.html#triggerCharacter // See https://github.com/microsoft/monaco-editor/issues/2530#issuecomment-861757198 triggerCharacters: ['#'], provideCompletionItems: (model, position) => { const suggestions = []; const currentWord = model.getWordUntilPosition(position); // suggestions for parser keyword const lineTextIncludingWordRange = { startLineNumber: position.lineNumber, startColumn: 1, endLineNumber: position.lineNumber, endColumn: currentWord.endColumn, }; const lineTextIncludingWord = model.getValueInRange(lineTextIncludingWordRange); // NOTE: make sure parser keyword suggestions only show up when the current word is the // the first word of the line since parser section header must not be preceded by anything if (!hasWhiteSpace(lineTextIncludingWord.trim())) { parserKeywordSuggestions.forEach((suggestion) => { suggestions.push({ label: { label: `${PARSER_SECTION_MARKER}${suggestion.text}`, description: suggestion.description, }, kind: monacoLanguagesAPI.CompletionItemKind.Keyword, insertText: `${PARSER_SECTION_MARKER}${suggestion.insertText}\n`, range: lineTextIncludingWordRange, documentation: suggestion.documentation ? suggestion.documentation.markdownText ? { value: suggestion.documentation.markdownText.value, } : suggestion.documentation.text : undefined, }); }); } // suggestions for parser element snippets const textUntilPosition = model.getValueInRange({ startLineNumber: 1, startColumn: 1, endLineNumber: position.lineNumber, endColumn: position.column, }); const allParserSectionHeaders = // NOTE: since `###Pure` is implicitly considered as the first section, we prepend it to the text `${PARSER_SECTION_MARKER}${PURE_PARSER.PURE}\n${textUntilPosition}` .split('\n') .filter((line) => line.startsWith(PARSER_SECTION_MARKER)); const currentSectionParserKeyword = getSectionParserNameFromLineText(allParserSectionHeaders[allParserSectionHeaders.length - 1] ?? ''); if (currentSectionParserKeyword) { getParserElementSnippetSuggestions(editorStore, currentSectionParserKeyword).forEach((snippetSuggestion) => { suggestions.push({ label: { label: snippetSuggestion.text, description: snippetSuggestion.description, }, kind: monacoLanguagesAPI.CompletionItemKind.Snippet, insertTextRules: monacoLanguagesAPI.CompletionItemInsertTextRule.InsertAsSnippet, insertText: `${snippetSuggestion.insertText}\n`, range: { startLineNumber: position.lineNumber, startColumn: currentWord.startColumn, endLineNumber: position.lineNumber, endColumn: currentWord.endColumn, }, documentation: snippetSuggestion.documentation ? snippetSuggestion.documentation.markdownText ? { value: snippetSuggestion.documentation.markdownText.value, } : snippetSuggestion.documentation.text : undefined, }); }); } getInlineSnippetSuggestions(editorStore).forEach((snippetSuggestion) => { suggestions.push({ label: { label: snippetSuggestion.text, description: snippetSuggestion.description, }, kind: monacoLanguagesAPI.CompletionItemKind.Snippet, insertTextRules: monacoLanguagesAPI.CompletionItemInsertTextRule.InsertAsSnippet, insertText: snippetSuggestion.insertText, range: { startLineNumber: position.lineNumber, startColumn: currentWord.startColumn, endLineNumber: position.lineNumber, endColumn: currentWord.endColumn, }, documentation: snippetSuggestion.documentation ? snippetSuggestion.documentation.markdownText ? { value: snippetSuggestion.documentation.markdownText.value, } : snippetSuggestion.documentation.text : undefined, }); }); return { suggestions }; }, }); /** * Reveal error has to be in an effect like this because, we want to reveal the error. * For this to happen, the editor needs to gain focus. However, if the user clicks on the * exit hackermode button, the editor loses focus, and the blocking modal pops up. This modal * in turn traps the focus and preventing the editor from gaining the focus to reveal the error. * As such we want to dismiss the modal before revealing the error, however, as of the current flow * dismissing the modal is called when we set the parser/compiler error. So if this logic belongs to * the normal rendering logic, and not an effect, it might happen just when the modal is still present * to make sure the modal is dismissed, we should place this logic in an effect to make sure it happens * slightly later, also it's better to have this as part of an effect in response to change in the errors */ useEffect(() => { if (editor) { if (error?.sourceInformation) { revealError(editor, error.sourceInformation.startLine, error.sourceInformation.startColumn); } } }, [editor, error, error?.sourceInformation]); /** * This effect helps to navigate to the currently selected element in the explorer tree * NOTE: this effect is placed after the effect to highlight and move cursor to error, * as even when there are errors, the user should be able to click on the explorer tree * to navigate to the element */ useEffect(() => { if (editor && currentElementLabelRegexString) { const editorModel = editor.getModel(); if (editorModel) { const match = editorModel.findMatches(currentElementLabelRegexString, true, true, true, null, true); if (Array.isArray(match) && match.length) { const range = guaranteeNonNullable(match[0]).range; editor.focus(); editor.revealPositionInCenter({ lineNumber: range.startLineNumber, column: range.startColumn, }); editor.setPosition({ column: range.startColumn, lineNumber: range.startLineNumber, }); } } } }, [editor, currentElementLabelRegexString]); // NOTE: dispose the editor to prevent potential memory-leak useEffect(() => () => { if (editor) { disposeEditor(editor); } // NOTE: make sure the call the disposer again after leaving this editor // else we would end up with duplicated suggestions and hover infos hoverProviderDisposer.current?.dispose(); suggestionProviderDisposer.current?.dispose(); }, [editor]); useApplicationNavigationContext(LEGEND_STUDIO_APPLICATION_NAVIGATION_CONTEXT_KEY.TEXT_MODE_EDITOR); return (_jsxs("div", { className: "panel edit-panel", children: [_jsxs(ContextMenu, { className: "panel__header edit-panel__header", disabled: true, children: [_jsxs("div", { className: "edit-panel__header__tabs", children: [_jsx("div", { className: "edit-panel__header__tab edit-panel__header__tab__exit-text-mode", children: _jsx("button", { className: "edit-panel__header__tab__label edit-panel__header__tab__exit-text-mode__label", disabled: editorStore.graphState.isApplicationLeavingTextMode, onClick: leaveTextMode, tabIndex: -1, title: "Click to exit text mode and go back to form mode", children: _jsx(MoreHorizontalIcon, {}) }) }), _jsxs(ContextMenu, { className: "edit-panel__header__tab edit-panel__header__tab__text-mode edit-panel__header__tab--active", content: _jsx(GrammarTextEditorHeaderTabContextMenu, {}), children: [_jsx("div", { className: "edit-panel__header__tab__icon", children: _jsx(HackerIcon, {}) }), _jsx("div", { className: "edit-panel__header__tab__label", children: "Text Mode" })] })] }), _jsx("div", { className: "edit-panel__header__actions", children: _jsx("button", { className: clsx('edit-panel__header__action', { 'edit-panel__header__action--active': grammarTextEditorState.wrapText, }), onClick: toggleWordWrap, tabIndex: -1, title: `[${grammarTextEditorState.wrapText ? 'on' : 'off'}] Toggle word wrap`, children: _jsx(WordWrapIcon, { className: "edit-panel__icon__word-wrap" }) }) })] }), _jsx("div", { className: "panel__content edit-panel__content", children: _jsx("div", { ref: ref, className: "text-editor__container", children: _jsx("div", { className: "text-editor__body", ref: textEditorRef }) }) })] })); }); //# sourceMappingURL=GrammarTextEditor.js.map