merge editor temp model (#161315)

Introduces hidden mergeEditor.useWorkingCopy setting,
implements reset when the merge editor opens
This commit is contained in:
Henning Dieterichs 2022-09-21 16:02:50 +02:00 committed by GitHub
parent 2046342bf3
commit 40a262dc13
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 871 additions and 261 deletions

View file

@ -1148,21 +1148,38 @@ export class CommandCenter {
}
@command('git.acceptMerge')
async acceptMerge(uri: Uri | unknown): Promise<void> {
if (!(uri instanceof Uri)) {
async acceptMerge(_uri: Uri | unknown): Promise<void> {
const { activeTab } = window.tabGroups.activeTabGroup;
if (!activeTab) {
return;
}
if (!(activeTab.input instanceof TabInputTextMerge)) {
return;
}
const uri = activeTab.input.result;
const repository = this.model.getRepository(uri);
if (!repository) {
console.log(`FAILED to accept merge because uri ${uri.toString()} doesn't belong to any repository`);
return;
}
const { activeTab } = window.tabGroups.activeTabGroup;
if (!activeTab) {
const result = await commands.executeCommand('mergeEditor.acceptMerge') as { successful: boolean };
if (result.successful) {
await repository.add([uri]);
await commands.executeCommand('workbench.view.scm');
}
/*
if (!(uri instanceof Uri)) {
return;
}
// make sure to save the merged document
const doc = workspace.textDocuments.find(doc => doc.uri.toString() === uri.toString());
if (!doc) {
@ -1185,7 +1202,7 @@ export class CommandCenter {
if (didCloseTab) {
await repository.add([uri]);
await commands.executeCommand('workbench.view.scm');
}
}*/
}
@command('git.runGitMerge')

View file

@ -51,13 +51,23 @@ export function waitForState<T, TState extends T>(observable: IObservable<T>, pr
export function waitForState<T>(observable: IObservable<T>, predicate: (state: T) => boolean): Promise<T>;
export function waitForState<T>(observable: IObservable<T>, predicate: (state: T) => boolean): Promise<T> {
return new Promise(resolve => {
let didRun = false;
let shouldDispose = false;
const d = autorun('waitForState', reader => {
const currentState = observable.read(reader);
if (predicate(currentState)) {
d.dispose();
if (!didRun) {
shouldDispose = true;
} else {
d.dispose();
}
resolve(currentState);
}
});
didRun = true;
if (shouldDispose) {
d.dispose();
}
});
}

View file

@ -3,6 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { assertFn, checkAdjacentItems } from 'vs/base/common/assert';
import { Position } from 'vs/editor/common/core/position';
import { Range } from 'vs/editor/common/core/range';
import { SequenceFromIntArray, OffsetRange, SequenceDiff, ISequence } from 'vs/editor/common/diff/algorithms/diffAlgorithm';
@ -91,7 +92,9 @@ export function lineRangeMappingFromRangeMappings(alignments: RangeMapping[]): L
const changes: LineRangeMapping[] = [];
for (const g of group(
alignments,
(a1, a2) => a2.modifiedRange.startLineNumber - (a1.modifiedRange.endLineNumber - (a1.modifiedRange.endColumn > 1 ? 0 : 1)) <= 1
(a1, a2) =>
(a2.originalRange.startLineNumber - (a1.originalRange.endLineNumber - (a1.originalRange.endColumn > 1 ? 0 : 1)) <= 1)
|| (a2.modifiedRange.startLineNumber - (a1.modifiedRange.endLineNumber - (a1.modifiedRange.endColumn > 1 ? 0 : 1)) <= 1)
)) {
const first = g[0];
const last = g[g.length - 1];
@ -108,6 +111,17 @@ export function lineRangeMappingFromRangeMappings(alignments: RangeMapping[]): L
g
));
}
assertFn(() => {
return checkAdjacentItems(changes,
(m1, m2) => m2.originalRange.startLineNumber - m1.originalRange.endLineNumberExclusive === m2.modifiedRange.startLineNumber - m1.modifiedRange.endLineNumberExclusive &&
// There has to be an unchanged line in between (otherwise both diffs should have been joined)
m1.originalRange.endLineNumberExclusive < m2.originalRange.startLineNumber &&
m1.modifiedRange.endLineNumberExclusive < m2.modifiedRange.startLineNumber,
);
});
return changes;
}

View file

@ -8,11 +8,13 @@ import { URI, UriComponents } from 'vs/base/common/uri';
import { localize } from 'vs/nls';
import { ILocalizedString } from 'vs/platform/action/common/action';
import { Action2, IAction2Options, MenuId } from 'vs/platform/actions/common/actions';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { ITextEditorOptions } from 'vs/platform/editor/common/editor';
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { IResourceMergeEditorInput } from 'vs/workbench/common/editor';
import { MergeEditorInputData } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput';
import { IEditorIdentifier, IResourceMergeEditorInput } from 'vs/workbench/common/editor';
import { MergeEditorInput, MergeEditorInputData } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput';
import { IMergeEditorInputModel } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInputModel';
import { MergeEditor } from 'vs/workbench/contrib/mergeEditor/browser/view/mergeEditor';
import { MergeEditorViewModel } from 'vs/workbench/contrib/mergeEditor/browser/view/viewModel';
import { ctxIsMergeEditor, ctxMergeEditorLayout, ctxMergeEditorShowBase } from 'vs/workbench/contrib/mergeEditor/common/mergeEditor';
@ -37,6 +39,41 @@ abstract class MergeEditorAction extends Action2 {
abstract runWithViewModel(viewModel: MergeEditorViewModel, accessor: ServicesAccessor): void;
}
interface MergeEditorAction2Args {
inputModel: IMergeEditorInputModel;
viewModel: MergeEditorViewModel;
input: MergeEditorInput;
editorIdentifier: IEditorIdentifier;
}
abstract class MergeEditorAction2 extends Action2 {
constructor(desc: Readonly<IAction2Options>) {
super(desc);
}
run(accessor: ServicesAccessor): void {
const { activeEditorPane } = accessor.get(IEditorService);
if (activeEditorPane instanceof MergeEditor) {
const vm = activeEditorPane.viewModel.get();
if (!vm) {
return;
}
return this.runWithMergeEditor({
viewModel: vm,
inputModel: activeEditorPane.inputModel.get()!,
input: activeEditorPane.input as MergeEditorInput,
editorIdentifier: {
editor: activeEditorPane.input,
groupId: activeEditorPane.group.id,
}
}, accessor) as any;
}
}
abstract runWithMergeEditor(args: MergeEditorAction2Args, accessor: ServicesAccessor): unknown;
}
export class OpenMergeEditor extends Action2 {
constructor() {
super({
@ -531,3 +568,49 @@ export class ResetDirtyConflictsToBaseCommand extends MergeEditorAction {
viewModel.model.resetDirtyConflictsToBase();
}
}
// this is an API command
export class AcceptMerge extends MergeEditorAction2 {
constructor() {
super({
id: 'mergeEditor.acceptMerge',
category: mergeEditorCategory,
title: {
value: localize(
'mergeEditor.acceptMerge',
'Accept Merge'
),
original: 'Accept Merge',
},
f1: false,
precondition: ctxIsMergeEditor
});
}
override async runWithMergeEditor({ inputModel, editorIdentifier, viewModel }: MergeEditorAction2Args, accessor: ServicesAccessor) {
const dialogService = accessor.get(IDialogService);
const editorService = accessor.get(IEditorService);
if (viewModel.model.unhandledConflictsCount.get() > 0) {
const confirmResult = await dialogService.confirm({
type: 'info',
message: localize('mergeEditor.acceptMerge.unhandledConflicts', "There are still unhandled conflicts. Are you sure you want to accept the merge?"),
primaryButton: localize('mergeEditor.acceptMerge.unhandledConflicts.accept', "Accept merge with unhandled conflicts"),
secondaryButton: localize('mergeEditor.acceptMerge.unhandledConflicts.cancel', "Cancel"),
});
if (!confirmResult.confirmed) {
return {
successful: false
};
}
}
await inputModel.accept();
await editorService.closeEditor(editorIdentifier);
return {
successful: true
};
}
}

View file

@ -11,10 +11,10 @@ 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, ShowHideBase, ToggleActiveConflictInput1, ToggleActiveConflictInput2 } from 'vs/workbench/contrib/mergeEditor/browser/commands/commands';
import { MergeEditorCopyContentsToJSON, MergeEditorSaveContentsToFolder, MergeEditorLoadContentsFromFolder } from 'vs/workbench/contrib/mergeEditor/browser/commands/devCommands';
import { AcceptAllInput1, AcceptAllInput2, AcceptMerge, CompareInput1WithBaseCommand, CompareInput2WithBaseCommand, GoToNextUnhandledConflict, GoToPreviousUnhandledConflict, OpenBaseFile, OpenMergeEditor, OpenResultResource, ResetDirtyConflictsToBaseCommand, ResetToBaseAndAutoMergeCommand, SetColumnLayout, SetMixedLayout, ShowHideBase, ToggleActiveConflictInput1, ToggleActiveConflictInput2 } from 'vs/workbench/contrib/mergeEditor/browser/commands/commands';
import { MergeEditorCopyContentsToJSON, MergeEditorLoadContentsFromFolder, MergeEditorSaveContentsToFolder } 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';
import { MergeEditor, MergeEditorOpenHandlerContribution, MergeEditorResolverContribution } from 'vs/workbench/contrib/mergeEditor/browser/view/mergeEditor';
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
import { MergeEditorSerializer } from './mergeEditorSerializer';
@ -70,6 +70,8 @@ registerAction2(AcceptAllInput2);
registerAction2(ResetToBaseAndAutoMergeCommand);
registerAction2(ResetDirtyConflictsToBaseCommand);
registerAction2(AcceptMerge);
// Dev Commands
registerAction2(MergeEditorCopyContentsToJSON);
registerAction2(MergeEditorSaveContentsToFolder);
@ -82,3 +84,23 @@ Registry
Registry
.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench)
.registerWorkbenchContribution(MergeEditorResolverContribution, 'MergeEditorResolverContribution', LifecyclePhase.Starting);
/*
class MergeEditorWorkbenchContribution extends Disposable implements IWorkbenchContribution {
constructor(@IWorkingCopyEditorService private readonly _workingCopyEditorService: IWorkingCopyEditorService) {
super();
this._register(
_workingCopyEditorService.registerHandler({
createEditor(workingCopy) {
throw new BugIndicatingError('not supported');
},
handles(workingCopy) {
return workingCopy.typeId === '';
},
isOpen(workingCopy, editor) {
return workingCopy.resource.toString() === that._model?.resultTextModel.uri.toString();
},
}));
}
}
*/

View file

@ -3,26 +3,22 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { DisposableStore } from 'vs/base/common/lifecycle';
import { basename, isEqual } from 'vs/base/common/resources';
import Severity from 'vs/base/common/severity';
import { assertFn } from 'vs/base/common/assert';
import { autorun } from 'vs/base/common/observable';
import { isEqual } from 'vs/base/common/resources';
import { isDefined } from 'vs/base/common/types';
import { URI } from 'vs/base/common/uri';
import { ITextModelService } from 'vs/editor/common/services/resolverService';
import { localize } from 'vs/nls';
import { ConfirmResult, IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
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 { DEFAULT_EDITOR_ASSOCIATION, EditorInputCapabilities, IEditorIdentifier, IResourceMergeEditorInput, isResourceMergeEditorInput, IUntypedEditorInput } from 'vs/workbench/common/editor';
import { DEFAULT_EDITOR_ASSOCIATION, EditorInputCapabilities, IResourceMergeEditorInput, IRevertOptions, isResourceMergeEditorInput, IUntypedEditorInput } from 'vs/workbench/common/editor';
import { EditorInput, IEditorCloseHandler } from 'vs/workbench/common/editor/editorInput';
import { AbstractTextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput';
import { MergeDiffComputer } from 'vs/workbench/contrib/mergeEditor/browser/model/diffComputer';
import { InputData, MergeEditorModel } from 'vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel';
import { IMergeEditorInputModel, TempFileMergeEditorModeFactory, WorkspaceMergeEditorModeFactory } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInputModel';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { ILanguageSupport, ITextFileEditorModel, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { autorun } from 'vs/base/common/observable';
import { WorkerBasedDocumentDiffProvider } from 'vs/editor/browser/widget/workerBasedDocumentDiffProvider';
import { ProjectedDiffComputer } from 'vs/workbench/contrib/mergeEditor/browser/model/projectedDocumentDiffProvider';
import { ILanguageSupport, ITextFileSaveOptions, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
export class MergeEditorInputData {
constructor(
@ -34,13 +30,22 @@ export class MergeEditorInputData {
}
export class MergeEditorInput extends AbstractTextResourceEditorInput implements ILanguageSupport {
static readonly ID = 'mergeEditor.Input';
private _model?: MergeEditorModel;
private _outTextModel?: ITextFileEditorModel;
private _inputModel?: IMergeEditorInputModel;
override closeHandler: MergeEditorCloseHandler | undefined;
override closeHandler: IEditorCloseHandler = {
showConfirm: () => this._inputModel?.shouldConfirmClose() ?? false,
confirm: async (editors) => {
assertFn(() => editors.every(e => e.editor instanceof MergeEditorInput));
const inputModels = editors.map(e => (e.editor as MergeEditorInput)._inputModel).filter(isDefined);
return await this._inputModel!.confirmClose(inputModels);
},
};
private get useWorkingCopy() {
return this.configurationService.getValue('mergeEditor.useWorkingCopy') ?? false;
}
constructor(
public readonly base: URI,
@ -48,34 +53,13 @@ export class MergeEditorInput extends AbstractTextResourceEditorInput implements
public readonly input2: MergeEditorInputData,
public readonly result: URI,
@IInstantiationService private readonly _instaService: IInstantiationService,
@ITextModelService private readonly _textModelService: ITextModelService,
@IEditorService editorService: IEditorService,
@ITextFileService textFileService: ITextFileService,
@ILabelService labelService: ILabelService,
@IFileService fileService: IFileService
@IFileService fileService: IFileService,
@IConfigurationService private readonly configurationService: IConfigurationService,
) {
super(result, undefined, editorService, textFileService, labelService, fileService);
const modelListener = new DisposableStore();
const handleDidCreate = (model: ITextFileEditorModel) => {
// TODO@jrieken copied from fileEditorInput.ts
if (isEqual(result, model.resource)) {
modelListener.clear();
this._outTextModel = model;
modelListener.add(model.onDidChangeDirty(() => this._onDidChangeDirty.fire()));
modelListener.add(model.onDidSaveError(() => this._onDidChangeDirty.fire()));
modelListener.add(model.onDidChangeReadonly(() => this._onDidChangeCapabilities.fire()));
modelListener.add(model.onWillDispose(() => {
this._outTextModel = undefined;
modelListener.clear();
}));
}
};
textFileService.files.onDidCreate(handleDidCreate, this, modelListener);
textFileService.files.models.forEach(handleDidCreate);
this._store.add(modelListener);
}
override dispose(): void {
@ -91,68 +75,47 @@ export class MergeEditorInput extends AbstractTextResourceEditorInput implements
}
override get capabilities(): EditorInputCapabilities {
return super.capabilities | EditorInputCapabilities.MultipleEditors;
return super.capabilities | EditorInputCapabilities.MultipleEditors | EditorInputCapabilities.Untitled;
}
override getName(): string {
return localize('name', "Merging: {0}", super.getName());
}
override async resolve(): Promise<MergeEditorModel> {
if (!this._model) {
const toInputData = async (data: MergeEditorInputData): Promise<InputData> => {
const ref = await this._textModelService.createModelReference(data.uri);
this._store.add(ref);
return {
textModel: ref.object.textEditorModel,
title: data.title,
description: data.description,
detail: data.detail,
};
};
private readonly mergeEditorModeFactory = this._instaService.createInstance(
this.useWorkingCopy
? TempFileMergeEditorModeFactory
: WorkspaceMergeEditorModeFactory
);
const [
base,
result,
input1Data,
input2Data,
] = await Promise.all([
this._textModelService.createModelReference(this.base),
this._textModelService.createModelReference(this.result),
toInputData(this.input1),
toInputData(this.input2),
]);
override async resolve(): Promise<IMergeEditorInputModel> {
if (!this._inputModel) {
const inputModel = this._register(await this.mergeEditorModeFactory.createInputModel({
base: this.base,
input1: this.input1,
input2: this.input2,
result: this.result,
}));
this._inputModel = inputModel;
this._store.add(base);
this._store.add(result);
const diffProvider = this._instaService.createInstance(WorkerBasedDocumentDiffProvider);
this._model = this._instaService.createInstance(
MergeEditorModel,
base.object.textEditorModel,
input1Data,
input2Data,
result.object.textEditorModel,
this._instaService.createInstance(MergeDiffComputer, diffProvider),
this._instaService.createInstance(MergeDiffComputer, this._instaService.createInstance(ProjectedDiffComputer, diffProvider)),
{
resetUnknownOnInitialization: false
},
);
this._store.add(this._model);
// set/unset the closeHandler whenever unhandled conflicts are detected
const closeHandler = this._instaService.createInstance(MergeEditorCloseHandler, this._model);
this._store.add(autorun('closeHandler', reader => {
const value = this._model!.hasUnhandledConflicts.read(reader);
this.closeHandler = value ? closeHandler : undefined;
this._register(autorun('fire dirty event', (reader) => {
inputModel.isDirty.read(reader);
this._onDidChangeDirty.fire();
}));
await this._model.onInitialized;
await this._inputModel.model.onInitialized;
}
return this._model;
return this._inputModel;
}
public async accept(): Promise<void> {
await this._inputModel?.accept();
}
override async save(group: number, options?: ITextFileSaveOptions | undefined): Promise<IUntypedEditorInput | undefined> {
await (await this.resolve()).save();
return undefined;
}
override toUntyped(): IResourceMergeEditorInput {
@ -188,125 +151,21 @@ export class MergeEditorInput extends AbstractTextResourceEditorInput implements
return false;
}
override async revert(group: number, options?: IRevertOptions | undefined): Promise<void> {
await (await this.resolve()).revert();
return undefined;
}
// ---- FileEditorInput
override isDirty(): boolean {
return Boolean(this._outTextModel?.isDirty());
return this._inputModel?.isDirty.get() ?? false;
}
setLanguageId(languageId: string, source?: string): void {
this._model?.setLanguageId(languageId, source);
this._inputModel?.model.setLanguageId(languageId, source);
}
// implement get/set languageId
// implement get/set encoding
}
class MergeEditorCloseHandler implements IEditorCloseHandler {
private _ignoreUnhandledConflicts: boolean = false;
constructor(
private readonly _model: MergeEditorModel,
@IDialogService private readonly _dialogService: IDialogService,
) { }
showConfirm(): boolean {
// unhandled conflicts -> 3wm asks to confirm UNLESS we explicitly set this input
// to ignore unhandled conflicts. This happens only after confirming to ignore unhandled changes
return !this._ignoreUnhandledConflicts && this._model.hasUnhandledConflicts.get();
}
async confirm(editors: readonly IEditorIdentifier[]): Promise<ConfirmResult> {
const handler: MergeEditorCloseHandler[] = [];
let someAreDirty = false;
for (const { editor } of editors) {
if (editor.closeHandler instanceof MergeEditorCloseHandler && editor.closeHandler._model.hasUnhandledConflicts.get()) {
handler.push(editor.closeHandler);
someAreDirty = someAreDirty || editor.isDirty();
}
}
if (handler.length === 0) {
// shouldn't happen
return ConfirmResult.SAVE;
}
const result = someAreDirty
? await this._confirmDirty(handler)
: await this._confirmNoneDirty(handler);
if (result !== ConfirmResult.CANCEL) {
// save or ignore: in both cases we tell the inputs to ignore unhandled conflicts
// for the dirty state computation.
for (const input of handler) {
input._ignoreUnhandledConflicts = true;
}
}
return result;
}
private async _confirmDirty(handler: MergeEditorCloseHandler[]): Promise<ConfirmResult> {
const isMany = handler.length > 1;
const message = isMany
? localize('messageN', 'Do you want to save the changes you made to {0} files?', handler.length)
: localize('message1', 'Do you want to save the changes you made to {0}?', basename(handler[0]._model.resultTextModel.uri));
const options = {
cancelId: 2,
detail: isMany
? localize('detailN', "The files contain unhandled conflicts. Your changes will be lost if you don't save them.")
: localize('detail1', "The file contains unhandled conflicts. Your changes will be lost if you don't save them.")
};
const actions: string[] = [
localize('saveWithConflict', "Save with Conflicts"),
localize('discard', "Don't Save"),
localize('cancel', "Cancel"),
];
const { choice } = await this._dialogService.show(Severity.Info, message, actions, options);
if (choice === options.cancelId) {
// cancel: stay in editor
return ConfirmResult.CANCEL;
} else if (choice === 0) {
// save with conflicts
return ConfirmResult.SAVE;
} else {
// discard changes
return ConfirmResult.DONT_SAVE;
}
}
private async _confirmNoneDirty(handler: MergeEditorCloseHandler[]): Promise<ConfirmResult> {
const isMany = handler.length > 1;
const message = isMany
? localize('conflictN', 'Do you want to close with conflicts in {0} files?', handler.length)
: localize('conflict1', 'Do you want to close with conflicts in {0}?', basename(handler[0]._model.resultTextModel.uri));
const options = {
cancelId: 1,
detail: isMany
? localize('detailNotDirtyN', "The files contain unhandled conflicts.")
: localize('detailNotDirty1', "The file contains unhandled conflicts.")
};
const actions = [
localize('closeWithConflicts', "Close with Conflicts"),
localize('cancel', "Cancel"),
];
const { choice } = await this._dialogService.show(Severity.Info, message, actions, options);
if (choice === options.cancelId) {
return ConfirmResult.CANCEL;
} else {
return ConfirmResult.SAVE;
}
}
}

View file

@ -0,0 +1,446 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { assertFn } from 'vs/base/common/assert';
import { BugIndicatingError } from 'vs/base/common/errors';
import { Event } from 'vs/base/common/event';
import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
import { derived, IObservable, observableFromEvent, observableValue } from 'vs/base/common/observable';
import { basename, isEqual } from 'vs/base/common/resources';
import Severity from 'vs/base/common/severity';
import { URI } from 'vs/base/common/uri';
import { WorkerBasedDocumentDiffProvider } from 'vs/editor/browser/widget/workerBasedDocumentDiffProvider';
import { IModelService } from 'vs/editor/common/services/model';
import { IResolvedTextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService';
import { localize } from 'vs/nls';
import { ConfirmResult, IDialogOptions, IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { IEditorModel } from 'vs/platform/editor/common/editor';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { EditorModel } from 'vs/workbench/common/editor/editorModel';
import { MergeEditorInputData } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput';
import { MergeDiffComputer } from 'vs/workbench/contrib/mergeEditor/browser/model/diffComputer';
import { InputData, MergeEditorModel } from 'vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel';
import { ProjectedDiffComputer } from 'vs/workbench/contrib/mergeEditor/browser/model/projectedDocumentDiffProvider';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { ITextFileEditorModel, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
export interface MergeEditorArgs {
base: URI;
input1: MergeEditorInputData;
input2: MergeEditorInputData;
result: URI;
}
export interface IMergeEditorInputModelFactory {
createInputModel(args: MergeEditorArgs): Promise<IMergeEditorInputModel>;
}
export interface IMergeEditorInputModel extends IDisposable, IEditorModel {
readonly resultUri: URI;
readonly model: MergeEditorModel;
readonly isDirty: IObservable<boolean>;
save(): Promise<void>;
/**
* If save resets the dirty state, revert must do so too.
*/
revert(): Promise<void>;
shouldConfirmClose(): boolean;
confirmClose(inputModels: IMergeEditorInputModel[]): Promise<ConfirmResult>;
/**
* Marks the merge as done. The merge editor must be closed afterwards.
*/
accept(): Promise<void>;
}
/* ================ Temp File ================ */
export class TempFileMergeEditorModeFactory implements IMergeEditorInputModelFactory {
constructor(
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@ITextModelService private readonly _textModelService: ITextModelService,
@IModelService private readonly _modelService: IModelService,
) {
}
async createInputModel(args: MergeEditorArgs): Promise<IMergeEditorInputModel> {
const store = new DisposableStore();
const [
base,
result,
input1Data,
input2Data,
] = await Promise.all([
this._textModelService.createModelReference(args.base),
this._textModelService.createModelReference(args.result),
toInputData(args.input1, this._textModelService, store),
toInputData(args.input2, this._textModelService, store),
]);
store.add(base);
store.add(result);
const tempResultUri = result.object.textEditorModel.uri.with({ scheme: 'merge-result' });
const temporaryResultModel = this._modelService.createModel(
'',
{
languageId: result.object.textEditorModel.getLanguageId(),
onDidChange: Event.None,
},
tempResultUri,
);
store.add(temporaryResultModel);
const diffProvider = this._instantiationService.createInstance(WorkerBasedDocumentDiffProvider);
const model = this._instantiationService.createInstance(
MergeEditorModel,
base.object.textEditorModel,
input1Data,
input2Data,
temporaryResultModel,
this._instantiationService.createInstance(MergeDiffComputer, diffProvider),
this._instantiationService.createInstance(MergeDiffComputer, this._instantiationService.createInstance(ProjectedDiffComputer, diffProvider)),
{
resetResult: true,
}
);
store.add(model);
await model.onInitialized;
return this._instantiationService.createInstance(TempFileMergeEditorInputModel, model, store, result.object, args.result);
}
}
class TempFileMergeEditorInputModel extends EditorModel implements IMergeEditorInputModel {
private readonly savedAltVersionId = observableValue('initialAltVersionId', this.model.resultTextModel.getAlternativeVersionId());
private readonly altVersionId = observableFromEvent(
e => this.model.resultTextModel.onDidChangeContent(e),
() =>
/** @description getAlternativeVersionId */ this.model.resultTextModel.getAlternativeVersionId()
);
public readonly isDirty = derived(
'isDirty',
(reader) => this.altVersionId.read(reader) !== this.savedAltVersionId.read(reader)
);
private finished = false;
constructor(
public readonly model: MergeEditorModel,
private readonly disposable: IDisposable,
private readonly result: IResolvedTextEditorModel,
public readonly resultUri: URI,
@ITextFileService private readonly textFileService: ITextFileService,
@IDialogService private readonly dialogService: IDialogService,
@IEditorService private readonly editorService: IEditorService,
) {
super();
}
override dispose(): void {
this.disposable.dispose();
super.dispose();
}
async accept(): Promise<void> {
const value = await this.model.resultTextModel.getValue();
this.result.textEditorModel.setValue(value);
this.savedAltVersionId.set(this.model.resultTextModel.getAlternativeVersionId(), undefined);
await this.textFileService.save(this.result.textEditorModel.uri);
this.finished = true;
}
private async _discard(): Promise<void> {
await this.textFileService.revert(this.model.resultTextModel.uri);
this.savedAltVersionId.set(this.model.resultTextModel.getAlternativeVersionId(), undefined);
this.finished = true;
}
public shouldConfirmClose(): boolean {
return true;
}
public async confirmClose(inputModels: TempFileMergeEditorInputModel[]): Promise<ConfirmResult> {
assertFn(
() => inputModels.some((m) => m === this)
);
const someDirty = inputModels.some((m) => m.isDirty.get());
let choice: number;
if (someDirty) {
const isMany = inputModels.length > 1;
const message = isMany
? localize('messageN', 'Do you want keep the merge result of {0} files?', inputModels.length)
: localize('message1', 'Do you want keep the merge result of {0}?', basename(inputModels[0].model.resultTextModel.uri));
const hasUnhandledConflicts = inputModels.some((m) => m.model.hasUnhandledConflicts.get());
const options: IDialogOptions = {
cancelId: 2,
detail:
hasUnhandledConflicts
? isMany
? localize('detailNConflicts', "The files contain unhandled conflicts. The merge results will be lost if you don't save them.")
: localize('detail1Conflicts', "The file contains unhandled conflicts. The merge result will be lost if you don't save it.")
: isMany
? localize('detailN', "The merge results will be lost if you don't save them.")
: localize('detail1', "The merge result will be lost if you don't save it.")
};
const actions: string[] = [
hasUnhandledConflicts ? localize('saveWithConflict', "Save With Conflicts") : localize('save', "Save"),
localize('discard', "Don't Save"),
localize('cancel', "Cancel"),
];
choice = (await this.dialogService.show(Severity.Info, message, actions, options)).choice;
} else {
choice = 1;
}
if (choice === 2) {
// cancel: stay in editor
return ConfirmResult.CANCEL;
} else if (choice === 0) {
// save with conflicts
await Promise.all(inputModels.map(m => m.accept()));
return ConfirmResult.SAVE; // Save is a no-op anyway
} else {
// discard changes
await Promise.all(inputModels.map(m => m._discard()));
return ConfirmResult.DONT_SAVE; // Revert is a no-op
}
}
public async save(): Promise<void> {
if (this.finished) {
return;
}
// It does not make sense to save anything in the temp file mode.
// The file stays dirty from the first edit on.
(async () => {
const result = await this.dialogService.show(
Severity.Info,
localize(
'saveTempFile',
"Do you want to accept the merge result? This will write the merge result to the original file and close the merge editor."
),
[
localize('acceptMerge', 'Accept Merge'),
localize('cancel', "Cancel"),
],
{ cancelId: 1 }
);
if (result.choice === 0) {
await this.accept();
const editors = this.editorService.findEditors(this.resultUri).filter(e => e.editor.typeId === 'mergeEditor.Input');
await this.editorService.closeEditors(editors);
}
})();
}
public async revert(): Promise<void> {
// no op
}
}
/* ================ Workspace ================ */
export class WorkspaceMergeEditorModeFactory implements IMergeEditorInputModelFactory {
constructor(
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@ITextModelService private readonly _textModelService: ITextModelService,
@ITextFileService private readonly textFileService: ITextFileService,
) {
}
public async createInputModel(args: MergeEditorArgs): Promise<IMergeEditorInputModel> {
const store = new DisposableStore();
let resultTextFileModel = undefined as ITextFileEditorModel | undefined;
const modelListener = store.add(new DisposableStore());
const handleDidCreate = (model: ITextFileEditorModel) => {
if (isEqual(args.result, model.resource)) {
modelListener.clear();
resultTextFileModel = model;
}
};
modelListener.add(this.textFileService.files.onDidCreate(handleDidCreate));
this.textFileService.files.models.forEach(handleDidCreate);
const [
base,
result,
input1Data,
input2Data,
] = await Promise.all([
this._textModelService.createModelReference(args.base),
this._textModelService.createModelReference(args.result),
toInputData(args.input1, this._textModelService, store),
toInputData(args.input2, this._textModelService, store),
]);
store.add(base);
store.add(result);
if (!resultTextFileModel) {
throw new BugIndicatingError();
}
// So that "Don't save" does revert the file
await resultTextFileModel.save();
const diffProvider = this._instantiationService.createInstance(WorkerBasedDocumentDiffProvider);
const model = this._instantiationService.createInstance(
MergeEditorModel,
base.object.textEditorModel,
input1Data,
input2Data,
result.object.textEditorModel,
this._instantiationService.createInstance(MergeDiffComputer, diffProvider),
this._instantiationService.createInstance(MergeDiffComputer, this._instantiationService.createInstance(ProjectedDiffComputer, diffProvider)),
{
resetResult: true
}
);
store.add(model);
return this._instantiationService.createInstance(WorkspaceMergeEditorInputModel, model, store, resultTextFileModel);
}
}
class WorkspaceMergeEditorInputModel extends EditorModel implements IMergeEditorInputModel {
public readonly isDirty = observableFromEvent(
Event.any(this.resultTextFileModel.onDidChangeDirty, this.resultTextFileModel.onDidSaveError),
() => /** @description isDirty */ this.resultTextFileModel.isDirty()
);
constructor(
public readonly model: MergeEditorModel,
private readonly disposableStore: DisposableStore,
private readonly resultTextFileModel: ITextFileEditorModel,
@IDialogService private readonly _dialogService: IDialogService,
) {
super();
}
public override dispose(): void {
this.disposableStore.dispose();
super.dispose();
}
public async accept(): Promise<void> {
await this.resultTextFileModel.save();
}
get resultUri(): URI {
return this.resultTextFileModel.resource;
}
async save(): Promise<void> {
await this.resultTextFileModel.save();
}
/**
* If save resets the dirty state, revert must do so too.
*/
async revert(): Promise<void> {
await this.resultTextFileModel.revert();
}
shouldConfirmClose(): boolean {
// Always confirm
return true;
//return this.resultTextFileModel.isDirty();
}
async confirmClose(inputModels: IMergeEditorInputModel[]): Promise<ConfirmResult> {
const isMany = inputModels.length > 1;
const someDirty = inputModels.some(m => m.isDirty.get());
const someUnhandledConflicts = inputModels.some(m => m.model.hasUnhandledConflicts.get());
if (someDirty) {
const message = isMany
? localize('workspace.messageN', 'Do you want to save the changes you made to {0} files?', inputModels.length)
: localize('workspace.message1', 'Do you want to save the changes you made to {0}?', basename(inputModels[0].resultUri));
const options: IDialogOptions = {
detail:
someUnhandledConflicts ?
isMany
? localize('workspace.detailN.unhandled', "The files contain unhandled conflicts. Your changes will be lost if you don't save them.")
: localize('workspace.detail1.unhandled', "The file contains unhandled conflicts. Your changes will be lost if you don't save them.")
: isMany
? localize('workspace.detailN.handled', "Your changes will be lost if you don't save them.")
: localize('workspace.detail1.handled', "Your changes will be lost if you don't save them.")
};
const actions: [string, ConfirmResult][] = [
[
someUnhandledConflicts
? localize('workspace.saveWithConflict', 'Save with Conflicts')
: localize('workspace.save', 'Save'),
ConfirmResult.SAVE,
],
[localize('workspace.doNotSave', "Don't Save"), ConfirmResult.DONT_SAVE],
// TODO [localize('workspace.discard', "Discard changes"), ConfirmResult.DONT_SAVE],
[localize('workspace.cancel', 'Cancel'), ConfirmResult.CANCEL],
];
const { choice } = await this._dialogService.show(Severity.Info, message, actions.map(a => a[0]), { ...options, cancelId: actions.length - 1 });
return actions[choice][1];
} else if (someUnhandledConflicts) {
const message = isMany
? localize('workspace.messageN.nonDirty', 'Do you want to close {0} merge editors?', inputModels.length)
: localize('workspace.message1.nonDirty', 'Do you want to close the merge editor for {0}?', basename(inputModels[0].resultUri));
const options: IDialogOptions = {
detail:
someUnhandledConflicts ?
isMany
? localize('workspace.detailN.unhandled.nonDirty', "The files contain unhandled conflicts.")
: localize('workspace.detail1.unhandled.nonDirty', "The file contains unhandled conflicts.")
: undefined
};
const actions: [string, ConfirmResult][] = [
[
someUnhandledConflicts
? localize('workspace.closeWithConflicts', 'Close with Conflicts')
: localize('workspace.close', 'Close'),
ConfirmResult.SAVE,
],
// TODO [localize('workspace.discard', "Discard changes"), ConfirmResult.DONT_SAVE],
[localize('workspace.cancel', 'Cancel'), ConfirmResult.CANCEL],
];
const { choice } = await this._dialogService.show(Severity.Info, message, actions.map(a => a[0]), { ...options, cancelId: actions.length - 1 });
return actions[choice][1];
} else {
// This shouldn't do anything
return ConfirmResult.SAVE;
}
}
}
/* ================= Utils ================== */
async function toInputData(data: MergeEditorInputData, textModelService: ITextModelService, store: DisposableStore): Promise<InputData> {
const ref = await textModelService.createModelReference(data.uri);
store.add(ref);
return {
textModel: ref.object.textEditorModel,
title: data.title,
description: data.description,
detail: data.detail,
};
}

View file

@ -47,6 +47,10 @@ export class MergeDiffComputer implements IMergeDiffComputer {
}
);
if (textModel1.isDisposed() || textModel2.isDisposed()) {
return { diffs: null };
}
const changes = result.changes.map(c =>
new DetailedLineRangeMapping(
toLineRange(c.originalRange),

View file

@ -58,9 +58,9 @@ export class MergeEditorModel extends EditorModel {
readonly resultTextModel: ITextModel,
private readonly diffComputer: IMergeDiffComputer,
private readonly diffComputerConflictProjection: IMergeDiffComputer,
options: { resetUnknownOnInitialization: boolean },
private readonly options: { resetResult: boolean },
@IModelService private readonly modelService: IModelService,
@ILanguageService private readonly languageService: ILanguageService
@ILanguageService private readonly languageService: ILanguageService,
) {
super();
@ -68,51 +68,122 @@ export class MergeEditorModel extends EditorModel {
this._register(keepAlive(this.input1ResultMapping));
this._register(keepAlive(this.input2ResultMapping));
let shouldRecomputeHandledFromAccepted = true;
this._register(
autorunHandleChanges(
'Merge Editor Model: Recompute State From Result',
{
handleChange: (ctx) => {
if (ctx.didChange(this.modifiedBaseRangeResultStates)) {
shouldRecomputeHandledFromAccepted = true;
}
return ctx.didChange(this.resultTextModelDiffs.diffs)
// Ignore non-text changes as we update the state directly
? ctx.change === TextModelDiffChangeReason.textChange
: true;
},
},
(reader) => {
const states = this.modifiedBaseRangeResultStates.read(reader);
if (!this.isUpToDate.read(reader)) {
return;
}
const resultDiffs = this.resultTextModelDiffs.diffs.read(reader);
transaction(tx => {
/** @description Merge Editor Model: Recompute State */
const initializePromise = this.initialize();
this.updateBaseRangeAcceptedState(resultDiffs, states, tx);
this.onInitialized = this.onInitialized.then(async () => {
await initializePromise;
});
if (shouldRecomputeHandledFromAccepted) {
shouldRecomputeHandledFromAccepted = false;
for (const [_range, observableState] of states) {
const state = observableState.accepted.get();
observableState.handled.set(!(state.isEmpty || state.conflicting), tx);
initializePromise.then(() => {
let shouldRecomputeHandledFromAccepted = true;
this._register(
autorunHandleChanges(
'Merge Editor Model: Recompute State From Result',
{
handleChange: (ctx) => {
if (ctx.didChange(this.modifiedBaseRangeResultStates)) {
shouldRecomputeHandledFromAccepted = true;
}
return ctx.didChange(this.resultTextModelDiffs.diffs)
// Ignore non-text changes as we update the state directly
? ctx.change === TextModelDiffChangeReason.textChange
: true;
},
},
(reader) => {
const states = this.modifiedBaseRangeResultStates.read(reader);
if (!this.isUpToDate.read(reader)) {
return;
}
});
}
)
);
const resultDiffs = this.resultTextModelDiffs.diffs.read(reader);
transaction(tx => {
/** @description Merge Editor Model: Recompute State */
if (options.resetUnknownOnInitialization) {
this.onInitialized = this.onInitialized.then(() => {
this.resetDirtyConflictsToBase();
});
this.updateBaseRangeAcceptedState(resultDiffs, states, tx);
if (shouldRecomputeHandledFromAccepted) {
shouldRecomputeHandledFromAccepted = false;
for (const [_range, observableState] of states) {
const state = observableState.accepted.get();
observableState.handled.set(!(state.isEmpty || state.conflicting), tx);
}
}
});
}
)
);
});
}
private async initialize(): Promise<void> {
if (this.options.resetResult) {
await this.reset();
}
}
public async reset(): Promise<void> {
await waitForState(this.inputDiffComputingState, state => state === MergeEditorModelState.upToDate);
const states = this.modifiedBaseRangeResultStates.get();
transaction(tx => {
/** @description Set initial state */
for (const [range, state] of states) {
let newState: ModifiedBaseRangeState;
let handled = false;
if (range.input1Diffs.length === 0) {
newState = ModifiedBaseRangeState.default.withInput2(true);
handled = true;
} else if (range.input2Diffs.length === 0) {
newState = ModifiedBaseRangeState.default.withInput1(true);
handled = true;
} else {
newState = ModifiedBaseRangeState.default;
handled = false;
}
state.accepted.set(newState, tx);
state.handled.set(handled, tx);
}
this.resultTextModel.setValue(this.computeAutoMergedResult());
});
}
private computeAutoMergedResult(): string {
const baseRanges = this.modifiedBaseRanges.get();
const baseLines = this.base.getLinesContent();
const input1Lines = this.input1.textModel.getLinesContent();
const input2Lines = this.input2.textModel.getLinesContent();
const resultLines: string[] = [];
function appendLinesToResult(source: string[], lineRange: LineRange) {
for (let i = lineRange.startLineNumber; i < lineRange.endLineNumberExclusive; i++) {
resultLines.push(source[i - 1]);
}
}
let baseStartLineNumber = 1;
for (const baseRange of baseRanges) {
appendLinesToResult(baseLines, LineRange.fromLineNumbers(baseStartLineNumber, baseRange.baseRange.startLineNumber));
baseStartLineNumber = baseRange.baseRange.endLineNumberExclusive;
if (baseRange.input1Diffs.length === 0) {
appendLinesToResult(input2Lines, baseRange.input2Range);
} else if (baseRange.input2Diffs.length === 0) {
appendLinesToResult(input1Lines, baseRange.input1Range);
} else {
appendLinesToResult(baseLines, baseRange.baseRange);
}
}
appendLinesToResult(baseLines, LineRange.fromLineNumbers(baseStartLineNumber, baseLines.length + 1));
return resultLines.join('\n');
}
public hasBaseRange(baseRange: ModifiedBaseRange): boolean {
return this.modifiedBaseRangeResultStates.get().has(baseRange);
}
@ -222,6 +293,21 @@ export class MergeEditorModel extends EditorModel {
return MergeEditorModelState.upToDate;
});
public readonly inputDiffComputingState = derived('inputDiffComputingState', reader => {
const states = [
this.input1TextModelDiffs,
this.input2TextModelDiffs,
].map((s) => s.state.read(reader));
if (states.some((s) => s === TextModelDiffState.initializing)) {
return MergeEditorModelState.initializing;
}
if (states.some((s) => s === TextModelDiffState.updating)) {
return MergeEditorModelState.updating;
}
return MergeEditorModelState.upToDate;
});
public readonly isUpToDate = derived('isUpToDate', reader => this.diffComputingState.read(reader) === MergeEditorModelState.upToDate);
public readonly onInitialized = waitForState(this.diffComputingState, state => state === MergeEditorModelState.upToDate).then(() => { });
@ -374,8 +460,8 @@ export class MergeEditorModel extends EditorModel {
}
public async resetResultToBaseAndAutoMerge() {
await waitForState(this.inputDiffComputingState, state => state === MergeEditorModelState.upToDate);
this.resultTextModel.setValue(this.base.getValue());
await waitForState(this.diffComputingState, state => state === MergeEditorModelState.upToDate);
this.acceptNonConflictingDiffs();
}
@ -384,7 +470,12 @@ export class MergeEditorModel extends EditorModel {
}
public setHandled(baseRange: ModifiedBaseRange, handled: boolean, tx: ITransaction): void {
this.modifiedBaseRangeResultStates.get().get(baseRange)!.handled.set(handled, tx);
const state = this.modifiedBaseRangeResultStates.get().get(baseRange)!;
if (state.handled.get() === handled) {
return;
}
state.handled.set(handled, tx);
}
public readonly unhandledConflictsCount = derived('unhandledConflictsCount', reader => {
@ -419,6 +510,54 @@ export class MergeEditorModel extends EditorModel {
}
return chunks.join();
}
public async getResultValueWithConflictMarkers(): Promise<string> {
await waitForState(this.diffComputingState, state => state === MergeEditorModelState.upToDate);
if (this.unhandledConflictsCount.get() === 0) {
return this.resultTextModel.getValue();
}
const resultLines = this.resultTextModel.getLinesContent();
const input1Lines = this.input1.textModel.getLinesContent();
const input2Lines = this.input2.textModel.getLinesContent();
const states = this.modifiedBaseRangeResultStates.get();
const outputLines: string[] = [];
function appendLinesToResult(source: string[], lineRange: LineRange) {
for (let i = lineRange.startLineNumber; i < lineRange.endLineNumberExclusive; i++) {
outputLines.push(source[i - 1]);
}
}
let resultStartLineNumber = 1;
for (const [range, state] of states) {
if (state.handled.get()) {
continue;
}
const resultRange = this.resultTextModelDiffs.getResultLineRange(range.baseRange);
appendLinesToResult(resultLines, LineRange.fromLineNumbers(resultStartLineNumber, Math.max(resultStartLineNumber, resultRange.startLineNumber)));
resultStartLineNumber = resultRange.endLineNumberExclusive;
outputLines.push('<<<<<<<');
if (state.accepted.get().conflicting) {
// to prevent loss of data, use modified result as "ours"
appendLinesToResult(resultLines, resultRange);
} else {
appendLinesToResult(input1Lines, range.input1Range);
}
outputLines.push('=======');
appendLinesToResult(input2Lines, range.input2Range);
outputLines.push('>>>>>>>');
}
appendLinesToResult(resultLines, LineRange.fromLineNumbers(resultStartLineNumber, resultLines.length + 1));
return outputLines.join('\n');
}
}
interface ModifiedBaseRangeData {

View file

@ -11,7 +11,7 @@ import { Color } from 'vs/base/common/color';
import { BugIndicatingError, onUnexpectedError } from 'vs/base/common/errors';
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable, DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { autorun, autorunWithStore, IObservable, IReader, observableValue } from 'vs/base/common/observable';
import { autorun, autorunWithStore, IObservable, IReader, observableValue, transaction } from 'vs/base/common/observable';
import { basename, isEqual } from 'vs/base/common/resources';
import { isDefined } from 'vs/base/common/types';
import { URI } from 'vs/base/common/uri';
@ -36,6 +36,7 @@ import { EditorInput } from 'vs/workbench/common/editor/editorInput';
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 { MergeEditorModel } from 'vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel';
import { deepMerge, thenIfNotDisposed } from 'vs/workbench/contrib/mergeEditor/browser/utils';
import { BaseCodeEditorView } from 'vs/workbench/contrib/mergeEditor/browser/view/editors/baseCodeEditorView';
@ -76,7 +77,13 @@ export class MergeEditor extends AbstractTextEditor<IMergeEditorViewState> {
private readonly _ctxShowBase: IContextKey<boolean>;
private readonly _ctxResultUri: IContextKey<string>;
private readonly _ctxBaseUri: IContextKey<string>;
public get model(): MergeEditorModel | undefined { return this._viewModel.get()?.model; }
private readonly _inputModel = observableValue<IMergeEditorInputModel | undefined>('inputModel', undefined);
public get inputModel(): IObservable<IMergeEditorInputModel | undefined> {
return this._inputModel;
}
public get model(): MergeEditorModel | undefined {
return this.inputModel.get()?.model;
}
private get inputsWritable(): boolean {
return !!this._configurationService.getValue<boolean>('mergeEditor.writableInputs');
@ -176,16 +183,23 @@ export class MergeEditor extends AbstractTextEditor<IMergeEditorViewState> {
await super.setInput(input, options, context, token);
this._sessionDisposables.clear();
this._viewModel.set(undefined, undefined);
transaction(tx => {
this._viewModel.set(undefined, tx);
this._inputModel.set(undefined, tx);
});
const model = await input.resolve();
const inputModel = await input.resolve();
const model = inputModel.model;
const viewModel = new MergeEditorViewModel(model, this.input1View, this.input2View, this.inputResultView, this.baseView);
this._viewModel.set(viewModel, undefined);
transaction(tx => {
this._viewModel.set(viewModel, tx);
this._inputModel.set(inputModel, tx);
});
this._sessionDisposables.add(viewModel);
// Set/unset context keys based on input
this._ctxResultUri.set(model.resultTextModel.uri.toString());
this._ctxResultUri.set(inputModel.resultUri.toString());
this._ctxBaseUri.set(model.base.uri.toString());
this._sessionDisposables.add(toDisposable(() => {
this._ctxBaseUri.reset();
@ -552,7 +566,7 @@ export class MergeEditor extends AbstractTextEditor<IMergeEditorViewState> {
}
protected computeEditorViewState(resource: URI): IMergeEditorViewState | undefined {
if (!isEqual(this.model?.resultTextModel.uri, resource)) {
if (!isEqual(this.inputModel.get()?.resultUri, resource)) {
return undefined;
}
const result = this.inputResultView.editor.saveViewState();

View file

@ -293,7 +293,9 @@ class MergeModelInterface extends Disposable {
resultTextModel,
diffComputer,
diffComputer,
{ resetUnknownOnInitialization: false }
{
resetResult: false
}
));
}