diff --git a/src/vs/workbench/contrib/mergeEditor/browser/commands/commands.ts b/src/vs/workbench/contrib/mergeEditor/browser/commands/commands.ts index bedc6f16ebf4c..355f73d76f68d 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/commands/commands.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/commands/commands.ts @@ -186,8 +186,8 @@ export class SetMixedLayoutWithBase extends Action2 { super({ id: 'merge.mixedLayoutWithBase', title: { - value: localize('layout.mixedWithBase', 'Mixed Layout With Base'), - original: 'Mixed Layout With Based', + value: localize('layout.mixedWithBase', 'Mixed Layout With Base At Top'), + original: 'Mixed Layout With Based At Top', }, toggled: ctxMergeEditorLayout.isEqualTo('mixedWithBase'), menu: [ @@ -210,6 +210,35 @@ export class SetMixedLayoutWithBase extends Action2 { } } +export class SetMixedLayoutWithBaseColumns extends Action2 { + constructor() { + super({ + id: 'merge.mixedLayoutWithBaseColumns', + title: { + value: localize('layout.mixedWithBaseColumns', 'Mixed Layout With Base'), + original: 'Mixed Layout With Based', + }, + toggled: ctxMergeEditorLayout.isEqualTo('mixedWithBaseColumns'), + menu: [ + { + id: MenuId.EditorTitle, + when: ctxIsMergeEditor, + group: '1_merge', + order: 9, + }, + ], + precondition: ctxIsMergeEditor, + }); + } + + run(accessor: ServicesAccessor): void { + const { activeEditorPane } = accessor.get(IEditorService); + if (activeEditorPane instanceof MergeEditor) { + activeEditorPane.setLayout('mixedWithBaseColumns'); + } + } +} + const mergeEditorCategory: ILocalizedString = { value: localize('mergeEditor', 'Merge Editor'), original: 'Merge Editor', diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts index b62b2445eef95..cbad5dac22184 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts @@ -11,7 +11,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { EditorPaneDescriptor, IEditorPaneRegistry } from 'vs/workbench/browser/editor'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { EditorExtensions, IEditorFactoryRegistry } from 'vs/workbench/common/editor'; -import { AcceptAllInput1, AcceptAllInput2, CompareInput1WithBaseCommand, CompareInput2WithBaseCommand, GoToNextUnhandledConflict, GoToPreviousUnhandledConflict, OpenBaseFile, OpenMergeEditor, OpenResultResource, ResetDirtyConflictsToBaseCommand, ResetToBaseAndAutoMergeCommand, SetColumnLayout, SetMixedLayout, SetMixedLayoutWithBase, ToggleActiveConflictInput1, ToggleActiveConflictInput2 } from 'vs/workbench/contrib/mergeEditor/browser/commands/commands'; +import { AcceptAllInput1, AcceptAllInput2, CompareInput1WithBaseCommand, CompareInput2WithBaseCommand, GoToNextUnhandledConflict, GoToPreviousUnhandledConflict, OpenBaseFile, OpenMergeEditor, OpenResultResource, ResetDirtyConflictsToBaseCommand, ResetToBaseAndAutoMergeCommand, SetColumnLayout, SetMixedLayout, SetMixedLayoutWithBase, SetMixedLayoutWithBaseColumns, ToggleActiveConflictInput1, ToggleActiveConflictInput2 } from 'vs/workbench/contrib/mergeEditor/browser/commands/commands'; import { MergeEditorCopyContentsToJSON, MergeEditorSaveContentsToFolder, MergeEditorLoadContentsFromFolder } from 'vs/workbench/contrib/mergeEditor/browser/commands/devCommands'; import { MergeEditorInput } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput'; import { MergeEditor, MergeEditorResolverContribution, MergeEditorOpenHandlerContribution } from 'vs/workbench/contrib/mergeEditor/browser/view/mergeEditor'; @@ -52,6 +52,7 @@ registerAction2(OpenResultResource); registerAction2(SetMixedLayout); registerAction2(SetColumnLayout); registerAction2(SetMixedLayoutWithBase); +registerAction2(SetMixedLayoutWithBaseColumns); registerAction2(OpenMergeEditor); registerAction2(OpenBaseFile); diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/editorGutter.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/editorGutter.ts index dc9acb417b202..56917808bce2e 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/editorGutter.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/editorGutter.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { h } from 'vs/base/browser/dom'; -import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; -import { autorun, IReader, observableFromEvent, observableSignalFromEvent } from 'vs/base/common/observable'; +import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { autorun, IReader, observableFromEvent, observableSignal, observableSignalFromEvent, transaction } from 'vs/base/common/observable'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; import { LineRange } from 'vs/workbench/contrib/mergeEditor/browser/model/lineRange'; @@ -22,6 +22,7 @@ export class EditorGutter extends D private readonly editorOnDidChangeViewZones = observableSignalFromEvent('onDidChangeViewZones', this._editor.onDidChangeViewZones); private readonly editorOnDidContentSizeChange = observableSignalFromEvent('onDidContentSizeChange', this._editor.onDidContentSizeChange); + private readonly domNodeSizeChanged = observableSignal('domNodeSizeChanged'); constructor( private readonly _editor: CodeEditorWidget, @@ -35,6 +36,15 @@ export class EditorGutter extends D .root ); + const o = new ResizeObserver(() => { + transaction(tx => { + /** @description ResizeObserver: size changed */ + this.domNodeSizeChanged.trigger(tx); + }); + }); + o.observe(this._domNode); + this._register(toDisposable(() => o.disconnect())); + this._register(autorun('update scroll decoration', (reader) => { scrollDecoration.className = this.isScrollTopZero.read(reader) ? '' : 'scroll-decoration'; })); @@ -49,6 +59,7 @@ export class EditorGutter extends D return; } + this.domNodeSizeChanged.read(reader); this.editorOnDidChangeViewZones.read(reader); this.editorOnDidContentSizeChange.read(reader); @@ -130,12 +141,6 @@ export interface IGutterItemProvider { export interface IGutterItemInfo { id: string; range: LineRange; - /* - - // To accommodate view zones: - offsetInPx: number; - additionalHeightInPx: number; - */ } export interface IGutterItemView extends IDisposable { diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/editors/baseCodeEditorView.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/editors/baseCodeEditorView.ts index 12027d1e5d065..749277e28350f 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/editors/baseCodeEditorView.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/editors/baseCodeEditorView.ts @@ -3,23 +3,26 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { derived } from 'vs/base/common/observable'; +import { reset } from 'vs/base/browser/dom'; +import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; +import { autorun, derived, IObservable } from 'vs/base/common/observable'; import { EditorExtensionsRegistry, IEditorContributionDescription } from 'vs/editor/browser/editorExtensions'; import { IModelDeltaDecoration, MinimapPosition, OverviewRulerLane } from 'vs/editor/common/model'; import { CodeLensContribution } from 'vs/editor/contrib/codelens/browser/codelensController'; +import { localize } from 'vs/nls'; import { MenuId } from 'vs/platform/actions/common/actions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { applyObservableDecorations } from 'vs/workbench/contrib/mergeEditor/browser/utils'; import { handledConflictMinimapOverViewRulerColor, unhandledConflictMinimapOverViewRulerColor } from 'vs/workbench/contrib/mergeEditor/browser/view/colors'; +import { MergeEditorViewModel } from 'vs/workbench/contrib/mergeEditor/browser/view/viewModel'; import { CodeEditorView, createSelectionsAutorun, TitleMenu } from './codeEditorView'; export class BaseCodeEditorView extends CodeEditorView { constructor( + viewModel: IObservable, @IInstantiationService instantiationService: IInstantiationService, ) { - super(instantiationService); - - this._register(applyObservableDecorations(this.editor, this.decorations)); + super(instantiationService, viewModel); this._register( createSelectionsAutorun(this, (baseRange, viewModel) => baseRange) @@ -28,6 +31,19 @@ export class BaseCodeEditorView extends CodeEditorView { this._register( instantiationService.createInstance(TitleMenu, MenuId.MergeBaseToolbar, this.htmlElements.title) ); + + this._register( + autorun('update labels & text model', (reader) => { + const vm = this.viewModel.read(reader); + if (!vm) { + return; + } + this.editor.setModel(vm.model.base); + reset(this.htmlElements.title, ...renderLabelWithIcons(localize('base', 'Base'))); + }) + ); + + this._register(applyObservableDecorations(this.editor, this.decorations)); } private readonly decorations = derived(`base.decorations`, reader => { diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts index 354cb70542b78..987c7ed5ab972 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts @@ -3,33 +3,29 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { h, reset } from 'vs/base/browser/dom'; +import { h } from 'vs/base/browser/dom'; import { IView, IViewSize } from 'vs/base/browser/ui/grid/grid'; -import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; import { IAction } from 'vs/base/common/actions'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; -import { autorun, derived, IObservable, observableFromEvent, observableValue, transaction } from 'vs/base/common/observable'; +import { autorun, derived, IObservable, observableFromEvent } from 'vs/base/common/observable'; import { IEditorContributionDescription } from 'vs/editor/browser/editorExtensions'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { Range } from 'vs/editor/common/core/range'; +import { Selection } from 'vs/editor/common/core/selection'; import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { MenuId, IMenuService } from 'vs/platform/actions/common/actions'; +import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { Range } from 'vs/editor/common/core/range'; -import { Selection } from 'vs/editor/common/core/selection'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { DEFAULT_EDITOR_MAX_DIMENSIONS, DEFAULT_EDITOR_MIN_DIMENSIONS } from 'vs/workbench/browser/parts/editor/editor'; -import { InputData } from 'vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel'; import { setStyle } from 'vs/workbench/contrib/mergeEditor/browser/utils'; import { MergeEditorViewModel } from 'vs/workbench/contrib/mergeEditor/browser/view/viewModel'; export abstract class CodeEditorView extends Disposable { - private readonly _viewModel = observableValue('viewModel', undefined); - readonly viewModel: IObservable = this._viewModel; - readonly model = this._viewModel.map(m => /** @description model */ m?.model); + readonly model = this.viewModel.map(m => /** @description model */ m?.model); protected readonly htmlElements = h('div.code-view', [ h('div.title@header', [ @@ -98,7 +94,8 @@ export abstract class CodeEditorView extends Disposable { constructor( @IInstantiationService - private readonly instantiationService: IInstantiationService + private readonly instantiationService: IInstantiationService, + public readonly viewModel: IObservable, ) { super(); } @@ -106,22 +103,6 @@ export abstract class CodeEditorView extends Disposable { protected getEditorContributions(): IEditorContributionDescription[] | undefined { return undefined; } - - public setModel( - viewModel: MergeEditorViewModel, - inputData: InputData - ): void { - this.editor.setModel(inputData.textModel); - - reset(this.htmlElements.title, ...renderLabelWithIcons(inputData.title || '')); - reset(this.htmlElements.description, ...(inputData.description ? renderLabelWithIcons(inputData.description) : [])); - reset(this.htmlElements.detail, ...(inputData.detail ? renderLabelWithIcons(inputData.detail) : [])); - - transaction(tx => { - /** @description CodeEditorView: Set Model */ - this._viewModel.set(viewModel, tx); - }); - } } export function createSelectionsAutorun( diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/editors/inputCodeEditorView.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/editors/inputCodeEditorView.ts index 0a4559c0cb936..0d3f633b359f7 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/editors/inputCodeEditorView.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/editors/inputCodeEditorView.ts @@ -3,7 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as dom from 'vs/base/browser/dom'; +import { addDisposableListener, EventType, h, reset } from 'vs/base/browser/dom'; +import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { Toggle } from 'vs/base/browser/ui/toggle/toggle'; import { Action, IAction, Separator } from 'vs/base/common/actions'; import { Codicon } from 'vs/base/common/codicons'; @@ -16,30 +17,27 @@ import { EditorExtensionsRegistry, IEditorContributionDescription } from 'vs/edi import { IModelDeltaDecoration, MinimapPosition, OverviewRulerLane } from 'vs/editor/common/model'; import { CodeLensContribution } from 'vs/editor/contrib/codelens/browser/codelensController'; import { localize } from 'vs/nls'; -import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { MenuId } from 'vs/platform/actions/common/actions'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { attachToggleStyler } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { InputState, ModifiedBaseRangeState } from 'vs/workbench/contrib/mergeEditor/browser/model/modifiedBaseRange'; +import { InputState, ModifiedBaseRange, ModifiedBaseRangeState } from 'vs/workbench/contrib/mergeEditor/browser/model/modifiedBaseRange'; import { applyObservableDecorations, setFields } from 'vs/workbench/contrib/mergeEditor/browser/utils'; import { handledConflictMinimapOverViewRulerColor, unhandledConflictMinimapOverViewRulerColor } from 'vs/workbench/contrib/mergeEditor/browser/view/colors'; +import { MergeEditorViewModel } from 'vs/workbench/contrib/mergeEditor/browser/view/viewModel'; import { EditorGutter, IGutterItemInfo, IGutterItemView } from '../editorGutter'; import { CodeEditorView, createSelectionsAutorun, TitleMenu } from './codeEditorView'; export class InputCodeEditorView extends CodeEditorView { constructor( public readonly inputNumber: 1 | 2, + viewModel: IObservable, @IInstantiationService instantiationService: IInstantiationService, @IContextMenuService contextMenuService: IContextMenuService, @IThemeService themeService: IThemeService, - @IMenuService menuService: IMenuService, - @IContextKeyService contextKeyService: IContextKeyService, ) { - super(instantiationService); - - this._register(applyObservableDecorations(this.editor, this.decorations)); + super(instantiationService, viewModel); this._register( new EditorGutter(this.editor, this.htmlElements.gutterDiv, { @@ -63,6 +61,34 @@ export class InputCodeEditorView extends CodeEditorView { this.htmlElements.toolbar ) ); + + this._register(autorun('input${this.inputNumber}: update labels & text model', reader => { + const vm = this.viewModel.read(reader); + if (!vm) { + return; + } + + this.editor.setModel(this.inputNumber === 1 ? vm.model.input1.textModel : vm.model.input2.textModel); + + const title = this.inputNumber === 1 + ? vm.model.input1.title || localize('input1', 'Input 1') + : vm.model.input2.title || localize('input2', 'Input 2'); + + const description = this.inputNumber === 1 + ? vm.model.input1.description + : vm.model.input2.description; + + const detail = this.inputNumber === 1 + ? vm.model.input1.detail + : vm.model.input2.detail; + + reset(this.htmlElements.title, ...renderLabelWithIcons(title)); + reset(this.htmlElements.description, ...(description ? renderLabelWithIcons(description) : [])); + reset(this.htmlElements.detail, ...(detail ? renderLabelWithIcons(detail) : [])); + })); + + + this._register(applyObservableDecorations(this.editor, this.decorations)); } private readonly modifiedBaseRangeGutterItemInfos = derived(`input${this.inputNumber}.modifiedBaseRangeGutterItemInfos`, reader => { @@ -73,141 +99,7 @@ export class InputCodeEditorView extends CodeEditorView { return model.modifiedBaseRanges.read(reader) .filter((r) => r.getInputDiffs(this.inputNumber).length > 0) - .map((baseRange, idx) => ({ - id: idx.toString(), - range: baseRange.getInputRange(this.inputNumber), - enabled: model.isUpToDate, - toggleState: derived('checkbox is checked', (reader) => { - const input = model - .getState(baseRange) - .read(reader) - .getInput(this.inputNumber); - return input === InputState.second && !baseRange.isOrderRelevant - ? InputState.first - : input; - } - ), - className: derived('checkbox classnames', (reader) => { - const classNames = []; - const active = viewModel.activeModifiedBaseRange.read(reader); - if (!model.hasBaseRange(baseRange)) { - return ''; // Invalid state, should only be observed temporarily - } - const isHandled = model.isHandled(baseRange).read(reader); - if (isHandled) { - classNames.push('handled'); - } - if (baseRange === active) { - classNames.push('focused'); - } - return classNames.join(' '); - }), - setState: (value, tx) => viewModel.setState( - baseRange, - model - .getState(baseRange) - .get() - .withInputValue(this.inputNumber, value), - tx - ), - toggleBothSides() { - transaction(tx => { - /** @description Context Menu: toggle both sides */ - const state = model - .getState(baseRange) - .get(); - model.setState( - baseRange, - state - .toggle(inputNumber) - .toggle(inputNumber === 1 ? 2 : 1), - true, - tx - ); - }); - }, - getContextMenuActions: () => { - const state = model.getState(baseRange).get(); - const handled = model.isHandled(baseRange).get(); - - const update = (newState: ModifiedBaseRangeState) => { - transaction(tx => { - /** @description Context Menu: Update Base Range State */ - return viewModel.setState(baseRange, newState, tx); - }); - }; - - function action(id: string, label: string, targetState: ModifiedBaseRangeState, checked: boolean) { - const action = new Action(id, label, undefined, true, () => { - update(targetState); - }); - action.checked = checked; - return action; - } - const both = state.input1 && state.input2; - - return [ - baseRange.input1Diffs.length > 0 - ? action( - 'mergeEditor.acceptInput1', - localize('mergeEditor.accept', 'Accept {0}', model.input1.title), - state.toggle(1), - state.input1 - ) - : undefined, - baseRange.input2Diffs.length > 0 - ? action( - 'mergeEditor.acceptInput2', - localize('mergeEditor.accept', 'Accept {0}', model.input2.title), - state.toggle(2), - state.input2 - ) - : undefined, - baseRange.isConflicting - ? setFields( - action( - 'mergeEditor.acceptBoth', - localize( - 'mergeEditor.acceptBoth', - 'Accept Both' - ), - state.withInput1(!both).withInput2(!both), - both - ), - { enabled: baseRange.canBeCombined } - ) - : undefined, - new Separator(), - baseRange.isConflicting - ? setFields( - action( - 'mergeEditor.swap', - localize('mergeEditor.swap', 'Swap'), - state.swap(), - false - ), - { enabled: !state.isEmpty && (!both || baseRange.isOrderRelevant) } - ) - : undefined, - - setFields( - new Action( - 'mergeEditor.markAsHandled', - localize('mergeEditor.markAsHandled', 'Mark as Handled'), - undefined, - true, - () => { - transaction((tx) => { - /** @description Context Menu: Mark as handled */ - model.setHandled(baseRange, !handled, tx); - }); - } - ), - { checked: handled } - ), - ].filter(isDefined); - } - })); + .map((baseRange, idx) => new ModifiedBaseRangeGutterItemModel(idx.toString(), baseRange, inputNumber, viewModel)); }); private readonly decorations = derived(`input${this.inputNumber}.decorations`, reader => { @@ -222,66 +114,67 @@ export class InputCodeEditorView extends CodeEditorView { const result = new Array(); for (const modifiedBaseRange of model.modifiedBaseRanges.read(reader)) { - const range = modifiedBaseRange.getInputRange(this.inputNumber); - if (range && !range.isEmpty) { - const blockClassNames = ['merge-editor-block']; - const isHandled = model.isHandled(modifiedBaseRange).read(reader); - if (isHandled) { - blockClassNames.push('handled'); - } - if (modifiedBaseRange === activeModifiedBaseRange) { - blockClassNames.push('focused'); - } - if (modifiedBaseRange.isConflicting) { - blockClassNames.push('conflicting'); + if (!range || !range.isEmpty) { + continue; + } + + const blockClassNames = ['merge-editor-block']; + const isHandled = model.isHandled(modifiedBaseRange).read(reader); + if (isHandled) { + blockClassNames.push('handled'); + } + if (modifiedBaseRange === activeModifiedBaseRange) { + blockClassNames.push('focused'); + } + if (modifiedBaseRange.isConflicting) { + blockClassNames.push('conflicting'); + } + const inputClassName = this.inputNumber === 1 ? 'input1' : 'input2'; + blockClassNames.push(inputClassName); + + result.push({ + range: range.toInclusiveRange()!, + options: { + isWholeLine: true, + blockClassName: blockClassNames.join(' '), + description: 'Merge Editor', + minimap: { + position: MinimapPosition.Gutter, + color: { id: isHandled ? handledConflictMinimapOverViewRulerColor : unhandledConflictMinimapOverViewRulerColor }, + }, + overviewRuler: modifiedBaseRange.isConflicting ? { + position: OverviewRulerLane.Center, + color: { id: isHandled ? handledConflictMinimapOverViewRulerColor : unhandledConflictMinimapOverViewRulerColor }, + } : undefined } - const inputClassName = this.inputNumber === 1 ? 'input1' : 'input2'; - blockClassNames.push(inputClassName); - - result.push({ - range: range.toInclusiveRange()!, - options: { - isWholeLine: true, - blockClassName: blockClassNames.join(' '), - description: 'Merge Editor', - minimap: { - position: MinimapPosition.Gutter, - color: { id: isHandled ? handledConflictMinimapOverViewRulerColor : unhandledConflictMinimapOverViewRulerColor }, - }, - overviewRuler: modifiedBaseRange.isConflicting ? { - position: OverviewRulerLane.Center, - color: { id: isHandled ? handledConflictMinimapOverViewRulerColor : unhandledConflictMinimapOverViewRulerColor }, - } : undefined + }); + + if (modifiedBaseRange.isConflicting || !model.isHandled(modifiedBaseRange).read(reader)) { + const inputDiffs = modifiedBaseRange.getInputDiffs(this.inputNumber); + for (const diff of inputDiffs) { + const range = diff.outputRange.toInclusiveRange(); + if (range) { + result.push({ + range, + options: { + className: `merge-editor-diff ${inputClassName}`, + description: 'Merge Editor', + isWholeLine: true, + } + }); } - }); - if (modifiedBaseRange.isConflicting || !model.isHandled(modifiedBaseRange).read(reader)) { - const inputDiffs = modifiedBaseRange.getInputDiffs(this.inputNumber); - for (const diff of inputDiffs) { - const range = diff.outputRange.toInclusiveRange(); - if (range) { + if (diff.rangeMappings) { + for (const d of diff.rangeMappings) { result.push({ - range, + range: d.outputRange, options: { - className: `merge-editor-diff ${inputClassName}`, - description: 'Merge Editor', - isWholeLine: true, + className: `merge-editor-diff-word ${inputClassName}`, + description: 'Merge Editor' } }); } - - if (diff.rangeMappings) { - for (const d of diff.rangeMappings) { - result.push({ - range: d.outputRange, - options: { - className: `merge-editor-diff-word ${inputClassName}`, - description: 'Merge Editor' - } - }); - } - } } } } @@ -294,23 +187,159 @@ export class InputCodeEditorView extends CodeEditorView { } } -export interface ModifiedBaseRangeGutterItemInfo extends IGutterItemInfo { - enabled: IObservable; - toggleState: IObservable; - setState(value: boolean, tx: ITransaction): void; - toggleBothSides(): void; - getContextMenuActions(): readonly IAction[]; - className: IObservable; +export class ModifiedBaseRangeGutterItemModel implements IGutterItemInfo { + private readonly model = this.viewModel.model; + public readonly range = this.baseRange.getInputRange(this.inputNumber); + + constructor( + public readonly id: string, + private readonly baseRange: ModifiedBaseRange, + private readonly inputNumber: 1 | 2, + private readonly viewModel: MergeEditorViewModel + ) { + } + + public readonly enabled = this.model.isUpToDate; + + public readonly toggleState: IObservable = derived('checkbox is checked', (reader) => { + const input = this.model + .getState(this.baseRange) + .read(reader) + .getInput(this.inputNumber); + return input === InputState.second && !this.baseRange.isOrderRelevant + ? InputState.first + : input; + }); + + public readonly state: IObservable<{ handled: boolean; focused: boolean }> = derived('checkbox state', (reader) => { + const active = this.viewModel.activeModifiedBaseRange.read(reader); + if (!this.model.hasBaseRange(this.baseRange)) { + return { handled: false, focused: false }; // Invalid state, should only be observed temporarily + } + return { + handled: this.model.isHandled(this.baseRange).read(reader), + focused: this.baseRange === active, + }; + }); + + public setState(value: boolean, tx: ITransaction): void { + this.viewModel.setState( + this.baseRange, + this.model + .getState(this.baseRange) + .get() + .withInputValue(this.inputNumber, value), + tx + ); + } + public toggleBothSides(): void { + transaction(tx => { + /** @description Context Menu: toggle both sides */ + const state = this.model + .getState(this.baseRange) + .get(); + this.model.setState( + this.baseRange, + state + .toggle(this.inputNumber) + .toggle(this.inputNumber === 1 ? 2 : 1), + true, + tx + ); + }); + } + + public getContextMenuActions(): readonly IAction[] { + const state = this.model.getState(this.baseRange).get(); + const handled = this.model.isHandled(this.baseRange).get(); + + const update = (newState: ModifiedBaseRangeState) => { + transaction(tx => { + /** @description Context Menu: Update Base Range State */ + return this.viewModel.setState(this.baseRange, newState, tx); + }); + }; + + function action(id: string, label: string, targetState: ModifiedBaseRangeState, checked: boolean) { + const action = new Action(id, label, undefined, true, () => { + update(targetState); + }); + action.checked = checked; + return action; + } + const both = state.input1 && state.input2; + + return [ + this.baseRange.input1Diffs.length > 0 + ? action( + 'mergeEditor.acceptInput1', + localize('mergeEditor.accept', 'Accept {0}', this.model.input1.title), + state.toggle(1), + state.input1 + ) + : undefined, + this.baseRange.input2Diffs.length > 0 + ? action( + 'mergeEditor.acceptInput2', + localize('mergeEditor.accept', 'Accept {0}', this.model.input2.title), + state.toggle(2), + state.input2 + ) + : undefined, + this.baseRange.isConflicting + ? setFields( + action( + 'mergeEditor.acceptBoth', + localize( + 'mergeEditor.acceptBoth', + 'Accept Both' + ), + state.withInput1(!both).withInput2(!both), + both + ), + { enabled: this.baseRange.canBeCombined } + ) + : undefined, + new Separator(), + this.baseRange.isConflicting + ? setFields( + action( + 'mergeEditor.swap', + localize('mergeEditor.swap', 'Swap'), + state.swap(), + false + ), + { enabled: !state.isEmpty && (!both || this.baseRange.isOrderRelevant) } + ) + : undefined, + + setFields( + new Action( + 'mergeEditor.markAsHandled', + localize('mergeEditor.markAsHandled', 'Mark as Handled'), + undefined, + true, + () => { + transaction((tx) => { + /** @description Context Menu: Mark as handled */ + this.model.setHandled(this.baseRange, !handled, tx); + }); + } + ), + { checked: handled } + ), + ].filter(isDefined); + } } -export class MergeConflictGutterItemView extends Disposable implements IGutterItemView { - private readonly item: ISettableObservable; +export class MergeConflictGutterItemView extends Disposable implements IGutterItemView { + private readonly item: ISettableObservable; private readonly checkboxDiv: HTMLDivElement; private readonly isMultiLine = observableValue('isMultiLine', false); constructor( - item: ModifiedBaseRangeGutterItemInfo, + item: ModifiedBaseRangeGutterItemModel, target: HTMLElement, contextMenuService: IContextMenuService, themeService: IThemeService @@ -324,11 +353,12 @@ export class MergeConflictGutterItemView extends Disposable implements IGutterIt title: '', icon: Codicon.check }); + checkBox.domNode.classList.add('accept-conflict-group'); this._register(attachToggleStyler(checkBox, themeService)); this._register( - dom.addDisposableListener(checkBox.domNode, dom.EventType.MOUSE_DOWN, (e) => { + addDisposableListener(checkBox.domNode, EventType.MOUSE_DOWN, (e) => { const item = this.item.get(); if (!item) { return; @@ -352,8 +382,6 @@ export class MergeConflictGutterItemView extends Disposable implements IGutterIt }) ); - checkBox.domNode.classList.add('accept-conflict-group'); - this._register( autorun('Update Checkbox', (reader) => { const item = this.item.read(reader)!; @@ -378,14 +406,14 @@ export class MergeConflictGutterItemView extends Disposable implements IGutterIt ); this._register(autorun('Update Checkbox CSS ClassNames', (reader) => { - let className = this.item.read(reader).className.read(reader); - className += ' merge-accept-gutter-marker'; - if (this.isMultiLine.read(reader)) { - className += ' multi-line'; - } else { - className += ' single-line'; - } - target.className = className; + const state = this.item.read(reader).state.read(reader); + const classNames = [ + 'merge-accept-gutter-marker', + state.handled && 'handled', + state.focused && 'focused', + this.isMultiLine.read(reader) ? 'multi-line' : 'single-line', + ]; + target.className = classNames.filter(c => typeof c === 'string').join(' '); })); this._register(checkBox.onChange(() => { @@ -395,9 +423,9 @@ export class MergeConflictGutterItemView extends Disposable implements IGutterIt }); })); - target.appendChild(dom.h('div.background', [noBreakWhitespace]).root); + target.appendChild(h('div.background', [noBreakWhitespace]).root); target.appendChild( - this.checkboxDiv = dom.h('div.checkbox', [dom.h('div.checkbox-background', [checkBox.domNode])]).root + this.checkboxDiv = h('div.checkbox', [h('div.checkbox-background', [checkBox.domNode])]).root ); } @@ -432,7 +460,7 @@ export class MergeConflictGutterItemView extends Disposable implements IGutterIt }); } - update(baseRange: ModifiedBaseRangeGutterItemInfo): void { + update(baseRange: ModifiedBaseRangeGutterItemModel): void { transaction(tx => { /** @description MergeConflictGutterItemView: Updating new base range */ this.item.set(baseRange, tx); diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/editors/resultCodeEditorView.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/editors/resultCodeEditorView.ts index 4998ecdd4975e..3c2d739087d74 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/editors/resultCodeEditorView.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/editors/resultCodeEditorView.ts @@ -3,24 +3,107 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { reset } from 'vs/base/browser/dom'; +import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { CompareResult } from 'vs/base/common/arrays'; import { BugIndicatingError } from 'vs/base/common/errors'; import { toDisposable } from 'vs/base/common/lifecycle'; -import { autorun, derived } from 'vs/base/common/observable'; +import { autorun, derived, IObservable } from 'vs/base/common/observable'; import { IModelDeltaDecoration, MinimapPosition, OverviewRulerLane } from 'vs/editor/common/model'; import { localize } from 'vs/nls'; import { MenuId } from 'vs/platform/actions/common/actions'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ILabelService } from 'vs/platform/label/common/label'; import { MergeMarkersController } from 'vs/workbench/contrib/mergeEditor/browser/mergeMarkers/mergeMarkersController'; import { LineRange } from 'vs/workbench/contrib/mergeEditor/browser/model/lineRange'; import { applyObservableDecorations, join } from 'vs/workbench/contrib/mergeEditor/browser/utils'; import { handledConflictMinimapOverViewRulerColor, unhandledConflictMinimapOverViewRulerColor } from 'vs/workbench/contrib/mergeEditor/browser/view/colors'; import { EditorGutter } from 'vs/workbench/contrib/mergeEditor/browser/view/editorGutter'; +import { MergeEditorViewModel } from 'vs/workbench/contrib/mergeEditor/browser/view/viewModel'; import { ctxIsMergeResultEditor } from 'vs/workbench/contrib/mergeEditor/common/mergeEditor'; import { CodeEditorView, createSelectionsAutorun, TitleMenu } from './codeEditorView'; export class ResultCodeEditorView extends CodeEditorView { + constructor( + viewModel: IObservable, + @IInstantiationService instantiationService: IInstantiationService, + @ILabelService private readonly _labelService: ILabelService, + ) { + super(instantiationService, viewModel); + + this.editor.invokeWithinContext(accessor => { + const contextKeyService = accessor.get(IContextKeyService); + const isMergeResultEditor = ctxIsMergeResultEditor.bindTo(contextKeyService); + isMergeResultEditor.set(true); + this._register(toDisposable(() => isMergeResultEditor.reset())); + }); + + this._register(new MergeMarkersController(this.editor, this.viewModel)); + + this.htmlElements.gutterDiv.style.width = '5px'; + + this._register( + new EditorGutter(this.editor, this.htmlElements.gutterDiv, { + getIntersectingGutterItems: (range, reader) => [], + createView: (item, target) => { throw new BugIndicatingError(); }, + }) + ); + + this._register(autorun('update labels & text model', reader => { + const vm = this.viewModel.read(reader); + if (!vm) { + return; + } + this.editor.setModel(vm.model.resultTextModel); + reset(this.htmlElements.title, ...renderLabelWithIcons(localize('result', 'Result'))); + reset(this.htmlElements.description, ...renderLabelWithIcons(this._labelService.getUriLabel(vm.model.resultTextModel.uri, { relative: true }))); + })); + + + this._register(autorun('update remainingConflicts label', reader => { + // this is a bit of a hack, but it's the easiest way to get the label to update + // when the view model updates, as the the base class resets the label in the setModel call. + this.viewModel.read(reader); + + const model = this.model.read(reader); + if (!model) { + return; + } + const count = model.unhandledConflictsCount.read(reader); + + this.htmlElements.detail.innerText = count === 1 + ? localize( + 'mergeEditor.remainingConflicts', + '{0} Conflict Remaining', + count + ) + : localize( + 'mergeEditor.remainingConflict', + '{0} Conflicts Remaining ', + count + ); + + })); + + + this._register(applyObservableDecorations(this.editor, this.decorations)); + + this._register( + createSelectionsAutorun(this, (baseRange, viewModel) => + viewModel.model.translateBaseRangeToResult(baseRange) + ) + ); + + this._register( + instantiationService.createInstance( + TitleMenu, + MenuId.MergeInputResultToolbar, + this.htmlElements.toolbar + ) + ); + } + private readonly decorations = derived('result.decorations', reader => { const viewModel = this.viewModel.read(reader); if (!viewModel) { @@ -111,69 +194,4 @@ export class ResultCodeEditorView extends CodeEditorView { } return result; }); - - constructor( - @IInstantiationService instantiationService: IInstantiationService, - ) { - super(instantiationService); - - this.editor.invokeWithinContext(accessor => { - const contextKeyService = accessor.get(IContextKeyService); - const isMergeResultEditor = ctxIsMergeResultEditor.bindTo(contextKeyService); - isMergeResultEditor.set(true); - this._register(toDisposable(() => isMergeResultEditor.reset())); - }); - - this._register(applyObservableDecorations(this.editor, this.decorations)); - - this._register(new MergeMarkersController(this.editor, this.viewModel)); - - this.htmlElements.gutterDiv.style.width = '5px'; - - this._register( - new EditorGutter(this.editor, this.htmlElements.gutterDiv, { - getIntersectingGutterItems: (range, reader) => [], - createView: (item, target) => { throw new BugIndicatingError(); }, - }) - ); - - this._register(autorun('update remainingConflicts label', reader => { - // this is a bit of a hack, but it's the easiest way to get the label to update - // when the view model updates, as the the base class resets the label in the setModel call. - this.viewModel.read(reader); - - const model = this.model.read(reader); - if (!model) { - return; - } - const count = model.unhandledConflictsCount.read(reader); - - this.htmlElements.detail.innerText = count === 1 - ? localize( - 'mergeEditor.remainingConflicts', - '{0} Conflict Remaining', - count - ) - : localize( - 'mergeEditor.remainingConflict', - '{0} Conflicts Remaining ', - count - ); - - })); - - this._register( - createSelectionsAutorun(this, (baseRange, viewModel) => - viewModel.model.translateBaseRangeToResult(baseRange) - ) - ); - - this._register( - instantiationService.createInstance( - TitleMenu, - MenuId.MergeInputResultToolbar, - this.htmlElements.toolbar - ) - ); - } } diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts index 2299cd64a0066..8ef55539449ea 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts @@ -4,17 +4,16 @@ *--------------------------------------------------------------------------------------------*/ import { $, Dimension, reset } from 'vs/base/browser/dom'; -import { Direction, Grid, IView, SerializableGrid } from 'vs/base/browser/ui/grid/grid'; -import { Orientation, Sizing } from 'vs/base/browser/ui/splitview/splitview'; +import { Grid, GridNodeDescriptor, IView, SerializableGrid } from 'vs/base/browser/ui/grid/grid'; +import { Orientation } from 'vs/base/browser/ui/splitview/splitview'; import { compareBy } from 'vs/base/common/arrays'; import { assertFn } from 'vs/base/common/assert'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Color } from 'vs/base/common/color'; import { BugIndicatingError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; -import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; -import { autorun, autorunWithStore, IObservable, IReader } from 'vs/base/common/observable'; -import { ObservableValue } from 'vs/base/common/observableImpl/base'; +import { Disposable, DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { autorun, autorunWithStore, IObservable, IReader, observableValue } from 'vs/base/common/observable'; import { basename, isEqual } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import 'vs/css!./media/mergeEditor'; @@ -33,7 +32,6 @@ import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/c import { IEditorOptions, ITextEditorOptions, ITextResourceEditorInput } from 'vs/platform/editor/common/editor'; import { IFileService } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ILabelService } from 'vs/platform/label/common/label'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -59,54 +57,33 @@ import './colors'; import { InputCodeEditorView } from './editors/inputCodeEditorView'; import { ResultCodeEditorView } from './editors/resultCodeEditorView'; -class MergeEditorLayout { - - private static readonly _key = 'mergeEditor/layout'; - private _value: MergeEditorLayoutTypes = 'mixed'; - - - constructor(@IStorageService private _storageService: IStorageService) { - const value = _storageService.get(MergeEditorLayout._key, StorageScope.PROFILE, 'mixed'); - if (value === 'mixed' || value === 'columns' || value === 'mixedWithBase') { - this._value = value; - } else { - this._value = 'mixed'; - } - } - - get value() { - return this._value; - } - - set value(value) { - if (this._value !== value) { - this._value = value; - this._storageService.store(MergeEditorLayout._key, this._value, StorageScope.PROFILE, StorageTarget.USER); - } - } -} - export class MergeEditor extends AbstractTextEditor { static readonly ID = 'mergeEditor'; private readonly _sessionDisposables = new DisposableStore(); + private readonly _viewModel = observableValue('viewModel', undefined); - private _grid!: Grid; - private readonly input1View = this._register(this.instantiationService.createInstance(InputCodeEditorView, 1)); - private readonly baseView = new ObservableValue('baseView', undefined); - private readonly baseViewOptions = new ObservableValue | undefined>('baseViewOptions', undefined); - private readonly input2View = this._register(this.instantiationService.createInstance(InputCodeEditorView, 2)); - private readonly inputResultView = this._register(this.instantiationService.createInstance(ResultCodeEditorView)); + public get viewModel(): IObservable { + return this._viewModel; + } + + private rootHtmlElement: HTMLElement | undefined; + private readonly _grid = this._register(new MutableDisposable>()); + private readonly input1View = this._register(this.instantiationService.createInstance(InputCodeEditorView, 1, this._viewModel)); + private readonly baseView = observableValue('baseView', undefined); + private readonly baseViewOptions = observableValue | undefined>('baseViewOptions', undefined); + private readonly input2View = this._register(this.instantiationService.createInstance(InputCodeEditorView, 2, this._viewModel)); + private readonly inputResultView = this._register(this.instantiationService.createInstance(ResultCodeEditorView, this._viewModel)); private readonly _layoutMode: MergeEditorLayout; private readonly _ctxIsMergeEditor: IContextKey; private readonly _ctxUsesColumnLayout: IContextKey; private readonly _ctxResultUri: IContextKey; + private readonly _ctxBaseUri: IContextKey; - private _model: MergeEditorModel | undefined; - public get model(): MergeEditorModel | undefined { return this._model; } + public get model(): MergeEditorModel | undefined { return this._viewModel.get()?.model; } private get inputsWritable(): boolean { return !!this._configurationService.getValue('mergeEditor.writableInputs'); @@ -114,7 +91,6 @@ export class MergeEditor extends AbstractTextEditor { constructor( @IInstantiationService instantiation: IInstantiationService, - @ILabelService private readonly _labelService: ILabelService, @IContextKeyService contextKeyService: IContextKeyService, @ITelemetryService telemetryService: ITelemetryService, @IStorageService storageService: IStorageService, @@ -240,10 +216,6 @@ export class MergeEditor extends AbstractTextEditor { ); } - public get viewModel(): IObservable { - return this.input1View.viewModel; - } - override dispose(): void { this._sessionDisposables.dispose(); this._ctxIsMergeEditor.reset(); @@ -273,35 +245,9 @@ export class MergeEditor extends AbstractTextEditor { } protected createEditorControl(parent: HTMLElement, initialOptions: ICodeEditorOptions): void { + this.rootHtmlElement = parent; parent.classList.add('merge-editor'); - - this._grid = SerializableGrid.from({ - orientation: Orientation.VERTICAL, - size: 100, - groups: [ - { - size: 38, - groups: [{ - data: this.input1View.view - }, { - data: this.input2View.view - }] - }, - { - size: 62, - data: this.inputResultView.view - }, - ] - }, { - styles: { separatorBorder: this.theme.getColor(settingsSashBorder) ?? Color.transparent }, - proportionalLayout: true - }); - - reset(parent, this._grid.element); - this._register(this._grid); - this.applyLayout(this._layoutMode.value); - this.applyOptions(initialOptions); } @@ -328,7 +274,7 @@ export class MergeEditor extends AbstractTextEditor { } layout(dimension: Dimension): void { - this._grid.layout(dimension.width, dimension.height); + this._grid.value?.layout(dimension.width, dimension.height); } override async setInput(input: EditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { @@ -340,28 +286,9 @@ export class MergeEditor extends AbstractTextEditor { this._sessionDisposables.clear(); const model = await input.resolve(); - this._model = model; const viewModel = new MergeEditorViewModel(model, this.input1View, this.input2View, this.inputResultView, this.baseView); - - this.input1View.setModel(viewModel, { ...model.input1, title: model.input1.title || localize('input1', 'Input 1') }); - this.input2View.setModel(viewModel, { ...model.input2, title: model.input2.title || localize('input2', 'Input 2') }); - - this._sessionDisposables.add(autorun('Set baseView viewModel', (reader) => { - const baseView = this.baseView.read(reader); - if (baseView) { - baseView.setModel(viewModel, { textModel: model.base, title: localize('base', 'Base'), description: '', detail: undefined }); - } - })); - - this.inputResultView.setModel(viewModel, - { - textModel: model.resultTextModel, - title: localize('result', 'Result'), - description: this._labelService.getUriLabel(model.resultTextModel.uri, { relative: true }), - detail: undefined, - }, - ); + this._viewModel.set(viewModel, undefined); // Set/unset context keys based on input this._ctxResultUri.set(model.resultTextModel.uri.toString()); @@ -621,31 +548,86 @@ export class MergeEditor extends AbstractTextEditor { } applyLayout(layout: MergeEditorLayoutTypes): void { - const showBaseView = (visible: boolean) => { - if (visible && !this.baseView.get()) { - this.baseView.set(this.instantiationService.createInstance(BaseCodeEditorView), undefined); - this._grid.addView(this.baseView.get()!.view, Sizing.Distribute, this.input1View.view, Direction.Right); - } else if (!visible && this.baseView.get()) { - this._grid.removeView(this.baseView.get()!.view); + const setBaseViewState = (enabled: boolean) => { + if (enabled && !this.baseView.get()) { + this.baseView.set(this.instantiationService.createInstance(BaseCodeEditorView, this.viewModel), undefined); + } else if (!enabled && this.baseView.get()) { this.baseView.get()!.dispose(); this.baseView.set(undefined, undefined); } }; if (layout === 'mixed') { - showBaseView(false); - this._grid.moveView(this.inputResultView.view, this._grid.height * .62, this.input1View.view, Direction.Down); - this._grid.moveView(this.input2View.view, Sizing.Distribute, this.input1View.view, Direction.Right); + setBaseViewState(false); + this.setGrid([ + { + size: 38, + groups: [{ data: this.input1View.view }, { data: this.input2View.view }] + }, + { + size: 62, + data: this.inputResultView.view + }, + ]); } else if (layout === 'columns') { - showBaseView(false); - this._grid.moveView(this.inputResultView.view, Sizing.Distribute, this.input1View.view, Direction.Right); + setBaseViewState(false); + this.setGrid([ + { + groups: [{ data: this.input1View.view }, { data: this.inputResultView.view }, { data: this.input2View.view }] + }, + ]); + } else if (layout === 'mixedWithBaseColumns') { + setBaseViewState(true); + + this.setGrid([ + { + size: 38, + groups: [{ data: this.input1View.view }, { data: this.baseView.get()!.view }, { data: this.input2View.view }] + }, + { + size: 62, + data: this.inputResultView.view + } + ]); } else if (layout === 'mixedWithBase') { - showBaseView(true); + setBaseViewState(true); - this._grid.moveView(this.inputResultView.view, this._grid.height * .62, this.input1View.view, Direction.Down); - this._grid.moveView(this.input2View.view, Sizing.Distribute, this.input1View.view, Direction.Right); - this._grid.moveView(this.baseView.get()!.view, Sizing.Distribute, this.input1View.view, Direction.Right); + this.setGrid([ + { + size: 38, + data: this.baseView.get()!.view + }, + { + size: 38, + groups: [{ data: this.input1View.view }, { data: this.input2View.view }] + }, + { + size: 62, + data: this.inputResultView.view + } + ]); + } + } + + private setGrid(descriptor: GridNodeDescriptor[]) { + let width = -1; + let height = -1; + if (this._grid.value) { + width = this._grid.value.width; + height = this._grid.value.height; + } + this._grid.value = SerializableGrid.from({ + orientation: Orientation.VERTICAL, + size: 100, + groups: descriptor, + }, { + styles: { separatorBorder: this.theme.getColor(settingsSashBorder) ?? Color.transparent }, + proportionalLayout: true + }); + if (width !== -1) { + this._grid.value.layout(width, height); } + reset(this.rootHtmlElement!, this._grid.value.element); } private _applyViewState(state: IMergeEditorViewState | undefined) { @@ -684,6 +666,33 @@ export class MergeEditor extends AbstractTextEditor { } } +class MergeEditorLayout { + + private static readonly _key = 'mergeEditor/layout'; + private _value: MergeEditorLayoutTypes = 'mixed'; + + + constructor(@IStorageService private _storageService: IStorageService) { + const value = _storageService.get(MergeEditorLayout._key, StorageScope.PROFILE, 'mixed'); + if (value === 'mixed' || value === 'columns' || value === 'mixedWithBase') { + this._value = value; + } else { + this._value = 'mixed'; + } + } + + get value() { + return this._value; + } + + set value(value) { + if (this._value !== value) { + this._value = value; + this._storageService.store(MergeEditorLayout._key, this._value, StorageScope.PROFILE, StorageTarget.USER); + } + } +} + export class MergeEditorOpenHandlerContribution extends Disposable { constructor( diff --git a/src/vs/workbench/contrib/mergeEditor/common/mergeEditor.ts b/src/vs/workbench/contrib/mergeEditor/common/mergeEditor.ts index 9cae55facafa5..9734e6cf1c1e4 100644 --- a/src/vs/workbench/contrib/mergeEditor/common/mergeEditor.ts +++ b/src/vs/workbench/contrib/mergeEditor/common/mergeEditor.ts @@ -6,7 +6,7 @@ import { localize } from 'vs/nls'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -export type MergeEditorLayoutTypes = 'mixed' | 'columns' | 'mixedWithBase'; +export type MergeEditorLayoutTypes = 'mixed' | 'columns' | 'mixedWithBase' | 'mixedWithBaseColumns'; export const ctxIsMergeEditor = new RawContextKey('isMergeEditor', false, { type: 'boolean', description: localize('is', 'The editor is a merge editor') }); export const ctxIsMergeResultEditor = new RawContextKey('isMergeResultEditor', false, { type: 'boolean', description: localize('isr', 'The editor is a the result editor of a merge editor.') });