@finos/legend-application-pure-ide
Version:
Legend Pure IDE application core
509 lines • 22.2 kB
JavaScript
/**
* 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 { CODE_EDITOR_LANGUAGE, isTokenOneOf, PURE_GRAMMAR_TOKEN, } from '@finos/legend-code-editor';
import { ELEMENT_PATH_DELIMITER, extractElementNameFromPath, PARSER_SECTION_MARKER, PURE_ELEMENT_NAME, PURE_PARSER, } from '@finos/legend-graph';
import { guaranteeNonNullable, isNonNullable, returnUndefOnError, } from '@finos/legend-shared';
import { languages as monacoLanguagesAPI, editor as monacoEditorAPI, } from 'monaco-editor';
import { deserialize } from 'serializr';
import { ConceptType } from '../server/models/ConceptTree.js';
import { AttributeSuggestion, ClassSuggestion, ElementSuggestion, VariableSuggestion, } from '../server/models/Suggestion.js';
import { BLANK_CLASS_SNIPPET, BLANK_FUNCTION_SNIPPET, BLANK_MAPPING_SNIPPET, BLANK_RELATIONAL_DATABASE_SNIPPET, CLASS_WITH_CONSTRAINT_SNIPPET, CLASS_WITH_INHERITANCE_SNIPPET, CLASS_WITH_PROPERTY_SNIPPET, MAPPING_WITH_ENUMERATION_MAPPING_SNIPPET, MAPPING_WITH_M2M_CLASS_MAPPING_SNIPPET, MAPPING_WITH_RELATIONAL_CLASS_MAPPING_SNIPPET, SIMPLE_ASSOCIATION_SNIPPET, SIMPLE_ENUMERATION_SNIPPET, SIMPLE_FUNCTION_SNIPPET, SIMPLE_PROFILE_SNIPPET, BLANK_DIAGRAM_SNIPPET, getCopyrightHeaderSnippet, } from '../__lib__/LegendPureIDECodeSnippet.js';
// NOTE: these are technically different parsers compared to the ones we have in `Legend Engine` so we will
// not try to reuse the constants from DSL diagram
export const PURE_GRAMMAR_DIAGRAM_PARSER_NAME = 'Diagram';
export const PURE_GRAMMAR_DIAGRAM_ELEMENT_TYPE_LABEL = 'Diagram';
export const collectParserKeywordSuggestions = () => [
{
text: PURE_PARSER.PURE,
description: `(core Pure)`,
insertText: PURE_PARSER.PURE,
},
{
text: PURE_PARSER.MAPPING,
description: `(dsl)`,
insertText: PURE_PARSER.MAPPING,
},
{
text: PURE_PARSER.CONNECTION,
description: `(dsl)`,
insertText: PURE_PARSER.CONNECTION,
},
{
text: PURE_PARSER.RUNTIME,
description: `(dsl)`,
insertText: PURE_PARSER.RUNTIME,
},
{
text: PURE_PARSER.RELATIONAL,
description: `(external store)`,
insertText: PURE_PARSER.RELATIONAL,
},
// NOTE: these are technically different parsers compared to the ones we have in `Legend Engine` so we will
// not try to reuse the constants from DSL diagram
{
text: PURE_GRAMMAR_DIAGRAM_PARSER_NAME,
description: `(dsl)`,
insertText: PURE_GRAMMAR_DIAGRAM_ELEMENT_TYPE_LABEL,
},
];
export const collectParserElementSnippetSuggestions = (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,
},
// 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.RELATIONAL: {
return [
{
text: PURE_ELEMENT_NAME.DATABASE,
description: '(blank)',
insertText: BLANK_RELATIONAL_DATABASE_SNIPPET,
},
];
}
case PURE_GRAMMAR_DIAGRAM_PARSER_NAME: {
return [
{
text: PURE_GRAMMAR_DIAGRAM_ELEMENT_TYPE_LABEL,
description: '(blank)',
insertText: BLANK_DIAGRAM_SNIPPET,
},
];
}
default: {
return [];
}
}
};
export const collectExtraInlineSnippetSuggestions = () => [
{
text: 'print',
description: '(io)',
insertText: `print(\${1:})`,
},
{
text: 'println',
description: '(io)',
insertText: `println(\${1:})`,
},
{
text: 'NULL',
description: '(nullish value)',
insertText: `[]`,
},
];
export const getCopyrightHeaderSuggestions = (position, model) => {
const results = [];
const textUntilPosition = model
.getValueInRange({
startLineNumber: 1,
startColumn: 1,
endLineNumber: position.lineNumber,
endColumn: position.column,
})
.trimStart();
if (['', '/'].includes(textUntilPosition)) {
results.push({
label: {
label: `/copyright`,
description: `(copyright header)`,
},
kind: monacoLanguagesAPI.CompletionItemKind.Snippet,
insertTextRules: monacoLanguagesAPI.CompletionItemInsertTextRule.InsertAsSnippet,
insertText: getCopyrightHeaderSnippet(),
// NOTE: only show this suggestion when the cursor is on the first line of the file
range: {
startLineNumber: 1,
startColumn: 1,
endLineNumber: 1,
endColumn: 1000,
},
});
}
return results;
};
const constructorClassSuggestionToCompletionItem = (suggestion) => ({
label: {
label: suggestion.pureName,
description: suggestion.pureId,
},
kind: monacoLanguagesAPI.CompletionItemKind.Class,
filterText: suggestion.pureName,
insertTextRules: monacoLanguagesAPI.CompletionItemInsertTextRule.InsertAsSnippet,
insertText: `${suggestion.pureName}(${suggestion.requiredClassProperties
.map((property, idx) => `${property}=\${${idx + 1}:}`)
.join(',')})`,
});
const createFunctionInvocationSnippet = (functionName, functionPureId, useArrowForm) => {
const fn = extractElementNameFromPath(functionPureId);
const functionType = returnUndefOnError(() => fn.substring(fn.indexOf('_'), fn.length - 1));
// NOTE: remove the return type and if use arrow function form, remove the first parameter
const parameters = functionType?.split('__') ?? [];
parameters.pop();
if (useArrowForm) {
parameters.shift();
}
return `${functionName}(${parameters
.map((param, idx) => `\${${idx + 1}:}`)
.join(',')})`;
};
const elementSuggestionToCompletionItem = (suggestion, options) => {
const type = suggestion.pureType;
const insertText = type === ConceptType.FUNCTION || type === ConceptType.NATIVE_FUNCTION
? createFunctionInvocationSnippet(suggestion.pureName, suggestion.pureId, Boolean(options?.preferArrowFunctionForm))
: suggestion.pureName;
const kind = type === ConceptType.PACKAGE
? monacoLanguagesAPI.CompletionItemKind.Folder
: type === ConceptType.CLASS
? monacoLanguagesAPI.CompletionItemKind.Class
: type === ConceptType.FUNCTION
? monacoLanguagesAPI.CompletionItemKind.Function
: type === ConceptType.ENUMERATION
? monacoLanguagesAPI.CompletionItemKind.Enum
: type === ConceptType.PROFILE
? monacoLanguagesAPI.CompletionItemKind.Module
: type === ConceptType.ASSOCIATION
? monacoLanguagesAPI.CompletionItemKind.Interface
: monacoLanguagesAPI.CompletionItemKind.Value;
return {
label: {
label: suggestion.pureName,
description: suggestion.text,
},
kind,
filterText: suggestion.pureName,
insertTextRules: monacoLanguagesAPI.CompletionItemInsertTextRule.InsertAsSnippet,
insertText,
// attempt to push package suggestions to the bottom of the list
sortText: type === ConceptType.PACKAGE
? `zzzz_${suggestion.text}`
: suggestion.text,
};
};
const INCOMPLETE_PATH_PATTERN = /(?<incompletePath>(?:\w[\w$]*::)+$)/;
const ARROW_FUNCTION_USAGE_WITH_INCOMPLETE_PATH_PATTERN = /->\s*(?:\w[\w$]*::)+$/;
const CONSTRUCTOR_USAGE_WITH_INCOMPLETE_PATH_PATTERN = /\^\s*(?:\w[\w$]*::)+$/;
export const getIncompletePathSuggestions = async (position, model, ideStore) => {
const incompletePathMatch = model
.getLineContent(position.lineNumber)
.substring(0, position.column - 1)
.match(INCOMPLETE_PATH_PATTERN);
if (incompletePathMatch?.groups?.incompletePath) {
const isUsingArrowFunction = Boolean(model
.getLineContent(position.lineNumber)
.substring(0, position.column - 1)
.match(ARROW_FUNCTION_USAGE_WITH_INCOMPLETE_PATH_PATTERN));
const isUsingConstructor = Boolean(model
.getLineContent(position.lineNumber)
.substring(0, position.column - 1)
.match(CONSTRUCTOR_USAGE_WITH_INCOMPLETE_PATH_PATTERN));
let suggestions = [];
try {
suggestions = (await ideStore.client.getSuggestionsForIncompletePath(incompletePathMatch.groups.incompletePath.substring(0, incompletePathMatch.groups.incompletePath.length -
ELEMENT_PATH_DELIMITER.length), isUsingConstructor
? [ConceptType.CLASS]
: isUsingArrowFunction
? [ConceptType.FUNCTION, ConceptType.NATIVE_FUNCTION]
: [])).map((child) => deserialize(ElementSuggestion, child));
}
catch {
// do nothing: provide no suggestions when error ocurred
}
return suggestions.map((suggestion) => isUsingConstructor
? constructorClassSuggestionToCompletionItem(suggestion)
: elementSuggestionToCompletionItem(suggestion, {
preferArrowFunctionForm: isUsingArrowFunction,
}));
}
return [];
};
const IMPORT_STATEMENT_PATTERN = /^\s*import\s+(?:(?<importPath>(?:(?:\w[\w$]*)::)*\w[\w$]*)::*)/;
const getCurrentSectionImportPaths = (position, model) => {
const textUntilPosition = model.getValueInRange({
startLineNumber: 1,
startColumn: 1,
endLineNumber: position.lineNumber,
endColumn: position.column,
});
const lines =
// 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');
return lines
.slice(lines
.map((line) => line.startsWith(PARSER_SECTION_MARKER))
.lastIndexOf(true))
.map((line) => line.match(IMPORT_STATEMENT_PATTERN)?.groups?.importPath)
.filter(isNonNullable);
};
const ARROW_FUNCTION_USAGE_PATTERN = /->\s*(?:\w[\w$]*)?$/;
const CONSTRUCTOR_USAGE_PATTERN = /\^\s*(?:\w[\w$]*)?$/;
export const getIdentifierSuggestions = async (position, model, ideStore) => {
const importPaths = getCurrentSectionImportPaths(position, model);
const isUsingArrowFunction = Boolean(model
.getLineContent(position.lineNumber)
.substring(0, position.column - 1)
.match(ARROW_FUNCTION_USAGE_PATTERN));
const isUsingConstructor = Boolean(model
.getLineContent(position.lineNumber)
.substring(0, position.column - 1)
.match(CONSTRUCTOR_USAGE_PATTERN));
let suggestions = [];
try {
suggestions = (await ideStore.client.getSuggestionsForIdentifier(importPaths, isUsingConstructor
? [ConceptType.CLASS]
: isUsingArrowFunction
? [ConceptType.FUNCTION, ConceptType.NATIVE_FUNCTION]
: [])).map((child) => deserialize(ElementSuggestion, child));
}
catch {
// do nothing: provide no suggestions when error ocurred
}
return suggestions.map((suggestion) => isUsingConstructor
? constructorClassSuggestionToCompletionItem(suggestion)
: elementSuggestionToCompletionItem(suggestion, {
preferArrowFunctionForm: isUsingArrowFunction,
}));
};
export const getArrowFunctionSuggestions = async (position, model, ideStore) => {
const importPaths = getCurrentSectionImportPaths(position, model);
let suggestions = [];
try {
suggestions = (await ideStore.client.getSuggestionsForIdentifier(importPaths, [
ConceptType.FUNCTION,
ConceptType.NATIVE_FUNCTION,
])).map((child) => deserialize(ElementSuggestion, child));
}
catch {
// do nothing: provide no suggestions when error ocurred
}
return suggestions.map((suggestion) => elementSuggestionToCompletionItem(suggestion, {
preferArrowFunctionForm: true,
}));
};
const attributeSuggestionToCompletionItem = (suggestion) => {
const type = suggestion.pureType;
const insertText = type === ConceptType.PROPERTY || type === ConceptType.QUALIFIED_PROPERTY
? `${suggestion.pureName}(\${1:})`
: suggestion.pureName;
const kind = type === ConceptType.PROPERTY
? monacoLanguagesAPI.CompletionItemKind.Property
: type === ConceptType.QUALIFIED_PROPERTY
? monacoLanguagesAPI.CompletionItemKind.Method
: type === ConceptType.TAG
? monacoLanguagesAPI.CompletionItemKind.Constant
: type === ConceptType.STEREOTYPE
? monacoLanguagesAPI.CompletionItemKind.Value
: type === ConceptType.ENUM_VALUE
? monacoLanguagesAPI.CompletionItemKind.Enum
: monacoLanguagesAPI.CompletionItemKind.Value;
return {
label: {
label: suggestion.pureName,
description: `${suggestion.owner}.${suggestion.pureName}`,
},
kind,
filterText: suggestion.pureName,
insertTextRules: monacoLanguagesAPI.CompletionItemInsertTextRule.InsertAsSnippet,
insertText,
};
};
const ATTRIBUTE_ACCESSOR_PATTERN = /^(?<owner>(?:(?:\w[\w$]*)::)*\w[\w$]*)\s*.$/;
export const getAttributeSuggestions = async (position, model, ideStore) => {
const attributeAccessorMatch = model
.getLineContent(position.lineNumber)
.substring(0, position.column - 1)
.match(ATTRIBUTE_ACCESSOR_PATTERN);
const importPaths = getCurrentSectionImportPaths(position, model);
if (attributeAccessorMatch?.groups?.owner) {
let suggestions = [];
try {
suggestions = (await ideStore.client.getSuggestionsForAttribute(importPaths, attributeAccessorMatch.groups.owner)).map((child) => deserialize(AttributeSuggestion, child));
}
catch {
// do nothing: provide no suggestions when error ocurred
}
return suggestions.map((suggestion) => attributeSuggestionToCompletionItem(suggestion));
}
return [];
};
export const getConstructorClassSuggestions = async (position, model, ideStore) => {
const importPaths = getCurrentSectionImportPaths(position, model);
let suggestions = [];
try {
suggestions = (await ideStore.client.getSuggestionsForClass(importPaths)).map((child) => deserialize(ClassSuggestion, child));
}
catch {
// do nothing: provide no suggestions when error ocurred
}
return suggestions.map((suggestion) => constructorClassSuggestionToCompletionItem(suggestion));
};
const castingClassSuggestionToCompletionItem = (suggestion) => ({
label: {
label: suggestion.pureName,
description: suggestion.pureId,
},
kind: monacoLanguagesAPI.CompletionItemKind.Class,
filterText: suggestion.pureName,
insertTextRules: monacoLanguagesAPI.CompletionItemInsertTextRule.InsertAsSnippet,
insertText: suggestion.pureName,
});
export const getCastingClassSuggestions = async (position, model, ideStore) => {
const importPaths = getCurrentSectionImportPaths(position, model);
let suggestions = [];
try {
suggestions = (await ideStore.client.getSuggestionsForClass(importPaths)).map((child) => deserialize(ClassSuggestion, child));
}
catch {
// do nothing: provide no suggestions when error ocurred
}
return suggestions.map((suggestion) => castingClassSuggestionToCompletionItem(suggestion));
};
const variableSuggestionToCompletionItem = (suggestion, isFromCompiledSource) => ({
label: suggestion.name,
kind: monacoLanguagesAPI.CompletionItemKind.Variable,
insertTextRules: monacoLanguagesAPI.CompletionItemInsertTextRule.InsertAsSnippet,
// if suggestions coming from compiled source, they are ranked higher
sortText: !isFromCompiledSource
? `zzzz_${suggestion.name}`
: suggestion.name,
insertText: suggestion.name,
});
const VARIABLE_SUGGESTION_SCANNING_RANGE = 10;
export const getVariableSuggestions = async (position, model, filePath, ideStore) => {
let suggestions = [];
// get suggestions from compiled source
try {
suggestions = (await ideStore.client.getSuggestionsForVariable(filePath, position.lineNumber, position.column)).map((child) => deserialize(VariableSuggestion, child));
}
catch {
// do nothing: provide no suggestions when error ocurred
}
// NOTE: potentially, we could scan for all tokens that come before the current position
// and filter out variable suggestions that nolonger available
// get suggestions from current (potentially non-compiled) source
const varNames = new Set();
let stopSearching = false;
for (let i = position.lineNumber - 1; i >
Math.max(0, position.lineNumber - 1 - VARIABLE_SUGGESTION_SCANNING_RANGE); --i) {
// NOTE: stop searching after reaching function definition or section marker
if (stopSearching) {
break;
}
const line = model.getLineContent(i + 1);
if (line.match(/^\s*function\s+/) || line.match(/^\s*###\w+/)) {
stopSearching = true;
}
// scan for potential variable/parameter declarations
const lineTokens = guaranteeNonNullable(monacoEditorAPI.tokenize(model.getLineContent(i + 1), CODE_EDITOR_LANGUAGE.PURE)[0]);
lineTokens.forEach((token, lineIndex) => {
if (
// must come before the current position
(i !== position.lineNumber - 1 || token.offset < position.column) &&
isTokenOneOf(token.type, [PURE_GRAMMAR_TOKEN.VARIABLE, PURE_GRAMMAR_TOKEN.PARAMETER], true)) {
varNames.add(model.getValueInRange({
startLineNumber: i + 1,
startColumn: token.offset + 1,
endLineNumber: i + 1,
endColumn: lineIndex === lineTokens.length - 1
? Number.MAX_SAFE_INTEGER
: guaranteeNonNullable(lineTokens[lineIndex + 1]).offset + 1,
}));
}
});
}
const variablesFoundFromSuggestions = suggestions.map((suggestion) => suggestion.name);
return suggestions
.map((suggestion) => variableSuggestionToCompletionItem(suggestion, true))
.concat(Array.from(varNames)
.filter((varName) => !variablesFoundFromSuggestions.includes(varName))
.map((varName) => {
const suggestion = new VariableSuggestion();
suggestion.name = varName;
return suggestion;
})
.map((suggestion) => variableSuggestionToCompletionItem(suggestion, false)));
};
//# sourceMappingURL=PureFileEditorUtils.js.map