UNPKG

@finos/legend-application-pure-ide

Version:
1,518 lines (1,448 loc) 52.5 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 { type InitializationActivity, type InitializationResult, InitializationFailureWithSourceResult, InitializationFailureResult, deserializeInitializationnResult, } from '../server/models/Initialization.js'; import { type CandidateWithPackageNotImported, type ExecutionActivity, type ExecutionResult, 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 { type ConceptInfo, getConceptInfoLabel, Usage, FIND_USAGE_FUNCTION_PATH, } from '../server/models/Usage.js'; import { type CommandResult, CommandFailureResult, deserializeCommandResult, } from '../server/models/Command.js'; import { ActionAlertActionType, ActionAlertType, type CommandRegistrar, } from '@finos/legend-application'; import { type GeneratorFn, type PlainObject, 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 type { LegendPureIDEApplicationStore } from './LegendPureIDEBaseStore.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 type { SourceModificationResult } from '../server/models/Source.js'; import { ConceptType } from '../server/models/ConceptTree.js'; import { setupTerminal } from './LegendPureIDETerminal.js'; import { type CodeFixSuggestion, UnknownSymbolCodeFixSuggestion, UnmatchedFunctionCodeFixSuggestion, } from './CodeFixSuggestion.js'; import { ReferenceUsageResult } from './ReferenceUsageResult.js'; import { TextSearchState } from './TextSearchState.js'; import type { TabState } from '@finos/legend-lego/application'; import { PCTAdapter } from '../server/models/Test.js'; export class PureIDEStore implements CommandRegistrar { readonly applicationStore: LegendPureIDEApplicationStore; readonly initState = ActionState.create(); readonly directoryTreeState: DirectoryTreeState; readonly conceptTreeState: ConceptTreeState; readonly client: PureServerClient; // Layout activePanelMode = PANEL_MODE.TERMINAL; readonly panelGroupDisplayState = new PanelDisplayState({ initial: 0, default: 300, snap: 100, }); activeActivity?: string = ACTIVITY_MODE.CONCEPT_EXPLORER; readonly sideBarDisplayState = new PanelDisplayState({ initial: 300, default: 300, snap: 150, }); readonly tabManagerState = new PureIDETabManagerState(this); readonly executionState = ActionState.create(); navigationStack: FileCoordinate[] = []; // TODO?: we might want to limit the number of items in this stack // File Search Command readonly fileSearchCommandLoadState = ActionState.create(); readonly fileSearchCommandState = new FileSearchCommandState(); openFileSearchCommand = false; fileSearchCommandResults: string[] = []; // Code-fix Suggestions Panel codeFixSuggestion?: CodeFixSuggestion | undefined; // Reference Usage Panel readonly referenceUsageLoadState = ActionState.create(); referenceUsageResult?: ReferenceUsageResult | undefined; // Text Search Panel readonly textSearchState: TextSearchState; // Test Runner Panel readonly testRunState = ActionState.create(); testRunnerState?: TestRunnerState | undefined; PCTAdapters: PCTAdapter[] = []; selectedPCTAdapter?: PCTAdapter | undefined; PCTRunPath?: string | undefined; constructor(applicationStore: LegendPureIDEApplicationStore) { 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: boolean): void { this.openFileSearchCommand = val; } setSelectedPCTAdapter(val: PCTAdapter | undefined): void { this.selectedPCTAdapter = val; } setPCTRunPath(val: string | undefined): void { this.PCTRunPath = val; } setActivePanelMode(val: PANEL_MODE): void { this.activePanelMode = val; } setCodeFixSuggestion(val: CodeFixSuggestion | undefined): void { this.codeFixSuggestion = val; } setReferenceUsageResult(val: ReferenceUsageResult | undefined): void { this.referenceUsageResult = val; } setTestRunnerState(val: TestRunnerState | undefined): void { this.testRunnerState = val; } cleanUp(): void { // 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: boolean, func: (() => Promise<void>) | undefined, mode: string | undefined, fastCompile: string | undefined, ): GeneratorFn<void> { 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 as { first: string; second: string }[] ).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) as PlainObject<InitializationResult>, ); 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?: string): GeneratorFn<void> { this.applicationStore.alertService.setBlockingAlert({ message: message ?? 'Checking IDE session...', showLoading: true, }); yield this.pullInitializationActivity( (activity: InitializationActivity) => { 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?: (activity: InitializationActivity) => void, ): Promise<void> { const result = (await this.client.getInitializationActivity()) as unknown as InitializationActivity; if (result.initializing) { return new Promise((resolve, reject) => setTimeout(() => { try { resolve(this.pullInitializationActivity()); } catch (error) { reject(error); } }, 1000), ); } return Promise.resolve(); } registerCommands(): void { 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(): void { [ 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: string, options?: { keepShowingIfMatchedCurrent?: boolean }, ): void { if (!this.sideBarDisplayState.isOpen) { this.sideBarDisplayState.open(); } else if ( activity === this.activeActivity && !options?.keepShowingIfMatchedCurrent ) { this.sideBarDisplayState.close(); } this.activeActivity = activity; } *loadDiagram( filePath: string, diagramPath: string, line: number, column: number, ): GeneratorFn<void> { let editorState = this.tabManagerState.tabs.find( (tab): tab is DiagramEditorState => 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: string, coordinate?: FileCoordinate): GeneratorFn<void> { try { let editorState = this.tabManagerState.tabs.find( (tab): tab is FileEditorState => 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: string): Promise<void> { const tabsToClose: TabState[] = []; 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: string, extraParams: Record<PropertyKey, unknown>, checkExecutionStatus: boolean, manageResult: ( result: ExecutionResult, potentiallyAffectedFiles: string[], ) => Promise<void>, command: string | undefined, options?: { /** * Some execution, such as find concept produces no output * so we should not reset the console text in that case */ silentTerminalOnSuccess?: boolean; clearTerminal?: boolean; }, ): GeneratorFn<void> { 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: PlainObject<ExecutionResult> | undefined; yield Promise.all<void>([ 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(): Promise<void> { const result = (await this.client.getExecutionActivity()) as unknown as ExecutionActivity; 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(): GeneratorFn<void> { yield flowResult( this.execute( 'executeGo', {}, true, (result: ExecutionResult, potentiallyAffectedFiles: string[]) => flowResult( this.manageExecuteGoResult(result, potentiallyAffectedFiles), ), LEGEND_PURE_IDE_TERMINAL_COMMAND.GO, { clearTerminal: true, }, ), ); } *runDebugger(command: { args: string[] }): GeneratorFn<void> { 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: ExecutionResult, potentiallyAffectedFiles: string[], ): GeneratorFn<void> { 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: string, relevantTestsOnly?: boolean | undefined, pctAdapter?: string | undefined, ): GeneratorFn<void> { 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: ExecutionResult, potentiallyAffectedFiles: string[]) => { 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?: boolean): GeneratorFn<void> { yield flowResult(this.executeTests(ROOT_PACKAGE_PATH, relevantTestsOnly)); } *executeNavigation(coordinate: FileCoordinate): GeneratorFn<void> { this.navigationStack.push(coordinate); yield flowResult( this.execute( 'getConcept', { file: coordinate.file, line: coordinate.line, column: coordinate.column, }, false, async (result: ExecutionResult, potentiallyAffectedFiles: string[]) => { 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(): GeneratorFn<void> { 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: boolean): GeneratorFn<void> { 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: ExecutionResult, potentiallyAffectedFiles: string[], ) => { 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: string[]): void { this.tabManagerState.tabs .filter(filterByType(FileEditorState)) .filter((tab) => files.includes(tab.filePath)) .forEach((tab) => tab.resetChangeDetection()); } async refreshTrees(): Promise<void> { 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: FileCoordinate): Promise<void> { const errorMessage = 'Error revealing concept. Please make sure that the code compiles and that you are looking for a valid concept'; let concept: ConceptInfo; 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: () => Promise<PlainObject<CommandResult>>, command: string, ): GeneratorFn<boolean> { try { const result = deserializeCommandResult( (yield fn()) as PlainObject<CommandResult>, ); 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: FileCoordinate, options?: { silent?: boolean | undefined; }, ): Promise<ConceptInfo | undefined> { 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: string, param: string[]): Promise<Usage[]> { return (await this.client.getUsages(func, param)).map((usage) => deserialize(Usage, usage), ); } *findUsagesFromCoordinate(coordinate: FileCoordinate): GeneratorFn<void> { const concept = (yield this.getConceptInfo(coordinate)) as | ConceptInfo | undefined; if (!concept) { return; } yield flowResult(this.findUsages(concept)); } *findUsages(concept: ConceptInfo): GeneratorFn<void> { 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}'`, ), )) as Usage[]; const searchResultCoordinates = ( (yield this.client.getTextSearchPreview( usages.map((usage) => serialize( SearchResultCoordinate, new SearchResultCoordinate( usage.source, usage.startLine, usage.startColumn, usage.endLine, usage.endColumn, ), ), ), )) as PlainObject<SearchResultCoordinate>[] ).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: string, newName: string, pureType: string, usages: Usage[], ): GeneratorFn<void> { 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: { pureName: string; pureType: string; sourcePackage: string; destinationPackage: string; usages: Usage[]; }[], ): GeneratorFn<void> { 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: CandidateWithPackageNotImported, ): GeneratorFn<void> { 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: string, line: number, column: number, add: boolean, message: string, ): GeneratorFn<void> { try { const result = (yield this.client.updateSource([ { path, line, column, message, add, }, ])) as SourceModificationResult; 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(): GeneratorFn<void> { 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, )) as string[]; this.fileSearchCommandLoadState.pass(); } *createNewDirectory(path: string): GeneratorFn<void> { 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: string): GeneratorFn<void> { try { const result = (yield flowResult( this.command( () => this.client.createFile(trimPathLeadingSlash(path)), LEGEND_PURE_IDE_TERMINAL_COMMAND.NEW_FILE, ), )) as boolean; yield flowResult(this.directoryTreeState.refreshTreeData()); if (result) { yield flowResult(this.loadFile(path)); } } catch (error) { assertErrorThrown(error); this.applicationStore.notificationService.notifyError(error); } } *renameFile(oldPath: string, newPath: string): Gen