@finos/legend-application-studio
Version:
Legend Studio application core
385 lines • 16.6 kB
JavaScript
/**
* Copyright (c) 2020-present, Goldman Sachs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { 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