UNPKG

@finos/legend-application-pure-ide

Version:
1,069 lines 50.8 kB
/** * 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