@finos/legend-studio
Version:
892 lines • 44.9 kB
JavaScript
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