@finos/legend-application-pure-ide
Version:
Legend Pure IDE application core
1,069 lines • 50.8 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 { action, flow, flowResult, makeObservable, observable, runInAction, } from 'mobx';
import { ACTIVITY_MODE, PANEL_MODE, ROOT_PACKAGE_PATH, WELCOME_FILE_PATH, } from './PureIDEConfig.js';
import { FileEditorState } from './FileEditorState.js';
import { serialize, deserialize } from 'serializr';
import { FileCoordinate, FileErrorCoordinate, File, trimPathLeadingSlash, } from '../server/models/File.js';
import { DirectoryTreeState } from './DirectoryTreeState.js';
import { ConceptTreeState } from './ConceptTreeState.js';
import { InitializationFailureWithSourceResult, InitializationFailureResult, deserializeInitializationnResult, } from '../server/models/Initialization.js';
import { TestExecutionResult, UnmatchedFunctionResult, UnknownSymbolResult, GetConceptResult, deserializeExecutionResult, ExecutionFailureResult, ExecutionSuccessResult, } from '../server/models/Execution.js';
import { SearchResultCoordinate } from '../server/models/SearchEntry.js';
import { TestRunnerState } from './TestRunnerState.js';
import { getConceptInfoLabel, Usage, FIND_USAGE_FUNCTION_PATH, } from '../server/models/Usage.js';
import { CommandFailureResult, deserializeCommandResult, } from '../server/models/Command.js';
import { ActionAlertActionType, ActionAlertType, } from '@finos/legend-application';
import { isNonNullable, NetworkClient, ActionState, assertErrorThrown, guaranteeNonNullable, uniq, filterByType, } from '@finos/legend-shared';
import { PureServerClient as PureServerClient } from '../server/PureServerClient.js';
import { PanelDisplayState } from '@finos/legend-art';
import { DiagramEditorState } from './DiagramEditorState.js';
import { DiagramInfo, serializeDiagram } from '../server/models/DiagramInfo.js';
import { FileSearchCommandState } from './FileSearchCommandState.js';
import { PureIDETabManagerState } from './PureIDETabManagerState.js';
import { LEGEND_PURE_IDE_COMMAND_KEY, LEGEND_PURE_IDE_TERMINAL_COMMAND, } from '../__lib__/LegendPureIDECommand.js';
import { ExecutionError } from '../server/models/ExecutionError.js';
import { ELEMENT_PATH_DELIMITER } from '@finos/legend-graph';
import { ConceptType } from '../server/models/ConceptTree.js';
import { setupTerminal } from './LegendPureIDETerminal.js';
import { UnknownSymbolCodeFixSuggestion, UnmatchedFunctionCodeFixSuggestion, } from './CodeFixSuggestion.js';
import { ReferenceUsageResult } from './ReferenceUsageResult.js';
import { TextSearchState } from './TextSearchState.js';
import { PCTAdapter } from '../server/models/Test.js';
export class PureIDEStore {
applicationStore;
initState = ActionState.create();
directoryTreeState;
conceptTreeState;
client;
// Layout
activePanelMode = PANEL_MODE.TERMINAL;
panelGroupDisplayState = new PanelDisplayState({
initial: 0,
default: 300,
snap: 100,
});
activeActivity = ACTIVITY_MODE.CONCEPT_EXPLORER;
sideBarDisplayState = new PanelDisplayState({
initial: 300,
default: 300,
snap: 150,
});
tabManagerState = new PureIDETabManagerState(this);
executionState = ActionState.create();
navigationStack = []; // TODO?: we might want to limit the number of items in this stack
// File Search Command
fileSearchCommandLoadState = ActionState.create();
fileSearchCommandState = new FileSearchCommandState();
openFileSearchCommand = false;
fileSearchCommandResults = [];
// Code-fix Suggestions Panel
codeFixSuggestion;
// Reference Usage Panel
referenceUsageLoadState = ActionState.create();
referenceUsageResult;
// Text Search Panel
textSearchState;
// Test Runner Panel
testRunState = ActionState.create();
testRunnerState;
PCTAdapters = [];
selectedPCTAdapter;
PCTRunPath;
constructor(applicationStore) {
makeObservable(this, {
activePanelMode: observable,
activeActivity: observable,
navigationStack: observable,
openFileSearchCommand: observable,
fileSearchCommandResults: observable,
fileSearchCommandState: observable,
codeFixSuggestion: observable,
referenceUsageResult: observable,
testRunnerState: observable,
PCTAdapters: observable.struct,
selectedPCTAdapter: observable,
setSelectedPCTAdapter: action,
PCTRunPath: observable,
setPCTRunPath: action,
setCodeFixSuggestion: action,
setReferenceUsageResult: action,
setOpenFileSearchCommand: action,
setActivePanelMode: action,
setActiveActivity: action,
setTestRunnerState: action,
pullInitializationActivity: action,
pullExecutionStatus: action,
initialize: flow,
checkIfSessionWakingUp: flow,
loadDiagram: flow,
loadFile: flow,
execute: flow,
executeGo: flow,
runDebugger: flow,
manageExecuteGoResult: flow,
executeTests: flow,
executeFullTestSuite: flow,
executeNavigation: flow,
navigateBack: flow,
fullReCompile: flow,
command: flow,
findUsagesFromCoordinate: flow,
findUsages: flow,
renameConcept: flow,
movePackageableElements: flow,
updateFileUsingSuggestionCandidate: flow,
updateFile: flow,
searchFile: flow,
createNewDirectory: flow,
createNewFile: flow,
renameFile: flow,
deleteDirectoryOrFile: flow,
});
this.applicationStore = applicationStore;
this.textSearchState = new TextSearchState(this);
this.directoryTreeState = new DirectoryTreeState(this);
this.conceptTreeState = new ConceptTreeState(this);
this.client = new PureServerClient(new NetworkClient({
baseUrl: this.applicationStore.config.useDynamicPureServer
? window.location.origin
: this.applicationStore.config.pureUrl,
}));
setupTerminal(this);
}
setOpenFileSearchCommand(val) {
this.openFileSearchCommand = val;
}
setSelectedPCTAdapter(val) {
this.selectedPCTAdapter = val;
}
setPCTRunPath(val) {
this.PCTRunPath = val;
}
setActivePanelMode(val) {
this.activePanelMode = val;
}
setCodeFixSuggestion(val) {
this.codeFixSuggestion = val;
}
setReferenceUsageResult(val) {
this.referenceUsageResult = val;
}
setTestRunnerState(val) {
this.testRunnerState = val;
}
cleanUp() {
// dismiss all the alerts as these are parts of application, if we don't do this, we might
// end up blocking other parts of the app
// e.g. trying going to an unknown workspace, we will be redirected to the home page
// but the blocking alert for not-found workspace will still block the app
this.applicationStore.alertService.setBlockingAlert(undefined);
this.applicationStore.alertService.setActionAlertInfo(undefined);
// dispose the terminal
this.applicationStore.terminalService.terminal.dispose();
}
/**
* This is the entry of the app logic where initialization of editor states happens
* Here, we ensure the order of calls after checking existence of current project and workspace
* If either of them does not exist, we cannot proceed.
*/
*initialize(fullInit, func, mode, fastCompile) {
if (!this.initState.isInInitialState) {
this.applicationStore.notificationService.notifyIllegalState('Editor store is re-initialized');
return;
}
// set PURE IDE mode
this.client.mode = mode;
this.client.compilerMode = fastCompile;
// initialize editor
this.initState.inProgress();
try {
this.applicationStore.alertService.setBlockingAlert({
message: 'Loading Pure IDE...',
prompt: 'Please be patient as we are building the initial application state',
showLoading: true,
});
const initializationPromise = this.client
.initialize(!fullInit)
.catch((error) => {
assertErrorThrown(error);
this.applicationStore.notificationService.notifyError(error);
this.initState.fail();
this.applicationStore.alertService.setBlockingAlert({
message: `Failed to initialize IDE`,
prompt: `Before debugging, make sure the server is running then restart the application`,
});
});
yield this.pullInitializationActivity();
this.applicationStore.alertService.setBlockingAlert(undefined);
const openWelcomeFilePromise = flowResult(this.loadFile(WELCOME_FILE_PATH)).then(() => {
const welcomeFileTab = this.tabManagerState.tabs.find((tab) => tab instanceof FileEditorState &&
tab.filePath === WELCOME_FILE_PATH);
if (welcomeFileTab) {
this.tabManagerState.pinTab(welcomeFileTab);
}
});
const directoryTreeInitPromise = this.directoryTreeState.initialize();
const conceptTreeInitPromise = this.conceptTreeState.initialize();
const getPCTAdaptersPromise = this.client
.getPCTAdapters()
.then((result) => {
runInAction(() => {
this.PCTAdapters = result.map((adapter) => new PCTAdapter(adapter.first, adapter.second));
this.selectedPCTAdapter =
this.PCTAdapters.find((adapter) => adapter.name === 'In-Memory') ?? (this.PCTAdapters.length ? this.PCTAdapters[0] : undefined);
});
});
const result = deserializeInitializationnResult((yield initializationPromise));
if (result.text) {
this.applicationStore.terminalService.terminal.output(result.text, {
systemCommand: 'initialize application',
});
}
this.setActivePanelMode(PANEL_MODE.TERMINAL);
this.panelGroupDisplayState.open();
if (result instanceof InitializationFailureResult) {
if (result.sessionError) {
this.applicationStore.alertService.setBlockingAlert({
message: 'Session corrupted',
prompt: result.sessionError,
});
}
else if (result instanceof InitializationFailureWithSourceResult) {
yield flowResult(this.loadFile(result.source, new FileErrorCoordinate(result.source, result.line, result.column, new ExecutionError((result.text ?? '').split('\n').filter(Boolean)[0]))));
}
}
else {
if (func) {
yield func();
}
yield Promise.all([
openWelcomeFilePromise,
directoryTreeInitPromise,
conceptTreeInitPromise,
getPCTAdaptersPromise,
]);
}
this.initState.pass();
}
catch (error) {
assertErrorThrown(error);
this.applicationStore.notificationService.notifyError(error);
this.initState.fail();
this.applicationStore.alertService.setActionAlertInfo({
message: `Failed to initialize IDE`,
prompt: `This can either due to an internal server error, which you would need to manually resolve; or a compilation, which you can proceed to debug`,
type: ActionAlertType.CAUTION,
actions: [
{
label: 'Compile to debug',
type: ActionAlertActionType.PROCEED_WITH_CAUTION,
default: true,
handler: () => {
flowResult(this.executeGo()).catch(this.applicationStore.alertUnhandledError);
},
},
],
});
}
finally {
// initialize the terminal
this.applicationStore.terminalService.terminal.clear();
}
}
*checkIfSessionWakingUp(message) {
this.applicationStore.alertService.setBlockingAlert({
message: message ?? 'Checking IDE session...',
showLoading: true,
});
yield this.pullInitializationActivity((activity) => {
if (activity.text) {
this.applicationStore.alertService.setBlockingAlert({
message: message ?? 'Checking IDE session...',
prompt: activity.text,
showLoading: true,
});
}
});
this.applicationStore.alertService.setBlockingAlert(undefined);
}
async pullInitializationActivity(fn) {
const result = (await this.client.getInitializationActivity());
if (result.initializing) {
return new Promise((resolve, reject) => setTimeout(() => {
try {
resolve(this.pullInitializationActivity());
}
catch (error) {
reject(error);
}
}, 1000));
}
return Promise.resolve();
}
registerCommands() {
this.applicationStore.commandService.registerCommand({
key: LEGEND_PURE_IDE_COMMAND_KEY.SEARCH_FILE,
action: () => this.setOpenFileSearchCommand(true),
});
this.applicationStore.commandService.registerCommand({
key: LEGEND_PURE_IDE_COMMAND_KEY.SEARCH_TEXT,
action: () => {
this.setActivePanelMode(PANEL_MODE.SEARCH);
this.panelGroupDisplayState.open();
this.textSearchState.focus();
this.textSearchState.select();
},
});
this.applicationStore.commandService.registerCommand({
key: LEGEND_PURE_IDE_COMMAND_KEY.GO_TO_FILE,
action: () => {
if (this.tabManagerState.currentTab instanceof FileEditorState) {
this.directoryTreeState.revealPath(this.tabManagerState.currentTab.filePath, {
forceOpenExplorerPanel: true,
});
}
},
});
this.applicationStore.commandService.registerCommand({
key: LEGEND_PURE_IDE_COMMAND_KEY.TOGGLE_TERMINAL_PANEL,
action: () => {
// toggle the panel and activate terminal tab if needs be
// if the terminal is already open, and not yet focused, focus on it
// else, close it
if (this.panelGroupDisplayState.isOpen) {
if (this.activePanelMode !== PANEL_MODE.TERMINAL) {
this.setActivePanelMode(PANEL_MODE.TERMINAL);
this.applicationStore.terminalService.terminal.focus();
}
else {
if (!this.applicationStore.terminalService.terminal.isFocused()) {
this.applicationStore.terminalService.terminal.focus();
}
else {
this.panelGroupDisplayState.close();
}
}
}
else {
this.setActivePanelMode(PANEL_MODE.TERMINAL);
this.panelGroupDisplayState.open();
}
},
});
this.applicationStore.commandService.registerCommand({
key: LEGEND_PURE_IDE_COMMAND_KEY.EXECUTE,
action: () => {
flowResult(this.executeGo()).catch(this.applicationStore.alertUnhandledError);
},
});
this.applicationStore.commandService.registerCommand({
key: LEGEND_PURE_IDE_COMMAND_KEY.FULL_RECOMPILE,
action: () => {
flowResult(this.fullReCompile(false)).catch(this.applicationStore.alertUnhandledError);
},
});
this.applicationStore.commandService.registerCommand({
key: LEGEND_PURE_IDE_COMMAND_KEY.FULL_RECOMPILE_WITH_FULL_INIT,
action: () => {
flowResult(this.fullReCompile(true)).catch(this.applicationStore.alertUnhandledError);
},
});
this.applicationStore.commandService.registerCommand({
key: LEGEND_PURE_IDE_COMMAND_KEY.RUN_ALL_TESTS,
action: () => {
flowResult(this.executeFullTestSuite(false)).catch(this.applicationStore.alertUnhandledError);
},
});
this.applicationStore.commandService.registerCommand({
key: LEGEND_PURE_IDE_COMMAND_KEY.RUN_RELAVANT_TESTS,
action: () => {
flowResult(this.executeFullTestSuite(true)).catch(this.applicationStore.alertUnhandledError);
},
});
}
deregisterCommands() {
[
LEGEND_PURE_IDE_COMMAND_KEY.SEARCH_FILE,
LEGEND_PURE_IDE_COMMAND_KEY.SEARCH_TEXT,
LEGEND_PURE_IDE_COMMAND_KEY.GO_TO_FILE,
LEGEND_PURE_IDE_COMMAND_KEY.TOGGLE_TERMINAL_PANEL,
LEGEND_PURE_IDE_COMMAND_KEY.EXECUTE,
LEGEND_PURE_IDE_COMMAND_KEY.FULL_RECOMPILE,
LEGEND_PURE_IDE_COMMAND_KEY.FULL_RECOMPILE_WITH_FULL_INIT,
LEGEND_PURE_IDE_COMMAND_KEY.RUN_ALL_TESTS,
LEGEND_PURE_IDE_COMMAND_KEY.RUN_RELAVANT_TESTS,
].forEach((key) => this.applicationStore.commandService.deregisterCommand(key));
}
setActiveActivity(activity, options) {
if (!this.sideBarDisplayState.isOpen) {
this.sideBarDisplayState.open();
}
else if (activity === this.activeActivity &&
!options?.keepShowingIfMatchedCurrent) {
this.sideBarDisplayState.close();
}
this.activeActivity = activity;
}
*loadDiagram(filePath, diagramPath, line, column) {
let editorState = this.tabManagerState.tabs.find((tab) => tab instanceof DiagramEditorState && tab.diagramPath === diagramPath);
if (!editorState) {
yield flowResult(this.checkIfSessionWakingUp());
editorState = new DiagramEditorState(this, deserialize(DiagramInfo, yield this.client.getDiagramInfo(diagramPath)), diagramPath, filePath, line, column);
}
this.tabManagerState.openTab(editorState);
}
*loadFile(filePath, coordinate) {
try {
let editorState = this.tabManagerState.tabs.find((tab) => tab instanceof FileEditorState && tab.filePath === filePath);
if (!editorState) {
yield flowResult(this.checkIfSessionWakingUp());
editorState = new FileEditorState(this, deserialize(File, yield this.client.getFile(filePath)), filePath);
}
this.tabManagerState.openTab(editorState);
if (coordinate) {
editorState.textEditorState.setForcedCursorPosition({
lineNumber: coordinate.line,
column: coordinate.column,
});
if (coordinate instanceof FileErrorCoordinate) {
editorState.showError(coordinate);
}
}
}
catch (error) {
assertErrorThrown(error);
this.applicationStore.terminalService.terminal.fail(error.message, {
systemCommand: `load file ${filePath}`,
});
}
}
async reloadFile(filePath) {
const tabsToClose = [];
await Promise.all(this.tabManagerState.tabs.map(async (tab) => {
if (tab instanceof FileEditorState && tab.filePath === filePath) {
tab.setFile(deserialize(File, await this.client.getFile(filePath)));
}
else if (tab instanceof DiagramEditorState &&
tab.filePath === filePath) {
try {
tab.rebuild(deserialize(DiagramInfo, await this.client.getDiagramInfo(tab.diagramPath)));
}
catch {
// something happened, most likely the diagram has been removed or renamed,
// we should close the tab then
tabsToClose.push(tab);
}
}
}));
tabsToClose.forEach((tab) => this.tabManagerState.closeTab(tab));
}
*execute(url, extraParams, checkExecutionStatus, manageResult, command, options) {
if (!this.initState.hasCompleted) {
this.applicationStore.notificationService.notifyWarning(`Can't execute while initializing application`);
return;
}
if (this.executionState.isInProgress) {
this.applicationStore.notificationService.notifyWarning('Another execution is already in progress!');
return;
}
// reset suggestions before execution
this.setCodeFixSuggestion(undefined);
this.executionState.inProgress();
const potentiallyAffectedFiles = this.tabManagerState.tabs
.filter(filterByType(FileEditorState))
.map((tab) => tab.filePath);
try {
const openedFiles = this.tabManagerState.tabs
.map((tab) => {
if (tab instanceof FileEditorState) {
return {
path: tab.filePath,
code: tab.file.content,
};
}
else if (tab instanceof DiagramEditorState) {
return {
diagram: tab.diagramPath,
code: serializeDiagram(tab.diagram),
};
}
return undefined;
})
.filter(isNonNullable);
const executionPromise = this.client.execute(openedFiles, url, extraParams);
// NOTE: when we execute, it could take a while, and by default, we run a status check which potentially
// blocks the screen, as such, to be less disruptive to the UX and to avoid creating the illusion of slowness
// we will have a wait time, if execution is below this threshold, we will not conduct the check.
// The current threshold we choose is 1000ms, i.e. the execution should be sub-second
const WAIT_TIME_TO_TRIGGER_STATUS_CHECK = 1000;
let executionPromiseFinished = false;
let executionPromiseResult;
yield Promise.all([
executionPromise.then((value) => {
executionPromiseFinished = true;
executionPromiseResult = value;
}),
new Promise((resolve, reject) => setTimeout(() => {
if (!executionPromiseFinished && checkExecutionStatus) {
this.applicationStore.alertService.setBlockingAlert({
message: 'Executing...',
prompt: 'Please do not refresh the application',
showLoading: true,
});
resolve(this.pullExecutionStatus().finally(() => {
this.applicationStore.alertService.setBlockingAlert(undefined);
}));
}
resolve();
}, WAIT_TIME_TO_TRIGGER_STATUS_CHECK, true)),
]);
const result = deserializeExecutionResult(guaranteeNonNullable(executionPromiseResult));
this.applicationStore.alertService.setBlockingAlert(undefined);
if (result instanceof ExecutionFailureResult) {
this.applicationStore.notificationService.notifyError(`Execution failed${result.text ? `: ${result.text}` : ''}`);
this.applicationStore.terminalService.terminal.fail(result.text, {
systemCommand: command ?? 'execute',
});
if (result.sessionError) {
this.applicationStore.alertService.setBlockingAlert({
message: 'Session corrupted',
prompt: result.sessionError,
});
}
else {
yield flowResult(manageResult(result, potentiallyAffectedFiles));
}
}
else {
if (!options?.silentTerminalOnSuccess) {
this.applicationStore.terminalService.terminal.output(result.text ?? '', {
clear: options?.clearTerminal,
systemCommand: command ?? 'execute',
});
}
if (result instanceof ExecutionSuccessResult) {
this.applicationStore.notificationService.notifySuccess('Execution succeeded!');
if (result.reinit) {
this.applicationStore.alertService.setBlockingAlert({
message: 'Reinitializing...',
prompt: 'Please do not refresh the application',
showLoading: true,
});
this.initState.reset();
yield flowResult(this.initialize(false, () => flowResult(this.execute(url, extraParams, checkExecutionStatus, manageResult, command)), this.client.mode, this.client.compilerMode));
}
else {
yield flowResult(manageResult(result, potentiallyAffectedFiles));
}
}
else {
yield flowResult(manageResult(result, potentiallyAffectedFiles));
}
}
}
catch (error) {
assertErrorThrown(error);
this.applicationStore.notificationService.notifyError(error);
this.applicationStore.terminalService.terminal.fail(error.message, {
systemCommand: command ?? 'execute',
});
}
finally {
this.applicationStore.alertService.setBlockingAlert(undefined);
this.executionState.reset();
}
}
// NOTE: currently backend do not suppor this operation, so we temporarily disable it, but
// in theory, this will pull up a blocking modal to show the execution status to user
async pullExecutionStatus() {
const result = (await this.client.getExecutionActivity());
this.applicationStore.alertService.setBlockingAlert({
message: 'Executing...',
prompt: result.text
? result.text
: 'Please do not refresh the application',
showLoading: true,
});
if (result.executing) {
return new Promise((resolve, reject) => setTimeout(() => {
try {
resolve(this.pullExecutionStatus());
}
catch (error) {
reject(error);
}
// NOTE: tune this slightly lower for better experience, also for sub-second execution, setting a high number
// might create the illusion that the system is slow
}, 500));
}
this.applicationStore.alertService.setBlockingAlert({
message: 'Executing...',
prompt: 'Please do not refresh the application',
showLoading: true,
});
return Promise.resolve();
}
*executeGo() {
yield flowResult(this.execute('executeGo', {}, true, (result, potentiallyAffectedFiles) => flowResult(this.manageExecuteGoResult(result, potentiallyAffectedFiles)), LEGEND_PURE_IDE_TERMINAL_COMMAND.GO, {
clearTerminal: true,
}));
}
*runDebugger(command) {
yield flowResult(this.client
.execute([], 'debugging', command)
.then((r) => {
const execResult = deserializeExecutionResult(guaranteeNonNullable(r));
if (execResult.text) {
this.applicationStore.terminalService.terminal.output(execResult.text);
}
})
.catch((er) => {
this.applicationStore.terminalService.terminal.fail(er.message);
}));
}
*manageExecuteGoResult(result, potentiallyAffectedFiles) {
const refreshTreesPromise = this.refreshTrees();
// reset errors on all tabs before potentially show the latest error
this.tabManagerState.tabs
.filter(filterByType(FileEditorState))
.filter((tab) => potentiallyAffectedFiles.includes(tab.filePath))
.forEach((tab) => tab.clearError());
if (result instanceof ExecutionFailureResult) {
if (result.source) {
yield flowResult(this.loadFile(result.source, new FileErrorCoordinate(result.source, result.line, result.column, new ExecutionError(result.text.split('\n').filter(Boolean)[0]))));
}
if (result instanceof UnmatchedFunctionResult) {
this.setCodeFixSuggestion(new UnmatchedFunctionCodeFixSuggestion(this, result));
this.setActivePanelMode(PANEL_MODE.CODE_FIX_SUGGESTION);
this.panelGroupDisplayState.open();
}
else if (result instanceof UnknownSymbolResult) {
this.setCodeFixSuggestion(new UnknownSymbolCodeFixSuggestion(this, result));
this.setActivePanelMode(PANEL_MODE.CODE_FIX_SUGGESTION);
this.panelGroupDisplayState.open();
}
this.resetChangeDetection(potentiallyAffectedFiles);
}
else if (result instanceof ExecutionSuccessResult) {
if (result.modifiedFiles.length) {
for (const path of result.modifiedFiles) {
yield this.reloadFile(path);
}
}
this.resetChangeDetection(potentiallyAffectedFiles.concat(result.modifiedFiles));
// NOTE: this is for the case where compilation failed during IDE initialization
// this is when we fix the compilation and execute for the first time, which in turn
// will properly `initialize` the application
// therefore, we will need to re-initialize the concept tree which was not initialized
// before
if (this.initState.hasFailed || !this.conceptTreeState.treeData) {
yield flowResult(this.conceptTreeState.initialize());
this.initState.pass();
}
}
yield refreshTreesPromise;
}
*executeTests(path, relevantTestsOnly, pctAdapter) {
if (relevantTestsOnly) {
this.applicationStore.notificationService.notifyUnsupportedFeature(`Run relevant tests! (reason: VCS required)`);
return;
}
if (this.testRunState.isInProgress) {
this.applicationStore.notificationService.notifyWarning('Test runner is working. Please try again later');
return;
}
this.testRunState.inProgress();
yield flowResult(this.execute('executeTests', {
path,
pctAdapter,
relevantTestsOnly,
}, false, async (result, potentiallyAffectedFiles) => {
const refreshTreesPromise = this.refreshTrees();
if (result instanceof ExecutionFailureResult) {
if (result.source) {
await flowResult(this.loadFile(result.source, new FileErrorCoordinate(result.source, result.line, result.column, new ExecutionError(result.text.split('\n').filter(Boolean)[0]))));
}
this.setActivePanelMode(PANEL_MODE.TERMINAL);
this.panelGroupDisplayState.open();
this.testRunState.fail();
}
else if (result instanceof TestExecutionResult) {
this.setActivePanelMode(PANEL_MODE.TEST_RUNNER);
this.panelGroupDisplayState.open();
const testRunnerState = new TestRunnerState(this, result);
this.setTestRunnerState(testRunnerState);
await flowResult(testRunnerState.buildTestTreeData());
if (testRunnerState.testExecutionResult.count <= 100) {
testRunnerState.expandTree();
}
// make sure we refresh tree so it is shown in the explorer panel
// NOTE: we could potentially expand the tree here, but this operation is expensive since we have all nodes observable
// so it will lag the UI if we have too many nodes open
testRunnerState.refreshTree();
await flowResult(testRunnerState.pollTestRunnerResult());
this.testRunState.pass();
}
this.resetChangeDetection(potentiallyAffectedFiles);
// do nothing?
await refreshTreesPromise;
}, `${LEGEND_PURE_IDE_TERMINAL_COMMAND.TEST} ${path}`));
}
*executeFullTestSuite(relevantTestsOnly) {
yield flowResult(this.executeTests(ROOT_PACKAGE_PATH, relevantTestsOnly));
}
*executeNavigation(coordinate) {
this.navigationStack.push(coordinate);
yield flowResult(this.execute('getConcept', {
file: coordinate.file,
line: coordinate.line,
column: coordinate.column,
}, false, async (result, potentiallyAffectedFiles) => {
if (result instanceof GetConceptResult) {
await flowResult(this.loadFile(result.jumpTo.source, new FileCoordinate(result.jumpTo.source, result.jumpTo.line, result.jumpTo.column)));
}
this.resetChangeDetection(potentiallyAffectedFiles);
}, `navigate`, { silentTerminalOnSuccess: true }));
}
*navigateBack() {
if (this.navigationStack.length === 0) {
this.applicationStore.notificationService.notifyWarning(`Can't navigate back any further - navigation stack is empty`);
return;
}
if (this.navigationStack.length > 0) {
const coordinate = this.navigationStack.pop();
if (coordinate) {
yield flowResult(this.loadFile(coordinate.file, coordinate));
}
}
}
*fullReCompile(fullInit) {
this.applicationStore.alertService.setActionAlertInfo({
message: 'Are you sure you want to perform a full re-compile?',
prompt: 'This may take a long time to complete',
type: ActionAlertType.CAUTION,
actions: [
{
label: 'Perform full re-compile',
type: ActionAlertActionType.PROCEED_WITH_CAUTION,
handler: () => {
flowResult(this.execute('executeSaveAndReset', {}, true, async (result, potentiallyAffectedFiles) => {
this.initState.reset();
await flowResult(this.initialize(fullInit, undefined, this.client.mode, this.client.compilerMode));
this.resetChangeDetection(potentiallyAffectedFiles);
this.setActiveActivity(ACTIVITY_MODE.CONCEPT_EXPLORER, {
keepShowingIfMatchedCurrent: true,
});
}, `recompile`)).catch(this.applicationStore.alertUnhandledError);
},
},
{
label: 'Abort',
type: ActionAlertActionType.PROCEED,
default: true,
},
],
});
}
resetChangeDetection(files) {
this.tabManagerState.tabs
.filter(filterByType(FileEditorState))
.filter((tab) => files.includes(tab.filePath))
.forEach((tab) => tab.resetChangeDetection());
}
async refreshTrees() {
await Promise.all([
this.directoryTreeState.refreshTreeData(),
this.conceptTreeState.refreshTreeData(),
]);
if (this.directoryTreeState.selectedNode) {
document
.getElementById(this.directoryTreeState.selectedNode.id)
?.scrollIntoView({
behavior: 'instant',
block: 'center',
});
}
if (this.conceptTreeState.selectedNode) {
document
.getElementById(this.conceptTreeState.selectedNode.id)
?.scrollIntoView({
behavior: 'instant',
block: 'center',
});
}
}
async revealConceptInTree(coordinate) {
const errorMessage = 'Error revealing concept. Please make sure that the code compiles and that you are looking for a valid concept';
let concept;
try {
concept = await this.client.getConceptInfo(coordinate.file, coordinate.line, coordinate.column);
}
catch {
this.applicationStore.notificationService.notifyWarning(`Can't find concept info. Please make sure that the code compiles and that you are looking for references of non primitive types!`);
return;
}
if (!concept.path) {
return;
}
this.applicationStore.alertService.setBlockingAlert({
message: 'Revealing concept in tree...',
showLoading: true,
});
try {
if (this.activeActivity !== ACTIVITY_MODE.CONCEPT_EXPLORER) {
this.setActiveActivity(ACTIVITY_MODE.CONCEPT_EXPLORER);
}
const parts = concept.path.split(ELEMENT_PATH_DELIMITER);
let currentPath = guaranteeNonNullable(parts[0]);
let currentNode = guaranteeNonNullable(this.conceptTreeState.getTreeData().nodes.get(currentPath));
for (let i = 1; i < parts.length; ++i) {
currentPath = `${currentPath}${ELEMENT_PATH_DELIMITER}${parts[i]}`;
if (!this.conceptTreeState.getTreeData().nodes.get(currentPath)) {
await flowResult(this.conceptTreeState.expandNode(currentNode));
}
currentNode = guaranteeNonNullable(this.conceptTreeState.getTreeData().nodes.get(currentPath));
}
this.conceptTreeState.setSelectedNode(currentNode);
document.getElementById(currentNode.id)?.scrollIntoView({
behavior: 'instant',
block: 'center',
});
}
catch {
this.applicationStore.notificationService.notifyWarning(errorMessage);
}
finally {
this.applicationStore.alertService.setBlockingAlert(undefined);
}
}
*command(fn, command) {
try {
const result = deserializeCommandResult((yield fn()));
if (result instanceof CommandFailureResult) {
if (result.errorDialog) {
this.applicationStore.notificationService.notifyWarning(`Can't run command '${command}': ${result.text}`);
}
else {
this.applicationStore.terminalService.terminal.output(result.text, {
systemCommand: command,
});
}
return false;
}
return true;
}
catch (error) {
assertErrorThrown(error);
this.applicationStore.notificationService.notifyError(error);
this.applicationStore.terminalService.terminal.fail(error.message, {
systemCommand: command,
});
return false;
}
}
async getConceptInfo(coordinate, options) {
try {
const concept = await this.client.getConceptInfo(coordinate.file, coordinate.line, coordinate.column);
return concept;
}
catch {
if (!options?.silent) {
this.applicationStore.notificationService.notifyWarning(`Can't find concept info. Please make sure that the code compiles and that you are looking for references of non primitive types!`);
}
return undefined;
}
}
async findConceptUsages(func, param) {
return (await this.client.getUsages(func, param)).map((usage) => deserialize(Usage, usage));
}
*findUsagesFromCoordinate(coordinate) {
const concept = (yield this.getConceptInfo(coordinate));
if (!concept) {
return;
}
yield flowResult(this.findUsages(concept));
}
*findUsages(concept) {
try {
this.referenceUsageLoadState.inProgress();
this.applicationStore.alertService.setBlockingAlert({
message: 'Finding concept usages...',
prompt: `Finding references of ${getConceptInfoLabel(concept)}`,
showLoading: true,
});
const usages = (yield this.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}'`)));
const searchResultCoordinates = (yield this.client.getTextSearchPreview(usages.map((usage) => serialize(SearchResultCoordinate, new SearchResultCoordinate(usage.source, usage.startLine, usage.startColumn, usage.endLine, usage.endColumn))))).map((preview) => deserialize(SearchResultCoordinate, preview));
this.setReferenceUsageResult(new ReferenceUsageResult(this, concept, usages, searchResultCoordinates));
this.setActivePanelMode(PANEL_MODE.REFERENCES);
this.panelGroupDisplayState.open();
}
catch (error) {
assertErrorThrown(error);
this.applicationStore.notificationService.notifyError(error);
}
finally {
this.applicationStore.alertService.setBlockingAlert(undefined);
this.referenceUsageLoadState.complete();
}
}
*renameConcept(oldName, newName, pureType, usages) {
try {
yield this.client.renameConcept({
oldName,
newName,
pureType,
sourceInformations: usages.map((usage) => ({
sourceId: usage.source,
line: usage.line,
column: usage.column,
})),
});
const potentiallyModifiedFiles = usages.map((usage) => usage.source);
for (const file of potentiallyModifiedFiles) {
yield this.reloadFile(file);
}
yield this.refreshTrees();
this.applicationStore.notificationService.notifyWarning(`Please re-compile the code after refacting`);
}
catch (error) {
assertErrorThrown(error);
this.applicationStore.notificationService.notifyError(`Can't rename concept '${oldName}'`);
}
}
*movePackageableElements(inputs) {
try {
yield this.client.movePackageableElements(inputs.map((input) => ({
pureName: input.pureName,
pureType: input.pureType,
sourcePackage: input.sourcePackage,
destinationPackage: input.destinationPackage,
sourceInformations: input.usages.map((usage) => ({
sourceId: usage.source,
line: usage.line,
column: usage.column,
})),
})));
const potentiallyModifiedFiles = uniq(inputs.flatMap((input) => input.usages.map((usage) => usage.source)));
for (const file of potentiallyModifiedFiles) {
yield this.reloadFile(file);
}
yield this.refreshTrees();
this.applicationStore.notificationService.notifyWarning(`Please re-compile the code after refacting`);
}
catch (error) {
assertErrorThrown(error);
this.applicationStore.notificationService.notifyError(`Can't move packageable elements:\n${error.message}`);
}
}
*updateFileUsingSuggestionCandidate(candidate) {
this.setCodeFixSuggestion(undefined);
yield flowResult(this.updateFile(candidate.fileToBeModified, candidate.lineToBeModified, candidate.columnToBeModified, candidate.add, candidate.messageToBeModified));
this.setActivePanelMode(PANEL_MODE.TERMINAL);
this.panelGroupDisplayState.open();
}
*updateFile(path, line, column, add, message) {
try {
const result = (yield this.client.updateSource([
{
path,
line,
column,
message,
add,
},
]));
if (result.modifiedFiles.length) {
for (const file of result.modifiedFiles) {
yield this.reloadFile(file);
}
}
this.applicationStore.notificationService.notifyWarning(`Please re-compile the code after refacting`);
}
catch (error) {
assertErrorThrown(error);
this.applicationStore.notificationService.notifyError(`Can't update file '${path}'`);
}
}
*searchFile() {
if (this.fileSearchCommandLoadState.isInProgress ||
this.fileSearchCommandState.text.length <= 3) {
return;
}
this.fileSearchCommandLoadState.inProgress();
this.fileSearchCommandResults = (yield this.client.findFiles(this.fileSearchCommandState.text, this.fileSearchCommandState.isRegExp));
this.fileSearchCommandLoadState.pass();
}
*createNewDirectory(path) {
try {
yield flowResult(this.command(() => this.client.createFolder(trimPathLeadingSlash(path)), LEGEND_PURE_IDE_TERMINAL_COMMAND.NEW_DIRECTORY));
yield flowResult(this.directoryTreeState.refreshTreeData());
}
catch (error) {
assertErrorThrown(error);
this.applicationStore.notificationService.notifyError(error);
}
}
*createNewFile(path) {
try {
const result = (yield flowResult(this.command(() => this.client.createFile(trimPathLeadingSlash(path)), LEGEND_PURE_IDE_TERMINAL_COMMAND.NEW_FILE)));
yield flowResult(this.directoryTreeState.refreshTreeData());
if (result) {
yield flowResult(this.loadFile(path));
}
}
catch (error) {
assertErrorThrown(error);
this.applicationStore.notificationService.notifyError(error);
}
}
*renameFile(oldPath, newPath) {
try {
yield flowResult(this.command(() => this.client.renameFile(oldPath, newPath), LEGEND_PURE_IDE_TERMINAL_COMMAND.MOVE));
yield flowResult(this.directoryTreeState.refreshTreeData());
const openTab = this.tabManagerState.tabs.find((tab) => tab instanceof FileEditorState && tab.filePath === oldPath);
if (openTab) {
this.tabManagerState.closeTab(openTab);
}
}
catch (error) {
assertErrorThrown(error);
this.applicationStore.notificationService.notifyError(error);
}
}
*deleteDirectoryOrFile(path, isDirectory, hasChildContent) {
const _delete = async () => {
await flowResult(this.command(() => this.client.deleteDirectoryOrFile(trimPathLeadingSlash(path)), LEGEND_PURE_IDE_TERMINAL_COMMAND.REMOVE));
const editorStatesToClose = this.tabManagerState.tabs.filter((tab) => tab instanceof FileEditorState &&
(isDirectory === undefined || isDirectory
? tab.filePath.startsWith(`${path}/`)
: tab.filePath === path));
editorStatesToClose.forEach((tab) => this.tabManagerState.closeTab(tab));
await flowResult(this.directoryTreeState.refreshTreeData());
};
if (isDirectory === undefined || hasChildContent === undefined) {
_delete().catch(this.applicationStore.alertUnhandledError);
}
else {
this.applicationStore.alertService.setActionAlertInfo({
message: `Are you sure you would like to delete this ${isDirectory ? 'directory' : 'file'}?`,
prompt: hasChildContent
? 'Beware! This directory is not empty