UNPKG

@finos/legend-application-studio

Version:
385 lines 16.6 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 { observable, action, flow, computed, makeObservable, flowResult, } from 'mobx'; import { EntityDiffViewerState, } from './EntityDiffEditorState.js'; import { assertErrorThrown, UnsupportedOperationError, } from '@finos/legend-shared'; import { mergeDiff3 } from 'node-diff3'; import { extractEntityNameFromPath } from '@finos/legend-storage'; import { EntityChangeConflictResolution } from '@finos/legend-server-sdlc'; import { ParserError } from '@finos/legend-graph'; const START_HEADER_MARKER = '<<<<<<<'; const COMMON_BASE_MARKER = '|||||||'; const SPLITTER_MARKER = '======='; const END_FOOTER_MARKER = '>>>>>>>'; const scanMergeConflict = (text) => { const lines = text.split('\n'); const conflicts = []; let currentConflict = null; for (let i = 0; i < lines.length; i++) { const line = lines[i]; const lineNumber = i + 1; // ignore empty lines if (!line?.trim()) { continue; } // Is this a start line? <<<<<<< if (line.startsWith(START_HEADER_MARKER)) { if (currentConflict !== null) { // Give up parsing, anything matched up this to this point will be decorated // anything after will not break; } // Create a new conflict starting at this line currentConflict = { startHeader: lineNumber }; } else if (currentConflict && !currentConflict.splitter && line.startsWith(COMMON_BASE_MARKER)) { // Are we within a conflict block and is this a common ancestors marker? ||||||| currentConflict.commonBase = lineNumber; } else if (currentConflict && !currentConflict.splitter && line.startsWith(SPLITTER_MARKER)) { // Are we within a conflict block and is this a splitter? ======= currentConflict.splitter = lineNumber; } else if (currentConflict && line.startsWith(END_FOOTER_MARKER)) { // Are we within a conflict block and is this a footer? >>>>>>> currentConflict.endFooter = lineNumber; if (currentConflict.splitter !== undefined) { conflicts.push(currentConflict); } // Reset the current conflict to be empty, so we can match the next // starting header marker. currentConflict = null; } } return conflicts; }; export var ENTITY_CHANGE_CONFLICT_EDITOR_VIEW_MODE; (function (ENTITY_CHANGE_CONFLICT_EDITOR_VIEW_MODE) { ENTITY_CHANGE_CONFLICT_EDITOR_VIEW_MODE["MERGE_VIEW"] = "MERGE_VIEW"; ENTITY_CHANGE_CONFLICT_EDITOR_VIEW_MODE["BASE_CURRENT"] = "BASE_CURRENT"; ENTITY_CHANGE_CONFLICT_EDITOR_VIEW_MODE["BASE_INCOMING"] = "BASE_INCOMING"; ENTITY_CHANGE_CONFLICT_EDITOR_VIEW_MODE["CURRENT_INCOMING"] = "CURRENT_INCOMING"; })(ENTITY_CHANGE_CONFLICT_EDITOR_VIEW_MODE || (ENTITY_CHANGE_CONFLICT_EDITOR_VIEW_MODE = {})); export class EntityChangeConflictEditorState extends EntityDiffViewerState { entityPath; // revision baseRevision; currentChangeRevision; incomingChangeRevision; // entity baseEntity; currentChangeEntity; incomingChangeEntity; // grammar baseGrammarText; currentChangeGrammarText; incomingChangeGrammarText; // entity getter/updater function baseEntityGetter; currentChangeEntityGetter; incomingChangeEntityGetter; // editor mergedText; mergeSucceeded = true; mergeConflicts = []; isReadOnly = false; currentMergeEditorConflict; currentMergeEditorLine; mergeEditorParserError; currentMode = ENTITY_CHANGE_CONFLICT_EDITOR_VIEW_MODE.MERGE_VIEW; conflictResolutionState; constructor(editorStore, conflictResolutionState, entityPath, baseRevision, currentChangeRevision, incomingChangeRevision, baseEntity, currentChangeEntity, incomingChangeEntity, baseEntityGetter, currentChangeEntityGetter, incomingChangeEntityGetter) { super(baseRevision, currentChangeRevision, editorStore); makeObservable(this, { entityPath: observable, baseRevision: observable, currentChangeRevision: observable, incomingChangeRevision: observable, baseEntity: observable.ref, currentChangeEntity: observable.ref, incomingChangeEntity: observable.ref, baseGrammarText: observable, currentChangeGrammarText: observable, incomingChangeGrammarText: observable, baseEntityGetter: observable, currentChangeEntityGetter: observable, incomingChangeEntityGetter: observable, mergedText: observable, mergeSucceeded: observable, mergeConflicts: observable, isReadOnly: observable, currentMergeEditorConflict: observable, currentMergeEditorLine: observable, mergeEditorParserError: observable, currentMode: observable, label: computed, sortedMergedConflicts: computed, canUseTheirs: computed, canUseYours: computed, canMarkAsResolved: computed, previousConflict: computed, nextConflict: computed, setReadOnly: action, setMergedText: action, setCurrentMode: action, setCurrentMergeEditorLine: action, setCurrentMergeEditorConflict: action, clearMergeEditorError: action, refreshMergeConflict: action, resetMergeEditorStateOnLeave: action, acceptCurrentChange: action, acceptIncomingChange: action, acceptBothChanges: action, rejectBothChanges: action, refresh: flow, getMergedText: flow, markAsResolved: flow, useCurrentChanges: flow, useIncomingChanges: flow, getGrammarForEntity: flow, }); this.entityPath = entityPath; // revision this.baseRevision = baseRevision; this.currentChangeRevision = currentChangeRevision; this.incomingChangeRevision = incomingChangeRevision; // entity this.baseEntity = baseEntity; this.currentChangeEntity = currentChangeEntity; this.incomingChangeEntity = incomingChangeEntity; // entity getter/updater function this.baseEntityGetter = baseEntityGetter; this.currentChangeEntityGetter = currentChangeEntityGetter; this.incomingChangeEntityGetter = incomingChangeEntityGetter; this.conflictResolutionState = conflictResolutionState; } setReadOnly(val) { this.isReadOnly = val; } setMergedText(val) { this.mergedText = val; } setCurrentMode(mode) { this.currentMode = mode; } setCurrentMergeEditorLine(val) { this.currentMergeEditorLine = val; } setCurrentMergeEditorConflict(conflict) { this.currentMergeEditorConflict = conflict; } clearMergeEditorError() { this.mergeEditorParserError = undefined; } refreshMergeConflict() { if (this.mergedText !== undefined) { this.mergeConflicts = scanMergeConflict(this.mergedText); } } get label() { return extractEntityNameFromPath(this.entityPath); } get sortedMergedConflicts() { return this.mergeConflicts.toSorted((a, b) => a.startHeader - b.startHeader); } get canUseTheirs() { return (this.currentMode !== ENTITY_CHANGE_CONFLICT_EDITOR_VIEW_MODE.BASE_CURRENT); } get canUseYours() { return (this.currentMode !== ENTITY_CHANGE_CONFLICT_EDITOR_VIEW_MODE.BASE_INCOMING); } get canMarkAsResolved() { return Boolean(!this.mergeConflicts.length && !this.mergeEditorParserError && this.currentMode === ENTITY_CHANGE_CONFLICT_EDITOR_VIEW_MODE.MERGE_VIEW); } get previousConflict() { const currentLine = this.currentMergeEditorLine ?? 0; return this.sortedMergedConflicts .slice() .reverse() .find((conflict) => conflict.endFooter && conflict.endFooter < currentLine); } get nextConflict() { const currentLine = this.currentMergeEditorLine ?? 0; return this.sortedMergedConflicts.find((conflict) => conflict.startHeader > currentLine); } match(tab) { return (tab instanceof EntityChangeConflictEditorState && tab.entityPath === this.entityPath); } getModeComparisonViewInfo(mode) { switch (mode) { case ENTITY_CHANGE_CONFLICT_EDITOR_VIEW_MODE.MERGE_VIEW: return { label: 'Merged changes', fromRevision: this.currentChangeRevision, toRevision: this.incomingChangeRevision, }; case ENTITY_CHANGE_CONFLICT_EDITOR_VIEW_MODE.BASE_CURRENT: return { label: 'Your changes', fromGrammarText: this.baseGrammarText ?? '', toGrammarText: this.currentChangeGrammarText ?? '', fromRevision: this.baseRevision, toRevision: this.currentChangeRevision, }; case ENTITY_CHANGE_CONFLICT_EDITOR_VIEW_MODE.BASE_INCOMING: return { label: 'Their changes', fromGrammarText: this.baseGrammarText ?? '', toGrammarText: this.incomingChangeGrammarText ?? '', fromRevision: this.baseRevision, toRevision: this.incomingChangeRevision, }; case ENTITY_CHANGE_CONFLICT_EDITOR_VIEW_MODE.CURRENT_INCOMING: return { label: 'Both changes', fromGrammarText: this.currentChangeGrammarText ?? '', toGrammarText: this.incomingChangeGrammarText ?? '', fromRevision: this.currentChangeRevision, toRevision: this.incomingChangeRevision, }; default: throw new UnsupportedOperationError(); } } resetMergeEditorStateOnLeave() { this.clearMergeEditorError(); this.currentMergeEditorLine = undefined; this.currentMergeEditorConflict = undefined; } *refresh() { this.baseEntity = this.baseEntityGetter ? this.baseEntityGetter(this.entityPath) : this.baseEntity; this.currentChangeEntity = this.currentChangeEntityGetter ? this.currentChangeEntityGetter(this.entityPath) : this.currentChangeEntity; this.incomingChangeEntity = this.incomingChangeEntityGetter ? this.incomingChangeEntityGetter(this.entityPath) : this.incomingChangeEntity; if (this.isReadOnly || this.mergedText === undefined) { yield flowResult(this.getMergedText()); } } *getMergedText() { this.baseGrammarText = (yield flowResult(this.getGrammarForEntity(this.baseEntity))); this.currentChangeGrammarText = (yield flowResult(this.getGrammarForEntity(this.currentChangeEntity))); this.incomingChangeGrammarText = (yield flowResult(this.getGrammarForEntity(this.incomingChangeEntity))); const result = mergeDiff3(this.currentChangeGrammarText, this.baseGrammarText, this.incomingChangeGrammarText, { stringSeparator: '\n', label: { a: 'Your Change', o: 'BASE', b: 'Their Change' }, }); this.mergedText = result.result.join('\n'); this.refreshMergeConflict(); this.mergeSucceeded = !this.mergeConflicts.length; } *getGrammarForEntity(entity) { if (entity) { const elementGrammar = (yield this.editorStore.graphManagerState.graphManager.entitiesToPureCode([entity], { pretty: true })); return elementGrammar; } return ''; } *markAsResolved() { try { const entities = (yield this.editorStore.graphManagerState.graphManager.pureCodeToEntities(this.mergedText ?? '')); if (!entities.length) { this.conflictResolutionState.resolveConflict(new EntityChangeConflictResolution(this.entityPath, undefined)); } else if (entities.length === 1) { this.conflictResolutionState.resolveConflict(new EntityChangeConflictResolution(this.entityPath, entities[0])); } else { this.editorStore.applicationStore.notificationService.notifyWarning(`Can't mark conflict as resolved: more than one element found in parsed text`); return; } } catch (error) { assertErrorThrown(error); if (error instanceof ParserError) { this.mergeEditorParserError = error; this.editorStore.applicationStore.notificationService.notifyWarning(`Can't mark conflict as resolved. Parsing error: ${this.mergeEditorParserError.message}`); } } yield flowResult(this.conflictResolutionState.markConflictAsResolved(this)); } *useCurrentChanges() { this.conflictResolutionState.resolveConflict(new EntityChangeConflictResolution(this.entityPath, this.currentChangeEntity)); yield flowResult(this.conflictResolutionState.markConflictAsResolved(this)); } *useIncomingChanges() { this.conflictResolutionState.resolveConflict(new EntityChangeConflictResolution(this.entityPath, this.incomingChangeEntity)); yield flowResult(this.conflictResolutionState.markConflictAsResolved(this)); } acceptCurrentChange(conflict) { if (this.mergedText === undefined) { return; } const lines = this.mergedText.split('\n'); this.setMergedText(lines .slice(0, conflict.startHeader - 1) .concat(lines.slice(conflict.startHeader, (conflict.commonBase ?? conflict.splitter) - 1)) // current change .concat(lines.slice(conflict.endFooter, lines.length)) .join('\n')); this.refreshMergeConflict(); } acceptIncomingChange(conflict) { if (this.mergedText === undefined) { return; } const lines = this.mergedText.split('\n'); this.setMergedText(lines .slice(0, conflict.startHeader - 1) .concat(lines.slice(conflict.splitter, conflict.endFooter - 1)) // incoming change .concat(lines.slice(conflict.endFooter, lines.length)) .join('\n')); this.refreshMergeConflict(); } acceptBothChanges(conflict) { if (this.mergedText === undefined) { return; } const lines = this.mergedText.split('\n'); this.setMergedText(lines .slice(0, conflict.startHeader - 1) .concat(lines.slice(conflict.startHeader, (conflict.commonBase ?? conflict.splitter) - 1)) // current change .concat(lines.slice(conflict.splitter, conflict.endFooter - 1)) // incoming change .concat(lines.slice(conflict.endFooter, lines.length)) .join('\n')); this.refreshMergeConflict(); } rejectBothChanges(conflict) { if (this.mergedText === undefined) { return; } const lines = this.mergedText.split('\n'); this.setMergedText(lines .slice(0, conflict.startHeader - 1) .concat(conflict.commonBase ? lines.slice(conflict.commonBase, conflict.splitter - 1) : []) // base .concat(lines.slice(conflict.endFooter, lines.length)) .join('\n')); this.refreshMergeConflict(); } } //# sourceMappingURL=EntityChangeConflictEditorState.js.map