UNPKG

@finos/legend-studio

Version:
984 lines 50.3 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, flowResult, makeAutoObservable } from 'mobx'; import { ClassEditorState } from './editor-state/element-editor-state/ClassEditorState.js'; import { ExplorerTreeState } from './ExplorerTreeState.js'; import { ACTIVITY_MODE, AUX_PANEL_MODE, GRAPH_EDITOR_MODE, EDITOR_MODE, LEGEND_STUDIO_HOTKEY, LEGEND_STUDIO_HOTKEY_MAP, } from './EditorConfig.js'; import { ElementEditorState } from './editor-state/element-editor-state/ElementEditorState.js'; import { MappingEditorState } from './editor-state/element-editor-state/mapping/MappingEditorState.js'; import { EditorGraphState, GraphBuilderStatus, } from './EditorGraphState.js'; import { ChangeDetectionState } from './ChangeDetectionState.js'; import { NewElementState } from './editor/NewElementState.js'; import { WorkspaceUpdaterState } from './sidebar-state/WorkspaceUpdaterState.js'; import { ProjectOverviewState } from './sidebar-state/ProjectOverviewState.js'; import { WorkspaceReviewState } from './sidebar-state/WorkspaceReviewState.js'; import { LocalChangesState } from './sidebar-state/LocalChangesState.js'; import { WorkspaceWorkflowManagerState } from './sidebar-state/WorkflowManagerState.js'; import { GrammarTextEditorState } from './editor-state/GrammarTextEditorState.js'; import { LogEvent, addUniqueEntry, isNonNullable, assertErrorThrown, guaranteeType, guaranteeNonNullable, UnsupportedOperationError, assertNonNullable, assertTrue, ActionState, filterByType, } from '@finos/legend-shared'; import { UMLEditorState } from './editor-state/element-editor-state/UMLEditorState.js'; import { ServiceEditorState } from './editor-state/element-editor-state/service/ServiceEditorState.js'; import { EditorSDLCState } from './EditorSDLCState.js'; import { ModelLoaderState } from './editor-state/ModelLoaderState.js'; import { EntityDiffViewState } from './editor-state/entity-diff-editor-state/EntityDiffViewState.js'; import { FunctionEditorState } from './editor-state/element-editor-state/FunctionEditorState.js'; import { ProjectConfigurationEditorState } from './editor-state/ProjectConfigurationEditorState.js'; import { PackageableRuntimeEditorState } from './editor-state/element-editor-state/RuntimeEditorState.js'; import { PackageableConnectionEditorState } from './editor-state/element-editor-state/connection/ConnectionEditorState.js'; import { PackageableDataEditorState } from './editor-state/element-editor-state/data/DataEditorState.js'; import { FileGenerationEditorState } from './editor-state/element-editor-state/FileGenerationEditorState.js'; import { EntityDiffEditorState } from './editor-state/entity-diff-editor-state/EntityDiffEditorState.js'; import { EntityChangeConflictEditorState } from './editor-state/entity-diff-editor-state/EntityChangeConflictEditorState.js'; import { CHANGE_DETECTION_EVENT } from './ChangeDetectionEvent.js'; import { GenerationSpecificationEditorState } from './editor-state/GenerationSpecificationEditorState.js'; import { UnsupportedElementEditorState } from './editor-state/UnsupportedElementEditorState.js'; import { FileGenerationViewerState } from './editor-state/FileGenerationViewerState.js'; import { DevToolState } from './aux-panel-state/DevToolState.js'; import { generateSetupRoute, generateViewProjectRoute, } from './LegendStudioRouter.js'; import { HotkeyConfiguration, NonBlockingDialogState, PanelDisplayState, } from '@finos/legend-art'; import { ProjectConfiguration, } from '@finos/legend-server-sdlc'; import { GRAPH_MANAGER_EVENT, PrimitiveType, Class, Enumeration, Profile, Association, ConcreteFunctionDefinition, Measure, Database, FlatData, Mapping, Service, PackageableRuntime, PackageableConnection, FileGenerationSpecification, GenerationSpecification, PRIMITIVE_TYPE, Package, DataElement, } from '@finos/legend-graph'; import { ActionAlertActionType, ActionAlertType, APPLICATION_EVENT, TAB_SIZE, buildElementOption, } from '@finos/legend-application'; import { LEGEND_STUDIO_APP_EVENT } from './LegendStudioAppEvent.js'; import { StandardEditorMode } from './editor/StandardEditorMode.js'; import { WorkspaceUpdateConflictResolutionState } from './sidebar-state/WorkspaceUpdateConflictResolutionState.js'; import { graph_addElement, graph_deleteElement, graph_deleteOwnElement, graph_renameElement, } from './graphModifier/GraphModifierHelper.js'; import { PACKAGEABLE_ELEMENT_TYPE } from './shared/ModelUtil.js'; import { GlobalTestRunnerState } from './sidebar-state/testable/GlobalTestRunnerState.js'; export class EditorExtensionState { /** * This helps to better type-check for this empty abtract type * See https://github.com/finos/legend-studio/blob/master/docs/technical/typescript-usage.md#understand-typescript-structual-type-system */ _$nominalTypeBrand; } export class EditorStore { applicationStore; sdlcServerClient; depotServerClient; pluginManager; editorMode; setEditorMode(val) { this.editorMode = val; } // NOTE: once we clear up the editor store to make modes more separated // we should remove these sets of functions. They are basically hacks to // ensure hiding parts of the UI based on the editing mode. // Instead, we will gradually move these `boolean` flags into `EditorMode` // See https://github.com/finos/legend-studio/issues/317 mode = EDITOR_MODE.STANDARD; setMode(val) { this.mode = val; } get isInViewerMode() { return this.mode === EDITOR_MODE.VIEWER; } get isInConflictResolutionMode() { return this.mode === EDITOR_MODE.CONFLICT_RESOLUTION; } editorExtensionStates = []; explorerTreeState; sdlcState; graphState; graphManagerState; changeDetectionState; grammarTextEditorState; modelLoaderState; projectConfigurationEditorState; projectOverviewState; workspaceWorkflowManagerState; globalTestRunnerState; workspaceUpdaterState; workspaceReviewState; localChangesState; conflictResolutionState; devToolState; _isDisposed = false; initState = ActionState.create(); graphEditMode = GRAPH_EDITOR_MODE.FORM; // Aux Panel activeAuxPanelMode = AUX_PANEL_MODE.CONSOLE; auxPanelDisplayState = new PanelDisplayState({ initial: 0, default: 300, snap: 100, }); // Side Bar activeActivity = ACTIVITY_MODE.EXPLORER; sideBarDisplayState = new PanelDisplayState({ initial: 300, default: 300, snap: 150, }); // Hot keys blockGlobalHotkeys = false; defaultHotkeys = []; hotkeys = []; // Tabs currentEditorState; openedEditorStates = []; newElementState; /** * Since we want to share element generation state across all element in the editor, we will create 1 element generate state * per file generation configuration type. */ elementGenerationStates = []; searchElementCommandState = new NonBlockingDialogState(); backdrop = false; ignoreNavigationBlocking = false; isDevToolEnabled = true; constructor(applicationStore, sdlcServerClient, depotServerClient, graphManagerState, pluginManager) { makeAutoObservable(this, { applicationStore: false, sdlcServerClient: false, depotServerClient: false, graphState: false, graphManagerState: false, setEditorMode: action, setMode: action, setDevTool: action, setHotkeys: action, addHotKey: action, resetHotkeys: action, setBlockGlobalHotkeys: action, setCurrentEditorState: action, setBackdrop: action, setActiveAuxPanelMode: action, setIgnoreNavigationBlocking: action, refreshCurrentEntityDiffEditorState: action, setBlockingAlert: action, setActionAlertInfo: action, cleanUp: action, reset: action, setGraphEditMode: action, setActiveActivity: action, closeState: action, closeAllOtherStates: action, closeAllStates: action, openState: action, openEntityDiff: action, openEntityChangeConflict: action, openSingletonEditorState: action, openElement: action, reprocessElementEditorState: action, openGeneratedFile: action, closeAllEditorTabs: action, }); this.applicationStore = applicationStore; this.sdlcServerClient = sdlcServerClient; this.depotServerClient = depotServerClient; this.pluginManager = pluginManager; this.editorMode = new StandardEditorMode(this); this.sdlcState = new EditorSDLCState(this); this.graphState = new EditorGraphState(this); this.graphManagerState = graphManagerState; this.changeDetectionState = new ChangeDetectionState(this, this.graphState); this.devToolState = new DevToolState(this); // side bar panels this.explorerTreeState = new ExplorerTreeState(this); this.projectOverviewState = new ProjectOverviewState(this, this.sdlcState); this.globalTestRunnerState = new GlobalTestRunnerState(this, this.sdlcState); this.workspaceWorkflowManagerState = new WorkspaceWorkflowManagerState(this, this.sdlcState); this.workspaceUpdaterState = new WorkspaceUpdaterState(this, this.sdlcState); this.workspaceReviewState = new WorkspaceReviewState(this, this.sdlcState); this.localChangesState = new LocalChangesState(this, this.sdlcState); this.conflictResolutionState = new WorkspaceUpdateConflictResolutionState(this, this.sdlcState); this.newElementState = new NewElementState(this); // special (singleton) editors this.grammarTextEditorState = new GrammarTextEditorState(this); this.modelLoaderState = new ModelLoaderState(this); this.projectConfigurationEditorState = new ProjectConfigurationEditorState(this, this.sdlcState); // extensions this.editorExtensionStates = this.pluginManager .getApplicationPlugins() .flatMap((plugin) => plugin.getExtraEditorExtensionStateCreators?.() ?? []) .map((creator) => creator(this)) .filter(isNonNullable); // hotkeys this.defaultHotkeys = [ // actions that need blocking new HotkeyConfiguration(LEGEND_STUDIO_HOTKEY.COMPILE, [LEGEND_STUDIO_HOTKEY_MAP.COMPILE], this.createGlobalHotKeyAction(() => { flowResult(this.graphState.globalCompileInFormMode()).catch(applicationStore.alertUnhandledError); })), new HotkeyConfiguration(LEGEND_STUDIO_HOTKEY.GENERATE, [LEGEND_STUDIO_HOTKEY_MAP.GENERATE], this.createGlobalHotKeyAction(() => { flowResult(this.graphState.graphGenerationState.globalGenerate()).catch(applicationStore.alertUnhandledError); })), new HotkeyConfiguration(LEGEND_STUDIO_HOTKEY.CREATE_ELEMENT, [LEGEND_STUDIO_HOTKEY_MAP.CREATE_ELEMENT], this.createGlobalHotKeyAction(() => this.newElementState.openModal())), new HotkeyConfiguration(LEGEND_STUDIO_HOTKEY.OPEN_ELEMENT, [LEGEND_STUDIO_HOTKEY_MAP.OPEN_ELEMENT], this.createGlobalHotKeyAction(() => this.searchElementCommandState.open())), new HotkeyConfiguration(LEGEND_STUDIO_HOTKEY.TOGGLE_TEXT_MODE, [LEGEND_STUDIO_HOTKEY_MAP.TOGGLE_TEXT_MODE], this.createGlobalHotKeyAction(() => { flowResult(this.toggleTextMode()).catch(applicationStore.alertUnhandledError); })), new HotkeyConfiguration(LEGEND_STUDIO_HOTKEY.TOGGLE_MODEL_LOADER, [LEGEND_STUDIO_HOTKEY_MAP.TOGGLE_MODEL_LOADER], this.createGlobalHotKeyAction(() => this.openState(this.modelLoaderState))), new HotkeyConfiguration(LEGEND_STUDIO_HOTKEY.SYNC_WITH_WORKSPACE, [LEGEND_STUDIO_HOTKEY_MAP.SYNC_WITH_WORKSPACE], this.createGlobalHotKeyAction(() => { flowResult(this.localChangesState.pushLocalChanges()).catch(applicationStore.alertUnhandledError); })), // simple actions (no blocking is needed) new HotkeyConfiguration(LEGEND_STUDIO_HOTKEY.TOGGLE_AUX_PANEL, [LEGEND_STUDIO_HOTKEY_MAP.TOGGLE_AUX_PANEL], this.createGlobalHotKeyAction(() => this.auxPanelDisplayState.toggle())), new HotkeyConfiguration(LEGEND_STUDIO_HOTKEY.TOGGLE_SIDEBAR_EXPLORER, [LEGEND_STUDIO_HOTKEY_MAP.TOGGLE_SIDEBAR_EXPLORER], this.createGlobalHotKeyAction(() => this.setActiveActivity(ACTIVITY_MODE.EXPLORER))), new HotkeyConfiguration(LEGEND_STUDIO_HOTKEY.TOGGLE_SIDEBAR_CHANGES, [LEGEND_STUDIO_HOTKEY_MAP.TOGGLE_SIDEBAR_CHANGES], this.createGlobalHotKeyAction(() => this.setActiveActivity(ACTIVITY_MODE.LOCAL_CHANGES))), new HotkeyConfiguration(LEGEND_STUDIO_HOTKEY.TOGGLE_SIDEBAR_WORKSPACE_REVIEW, [LEGEND_STUDIO_HOTKEY_MAP.TOGGLE_SIDEBAR_WORKSPACE_REVIEW], this.createGlobalHotKeyAction(() => this.setActiveActivity(ACTIVITY_MODE.WORKSPACE_REVIEW))), new HotkeyConfiguration(LEGEND_STUDIO_HOTKEY.TOGGLE_SIDEBAR_WORKSPACE_UPDATER, [LEGEND_STUDIO_HOTKEY_MAP.TOGGLE_SIDEBAR_WORKSPACE_UPDATER], this.createGlobalHotKeyAction(() => this.setActiveActivity(ACTIVITY_MODE.WORKSPACE_UPDATER))), ]; this.hotkeys = this.defaultHotkeys; } get isInitialized() { return (Boolean(this.sdlcState.currentProject && this.sdlcState.currentWorkspace && this.sdlcState.currentRevision && this.sdlcState.remoteWorkspaceRevision) && this.graphManagerState.systemBuildState.hasSucceeded); } get isInGrammarTextMode() { return this.graphEditMode === GRAPH_EDITOR_MODE.GRAMMAR_TEXT; } get isInFormMode() { return this.graphEditMode === GRAPH_EDITOR_MODE.FORM; } get hasUnpushedChanges() { return Boolean(this.changeDetectionState.workspaceLocalLatestRevisionState.changes .length); } setDevTool(val) { this.isDevToolEnabled = val; } setHotkeys(val) { this.hotkeys = val; } addHotKey(val) { addUniqueEntry(this.hotkeys, val); } resetHotkeys() { this.hotkeys = this.defaultHotkeys; } setBlockGlobalHotkeys(val) { this.blockGlobalHotkeys = val; } setCurrentEditorState(val) { this.currentEditorState = val; } setBackdrop(val) { this.backdrop = val; } setActiveAuxPanelMode(val) { this.activeAuxPanelMode = val; } setIgnoreNavigationBlocking(val) { this.ignoreNavigationBlocking = val; } refreshCurrentEntityDiffEditorState() { if (this.currentEditorState instanceof EntityDiffEditorState) { this.currentEditorState.refresh(); } } setBlockingAlert(alertInfo) { if (this._isDisposed) { return; } this.setBlockGlobalHotkeys(Boolean(alertInfo)); // block global hotkeys if alert is shown this.applicationStore.setBlockingAlert(alertInfo); } setActionAlertInfo(alertInfo) { if (this._isDisposed) { return; } this.applicationStore.setActionAlertInfo(alertInfo); } 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.setBlockingAlert(undefined); this.setActionAlertInfo(undefined); // stop change detection to avoid memory-leak this.changeDetectionState.stop(); this._isDisposed = true; } reset() { this.closeAllEditorTabs(); this.projectConfigurationEditorState = new ProjectConfigurationEditorState(this, this.sdlcState); this.explorerTreeState = new ExplorerTreeState(this); } /** * This is the entry of the app logic where the 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(projectId, workspaceId, workspaceType) { if (!this.initState.isInInitialState) { /** * Since React `fast-refresh` will sometimes cause `Editor` to rerender, this method will be called again * as all hooks are recalled, as such, ONLY IN DEVELOPMENT mode we allow this to not fail-fast * we also have to `undo` some of what the `cleanUp` does to this store as the cleanup part of all hooks will be triggered * as well */ // eslint-disable-next-line no-process-env if (process.env.NODE_ENV === 'development') { this.applicationStore.log.info(LogEvent.create(APPLICATION_EVENT.DEVELOPMENT_ISSUE), `Fast-refreshing the app - undoing cleanUp() and preventing initialize() recall in editor store...`); this.changeDetectionState.start(); this._isDisposed = false; return; } this.applicationStore.notifyIllegalState('Editor store is re-initialized'); return; } this.initState.inProgress(); const onLeave = (hasBuildSucceeded) => { this.initState.complete(hasBuildSucceeded); this.initState.setMessage(undefined); }; this.initState.setMessage(`Setting up workspace...`); yield flowResult(this.sdlcState.fetchCurrentProject(projectId, { suppressNotification: true, })); if (!this.sdlcState.currentProject) { // If the project is not found or the user does not have access to it, // we will not automatically redirect them to the setup page as they will lose the URL // instead, we give them the option to: // - reload the page (in case they later gain access) // - back to the setup page this.setActionAlertInfo({ message: `Project not found or inaccessible`, prompt: 'Please check that the project exists and request access to it', type: ActionAlertType.STANDARD, onEnter: () => this.setBlockGlobalHotkeys(true), onClose: () => this.setBlockGlobalHotkeys(false), actions: [ { label: 'Reload application', default: true, type: ActionAlertActionType.STANDARD, handler: () => { this.applicationStore.navigator.reload(); }, }, { label: 'Back to setup page', type: ActionAlertActionType.STANDARD, handler: () => { this.applicationStore.navigator.goTo(generateSetupRoute(undefined)); }, }, ], }); onLeave(false); return; } yield flowResult(this.sdlcState.fetchCurrentWorkspace(projectId, workspaceId, workspaceType, { suppressNotification: true, })); if (!this.sdlcState.currentWorkspace) { // If the workspace is not found, // we will not automatically redirect the user to the setup page as they will lose the URL // instead, we give them the option to: // - create the workspace // - view project // - back to the setup page const createWorkspaceAndRelaunch = async () => { try { this.applicationStore.setBlockingAlert({ message: 'Creating workspace...', prompt: 'Please do not close the application', }); const workspace = await this.sdlcServerClient.createWorkspace(projectId, workspaceId, workspaceType); this.applicationStore.setBlockingAlert(undefined); this.applicationStore.notifySuccess(`Workspace '${workspace.workspaceId}' is succesfully created. Reloading application...`); this.applicationStore.navigator.reload(); } catch (error) { assertErrorThrown(error); this.applicationStore.log.error(LogEvent.create(LEGEND_STUDIO_APP_EVENT.WORKSPACE_SETUP_FAILURE), error); this.applicationStore.notifyError(error); } }; this.setActionAlertInfo({ message: 'Workspace not found', prompt: `Please note that you can check out the project in viewer mode. Workspace is only required if you need to work on the project.`, type: ActionAlertType.STANDARD, onEnter: () => this.setBlockGlobalHotkeys(true), onClose: () => this.setBlockGlobalHotkeys(false), actions: [ { label: 'View project', default: true, type: ActionAlertActionType.STANDARD, handler: () => { this.applicationStore.navigator.goTo(generateViewProjectRoute(projectId)); }, }, { label: 'Create workspace', type: ActionAlertActionType.STANDARD, handler: () => { createWorkspaceAndRelaunch().catch(this.applicationStore.alertUnhandledError); }, }, { label: 'Back to setup page', type: ActionAlertActionType.STANDARD, handler: () => { this.applicationStore.navigator.goTo(generateSetupRoute(projectId, workspaceId, workspaceType)); }, }, ], }); onLeave(false); return; } yield Promise.all([ this.sdlcState.fetchCurrentRevision(projectId, this.sdlcState.activeWorkspace), this.graphManagerState.initializeSystem(), this.graphManagerState.graphManager.initialize({ env: this.applicationStore.config.env, tabSize: TAB_SIZE, clientConfig: { baseUrl: this.applicationStore.config.engineServerUrl, queryBaseUrl: this.applicationStore.config.engineQueryServerUrl, enableCompression: true, }, }, { tracerService: this.applicationStore.tracerService, }), ]); yield flowResult(this.initMode()); onLeave(true); } *initMode() { switch (this.mode) { case EDITOR_MODE.STANDARD: yield flowResult(this.initStandardMode()); return; case EDITOR_MODE.CONFLICT_RESOLUTION: yield flowResult(this.initConflictResolutionMode()); return; default: throw new UnsupportedOperationError(`Can't initialize editor for unsupported mode '${this.mode}'`); } } *initStandardMode() { yield Promise.all([ this.buildGraph(), this.sdlcState.checkIfWorkspaceIsOutdated(), this.workspaceReviewState.fetchCurrentWorkspaceReview(), this.workspaceUpdaterState.fetchLatestCommittedReviews(), this.projectConfigurationEditorState.fetchLatestProjectStructureVersion(), this.graphState.graphGenerationState.fetchAvailableFileGenerationDescriptions(), this.graphState.graphGenerationState.externalFormatState.fetchExternalFormatsDescriptions(), this.modelLoaderState.fetchAvailableModelImportDescriptions(), this.sdlcState.fetchProjectVersions(), ]); } *initConflictResolutionMode() { this.setActionAlertInfo({ message: 'Failed to update workspace.', prompt: 'You can discard all of your changes or review them, resolve all merge conflicts and fix any potential compilation issues as well as test failures', type: ActionAlertType.CAUTION, onEnter: () => this.setBlockGlobalHotkeys(true), onClose: () => this.setBlockGlobalHotkeys(false), actions: [ { label: 'Discard your changes', type: ActionAlertActionType.PROCEED_WITH_CAUTION, handler: () => { this.setActiveActivity(ACTIVITY_MODE.CONFLICT_RESOLUTION); flowResult(this.conflictResolutionState.discardConflictResolutionChanges()).catch((error) => this.applicationStore.alertUnhandledError(error)); }, }, { label: 'Resolve merge conflicts', default: true, type: ActionAlertActionType.STANDARD, }, ], }); yield Promise.all([ this.conflictResolutionState.initialize(), this.sdlcState.checkIfWorkspaceIsOutdated(), this.projectConfigurationEditorState.fetchLatestProjectStructureVersion(), this.graphState.graphGenerationState.fetchAvailableFileGenerationDescriptions(), this.graphState.graphGenerationState.externalFormatState.fetchExternalFormatsDescriptions(), this.modelLoaderState.fetchAvailableModelImportDescriptions(), this.sdlcState.fetchProjectVersions(), ]); } *buildGraph(graphEntities) { const startTime = Date.now(); let entities; let projectConfiguration; this.initState.setMessage(`Fetching entities...`); try { // fetch workspace entities and config at the same time const projectId = this.sdlcState.activeProject.projectId; const activeWorkspace = this.sdlcState.activeWorkspace; const result = (yield Promise.all([ this.sdlcServerClient.getEntities(projectId, activeWorkspace), this.sdlcServerClient.getConfiguration(projectId, activeWorkspace), ])); entities = result[0]; projectConfiguration = result[1]; this.projectConfigurationEditorState.setProjectConfiguration(ProjectConfiguration.serialization.fromJson(projectConfiguration)); // make sure we set the original project configuration to a different object this.projectConfigurationEditorState.setOriginalProjectConfiguration(ProjectConfiguration.serialization.fromJson(projectConfiguration)); this.changeDetectionState.workspaceLocalLatestRevisionState.setEntities(entities); this.applicationStore.log.info(LogEvent.create(GRAPH_MANAGER_EVENT.GRAPH_ENTITIES_FETCHED), Date.now() - startTime, 'ms'); } catch { return; } finally { this.initState.setMessage(undefined); } try { const result = (yield flowResult( // NOTE: if graph entities are provided, we will use that to build the graph. // We use this method as a way to fully reset the application with the entities, but we still use // the workspace entities for hashing as those are the base entities. this.graphState.buildGraph(graphEntities ?? entities))); if (result.error) { if (result.status === GraphBuilderStatus.REDIRECTED_TO_TEXT_MODE) { yield flowResult(this.changeDetectionState.workspaceLocalLatestRevisionState.buildEntityHashesIndex(entities, LogEvent.create(CHANGE_DETECTION_EVENT.CHANGE_DETECTION_LOCAL_HASHES_INDEX_BUILT))); } return; } // build explorer tree this.explorerTreeState.buildImmutableModelTrees(); this.explorerTreeState.build(); // ======= (RE)START CHANGE DETECTION ======= this.changeDetectionState.stop(); yield flowResult(this.changeDetectionState.observeGraph()); yield Promise.all([ this.changeDetectionState.preComputeGraphElementHashes(), this.changeDetectionState.workspaceLocalLatestRevisionState.buildEntityHashesIndex(entities, LogEvent.create(CHANGE_DETECTION_EVENT.CHANGE_DETECTION_LOCAL_HASHES_INDEX_BUILT)), this.sdlcState.buildWorkspaceBaseRevisionEntityHashesIndex(), this.sdlcState.buildProjectLatestRevisionEntityHashesIndex(), ]); this.changeDetectionState.start(); yield Promise.all([ this.changeDetectionState.computeAggregatedWorkspaceChanges(true), this.changeDetectionState.computeAggregatedProjectLatestChanges(true), ]); this.applicationStore.log.info(LogEvent.create(CHANGE_DETECTION_EVENT.CHANGE_DETECTION_RESTARTED), '[ASNYC]'); // ======= FINISHED (RE)START CHANGE DETECTION ======= } catch (error) { assertErrorThrown(error); this.applicationStore.log.error(LogEvent.create(GRAPH_MANAGER_EVENT.GRAPH_BUILDER_FAILURE), error); // since errors have been handled accordingly, we don't need to do anything here return; } } getCurrentEditorState(clazz) { return guaranteeType(this.currentEditorState, clazz, `Current editor state is not of the specified type (this is likely caused by calling this method at the wrong place)`); } getEditorExtensionState(clazz) { return guaranteeNonNullable(this.editorExtensionStates.find(filterByType(clazz)), `Can't find extension editor state of the specified type: no built extension editor state available from plugins`); } setGraphEditMode(graphEditor) { this.graphEditMode = graphEditor; this.graphState.clearCompilationError(); } setActiveActivity(activity, options) { if (!this.sideBarDisplayState.isOpen) { this.sideBarDisplayState.open(); } else if (activity === this.activeActivity && !options?.keepShowingIfMatchedCurrent) { this.sideBarDisplayState.close(); } this.activeActivity = activity; } closeState(editorState) { const elementIndex = this.openedEditorStates.findIndex((e) => e === editorState); assertTrue(elementIndex !== -1, `Can't close a tab which is not opened`); this.openedEditorStates.splice(elementIndex, 1); if (this.currentEditorState === editorState) { if (this.openedEditorStates.length) { const openIndex = elementIndex - 1; this.setCurrentEditorState(openIndex >= 0 ? this.openedEditorStates[openIndex] : this.openedEditorStates[0]); } else { this.setCurrentEditorState(undefined); } } this.explorerTreeState.reprocess(); } closeAllOtherStates(editorState) { assertNonNullable(this.openedEditorStates.find((e) => e === editorState), 'Editor tab should be currently opened'); this.currentEditorState = editorState; this.openedEditorStates = [editorState]; this.explorerTreeState.reprocess(); } closeAllStates() { this.closeAllEditorTabs(); this.explorerTreeState.reprocess(); } openState(editorState) { if (editorState instanceof ElementEditorState) { this.openElement(editorState.element); } else if (editorState instanceof EntityDiffViewState) { this.openEntityDiff(editorState); } else if (editorState instanceof EntityChangeConflictEditorState) { this.openEntityChangeConflict(editorState); } else if (editorState instanceof FileGenerationViewerState) { this.openGeneratedFile(editorState.generatedFile); } else if (editorState === this.modelLoaderState) { this.openSingletonEditorState(this.modelLoaderState); } else if (editorState === this.projectConfigurationEditorState) { this.openSingletonEditorState(this.projectConfigurationEditorState); } else { throw new UnsupportedOperationError(`Can't open editor state`, editorState); } this.explorerTreeState.reprocess(); } openEntityDiff(entityDiffEditorState) { const existingEditorState = this.openedEditorStates.find((editorState) => editorState instanceof EntityDiffViewState && editorState.fromEntityPath === entityDiffEditorState.fromEntityPath && editorState.toEntityPath === entityDiffEditorState.toEntityPath && editorState.fromRevision === entityDiffEditorState.fromRevision && editorState.toRevision === entityDiffEditorState.toRevision); const diffEditorState = existingEditorState ?? entityDiffEditorState; if (!existingEditorState) { this.openedEditorStates.push(diffEditorState); } this.setCurrentEditorState(diffEditorState); } openEntityChangeConflict(entityChangeConflictEditorState) { const existingEditorState = this.openedEditorStates.find((editorState) => editorState instanceof EntityChangeConflictEditorState && editorState.entityPath === entityChangeConflictEditorState.entityPath); const conflictEditorState = existingEditorState ?? entityChangeConflictEditorState; if (!existingEditorState) { this.openedEditorStates.push(conflictEditorState); } this.setCurrentEditorState(conflictEditorState); } /** * This method helps open editor that only exists one instance at at time such as model-loader, project config, settings ... */ openSingletonEditorState(singularEditorState) { const existingEditorState = this.openedEditorStates.find((e) => e === singularEditorState); const editorState = existingEditorState ?? singularEditorState; if (!existingEditorState) { this.openedEditorStates.push(editorState); } this.setCurrentEditorState(editorState); } createElementState(element) { if (element instanceof PrimitiveType) { throw new UnsupportedOperationError(`Can't create element state for primitive type`); } else if (element instanceof Class) { return new ClassEditorState(this, element); } else if (element instanceof Association || element instanceof Enumeration || element instanceof Profile) { return new UMLEditorState(this, element); } else if (element instanceof ConcreteFunctionDefinition) { return new FunctionEditorState(this, element); } else if (element instanceof Measure || element instanceof Database || element instanceof FlatData) { return new UnsupportedElementEditorState(this, element); } else if (element instanceof PackageableRuntime) { return new PackageableRuntimeEditorState(this, element); } else if (element instanceof PackageableConnection) { return new PackageableConnectionEditorState(this, element); } else if (element instanceof Mapping) { return new MappingEditorState(this, element); } else if (element instanceof Service) { return new ServiceEditorState(this, element); } else if (element instanceof GenerationSpecification) { return new GenerationSpecificationEditorState(this, element); } else if (element instanceof FileGenerationSpecification) { return new FileGenerationEditorState(this, element); } else if (element instanceof DataElement) { return new PackageableDataEditorState(this, element); } const extraElementEditorStateCreators = this.pluginManager .getApplicationPlugins() .flatMap((plugin) => plugin.getExtraElementEditorStateCreators?.() ?? []); for (const creator of extraElementEditorStateCreators) { const elementEditorState = creator(this, element); if (elementEditorState) { return elementEditorState; } } throw new UnsupportedOperationError(`Can't create editor state for element: no compatible editor state creator available from plugins`, element); } openElement(element) { if (this.isInGrammarTextMode) { // in text mode, we want to select the block of code that corresponds to the element if possible // the cheap way to do this is to search by element label text, e.g. `Mapping some::package::someMapping` this.grammarTextEditorState.setCurrentElementLabelRegexString(element); } else { if (!(element instanceof Package)) { const existingElementState = this.openedEditorStates.find((state) => state instanceof ElementEditorState && state.element === element); const elementState = existingElementState ?? this.createElementState(element); if (elementState && !existingElementState) { this.openedEditorStates.push(elementState); } this.setCurrentEditorState(elementState); } // expand tree node this.explorerTreeState.openNode(element); } } *addElement(element, packagePath, openAfterCreate) { graph_addElement(this.graphManagerState.graph, element, packagePath, this.changeDetectionState.observerContext); this.explorerTreeState.reprocess(); if (openAfterCreate) { this.openElement(element); } } *deleteElement(element) { if (this.graphState.checkIfApplicationUpdateOperationIsRunning() || this.graphManagerState.isElementReadOnly(element)) { return; } const generatedChildrenElements = (this.graphState.graphGenerationState.generatedEntities.get(element.path) ?? []) .map((genChildEntity) => this.graphManagerState.graph.generationModel.allOwnElements.find((genElement) => genElement.path === genChildEntity.path)) .filter(isNonNullable); const elementsToDelete = [element, ...generatedChildrenElements]; this.openedEditorStates = this.openedEditorStates.filter((elementState) => { if (elementState instanceof ElementEditorState) { if (elementState === this.currentEditorState) { // avoid closing the current editor state as this will be taken care of // by the `closeState()` call later return true; } return !generatedChildrenElements.includes(elementState.element); } return true; }); if (this.currentEditorState && this.currentEditorState instanceof ElementEditorState && elementsToDelete.includes(this.currentEditorState.element)) { this.closeState(this.currentEditorState); } // remove/retire the element's generated children before remove the element itself generatedChildrenElements.forEach((el) => graph_deleteOwnElement(this.graphManagerState.graph.generationModel, el)); graph_deleteElement(this.graphManagerState.graph, element); const extraElementEditorPostDeleteActions = this.pluginManager .getApplicationPlugins() .flatMap((plugin) => plugin.getExtraElementEditorPostDeleteActions?.() ?? []); for (const postDeleteAction of extraElementEditorPostDeleteActions) { postDeleteAction(this, element); } // reprocess project explorer tree this.explorerTreeState.reprocess(); // recompile yield flowResult(this.graphState.globalCompileInFormMode({ message: `Can't compile graph after deletion and error cannot be located in form mode. Redirected to text mode for debugging`, })); } *renameElement(element, newPath) { if (this.graphManagerState.isElementReadOnly(element)) { return; } graph_renameElement(this.graphManagerState.graph, element, newPath, this.changeDetectionState.observerContext); const extraElementEditorPostRenameActions = this.pluginManager .getApplicationPlugins() .flatMap((plugin) => plugin.getExtraElementEditorPostRenameActions?.() ?? []); for (const postRenameAction of extraElementEditorPostRenameActions) { postRenameAction(this, element); } // reprocess project explorer tree this.explorerTreeState.reprocess(); if (element instanceof Package) { this.explorerTreeState.openNode(element); } else if (element.package) { this.explorerTreeState.openNode(element.package); } // recompile yield flowResult(this.graphState.globalCompileInFormMode({ message: `Can't compile graph after renaming and error cannot be located in form mode. Redirected to text mode for debugging`, })); } // TODO: to be removed when we process editor states properly reprocessElementEditorState = (editorState) => { if (editorState instanceof ElementEditorState) { const correspondingElement = this.graphManagerState.graph.getNullableElement(editorState.element.path); if (correspondingElement) { return editorState.reprocess(correspondingElement, this); } } // No need to reprocess generated file state as it has no reference to any of the graphs if (editorState instanceof FileGenerationViewerState) { return editorState; } return undefined; }; // TODO: to be removed when we process editor states properly findCurrentEditorState = (editor) => { if (editor instanceof ElementEditorState) { return this.openedEditorStates.find((es) => es instanceof ElementEditorState && es.element.path === editor.element.path); } if (editor instanceof FileGenerationViewerState) { return this.openedEditorStates.find((e) => e === editor); } return undefined; }; openGeneratedFile(file) { const existingGeneratedFileState = this.openedEditorStates.find((editorState) => editorState instanceof FileGenerationViewerState && editorState.generatedFile === file); const generatedFileState = existingGeneratedFileState ?? new FileGenerationViewerState(this, file); if (!existingGeneratedFileState) { this.openedEditorStates.push(generatedFileState); } this.setCurrentEditorState(generatedFileState); } createGlobalHotKeyAction = (handler, preventDefault = true) => (event) => { if (preventDefault) { event?.preventDefault(); } // TODO: maybe we should come up with a better way to block global hot keys, this seems highly restrictive. const isResolvingConflicts = this.isInConflictResolutionMode && !this.conflictResolutionState.hasResolvedAllConflicts; if ((this.isInitialized && !isResolvingConflicts && !this.blockGlobalHotkeys) || this.isInViewerMode) { handler(event); } }; closeAllEditorTabs() { this.setCurrentEditorState(undefined); this.openedEditorStates = []; } *toggleTextMode() { if (this.isInFormMode) { if (this.graphState.checkIfApplicationUpdateOperationIsRunning()) { return; } this.setBlockingAlert({ message: 'Switching to text mode...', showLoading: true, }); try { const graphGrammar = (yield this.graphManagerState.graphManager.graphToPureCode(this.graphManagerState.graph)); yield flowResult(this.grammarTextEditorState.setGraphGrammarText(graphGrammar)); } catch (error) { assertErrorThrown(error); this.applicationStore.notifyWarning(`Can't enter text mode: transformation to grammar text failed. Error: ${error.message}`); this.setBlockingAlert(undefined); return; } this.setBlockingAlert(undefined); this.setGraphEditMode(GRAPH_EDITOR_MODE.GRAMMAR_TEXT); // navigate to the currently opened element immediately after entering text mode editor if (this.currentEditorState instanceof ElementEditorState) { this.grammarTextEditorState.setCurrentElementLabelRegexString(this.currentEditorState.element); } } else if (this.isInGrammarTextMode) { yield flowResult(this.graphState.leaveTextMode()); } else { throw new UnsupportedOperationError('Editor only support form mode and text mode at the moment'); } } get enumerationOptions() { return this.graphManagerState.graph.ownEnumerations .concat(this.graphManagerState.graph.dependencyManager.enumerations) .map(buildElementOption); } get classOptions() { return this.graphManagerState.graph.ownClasses .concat(this.graphManagerState.filterSystemElementOptions(this.graphManagerState.graph.systemModel.ownClasses)) .concat(this.graphManagerState.graph.dependencyManager.classes) .map(buildElementOption); } get associationOptions() { return this.graphManagerState.graph.ownAssociations .concat(this.graphManagerState.filterSystemElementOptions(this.graphManagerState.graph.systemModel.ownAssociations)) .concat(this.graphManagerState.graph.dependencyManager.associations) .map(buildElementOption); } get profileOptions() { return this.graphManagerState.graph.ownProfiles .concat(this.graphManagerState.filterSystemElementOptions(this.graphManagerState.graph.systemModel.ownProfiles)) .concat(this.graphManagerState.graph.dependencyManager.profiles) .map(buildElementOption); } get classPropertyGenericTypeOptions() { return this.graphManagerState.graph.primitiveTypes .filter((p) => p.path !== PRIMITIVE_TYPE.LATESTDATE) .map(buildElementOption) .concat(this.graphManagerState.graph.ownTypes .concat(this.graphManagerState.filterSystemElementOptions(this.graphManagerState.graph.systemModel.ownTypes)) .concat(this.graphManagerState.graph.dependencyManager.types) .map(buildElementOption)); } get mappingOptions() { return this.graphManagerState.graph.ownMappings .concat(this.graphManagerState.graph.dependencyManager.mappings) .map(buildElementOption); } get runtimeOptions() { return this.graphManagerState.graph.ownRuntimes .concat(this.graphManagerState.graph.dependencyManager.runtimes) .map(buildElementOption); } get serviceOptions() { return this.graphManagerState.graph.ownServices .concat(this.graphManagerState.graph.dependencyManager.services) .map(buildElementOption); } get storeOptions() { return this.graphManagerState.graph.ownStores .concat(this.graphManagerState.graph.dependencyManager.stores) .map(buildElementOption); } get dataOptions() { return this.graphManagerState.graph.ownDataElements .concat(this.graphManagerState.graph.dependencyManager.dataElements) .map(buildElementOption); } getSupportedElementTypes() { return [ PACKAGEABLE_ELEMENT_TYPE.CLASS, PACKAGEABLE_ELEMENT_TYPE.ENUMERATION, PACKAGEABLE_ELEMENT_TYPE.PROFILE, PACKAGEABLE_ELEMENT_TYPE.ASSOCIATION, PACKAGEABLE_ELEMENT_TYPE.FUNCTION, PACKAGEABLE_ELEMENT_TYPE.MEASURE, PACKAGEABLE_ELEMENT_TYPE.MAPPING, PACKAGEABLE_ELEMENT_TYPE.RUNTIME, PACKAGEABLE_ELEMENT_TYPE.CONNECTION, PACKAGEABLE_ELEMENT_TYPE.SERVICE, PACKAGEABLE_ELEMENT_TYPE.GENERATION_SPECIFICATION, PACKAGEABLE_ELEMENT_TYPE.FILE_GENERATION, PACKAGEABLE_ELEMENT_