// ***************************************************************************** // 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/view/conflictActions.ts import { DerivedObservable, ObservableUtils } from '@theia/core/lib/common/observable'; import { MergeRange, MergeRangeAcceptedState, MergeSide } from '../model/merge-range'; import { MergeEditor } from '../merge-editor'; import { nls } from '@theia/core'; export interface MergeRangeAction { readonly text: string; readonly tooltip?: string; run?(): unknown; } export class MergeRangeActions { readonly side1ActionsObservable = DerivedObservable.create(() => this.getActionsForSide(1)); readonly side2ActionsObservable = DerivedObservable.create(() => this.getActionsForSide(2)); readonly resultActionsObservable = DerivedObservable.create(() => this.getResultActions()); protected readonly hasSideActionsObservable = DerivedObservable.create(() => this.side1ActionsObservable.get().length + this.side2ActionsObservable.get().length > 0); get hasSideActions(): boolean { return this.hasSideActionsObservable.get(); } protected readonly hasResultActionsObservable = DerivedObservable.create(() => this.resultActionsObservable.get().length > 0); get hasResultActions(): boolean { return this.hasResultActionsObservable.get(); } constructor( protected readonly mergeEditor: MergeEditor, protected readonly mergeRange: MergeRange ) {} protected getActionsForSide(side: MergeSide): readonly MergeRangeAction[] { const { mergeEditor, mergeRange } = this; const { model, side1Title, side2Title } = mergeEditor; if (!model.hasMergeRange(mergeRange)) { return []; } const result: MergeRangeAction[] = []; const sideTitle = side === 1 ? side1Title : side2Title; const state = model.getMergeRangeResultState(mergeRange); if (state !== 'Unrecognized' && !state.includes('Side' + side) && mergeRange.getChanges(side).length && (state === 'Base' || !(mergeRange.isEqualChange || mergeRange.getModifiedRange(side).isEmpty)) ) { result.push({ text: nls.localizeByDefault('Accept {0}', sideTitle), tooltip: nls.localizeByDefault('Accept {0} in the result document.', sideTitle), run: () => this.applyMergeRangeAcceptedState(mergeRange, MergeRangeAcceptedState.addSide(state, side)) }); if (mergeRange.canBeSmartCombined(side)) { result.push({ text: mergeRange.isSmartCombinationOrderRelevant ? nls.localizeByDefault('Accept Combination ({0} First)', sideTitle) : nls.localizeByDefault('Accept Combination'), tooltip: nls.localizeByDefault('Accept an automatic combination of both sides in the result document.'), run: () => this.applyMergeRangeAcceptedState(mergeRange, MergeRangeAcceptedState.addSide( side === 1 ? 'Side1' : 'Side2', side === 1 ? 2 : 1, { smartCombination: true })) }); } } return result; } protected getResultActions(): readonly MergeRangeAction[] { const { mergeEditor, mergeRange } = this; const { model, side1Title, side2Title } = mergeEditor; if (!model.hasMergeRange(mergeRange)) { return []; } const result: MergeRangeAction[] = []; const state = model.getMergeRangeResultState(mergeRange); if (state === 'Unrecognized') { result.push({ text: nls.localizeByDefault('Manual Resolution'), tooltip: nls.localizeByDefault('This conflict has been resolved manually.') }); result.push({ text: nls.localizeByDefault('Reset to base'), tooltip: nls.localizeByDefault('Reset this conflict to the common ancestor of both the right and left changes.'), run: () => this.applyMergeRangeAcceptedState(mergeRange, 'Base') }); } else if (state === 'Base') { result.push({ text: nls.localizeByDefault('No Changes Accepted'), tooltip: nls.localizeByDefault('The current resolution of this conflict equals the common ancestor of both the right and left changes.') }); if (!model.isMergeRangeHandled(mergeRange)) { result.push({ text: nls.localizeByDefault('Mark as Handled'), run: () => this.markMergeRangeAsHandled(mergeRange) }); } } else if (mergeRange.isEqualChange) { result.push({ text: side1Title + ' = ' + side2Title }); result.push({ text: nls.localizeByDefault('Reset to base'), tooltip: nls.localizeByDefault('Reset this conflict to the common ancestor of both the right and left changes.'), run: () => this.applyMergeRangeAcceptedState(mergeRange, 'Base') }); } else { const labels: string[] = []; const stateToggles: MergeRangeAction[] = []; if (state.includes('Side1')) { labels.push(side1Title); stateToggles.push({ text: nls.localizeByDefault('Remove {0}', side1Title), tooltip: nls.localizeByDefault('Remove {0} from the result document.', side1Title), run: () => this.applyMergeRangeAcceptedState(mergeRange, MergeRangeAcceptedState.removeSide(state, 1)) }); } if (state.includes('Side2')) { labels.push(side2Title); stateToggles.push({ text: nls.localizeByDefault('Remove {0}', side2Title), tooltip: nls.localizeByDefault('Remove {0} from the result document.', side2Title), run: () => this.applyMergeRangeAcceptedState(mergeRange, MergeRangeAcceptedState.removeSide(state, 2)) }); } if (state.startsWith('Side2')) { labels.reverse(); stateToggles.reverse(); } if (labels.length) { result.push({ text: labels.join(' + ') }); } result.push(...stateToggles); } return result; } protected async applyMergeRangeAcceptedState(mergeRange: MergeRange, state: MergeRangeAcceptedState): Promise { const { model, resultPane } = this.mergeEditor; resultPane.activate(); await ObservableUtils.waitForState(model.isUpToDateObservable); resultPane.goToMergeRange(mergeRange, { reveal: false }); // set the cursor state that will be restored when undoing the operation model.applyMergeRangeAcceptedState(mergeRange, state); await ObservableUtils.waitForState(model.isUpToDateObservable); resultPane.goToMergeRange(mergeRange, { reveal: false }); // set the resulting cursor state } protected async markMergeRangeAsHandled(mergeRange: MergeRange): Promise { const { model, resultPane } = this.mergeEditor; resultPane.activate(); await ObservableUtils.waitForState(model.isUpToDateObservable); resultPane.goToMergeRange(mergeRange, { reveal: false }); const { cursor } = resultPane.editor; const editorRef = new WeakRef(resultPane.editor); const revealMergeRange = () => { const editor = editorRef.deref(); if (editor && !editor.isDisposed()) { editor.cursor = cursor; editor.revealPosition(cursor, { vertical: 'auto' }); } }; model.markMergeRangeAsHandled(mergeRange, { undoRedo: { callback: { didUndo(): void { revealMergeRange(); }, didRedo(): void { revealMergeRange(); } } } }); } }