@finos/legend-application-pure-ide
Version:
Legend Pure IDE application core
405 lines • 17.4 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 { ActionAlertActionType, ActionAlertType, } from '@finos/legend-application';
import { clearMarkers, setErrorMarkers, CODE_EDITOR_LANGUAGE, } from '@finos/legend-code-editor';
import { DIRECTORY_PATH_DELIMITER } from '@finos/legend-graph';
import { at, assertErrorThrown } from '@finos/legend-shared';
import { action, computed, flow, flowResult, makeObservable, observable, } from 'mobx';
import { editor as monacoEditorAPI, Uri, } from 'monaco-editor';
import { ConceptType } from '../server/models/ConceptTree.js';
import { FileCoordinate, trimPathLeadingSlash, } from '../server/models/File.js';
import { FIND_USAGE_FUNCTION_PATH, } from '../server/models/Usage.js';
import { PureIDETabState } from './PureIDETabManagerState.js';
import { LEGEND_PURE_IDE_PURE_FILE_EDITOR_COMMAND_KEY } from '../__lib__/LegendPureIDECommand.js';
const getFileEditorLanguage = (filePath) => {
const extension = filePath.split('.').at(-1);
switch (extension) {
case 'pure':
return CODE_EDITOR_LANGUAGE.PURE;
case 'java':
return CODE_EDITOR_LANGUAGE.JAVA;
case 'md':
return CODE_EDITOR_LANGUAGE.MARKDOWN;
case 'sql':
return CODE_EDITOR_LANGUAGE.SQL;
case 'json':
return CODE_EDITOR_LANGUAGE.JSON;
case 'xml':
return CODE_EDITOR_LANGUAGE.XML;
case 'yml':
case 'yaml':
return CODE_EDITOR_LANGUAGE.YAML;
case 'graphql':
return CODE_EDITOR_LANGUAGE.GRAPHQL;
default:
return CODE_EDITOR_LANGUAGE.TEXT;
}
};
class FileTextEditorState {
model;
editor;
_dummyCursorObservable = {};
language;
viewState;
forcedCursorPosition;
wrapText = false;
constructor(fileEditorState) {
makeObservable(this, {
viewState: observable.ref,
editor: observable.ref,
_dummyCursorObservable: observable.ref,
forcedCursorPosition: observable.ref,
wrapText: observable,
cursorObserver: computed,
notifyCursorObserver: action,
setViewState: action,
setEditor: action,
setForcedCursorPosition: action,
setWrapText: action,
});
this.language = getFileEditorLanguage(fileEditorState.filePath);
this.model = monacoEditorAPI.createModel('', this.language, Uri.file(`/${fileEditorState.uuid}.pure`));
this.model.setValue(fileEditorState.file.content);
}
// trigger for the manual observer of editor cursor
notifyCursorObserver() {
this._dummyCursorObservable = {};
}
// subscriber for the manual observer of editor cursor
get cursorObserver() {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
this._dummyCursorObservable; // manually trigger cursor observer
return this.editor
? {
position: this.editor.getPosition() ?? undefined,
selection: this.editor.getSelection() ?? undefined,
}
: undefined;
}
setViewState(val) {
this.viewState = val;
}
setEditor(val) {
this.editor = val;
}
setForcedCursorPosition(val) {
this.forcedCursorPosition = val;
}
setWrapText(val) {
const oldVal = this.wrapText;
this.wrapText = val;
if (oldVal !== val && this.editor) {
this.editor.updateOptions({
wordWrap: val ? 'on' : 'off',
});
this.editor.focus();
}
}
}
export class FileEditorRenameConceptState {
fileEditorState;
concept;
coordinate;
constructor(fileEditorState, concept, coordiate) {
this.fileEditorState = fileEditorState;
this.concept = concept;
this.coordinate = coordiate;
}
}
export class FileEditorState extends PureIDETabState {
filePath;
textEditorState;
_currentHashCode;
file;
renameConceptState;
showGoToLinePrompt = false;
constructor(ideStore, file, filePath) {
super(ideStore);
makeObservable(this, {
_currentHashCode: observable,
file: observable,
renameConceptState: observable,
showGoToLinePrompt: observable,
hasChanged: computed,
resetChangeDetection: action,
setFile: action,
setShowGoToLinePrompt: action,
setConceptToRenameState: flow,
runTest: flow,
});
this.file = file;
this._currentHashCode = file.hashCode;
this.filePath = filePath;
this.textEditorState = new FileTextEditorState(this);
}
get label() {
return trimPathLeadingSlash(this.filePath);
}
get description() {
return `File: ${trimPathLeadingSlash(this.filePath)}${this.file.RO ? ' (readonly)' : ''}`;
}
get fileName() {
return at(this.filePath.split(DIRECTORY_PATH_DELIMITER), -1);
}
match(tab) {
return tab instanceof FileEditorState && this.filePath === tab.filePath;
}
onClose() {
// dispose text model to avoid memory leak
this.textEditorState.model.dispose();
}
get hasChanged() {
return this._currentHashCode !== this.file.hashCode;
}
resetChangeDetection() {
this._currentHashCode = this.file.hashCode;
}
setFile(val) {
this.file = val;
this.textEditorState.model.setValue(val.content);
this.resetChangeDetection();
}
setShowGoToLinePrompt(val) {
this.showGoToLinePrompt = val;
}
*setConceptToRenameState(coordinate) {
if (!coordinate) {
this.renameConceptState = undefined;
return;
}
if (this.hasChanged) {
this.ideStore.applicationStore.notificationService.notifyWarning(`Can't rename concept: source is not compiled`);
return;
}
const concept = (yield this.ideStore.getConceptInfo(coordinate));
this.renameConceptState = concept
? new FileEditorRenameConceptState(this, concept, coordinate)
: undefined;
}
*runTest(coordinate) {
if (!coordinate) {
return;
}
if (this.hasChanged) {
this.ideStore.applicationStore.notificationService.notifyWarning(`Can't run test: source is not compiled`);
return;
}
const concept = (yield this.ideStore.getConceptInfo(coordinate));
if (concept?.pureType === ConceptType.FUNCTION) {
if (concept.pct) {
this.ideStore.setPCTRunPath(concept.path);
}
else if (concept.test) {
flowResult(this.ideStore.executeTests(concept.path, false)).catch(this.ideStore.applicationStore.alertUnhandledError);
}
else {
this.ideStore.applicationStore.notificationService.notifyWarning(`Can't run test: function not marked as test`);
}
}
else {
this.ideStore.applicationStore.notificationService.notifyWarning(`Can't run test: not a function`);
}
}
showError(coordinate) {
setErrorMarkers(this.textEditorState.model, [
{
message: coordinate.error.message,
startLineNumber: coordinate.line,
startColumn: coordinate.column,
endLineNumber: coordinate.line,
endColumn: coordinate.column,
},
], this.uuid);
}
clearError() {
clearMarkers(this.uuid);
}
registerCommands() {
if (this.textEditorState.language === CODE_EDITOR_LANGUAGE.PURE) {
this.ideStore.applicationStore.commandService.registerCommand({
key: LEGEND_PURE_IDE_PURE_FILE_EDITOR_COMMAND_KEY.GO_TO_DEFINITION,
trigger: () => this.ideStore.tabManagerState.currentTab === this &&
Boolean(this.textEditorState.editor?.hasTextFocus()),
action: () => {
const currentPosition = this.textEditorState.editor?.getPosition();
if (currentPosition) {
flowResult(this.ideStore.executeNavigation(new FileCoordinate(this.filePath, currentPosition.lineNumber, currentPosition.column))).catch(this.ideStore.applicationStore.alertUnhandledError);
}
},
});
this.ideStore.applicationStore.commandService.registerCommand({
key: LEGEND_PURE_IDE_PURE_FILE_EDITOR_COMMAND_KEY.GO_BACK,
action: () => {
flowResult(this.ideStore.navigateBack()).catch(this.ideStore.applicationStore.alertUnhandledError);
},
});
this.ideStore.applicationStore.commandService.registerCommand({
key: LEGEND_PURE_IDE_PURE_FILE_EDITOR_COMMAND_KEY.REVEAL_CONCEPT_IN_TREE,
trigger: () => this.ideStore.tabManagerState.currentTab === this &&
Boolean(this.textEditorState.editor?.hasTextFocus()),
action: () => {
const currentPosition = this.textEditorState.editor?.getPosition();
if (currentPosition) {
this.ideStore
.revealConceptInTree(new FileCoordinate(this.filePath, currentPosition.lineNumber, currentPosition.column))
.catch(this.ideStore.applicationStore.alertUnhandledError);
}
},
});
this.ideStore.applicationStore.commandService.registerCommand({
key: LEGEND_PURE_IDE_PURE_FILE_EDITOR_COMMAND_KEY.FIND_USAGES,
trigger: () => this.ideStore.tabManagerState.currentTab === this &&
Boolean(this.textEditorState.editor?.hasTextFocus()),
action: () => {
const currentPosition = this.textEditorState.editor?.getPosition();
if (currentPosition) {
const coordinate = new FileCoordinate(this.filePath, currentPosition.lineNumber, currentPosition.column);
this.findConceptUsages(coordinate);
}
},
});
this.ideStore.applicationStore.commandService.registerCommand({
key: LEGEND_PURE_IDE_PURE_FILE_EDITOR_COMMAND_KEY.RENAME_CONCEPT,
trigger: () => this.ideStore.tabManagerState.currentTab === this &&
Boolean(this.textEditorState.editor?.hasTextFocus()),
action: () => {
const currentPosition = this.textEditorState.editor?.getPosition();
if (currentPosition) {
const currentWord = this.textEditorState.model.getWordAtPosition(currentPosition);
if (!currentWord) {
return;
}
const coordinate = new FileCoordinate(this.filePath, currentPosition.lineNumber, currentPosition.column);
flowResult(this.setConceptToRenameState(coordinate)).catch(this.ideStore.applicationStore.alertUnhandledError);
}
},
});
}
this.ideStore.applicationStore.commandService.registerCommand({
key: LEGEND_PURE_IDE_PURE_FILE_EDITOR_COMMAND_KEY.DELETE_LINE,
trigger: () => this.ideStore.tabManagerState.currentTab === this &&
Boolean(this.textEditorState.editor?.hasTextFocus()),
action: () => {
const currentPosition = this.textEditorState.editor?.getPosition();
if (currentPosition) {
this.textEditorState.model.pushEditOperations([], [
{
range: {
startLineNumber: currentPosition.lineNumber,
startColumn: 1,
endLineNumber: currentPosition.lineNumber + 1,
endColumn: 1,
},
text: '',
forceMoveMarkers: true,
},
], () => null);
}
},
});
this.ideStore.applicationStore.commandService.registerCommand({
key: LEGEND_PURE_IDE_PURE_FILE_EDITOR_COMMAND_KEY.GO_TO_LINE,
trigger: () => this.ideStore.tabManagerState.currentTab === this,
action: () => {
this.setShowGoToLinePrompt(true);
},
});
this.ideStore.applicationStore.commandService.registerCommand({
key: LEGEND_PURE_IDE_PURE_FILE_EDITOR_COMMAND_KEY.TOGGLE_TEXT_WRAP,
trigger: () => this.ideStore.tabManagerState.currentTab === this,
action: () => {
this.textEditorState.setWrapText(!this.textEditorState.wrapText);
},
});
}
findConceptUsages(coordinate) {
const proceed = () => {
flowResult(this.ideStore.findUsagesFromCoordinate(coordinate)).catch(this.ideStore.applicationStore.alertUnhandledError);
};
if (this.hasChanged) {
this.ideStore.applicationStore.alertService.setActionAlertInfo({
message: 'Source is not compiled, finding concept usages might be inaccurate. Do you want compile to proceed?',
type: ActionAlertType.CAUTION,
actions: [
{
label: 'Compile and Proceed',
type: ActionAlertActionType.PROCEED_WITH_CAUTION,
handler: () => {
flowResult(this.ideStore.executeGo())
.then(proceed)
.catch(this.ideStore.applicationStore.alertUnhandledError);
},
},
{
label: 'Abort',
type: ActionAlertActionType.PROCEED,
default: true,
},
],
});
}
else {
proceed();
}
}
async renameConcept(newName) {
if (!this.renameConceptState) {
return;
}
const concept = this.renameConceptState.concept;
try {
this.ideStore.applicationStore.alertService.setBlockingAlert({
message: 'Finding concept usages...',
showLoading: true,
});
const usages = await this.ideStore.findConceptUsages(concept.pureType === ConceptType.ENUM_VALUE
? FIND_USAGE_FUNCTION_PATH.ENUM
: concept.pureType === ConceptType.PROPERTY ||
concept.pureType === ConceptType.QUALIFIED_PROPERTY
? FIND_USAGE_FUNCTION_PATH.PROPERTY
: FIND_USAGE_FUNCTION_PATH.ELEMENT, (concept.owner ? [`'${concept.owner}'`] : []).concat(`'${concept.path}'`));
await flowResult(this.ideStore.renameConcept(concept.pureName, newName, concept.pureType, usages));
this.textEditorState.setForcedCursorPosition({
lineNumber: this.renameConceptState.coordinate.line,
column: this.renameConceptState.coordinate.column,
});
}
catch (error) {
assertErrorThrown(error);
this.ideStore.applicationStore.notificationService.notifyError(error);
}
finally {
this.ideStore.applicationStore.alertService.setBlockingAlert(undefined);
}
}
deregisterCommands() {
if (this.textEditorState.language === CODE_EDITOR_LANGUAGE.PURE) {
[
LEGEND_PURE_IDE_PURE_FILE_EDITOR_COMMAND_KEY.GO_TO_DEFINITION,
LEGEND_PURE_IDE_PURE_FILE_EDITOR_COMMAND_KEY.GO_BACK,
LEGEND_PURE_IDE_PURE_FILE_EDITOR_COMMAND_KEY.REVEAL_CONCEPT_IN_TREE,
LEGEND_PURE_IDE_PURE_FILE_EDITOR_COMMAND_KEY.FIND_USAGES,
LEGEND_PURE_IDE_PURE_FILE_EDITOR_COMMAND_KEY.RENAME_CONCEPT,
].forEach((key) => this.ideStore.applicationStore.commandService.deregisterCommand(key));
}
[
LEGEND_PURE_IDE_PURE_FILE_EDITOR_COMMAND_KEY.GO_TO_LINE,
LEGEND_PURE_IDE_PURE_FILE_EDITOR_COMMAND_KEY.DELETE_LINE,
LEGEND_PURE_IDE_PURE_FILE_EDITOR_COMMAND_KEY.TOGGLE_TEXT_WRAP,
].forEach((key) => this.ideStore.applicationStore.commandService.deregisterCommand(key));
}
}
//# sourceMappingURL=FileEditorState.js.map