mirror of
https://github.com/Microsoft/vscode
synced 2024-08-27 21:09:43 +00:00
merge editor temp model (#161315)
Introduces hidden mergeEditor.useWorkingCopy setting, implements reset when the merge editor opens
This commit is contained in:
parent
2046342bf3
commit
40a262dc13
|
@ -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')
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
}));
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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),
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -293,7 +293,9 @@ class MergeModelInterface extends Disposable {
|
|||
resultTextModel,
|
||||
diffComputer,
|
||||
diffComputer,
|
||||
{ resetUnknownOnInitialization: false }
|
||||
{
|
||||
resetResult: false
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue