// ***************************************************************************** // Copyright (C) 2025 1C-Soft LLC and others. // // This program and the accompanying materials are made available under the // terms of the Eclipse Public License v. 2.0 which is available at // http://www.eclipse.org/legal/epl-2.0. // // This Source Code may also be made available under the following Secondary // Licenses when the conditions for such availability set forth in the Eclipse // Public License v. 2.0 are satisfied: GNU General Public License, version 2 // with the GNU Classpath Exception which is available at // https://www.gnu.org/software/classpath/license.html. // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ // copied and modified from https://github.com/microsoft/vscode/blob/1.96.3/src/vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel.ts, // https://github.com/microsoft/vscode/blob/1.96.3/src/vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; import { ArrayUtils, Disposable, DisposableCollection, nls } from '@theia/core'; import { Autorun, DerivedObservable, Observable, ObservableUtils, SettableObservable } from '@theia/core/lib/common/observable'; import { DiffComputer } from '@theia/core/lib/common/diff'; import { Range } from '@theia/core/shared/vscode-languageserver-protocol'; import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor'; import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model'; import { MonacoToProtocolConverter } from '@theia/monaco/lib/browser/monaco-to-protocol-converter'; import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices'; import { IUndoRedoService, UndoRedoElementType } from '@theia/monaco-editor-core/esm/vs/platform/undoRedo/common/undoRedo'; import { MergeRange, MergeRangeAcceptedState, MergeRangeResultState, MergeSide } from './merge-range'; import { DetailedLineRangeMapping, DocumentLineRangeMap, DocumentRangeMap, LineRangeMapping, RangeMapping } from './range-mapping'; import { LiveDiff, LiveDiffState } from './live-diff'; import { LineRange } from './line-range'; import { LineRangeEdit } from './range-editing'; import { RangeUtils } from './range-utils'; export const MergeEditorModelProps = Symbol('MergeEditorModelProps'); export interface MergeEditorModelProps { readonly baseEditor: MonacoEditor; readonly side1Editor: MonacoEditor; readonly side2Editor: MonacoEditor; readonly resultEditor: MonacoEditor; readonly options?: { readonly resetResult?: boolean; } } @injectable() export class MergeEditorModel implements Disposable { @inject(MergeEditorModelProps) protected readonly props: MergeEditorModelProps; @inject(DiffComputer) protected readonly diffComputer: DiffComputer; @inject(MonacoToProtocolConverter) private readonly m2p: MonacoToProtocolConverter; protected readonly toDispose = new DisposableCollection(); protected side1LiveDiff: LiveDiff; protected side2LiveDiff: LiveDiff; protected resultLiveDiff: LiveDiff; protected shouldRecomputeHandledState = true; protected readonly mergeRangesObservable = DerivedObservable.create(() => this.computeMergeRanges()); get mergeRanges(): readonly MergeRange[] { return this.mergeRangesObservable.get(); } protected readonly mergeRangesDataObservable = DerivedObservable.create(() => new Map( this.mergeRanges.map(mergeRange => [mergeRange, this.newMergeRangeData()]) )); // #region Line Range Mapping protected readonly side1ToResultLineRangeMapObservable = DerivedObservable.create(() => this.newDocumentLineRangeMap( this.computeSideToResultDiff(this.side1Changes, this.resultChanges) )); get side1ToResultLineRangeMap(): DocumentLineRangeMap { return this.side1ToResultLineRangeMapObservable.get(); } protected readonly resultToSide1LineRangeMapObservable = DerivedObservable.create(() => this.side1ToResultLineRangeMap.reverse()); get resultToSide1LineRangeMap(): DocumentLineRangeMap { return this.resultToSide1LineRangeMapObservable.get(); } protected readonly side2ToResultLineRangeMapObservable = DerivedObservable.create(() => this.newDocumentLineRangeMap( this.computeSideToResultDiff(this.side2Changes, this.resultChanges) )); get side2ToResultLineRangeMap(): DocumentLineRangeMap { return this.side2ToResultLineRangeMapObservable.get(); } protected readonly resultToSide2LineRangeMapObservable = DerivedObservable.create(() => this.side2ToResultLineRangeMap.reverse()); get resultToSide2LineRangeMap(): DocumentLineRangeMap { return this.resultToSide2LineRangeMapObservable.get(); } protected readonly baseToSide1LineRangeMapObservable = DerivedObservable.create(() => this.newDocumentLineRangeMap(this.side1Changes)); get baseToSide1LineRangeMap(): DocumentLineRangeMap { return this.baseToSide1LineRangeMapObservable.get(); } protected readonly side1ToBaseLineRangeMapObservable = DerivedObservable.create(() => this.baseToSide1LineRangeMap.reverse()); get side1ToBaseLineRangeMap(): DocumentLineRangeMap { return this.side1ToBaseLineRangeMapObservable.get(); } protected readonly baseToSide2LineRangeMapObservable = DerivedObservable.create(() => this.newDocumentLineRangeMap(this.side2Changes)); get baseToSide2LineRangeMap(): DocumentLineRangeMap { return this.baseToSide2LineRangeMapObservable.get(); } protected readonly side2ToBaseLineRangeMapObservable = DerivedObservable.create(() => this.baseToSide2LineRangeMap.reverse()); get side2ToBaseLineRangeMap(): DocumentLineRangeMap { return this.side2ToBaseLineRangeMapObservable.get(); } protected readonly baseToResultLineRangeMapObservable = DerivedObservable.create(() => this.newDocumentLineRangeMap(this.resultChanges)); get baseToResultLineRangeMap(): DocumentLineRangeMap { return this.baseToResultLineRangeMapObservable.get(); } protected readonly resultToBaseLineRangeMapObservable = DerivedObservable.create(() => this.baseToResultLineRangeMap.reverse()); get resultToBaseLineRangeMap(): DocumentLineRangeMap { return this.resultToBaseLineRangeMapObservable.get(); } // #endregion // #region Range Mapping protected readonly baseToSide1RangeMapObservable = DerivedObservable.create(() => this.newDocumentRangeMap( this.side1Changes.flatMap(change => change.rangeMappings) )); get baseToSide1RangeMap(): DocumentRangeMap { return this.baseToSide1RangeMapObservable.get(); } protected readonly side1ToBaseRangeMapObservable = DerivedObservable.create(() => this.baseToSide1RangeMap.reverse()); get side1ToBaseRangeMap(): DocumentRangeMap { return this.side1ToBaseRangeMapObservable.get(); } protected readonly baseToSide2RangeMapObservable = DerivedObservable.create(() => this.newDocumentRangeMap( this.side2Changes.flatMap(change => change.rangeMappings) )); get baseToSide2RangeMap(): DocumentRangeMap { return this.baseToSide2RangeMapObservable.get(); } protected readonly side2ToBaseRangeMapObservable = DerivedObservable.create(() => this.baseToSide2RangeMap.reverse()); get side2ToBaseRangeMap(): DocumentRangeMap { return this.side2ToBaseRangeMapObservable.get(); } protected readonly baseToResultRangeMapObservable = DerivedObservable.create(() => this.newDocumentRangeMap( this.resultChanges.flatMap(change => change.rangeMappings) )); get baseToResultRangeMap(): DocumentRangeMap { return this.baseToResultRangeMapObservable.get(); } protected readonly resultToBaseRangeMapObservable = DerivedObservable.create(() => this.baseToResultRangeMap.reverse()); get resultToBaseRangeMap(): DocumentRangeMap { return this.resultToBaseRangeMapObservable.get(); } // #endregion protected readonly diffComputingStateObservable = DerivedObservable.create(() => this.getDiffComputingState(this.side1LiveDiff, this.side2LiveDiff, this.resultLiveDiff)); protected readonly diffComputingStateForSidesObservable = DerivedObservable.create(() => this.getDiffComputingState(this.side1LiveDiff, this.side2LiveDiff)); readonly isUpToDateObservable = DerivedObservable.create(() => this.diffComputingStateObservable.get() === DiffComputingState.UpToDate); protected readonly unhandledMergeRangesCountObservable = DerivedObservable.create(() => { let result = 0; const mergeRangesData = this.mergeRangesDataObservable.get(); for (const mergeRangeData of mergeRangesData.values()) { if (!mergeRangeData.isHandledObservable.get()) { result++; } } return result; }); get unhandledMergeRangesCount(): number { return this.unhandledMergeRangesCountObservable.get(); } protected _onInitialized: Promise; get onInitialized(): Promise { return this._onInitialized; } get baseDocument(): MonacoEditorModel { return this.props.baseEditor.document; } get side1Document(): MonacoEditorModel { return this.props.side1Editor.document; } get side2Document(): MonacoEditorModel { return this.props.side2Editor.document; } get resultDocument(): MonacoEditorModel { return this.props.resultEditor.document; } protected get resultEditor(): MonacoEditor { return this.props.resultEditor; } get side1Changes(): readonly DetailedLineRangeMapping[] { return this.side1LiveDiff.changes; } get side2Changes(): readonly DetailedLineRangeMapping[] { return this.side2LiveDiff.changes; } get resultChanges(): readonly DetailedLineRangeMapping[] { return this.resultLiveDiff.changes; } @postConstruct() protected init(): void { this.toDispose.push(this.side1LiveDiff = this.newLiveDiff(this.baseDocument, this.side1Document)); this.toDispose.push(this.side2LiveDiff = this.newLiveDiff(this.baseDocument, this.side2Document)); this.toDispose.push(this.resultLiveDiff = this.newLiveDiff(this.baseDocument, this.resultDocument)); this.toDispose.push(Observable.keepObserved(this.mergeRangesDataObservable)); this.toDispose.push(Observable.keepObserved(this.side1ToResultLineRangeMapObservable)); this.toDispose.push(Observable.keepObserved(this.resultToSide1LineRangeMapObservable)); this.toDispose.push(Observable.keepObserved(this.side2ToResultLineRangeMapObservable)); this.toDispose.push(Observable.keepObserved(this.resultToSide2LineRangeMapObservable)); this.toDispose.push(Observable.keepObserved(this.baseToSide1LineRangeMapObservable)); this.toDispose.push(Observable.keepObserved(this.side1ToBaseLineRangeMapObservable)); this.toDispose.push(Observable.keepObserved(this.baseToSide2LineRangeMapObservable)); this.toDispose.push(Observable.keepObserved(this.side2ToBaseLineRangeMapObservable)); this.toDispose.push(Observable.keepObserved(this.baseToResultLineRangeMapObservable)); this.toDispose.push(Observable.keepObserved(this.resultToBaseLineRangeMapObservable)); this.toDispose.push(Observable.keepObserved(this.baseToSide1RangeMapObservable)); this.toDispose.push(Observable.keepObserved(this.side1ToBaseRangeMapObservable)); this.toDispose.push(Observable.keepObserved(this.baseToSide2RangeMapObservable)); this.toDispose.push(Observable.keepObserved(this.side2ToBaseRangeMapObservable)); this.toDispose.push(Observable.keepObserved(this.baseToResultRangeMapObservable)); this.toDispose.push(Observable.keepObserved(this.resultToBaseRangeMapObservable)); const initializePromise = this.doInit(); this._onInitialized = ObservableUtils.waitForState(this.isUpToDateObservable).then(() => initializePromise); initializePromise.then(() => { this.toDispose.push(Autorun.create(() => { if (!this.isUpToDateObservable.get()) { return; } Observable.update(() => { const mergeRangesData = this.mergeRangesDataObservable.get(); for (const [mergeRange, mergeRangeData] of mergeRangesData) { const state = this.computeMergeRangeStateFromResult(mergeRange); mergeRangeData.resultStateObservable.set(state); if (this.shouldRecomputeHandledState) { mergeRangeData.isHandledObservable.set(state !== 'Base'); } } this.shouldRecomputeHandledState = false; }); }, { willHandleChange: ctx => { if (ctx.isChangeOf(this.mergeRangesDataObservable)) { this.shouldRecomputeHandledState = true; } return true; } })); const attachedHistory = new AttachedHistory(this.resultDocument); this.toDispose.push(attachedHistory); this.toDispose.push(this.resultDocument.textEditorModel.onDidChangeContent(event => { if (event.isRedoing || event.isUndoing) { return; } // Mark merge ranges affected by content changes as handled. const mergeRanges: MergeRange[] = []; for (const change of event.changes) { const changeBaseRange = this.translateResultRangeToBase(this.m2p.asRange(change.range)); const affectedMergeRanges = this.mergeRanges.filter(mergeRange => RangeUtils.touches(mergeRange.baseRange.toRange(), changeBaseRange) ); for (const mergeRange of affectedMergeRanges) { if (!this.isMergeRangeHandled(mergeRange)) { mergeRanges.push(mergeRange); } } } if (mergeRanges.length === 0) { return; } const markMergeRangesAsHandled = (handled: boolean) => { Observable.update(() => { const mergeRangesData = this.mergeRangesDataObservable.get(); for (const mergeRange of mergeRanges) { const mergeRangeData = mergeRangesData.get(mergeRange); if (mergeRangeData) { mergeRangeData.isHandledObservable.set(handled); } } }); }; const element: IAttachedHistoryElement = { redo: () => { markMergeRangesAsHandled(true); }, undo: () => { markMergeRangesAsHandled(false); } }; attachedHistory.pushAttachedHistoryElement(element); element.redo(); })); }); } protected computeMergeRangeStateFromResult(mergeRange: MergeRange): MergeRangeResultState { const { originalRange: baseRange, modifiedRange: resultRange } = this.getResultLineRangeMapping(mergeRange); if (!mergeRange.baseRange.equals(baseRange)) { return 'Unrecognized'; } const existingLines = resultRange.getLines(this.resultDocument); const states: MergeRangeAcceptedState[] = [ 'Base', 'Side1', 'Side2', 'Side1Side2Smart', 'Side2Side1Smart', 'Side1Side2', 'Side2Side1' ]; for (const state of states) { const edit = mergeRange.getBaseRangeEdit(state); if (ArrayUtils.equals(edit.newLines, existingLines)) { return state; } } return 'Unrecognized'; } protected async doInit(): Promise { if (this.props.options?.resetResult) { await this.reset(); } } dispose(): void { this.toDispose.dispose(); } isDisposed(): boolean { return this.toDispose.disposed; } async reset(): Promise { await ObservableUtils.waitForState(this.diffComputingStateForSidesObservable, state => state === DiffComputingState.UpToDate); this.shouldRecomputeHandledState = true; this.resultDocument.textEditorModel.setValue(this.computeAutoMergedResult()); } protected computeAutoMergedResult(): string { const baseLines = this.baseDocument.textEditorModel.getLinesContent(); const side1Lines = this.side1Document.textEditorModel.getLinesContent(); const side2Lines = this.side2Document.textEditorModel.getLinesContent(); const resultLines: string[] = []; function appendLinesToResult(documentLines: string[], lineRange: LineRange): void { for (let i = lineRange.startLineNumber; i < lineRange.endLineNumberExclusive; i++) { resultLines.push(documentLines[i]); } } let baseStartLineNumber = 0; for (const mergeRange of this.mergeRanges) { appendLinesToResult(baseLines, LineRange.fromLineNumbers(baseStartLineNumber, mergeRange.baseRange.startLineNumber)); if (mergeRange.side1Changes.length === 0) { appendLinesToResult(side2Lines, mergeRange.side2Range); } else if (mergeRange.side2Changes.length === 0) { appendLinesToResult(side1Lines, mergeRange.side1Range); } else if (mergeRange.isEqualChange) { appendLinesToResult(side1Lines, mergeRange.side1Range); } else { appendLinesToResult(baseLines, mergeRange.baseRange); } baseStartLineNumber = mergeRange.baseRange.endLineNumberExclusive; } appendLinesToResult(baseLines, LineRange.fromLineNumbers(baseStartLineNumber, baseLines.length)); return resultLines.join(this.resultDocument.textEditorModel.getEOL()); } protected computeMergeRanges(): MergeRange[] { return MergeRange.computeMergeRanges(this.side1Changes, this.side2Changes, this.baseDocument, this.side1Document, this.side2Document); } hasMergeRange(mergeRange: MergeRange): boolean { return this.mergeRangesDataObservable.get().has(mergeRange); } protected getMergeRangeData(mergeRange: MergeRange): MergeRangeData { const mergeRangeData = this.mergeRangesDataObservable.get().get(mergeRange); if (!mergeRangeData) { throw new Error('Unknown merge range'); } return mergeRangeData; } getMergeRangeResultState(mergeRange: MergeRange): MergeRangeResultState { return this.getMergeRangeData(mergeRange).resultStateObservable.get(); } applyMergeRangeAcceptedState(mergeRange: MergeRange, state: MergeRangeAcceptedState): void { if (!this.isUpToDateObservable.get()) { throw new Error('Cannot apply merge range accepted state while updating'); } if (state !== 'Base' && this.getMergeRangeResultState(mergeRange) === 'Unrecognized') { throw new Error('Cannot apply merge range accepted state to an unrecognized result state'); } const { originalRange: baseRange, modifiedRange: resultRange } = this.getResultLineRangeMapping(mergeRange); let newLines: string[]; if (state === 'Base') { newLines = baseRange.getLines(this.baseDocument); } else { if (!baseRange.equals(mergeRange.baseRange)) { throw new Error('Assertion error'); } newLines = mergeRange.getBaseRangeEdit(state).newLines; } const resultEdit = new LineRangeEdit(resultRange, newLines); const editOperation = resultEdit.toRangeEdit(this.resultDocument.lineCount).toMonacoEdit(); const cursorState = this.resultEditor.getControl().getSelections(); this.resultDocument.textEditorModel.pushStackElement(); this.resultDocument.textEditorModel.pushEditOperations(cursorState, [editOperation], () => cursorState); this.resultDocument.textEditorModel.pushStackElement(); } isMergeRangeHandled(mergeRange: MergeRange): boolean { return this.getMergeRangeData(mergeRange).isHandledObservable.get(); } markMergeRangeAsHandled(mergeRange: MergeRange, options?: { undoRedo?: false | { callback?: { didUndo(): void; didRedo(): void; } } }): void { const mergeRangeData = this.getMergeRangeData(mergeRange); if (mergeRangeData.isHandledObservable.get()) { return; } mergeRangeData.isHandledObservable.set(true); if (options?.undoRedo === false) { return; } const undoRedoCallback = options?.undoRedo?.callback; const modelRef = new WeakRef(this); const dataRef = new WeakRef(mergeRangeData); StandaloneServices.get(IUndoRedoService).pushElement({ type: UndoRedoElementType.Resource, resource: this.resultDocument.textEditorModel.uri, label: nls.localizeByDefault('Undo Mark As Handled'), code: 'markMergeRangeAsHandled', undo(): void { const model = modelRef.deref(); const data = dataRef.deref(); if (model && !model.isDisposed() && data) { data.isHandledObservable.set(false); undoRedoCallback?.didUndo(); } }, redo(): void { const model = modelRef.deref(); const data = dataRef.deref(); if (model && !model.isDisposed() && data) { data.isHandledObservable.set(true); undoRedoCallback?.didRedo(); } } }); } getLineRangeInResult(mergeRange: MergeRange): LineRange { return this.getResultLineRangeMapping(mergeRange).modifiedRange; } protected getResultLineRangeMapping(mergeRange: MergeRange): LineRangeMapping { const projectLine = (lineNumber: number): number | LineRangeMapping => { let offset = 0; const changes = this.resultChanges; for (const change of changes) { const { originalRange } = change; if (originalRange.containsLine(lineNumber) || originalRange.endLineNumberExclusive === lineNumber) { return change; } else if (originalRange.endLineNumberExclusive < lineNumber) { offset = change.modifiedRange.endLineNumberExclusive - originalRange.endLineNumberExclusive; } else { break; } } return lineNumber + offset; }; let startBase = mergeRange.baseRange.startLineNumber; let startResult = projectLine(startBase); if (typeof startResult !== 'number') { startBase = startResult.originalRange.startLineNumber; startResult = startResult.modifiedRange.startLineNumber; } let endExclusiveBase = mergeRange.baseRange.endLineNumberExclusive; let endExclusiveResult = projectLine(endExclusiveBase); if (typeof endExclusiveResult !== 'number') { endExclusiveBase = endExclusiveResult.originalRange.endLineNumberExclusive; endExclusiveResult = endExclusiveResult.modifiedRange.endLineNumberExclusive; } return new LineRangeMapping(LineRange.fromLineNumbers(startBase, endExclusiveBase), LineRange.fromLineNumbers(startResult, endExclusiveResult)); } translateBaseRangeToSide(range: Range, side: MergeSide): Range { const rangeMap = side === 1 ? this.baseToSide1RangeMap : this.baseToSide2RangeMap; return rangeMap.projectRange(range).modifiedRange; } translateSideRangeToBase(range: Range, side: MergeSide): Range { const rangeMap = side === 1 ? this.side1ToBaseRangeMap : this.side2ToBaseRangeMap; return rangeMap.projectRange(range).modifiedRange; } translateBaseRangeToResult(range: Range): Range { return this.baseToResultRangeMap.projectRange(range).modifiedRange; } translateResultRangeToBase(range: Range): Range { return this.resultToBaseRangeMap.projectRange(range).modifiedRange; } protected computeSideToResultDiff(sideChanges: readonly LineRangeMapping[], resultChanges: readonly LineRangeMapping[]): readonly LineRangeMapping[] { return DocumentLineRangeMap.betweenModifiedSides(sideChanges, resultChanges).lineRangeMappings; } protected newMergeRangeData(): MergeRangeData { return new MergeRangeData(); } protected newLiveDiff(originalDocument: MonacoEditorModel, modifiedDocument: MonacoEditorModel): LiveDiff { return new LiveDiff(originalDocument, modifiedDocument, this.diffComputer); } protected newDocumentLineRangeMap(lineRangeMappings: readonly LineRangeMapping[]): DocumentLineRangeMap { return new DocumentLineRangeMap(lineRangeMappings); } protected newDocumentRangeMap(rangeMappings: readonly RangeMapping[]): DocumentRangeMap { return new DocumentRangeMap(rangeMappings); } protected getDiffComputingState(...liveDiffs: LiveDiff[]): DiffComputingState { const liveDiffStates = liveDiffs.map(liveDiff => liveDiff.state); if (liveDiffStates.some(state => state === LiveDiffState.Initializing)) { return DiffComputingState.Initializing; } if (liveDiffStates.some(state => state === LiveDiffState.Updating)) { return DiffComputingState.Updating; } return DiffComputingState.UpToDate; } } export const enum DiffComputingState { Initializing, UpToDate, Updating } export class MergeRangeData { readonly resultStateObservable = SettableObservable.create('Base'); readonly isHandledObservable = SettableObservable.create(false); } class AttachedHistory implements Disposable { private readonly toDispose = new DisposableCollection(); private readonly attachedHistory: { element: IAttachedHistoryElement; altId: number }[] = []; constructor(private readonly model: MonacoEditorModel) { let previousAltId = this.model.textEditorModel.getAlternativeVersionId(); this.toDispose.push(model.textEditorModel.onDidChangeContent(event => { const currentAltId = model.textEditorModel.getAlternativeVersionId(); if (event.isRedoing) { for (const item of this.attachedHistory) { if (previousAltId < item.altId && item.altId <= currentAltId) { item.element.redo(); } } } else if (event.isUndoing) { for (let i = this.attachedHistory.length - 1; i >= 0; i--) { const item = this.attachedHistory[i]; if (currentAltId < item.altId && item.altId <= previousAltId) { item.element.undo(); } } } else { // The user destroyed the redo stack by performing a non redo/undo operation. while ( this.attachedHistory.length > 0 && this.attachedHistory[this.attachedHistory.length - 1].altId > previousAltId ) { this.attachedHistory.pop(); } } previousAltId = currentAltId; })); } dispose(): void { this.toDispose.dispose(); } pushAttachedHistoryElement(element: IAttachedHistoryElement): void { this.attachedHistory.push({ altId: this.model.textEditorModel.getAlternativeVersionId(), element }); } } interface IAttachedHistoryElement { undo(): void; redo(): void; }