From bf85140dfdb36293e8169d01ce5a3dbeff1d3f6c Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Thu, 13 Oct 2022 15:36:42 +0200 Subject: [PATCH] Fixes #162173 (#163548) --- .../browser/view/conflictActions.ts | 246 +++++++++-------- .../browser/view/fixedZoneWidget.ts | 5 +- .../mergeEditor/browser/view/mergeEditor.ts | 200 +++----------- .../mergeEditor/browser/view/viewZones.ts | 253 ++++++++++++++++++ 4 files changed, 421 insertions(+), 283 deletions(-) create mode 100644 src/vs/workbench/contrib/mergeEditor/browser/view/viewZones.ts diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/conflictActions.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/conflictActions.ts index 5e7cdcc25854..6e5658bee15f 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/conflictActions.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/conflictActions.ts @@ -74,118 +74,32 @@ export class ConflictActionsFactory extends Disposable { }; } - addResultWidget(viewZoneChangeAccessor: IViewZoneChangeAccessor, lineNumber: number, viewModel: MergeEditorViewModel, modifiedBaseRange: ModifiedBaseRange): IDisposable { - function command(title: string, action: () => Promise, tooltip?: string): IContentWidgetAction { - return { - text: title, - action, - tooltip - }; - } - - const items = derived('items', reader => { - const state = viewModel.model.getState(modifiedBaseRange).read(reader); - const model = viewModel.model; - - const result: IContentWidgetAction[] = []; - - - if (state.conflicting) { - result.push({ - text: localize('manualResolution', "Manual Resolution"), - tooltip: localize('manualResolutionTooltip', "This conflict has been resolved manually."), - }); - } else if (state.isEmpty) { - result.push({ - text: localize('noChangesAccepted', 'No Changes Accepted'), - tooltip: localize( - 'noChangesAcceptedTooltip', - 'The current resolution of this conflict equals the common ancestor of both the right and left changes.' - ), - }); - - } else { - const labels = []; - if (state.input1) { - labels.push(model.input1.title); - } - if (state.input2) { - labels.push(model.input2.title); - } - if (state.input2First) { - labels.reverse(); - } - result.push({ - text: `${labels.join(' + ')}` - }); - } - - const stateToggles: IContentWidgetAction[] = []; - if (state.input1) { - result.push(command(localize('remove', "Remove {0}", model.input1.title), async () => { - transaction((tx) => { - model.setState( - modifiedBaseRange, - state.withInputValue(1, false), - true, - tx - ); - }); - }, localize('removeTooltip', "Remove {0} from the result document.", model.input1.title)), - ); - } - if (state.input2) { - result.push(command(localize('remove', "Remove {0}", model.input2.title), async () => { - transaction((tx) => { - model.setState( - modifiedBaseRange, - state.withInputValue(2, false), - true, - tx - ); - }); - }, localize('removeTooltip', "Remove {0} from the result document.", model.input2.title)), - ); - } - if (state.input2First) { - stateToggles.reverse(); - } - result.push(...stateToggles); - - - - if (state.conflicting) { - result.push( - command(localize('resetToBase', "Reset to base"), async () => { - transaction((tx) => { - model.setState( - modifiedBaseRange, - ModifiedBaseRangeState.default, - true, - tx - ); - }); - }, localize('resetToBaseTooltip', "Reset this conflict to the common ancestor of both the right and left changes.")), - ); - } - return result; - }); - + public createWidget(viewZoneChangeAccessor: IViewZoneChangeAccessor, lineNumber: number, items: IObservable, viewZoneIdsToCleanUp: string[]): IDisposable { const layoutInfo = this._getLayoutInfo(); + return new ActionsContentWidget( + this._editor, + viewZoneChangeAccessor, + lineNumber, + layoutInfo.codeLensHeight + 2, + this._styleClassName, + items, + viewZoneIdsToCleanUp, + ); + } +} - return new ActionsContentWidget(this._editor, viewZoneChangeAccessor, lineNumber, layoutInfo.codeLensHeight + 2, this._styleClassName, items); +export class ActionsSource { + constructor( + private readonly viewModel: MergeEditorViewModel, + private readonly modifiedBaseRange: ModifiedBaseRange, + ) { } - addContentWidget(viewZoneChangeAccessor: IViewZoneChangeAccessor, lineNumber: number, viewModel: MergeEditorViewModel, modifiedBaseRange: ModifiedBaseRange, inputNumber: 1 | 2): IDisposable { - function command(title: string, action: () => Promise, tooltip?: string): IContentWidgetAction { - return { - text: title, - action, - tooltip, - }; - } + private getItemsInput(inputNumber: 1 | 2): IObservable { + return derived('items', reader => { + const viewModel = this.viewModel; + const modifiedBaseRange = this.modifiedBaseRange; - const items = derived('items', reader => { if (!viewModel.model.hasBaseRange(modifiedBaseRange)) { return []; } @@ -260,15 +174,120 @@ export class ConflictActionsFactory extends Disposable { } return result; }); - - const layoutInfo = this._getLayoutInfo(); - - return new ActionsContentWidget(this._editor, viewZoneChangeAccessor, lineNumber, layoutInfo.codeLensHeight + 2, this._styleClassName, items); } + + public readonly itemsInput1 = this.getItemsInput(1); + public readonly itemsInput2 = this.getItemsInput(2); + + public readonly resultItems = derived('items', reader => { + const viewModel = this.viewModel; + const modifiedBaseRange = this.modifiedBaseRange; + + const state = viewModel.model.getState(modifiedBaseRange).read(reader); + const model = viewModel.model; + + const result: IContentWidgetAction[] = []; + + + if (state.conflicting) { + result.push({ + text: localize('manualResolution', "Manual Resolution"), + tooltip: localize('manualResolutionTooltip', "This conflict has been resolved manually."), + }); + } else if (state.isEmpty) { + result.push({ + text: localize('noChangesAccepted', 'No Changes Accepted'), + tooltip: localize( + 'noChangesAcceptedTooltip', + 'The current resolution of this conflict equals the common ancestor of both the right and left changes.' + ), + }); + + } else { + const labels = []; + if (state.input1) { + labels.push(model.input1.title); + } + if (state.input2) { + labels.push(model.input2.title); + } + if (state.input2First) { + labels.reverse(); + } + result.push({ + text: `${labels.join(' + ')}` + }); + } + + const stateToggles: IContentWidgetAction[] = []; + if (state.input1) { + result.push(command(localize('remove', "Remove {0}", model.input1.title), async () => { + transaction((tx) => { + model.setState( + modifiedBaseRange, + state.withInputValue(1, false), + true, + tx + ); + }); + }, localize('removeTooltip', "Remove {0} from the result document.", model.input1.title)), + ); + } + if (state.input2) { + result.push(command(localize('remove', "Remove {0}", model.input2.title), async () => { + transaction((tx) => { + model.setState( + modifiedBaseRange, + state.withInputValue(2, false), + true, + tx + ); + }); + }, localize('removeTooltip', "Remove {0} from the result document.", model.input2.title)), + ); + } + if (state.input2First) { + stateToggles.reverse(); + } + result.push(...stateToggles); + + + + if (state.conflicting) { + result.push( + command(localize('resetToBase', "Reset to base"), async () => { + transaction((tx) => { + model.setState( + modifiedBaseRange, + ModifiedBaseRangeState.default, + true, + tx + ); + }); + }, localize('resetToBaseTooltip', "Reset this conflict to the common ancestor of both the right and left changes.")), + ); + } + return result; + }); + + public readonly isEmpty = derived('isEmpty', reader => { + return this.itemsInput1.read(reader).length + this.itemsInput2.read(reader).length + this.resultItems.read(reader).length === 0; + }); + + public readonly inputIsEmpty = derived('inputIsEmpty', reader => { + return this.itemsInput1.read(reader).length + this.itemsInput2.read(reader).length === 0; + }); } +function command(title: string, action: () => Promise, tooltip?: string): IContentWidgetAction { + return { + text: title, + action, + tooltip, + }; +} -interface IContentWidgetAction { +export interface IContentWidgetAction { text: string; tooltip?: string; action?: () => Promise; @@ -285,8 +304,9 @@ class ActionsContentWidget extends FixedZoneWidget { className: string, items: IObservable, + viewZoneIdsToCleanUp: string[], ) { - super(editor, viewZoneAccessor, afterLineNumber, height); + super(editor, viewZoneAccessor, afterLineNumber, height, viewZoneIdsToCleanUp); this.widgetDomNode.appendChild(this._domNode); diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/fixedZoneWidget.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/fixedZoneWidget.ts index 12c2f9309336..e761905679de 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/fixedZoneWidget.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/fixedZoneWidget.ts @@ -24,6 +24,7 @@ export abstract class FixedZoneWidget extends Disposable { viewZoneAccessor: IViewZoneChangeAccessor, afterLineNumber: number, height: number, + viewZoneIdsToCleanUp: string[], ) { super(); @@ -38,6 +39,7 @@ export abstract class FixedZoneWidget extends Disposable { this.widgetDomNode.style.top = `${top}px`; } }); + viewZoneIdsToCleanUp.push(this.viewZoneId); this.widgetDomNode.style.left = this.editor.getLayoutInfo().contentLeft + 'px'; @@ -46,9 +48,6 @@ export abstract class FixedZoneWidget extends Disposable { this._register({ dispose: () => { this.editor.removeOverlayWidget(this.overlayWidget); - this.editor.changeViewZones(accessor => { - accessor.removeZone(this.viewZoneId); - }); }, }); } diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts index bd4acc119804..6934099796b5 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts @@ -3,10 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { $, Dimension, reset } from 'vs/base/browser/dom'; +import { Dimension, reset } from 'vs/base/browser/dom'; import { Grid, GridNodeDescriptor, IView, SerializableGrid } from 'vs/base/browser/ui/grid/grid'; import { Orientation } from 'vs/base/browser/ui/splitview/splitview'; -import { CompareResult, lastOrDefault } from 'vs/base/common/arrays'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Color } from 'vs/base/common/color'; import { BugIndicatingError, onUnexpectedError } from 'vs/base/common/errors'; @@ -38,24 +37,20 @@ import { applyTextEditorOptions } from 'vs/workbench/common/editor/editorOptions import { readTransientState, writeTransientState } from 'vs/workbench/contrib/codeEditor/browser/toggleWordWrap'; import { MergeEditorInput } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput'; import { IMergeEditorInputModel } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInputModel'; -import { LineRange } from 'vs/workbench/contrib/mergeEditor/browser/model/lineRange'; -import { DetailedLineRangeMapping } from 'vs/workbench/contrib/mergeEditor/browser/model/mapping'; import { MergeEditorModel } from 'vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel'; -import { ModifiedBaseRange } from 'vs/workbench/contrib/mergeEditor/browser/model/modifiedBaseRange'; -import { deepMerge, join, PersistentStore, thenIfNotDisposed } from 'vs/workbench/contrib/mergeEditor/browser/utils'; +import { deepMerge, PersistentStore, thenIfNotDisposed } from 'vs/workbench/contrib/mergeEditor/browser/utils'; import { BaseCodeEditorView } from 'vs/workbench/contrib/mergeEditor/browser/view/editors/baseCodeEditorView'; import { ScrollSynchronizer } from 'vs/workbench/contrib/mergeEditor/browser/view/scrollSynchronizer'; import { MergeEditorViewModel } from 'vs/workbench/contrib/mergeEditor/browser/view/viewModel'; +import { ViewZoneComputer } from 'vs/workbench/contrib/mergeEditor/browser/view/viewZones'; import { ctxIsMergeEditor, ctxMergeBaseUri, ctxMergeEditorLayout, ctxMergeEditorShowBase, ctxMergeEditorShowBaseAtTop, ctxMergeEditorShowNonConflictingChanges, ctxMergeResultUri, MergeEditorLayoutKind } from 'vs/workbench/contrib/mergeEditor/common/mergeEditor'; import { settingsSashBorder } from 'vs/workbench/contrib/preferences/common/settingsEditorColorRegistry'; import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorResolverService, MergeEditorInputFactoryFunction, RegisteredEditorPriority } from 'vs/workbench/services/editor/common/editorResolverService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import './colors'; -import { ConflictActionsFactory } from './conflictActions'; import { InputCodeEditorView } from './editors/inputCodeEditorView'; import { ResultCodeEditorView } from './editors/resultCodeEditorView'; -import { getAlignments } from './lineAlignment'; export class MergeEditor extends AbstractTextEditor { @@ -97,9 +92,11 @@ export class MergeEditor extends AbstractTextEditor { return !!this._configurationService.getValue('mergeEditor.writableInputs'); } - private readonly conflictActionsFactoryInput1 = new ConflictActionsFactory(this.input1View.editor); - private readonly conflictActionsFactoryInput2 = new ConflictActionsFactory(this.input2View.editor); - private readonly conflictActionsFactoryResult = new ConflictActionsFactory(this.inputResultView.editor); + private readonly viewZoneComputer = new ViewZoneComputer( + this.input1View.editor, + this.input2View.editor, + this.inputResultView.editor, + ); protected readonly codeLensesVisible = observableFromEvent( this.configurationService.onDidChangeConfiguration, @@ -351,32 +348,38 @@ export class MergeEditor extends AbstractTextEditor { resultViewZoneAccessor: IViewZoneChangeAccessor, shouldAlignResult: boolean, ): IDisposable { - let input1LinesAdded = 0; - let input2LinesAdded = 0; - let baseLinesAdded = 0; - let resultLinesAdded = 0; - - const model = viewModel.model; - const input1ViewZoneIds: string[] = []; const input2ViewZoneIds: string[] = []; const baseViewZoneIds: string[] = []; const resultViewZoneIds: string[] = []; - const resultDiffs = model.baseResultDiffs.read(reader); - const baseRangeWithStoreAndTouchingDiffs = join( - model.modifiedBaseRanges.read(reader), - resultDiffs, - (baseRange, diff) => - baseRange.baseRange.touches(diff.inputRange) - ? CompareResult.neitherLessOrGreaterThan - : LineRange.compareByStart( - baseRange.baseRange, - diff.inputRange - ) - ); + const viewZones = this.viewZoneComputer.computeViewZones(reader, viewModel, { + codeLensesVisible: this.codeLensesVisible.read(reader), + showNonConflictingChanges: this.showNonConflictingChanges.read(reader), + shouldAlignBase, + shouldAlignResult, + }); const disposableStore = new DisposableStore(); + + if (baseViewZoneAccessor) { + for (const v of viewZones.baseViewZones) { + v.create(baseViewZoneAccessor, baseViewZoneIds, disposableStore); + } + } + + for (const v of viewZones.resultViewZones) { + v.create(resultViewZoneAccessor, resultViewZoneIds, disposableStore); + } + + for (const v of viewZones.input1ViewZones) { + v.create(input1ViewZoneAccessor, input1ViewZoneIds, disposableStore); + } + + for (const v of viewZones.input2ViewZones) { + v.create(input2ViewZoneAccessor, input2ViewZoneIds, disposableStore); + } + disposableStore.add({ dispose: () => { input1Editor.changeViewZones(a => { @@ -402,143 +405,6 @@ export class MergeEditor extends AbstractTextEditor { } }); - const shouldShowCodeLenses = this.codeLensesVisible.read(reader); - const showNonConflictingChanges = this.showNonConflictingChanges.read(reader); - - let lastModifiedBaseRange: ModifiedBaseRange | undefined = undefined; - let lastBaseResultDiff: DetailedLineRangeMapping | undefined = undefined; - for (const m of baseRangeWithStoreAndTouchingDiffs) { - if (shouldShowCodeLenses && m.left && (m.left.isConflicting || showNonConflictingChanges || !model.isHandled(m.left).read(reader))) { - disposableStore.add( - this.conflictActionsFactoryInput1.addContentWidget( - input1ViewZoneAccessor, m.left.input1Range.startLineNumber - 1, viewModel, m.left, this.input1View.inputNumber - ) - ); - disposableStore.add( - this.conflictActionsFactoryInput2.addContentWidget( - input2ViewZoneAccessor, m.left.input2Range.startLineNumber - 1, viewModel, m.left, this.input2View.inputNumber - ) - ); - - const afterLineNumber = m.left.baseRange.startLineNumber + (lastBaseResultDiff?.resultingDeltaFromOriginalToModified ?? 0) - 1; - - disposableStore.add(this.conflictActionsFactoryResult.addResultWidget(resultViewZoneAccessor, afterLineNumber, viewModel, m.left)); - - if (shouldAlignBase && baseViewZoneAccessor) { - baseViewZoneIds.push(baseViewZoneAccessor.addZone({ - afterLineNumber: m.left.baseRange.startLineNumber - 1, - heightInPx: 16, - domNode: $('div.conflict-actions-placeholder'), - })); - } - } - - interface LineAlignment { - baseLine: number; - input1Line?: number; - input2Line?: number; - resultLine?: number; - } - - const lastResultDiff = lastOrDefault(m.rights)!; - if (lastResultDiff) { - lastBaseResultDiff = lastResultDiff; - } - let alignedLines: LineAlignment[]; - if (m.left) { - alignedLines = getAlignments(m.left).map(a => ({ - input1Line: a[0], - baseLine: a[1], - input2Line: a[2], - resultLine: undefined, - })); - - lastModifiedBaseRange = m.left; - // This is a total hack. - alignedLines[alignedLines.length - 1].resultLine = - m.left.baseRange.endLineNumberExclusive - + (lastBaseResultDiff ? lastBaseResultDiff.resultingDeltaFromOriginalToModified : 0); - - } else { - alignedLines = [{ - baseLine: lastResultDiff.inputRange.endLineNumberExclusive, - input1Line: lastResultDiff.inputRange.endLineNumberExclusive + (lastModifiedBaseRange ? (lastModifiedBaseRange.input1Range.endLineNumberExclusive - lastModifiedBaseRange.baseRange.endLineNumberExclusive) : 0), - input2Line: lastResultDiff.inputRange.endLineNumberExclusive + (lastModifiedBaseRange ? (lastModifiedBaseRange.input2Range.endLineNumberExclusive - lastModifiedBaseRange.baseRange.endLineNumberExclusive) : 0), - resultLine: lastResultDiff.outputRange.endLineNumberExclusive, - }]; - } - - for (const { input1Line, baseLine, input2Line, resultLine } of alignedLines) { - if (!shouldAlignBase && (input1Line === undefined || input2Line === undefined)) { - continue; - } - - const input1Line_ = - input1Line !== undefined ? input1Line + input1LinesAdded : -1; - const input2Line_ = - input2Line !== undefined ? input2Line + input2LinesAdded : -1; - const baseLine_ = baseLine + baseLinesAdded; - const resultLine_ = resultLine !== undefined ? resultLine + resultLinesAdded : -1; - - const max = Math.max(shouldAlignBase ? baseLine_ : 0, input1Line_, input2Line_, shouldAlignResult ? resultLine_ : 0); - - if (input1Line !== undefined) { - const diffInput1 = max - input1Line_; - if (diffInput1 > 0) { - input1ViewZoneIds.push( - input1ViewZoneAccessor.addZone({ - afterLineNumber: input1Line - 1, - heightInLines: diffInput1, - domNode: $('div.diagonal-fill'), - }) - ); - input1LinesAdded += diffInput1; - } - } - - if (input2Line !== undefined) { - const diffInput2 = max - input2Line_; - if (diffInput2 > 0) { - input2ViewZoneIds.push( - input2ViewZoneAccessor.addZone({ - afterLineNumber: input2Line - 1, - heightInLines: diffInput2, - domNode: $('div.diagonal-fill'), - }) - ); - input2LinesAdded += diffInput2; - } - } - - if (shouldAlignBase && baseViewZoneAccessor) { - const diffBase = max - baseLine_; - if (diffBase > 0) { - baseViewZoneIds.push( - baseViewZoneAccessor.addZone({ - afterLineNumber: baseLine - 1, - heightInLines: diffBase, - domNode: $('div.diagonal-fill'), - }) - ); - baseLinesAdded += diffBase; - } - } - - if (shouldAlignResult && resultViewZoneAccessor && resultLine !== undefined) { - const diffResult = max - resultLine_; - if (diffResult > 0) { - resultViewZoneIds.push( - resultViewZoneAccessor.addZone({ - afterLineNumber: resultLine - 1, - heightInLines: diffResult, - domNode: $('div.diagonal-fill'), - }) - ); - resultLinesAdded += diffResult; - } - } - } - } return disposableStore; } diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/viewZones.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/viewZones.ts new file mode 100644 index 000000000000..ce3ec9673751 --- /dev/null +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/viewZones.ts @@ -0,0 +1,253 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { $ } from 'vs/base/browser/dom'; +import { CompareResult, lastOrDefault } from 'vs/base/common/arrays'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { IObservable, IReader } from 'vs/base/common/observable'; +import { ICodeEditor, IViewZoneChangeAccessor } from 'vs/editor/browser/editorBrowser'; +import { LineRange } from 'vs/workbench/contrib/mergeEditor/browser/model/lineRange'; +import { DetailedLineRangeMapping } from 'vs/workbench/contrib/mergeEditor/browser/model/mapping'; +import { ModifiedBaseRange } from 'vs/workbench/contrib/mergeEditor/browser/model/modifiedBaseRange'; +import { join } from 'vs/workbench/contrib/mergeEditor/browser/utils'; +import { ActionsSource, ConflictActionsFactory, IContentWidgetAction } from 'vs/workbench/contrib/mergeEditor/browser/view/conflictActions'; +import { getAlignments } from 'vs/workbench/contrib/mergeEditor/browser/view/lineAlignment'; +import { MergeEditorViewModel } from 'vs/workbench/contrib/mergeEditor/browser/view/viewModel'; + +export class ViewZoneComputer { + private readonly conflictActionsFactoryInput1 = new ConflictActionsFactory(this.input1Editor); + private readonly conflictActionsFactoryInput2 = new ConflictActionsFactory(this.input2Editor); + private readonly conflictActionsFactoryResult = new ConflictActionsFactory(this.resultEditor); + + constructor( + private readonly input1Editor: ICodeEditor, + private readonly input2Editor: ICodeEditor, + private readonly resultEditor: ICodeEditor, + ) { } + + public computeViewZones( + reader: IReader, + viewModel: MergeEditorViewModel, + options: { + shouldAlignResult: boolean; + shouldAlignBase: boolean; + codeLensesVisible: boolean; + showNonConflictingChanges: boolean; + } + ): MergeEditorViewZones { + let input1LinesAdded = 0; + let input2LinesAdded = 0; + let baseLinesAdded = 0; + let resultLinesAdded = 0; + + const input1ViewZones: MergeEditorViewZone[] = []; + const input2ViewZones: MergeEditorViewZone[] = []; + const baseViewZones: MergeEditorViewZone[] = []; + const resultViewZones: MergeEditorViewZone[] = []; + + const model = viewModel.model; + + const resultDiffs = model.baseResultDiffs.read(reader); + const baseRangeWithStoreAndTouchingDiffs = join( + model.modifiedBaseRanges.read(reader), + resultDiffs, + (baseRange, diff) => + baseRange.baseRange.touches(diff.inputRange) + ? CompareResult.neitherLessOrGreaterThan + : LineRange.compareByStart( + baseRange.baseRange, + diff.inputRange + ) + ); + + const shouldShowCodeLenses = options.codeLensesVisible; + const showNonConflictingChanges = options.showNonConflictingChanges; + + let lastModifiedBaseRange: ModifiedBaseRange | undefined = undefined; + let lastBaseResultDiff: DetailedLineRangeMapping | undefined = undefined; + for (const m of baseRangeWithStoreAndTouchingDiffs) { + if (shouldShowCodeLenses && m.left && (m.left.isConflicting || showNonConflictingChanges || !model.isHandled(m.left).read(reader))) { + const actions = new ActionsSource(viewModel, m.left); + if (options.shouldAlignResult || !actions.inputIsEmpty.read(reader)) { + input1ViewZones.push(new CommandViewZone(this.conflictActionsFactoryInput1, m.left.input1Range.startLineNumber - 1, actions.itemsInput1)); + input2ViewZones.push(new CommandViewZone(this.conflictActionsFactoryInput2, m.left.input2Range.startLineNumber - 1, actions.itemsInput2)); + if (options.shouldAlignBase) { + baseViewZones.push(new Placeholder(m.left.baseRange.startLineNumber - 1, 16)); + } + } + const afterLineNumber = m.left.baseRange.startLineNumber + (lastBaseResultDiff?.resultingDeltaFromOriginalToModified ?? 0) - 1; + resultViewZones.push(new CommandViewZone(this.conflictActionsFactoryResult, afterLineNumber, actions.resultItems)); + + } + + const lastResultDiff = lastOrDefault(m.rights)!; + if (lastResultDiff) { + lastBaseResultDiff = lastResultDiff; + } + let alignedLines: LineAlignment[]; + if (m.left) { + alignedLines = getAlignments(m.left).map(a => ({ + input1Line: a[0], + baseLine: a[1], + input2Line: a[2], + resultLine: undefined, + })); + + lastModifiedBaseRange = m.left; + // This is a total hack. + alignedLines[alignedLines.length - 1].resultLine = + m.left.baseRange.endLineNumberExclusive + + (lastBaseResultDiff ? lastBaseResultDiff.resultingDeltaFromOriginalToModified : 0); + + } else { + alignedLines = [{ + baseLine: lastResultDiff.inputRange.endLineNumberExclusive, + input1Line: lastResultDiff.inputRange.endLineNumberExclusive + (lastModifiedBaseRange ? (lastModifiedBaseRange.input1Range.endLineNumberExclusive - lastModifiedBaseRange.baseRange.endLineNumberExclusive) : 0), + input2Line: lastResultDiff.inputRange.endLineNumberExclusive + (lastModifiedBaseRange ? (lastModifiedBaseRange.input2Range.endLineNumberExclusive - lastModifiedBaseRange.baseRange.endLineNumberExclusive) : 0), + resultLine: lastResultDiff.outputRange.endLineNumberExclusive, + }]; + } + + for (const { input1Line, baseLine, input2Line, resultLine } of alignedLines) { + if (!options.shouldAlignBase && (input1Line === undefined || input2Line === undefined)) { + continue; + } + + const input1Line_ = + input1Line !== undefined ? input1Line + input1LinesAdded : -1; + const input2Line_ = + input2Line !== undefined ? input2Line + input2LinesAdded : -1; + const baseLine_ = baseLine + baseLinesAdded; + const resultLine_ = resultLine !== undefined ? resultLine + resultLinesAdded : -1; + + const max = Math.max(options.shouldAlignBase ? baseLine_ : 0, input1Line_, input2Line_, options.shouldAlignResult ? resultLine_ : 0); + + if (input1Line !== undefined) { + const diffInput1 = max - input1Line_; + if (diffInput1 > 0) { + input1ViewZones.push(new Spacer(input1Line - 1, diffInput1)); + input1LinesAdded += diffInput1; + } + } + + if (input2Line !== undefined) { + const diffInput2 = max - input2Line_; + if (diffInput2 > 0) { + input2ViewZones.push(new Spacer(input2Line - 1, diffInput2)); + input2LinesAdded += diffInput2; + } + } + + if (options.shouldAlignBase) { + const diffBase = max - baseLine_; + if (diffBase > 0) { + baseViewZones.push(new Spacer(baseLine - 1, diffBase)); + baseLinesAdded += diffBase; + } + } + + if (options.shouldAlignResult && resultLine !== undefined) { + const diffResult = max - resultLine_; + if (diffResult > 0) { + resultViewZones.push(new Spacer(resultLine - 1, diffResult)); + resultLinesAdded += diffResult; + } + } + } + } + + return new MergeEditorViewZones(input1ViewZones, input2ViewZones, baseViewZones, resultViewZones); + } +} + +interface LineAlignment { + baseLine: number; + input1Line?: number; + input2Line?: number; + resultLine?: number; +} + +export class MergeEditorViewZones { + constructor( + public readonly input1ViewZones: readonly MergeEditorViewZone[], + public readonly input2ViewZones: readonly MergeEditorViewZone[], + public readonly baseViewZones: readonly MergeEditorViewZone[], + public readonly resultViewZones: readonly MergeEditorViewZone[], + ) { } +} + +/** + * This is an abstract class to create various editor view zones. +*/ +export abstract class MergeEditorViewZone { + abstract create(viewZoneChangeAccessor: IViewZoneChangeAccessor, viewZoneIdsToCleanUp: string[], disposableStore: DisposableStore): void; +} + +class Spacer extends MergeEditorViewZone { + constructor( + private readonly afterLineNumber: number, + private readonly heightInLines: number + ) { + super(); + } + + override create( + viewZoneChangeAccessor: IViewZoneChangeAccessor, + viewZoneIdsToCleanUp: string[], + disposableStore: DisposableStore + ): void { + viewZoneIdsToCleanUp.push( + viewZoneChangeAccessor.addZone({ + afterLineNumber: this.afterLineNumber, + heightInLines: this.heightInLines, + domNode: $('div.diagonal-fill'), + }) + ); + } +} + +class Placeholder extends MergeEditorViewZone { + constructor( + private readonly afterLineNumber: number, + private readonly heightPx: number + ) { + super(); + } + + override create( + viewZoneChangeAccessor: IViewZoneChangeAccessor, + viewZoneIdsToCleanUp: string[], + disposableStore: DisposableStore + ): void { + viewZoneIdsToCleanUp.push( + viewZoneChangeAccessor.addZone({ + afterLineNumber: this.afterLineNumber, + heightInPx: this.heightPx, + domNode: $('div.conflict-actions-placeholder'), + }) + ); + } +} + +class CommandViewZone extends MergeEditorViewZone { + constructor( + private readonly conflictActionsFactory: ConflictActionsFactory, + private readonly lineNumber: number, + private readonly items: IObservable, + ) { + super(); + } + + override create(viewZoneChangeAccessor: IViewZoneChangeAccessor, viewZoneIdsToCleanUp: string[], disposableStore: DisposableStore): void { + disposableStore.add( + this.conflictActionsFactory.createWidget( + viewZoneChangeAccessor, + this.lineNumber, + this.items, + viewZoneIdsToCleanUp, + ) + ); + } +}