提交 793c1b28 编写于 作者: S Sandeep Somavarapu

#100346 Use resource previews and remove conflict type

上级 80257234
......@@ -9,7 +9,7 @@ import { VSBuffer } from 'vs/base/common/buffer';
import { URI } from 'vs/base/common/uri';
import {
SyncResource, SyncStatus, IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, UserDataSyncError, IUserDataSyncLogService, IUserDataSyncUtilService,
IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService, Conflict, ISyncResourceHandle, USER_DATA_SYNC_SCHEME, ISyncResourcePreview as IBaseSyncResourcePreview,
IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService, ISyncResourceHandle, USER_DATA_SYNC_SCHEME, ISyncResourcePreview as IBaseSyncResourcePreview,
IUserDataManifest, ISyncData, IRemoteUserData, PREVIEW_DIR_NAME, IResourcePreview as IBaseResourcePreview, Change
} from 'vs/platform/userDataSync/common/userDataSync';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
......@@ -78,10 +78,11 @@ export abstract class AbstractSynchroniser extends Disposable {
private _onDidChangStatus: Emitter<SyncStatus> = this._register(new Emitter<SyncStatus>());
readonly onDidChangeStatus: Event<SyncStatus> = this._onDidChangStatus.event;
private _conflicts: Conflict[] = [];
get conflicts(): Conflict[] { return this._conflicts; }
private _onDidChangeConflicts: Emitter<Conflict[]> = this._register(new Emitter<Conflict[]>());
readonly onDidChangeConflicts: Event<Conflict[]> = this._onDidChangeConflicts.event;
private _resourcePreviews: IResourcePreview[] = [];
get resourcePreviews(): IResourcePreview[] { return this._resourcePreviews; }
get conflicts(): IResourcePreview[] { return this._resourcePreviews.filter(({ hasConflicts }) => hasConflicts); }
private _onDidChangeConflicts: Emitter<IResourcePreview[]> = this._register(new Emitter<IResourcePreview[]>());
readonly onDidChangeConflicts: Event<IResourcePreview[]> = this._onDidChangeConflicts.event;
private readonly localChangeTriggerScheduler = new RunOnceScheduler(() => this.doTriggerLocalChange(), 50);
private readonly _onDidChangeLocal: Emitter<void> = this._register(new Emitter<void>());
......@@ -155,16 +156,6 @@ export abstract class AbstractSynchroniser extends Disposable {
// Log to telemetry when conflicts are resolved
this.telemetryService.publicLog2<{ source: string }, SyncSourceClassification>('sync/conflictsResolved', { source: this.resource });
}
if (this.status !== SyncStatus.HasConflicts) {
this.setConflicts([]);
}
}
}
private setConflicts(conflicts: Conflict[]) {
if (!equals(this._conflicts, conflicts, (a, b) => isEqual(a.local, b.local) && isEqual(a.remote, b.remote))) {
this._conflicts = conflicts;
this._onDidChangeConflicts.fire(this._conflicts);
}
}
......@@ -364,6 +355,9 @@ export abstract class AbstractSynchroniser extends Disposable {
// reset preview
this.syncPreviewPromise = null;
// reset resource previews
await this.updateResourcePreviews([], CancellationToken.None);
return SyncStatus.Idle;
} catch (error) {
......@@ -374,47 +368,50 @@ export abstract class AbstractSynchroniser extends Disposable {
}
}
async acceptConflict(conflictUri: URI, conflictContent: string): Promise<void> {
let preview = this.syncPreviewPromise ? await this.syncPreviewPromise : null;
if (!preview || !preview.resourcePreviews.some(({ hasConflicts }) => hasConflicts)) {
async acceptPreviewContent(resource: URI, content: string): Promise<void> {
if (!this.syncPreviewPromise) {
return;
}
let preview = await this.syncPreviewPromise;
this.syncPreviewPromise = createCancelablePromise(async token => {
const newPreview = await this.updateSyncResourcePreviewWithConflict(preview!, conflictUri, conflictContent, token);
await this.updateConflicts(newPreview.resourcePreviews, token);
const newPreview = await this.updateSyncResourcePreviewContent(preview, resource, content, token);
if (!token.isCancellationRequested) {
await this.updateResourcePreviews(newPreview.resourcePreviews, token);
}
return newPreview;
});
preview = await this.syncPreviewPromise;
if (!preview.resourcePreviews.some(({ hasConflicts }) => hasConflicts)) {
// apply preview
// Apply preview if there are no conflicts
await this.applyPreview(preview.remoteUserData, preview.lastSyncUserData, preview.resourcePreviews, false);
// reset preview
this.syncPreviewPromise = null;
// reset resource previews
await this.updateResourcePreviews([], CancellationToken.None);
// reset status
this.setStatus(SyncStatus.Idle);
}
}
private async updateSyncResourcePreviewWithConflict(preview: ISyncResourcePreview, conflictResource: URI, previewContent: string, token: CancellationToken): Promise<ISyncResourcePreview> {
const conflict = this.conflicts.find(({ local, remote }) => isEqual(local, conflictResource) || isEqual(remote, conflictResource));
if (!conflict) {
return preview;
}
const index = preview.resourcePreviews.findIndex(({ previewResource }) => previewResource && isEqual(previewResource, conflict.local));
if (index === -1) {
return preview;
private async updateSyncResourcePreviewContent(preview: ISyncResourcePreview, resource: URI, previewContent: string, token: CancellationToken): Promise<ISyncResourcePreview> {
const index = preview.resourcePreviews.findIndex(({ localResource, remoteResource, previewResource }) => isEqual(localResource, resource) || isEqual(remoteResource, resource) || isEqual(previewResource, resource));
if (index !== -1) {
const resourcePreviews = [...preview.resourcePreviews];
const resourcePreview = await this.updateResourcePreviewContent(resourcePreviews[index], resource, previewContent, token);
resourcePreviews[index] = resourcePreview;
preview = {
...preview,
resourcePreviews
};
}
const resourcePreviews = [...preview.resourcePreviews];
const resourcePreview = await this.updateResourcePreviewContent(resourcePreviews[index], conflictResource, previewContent, token);
resourcePreviews[index] = resourcePreview;
return {
...preview,
resourcePreviews
};
return preview;
}
protected async updateResourcePreviewContent(resourcePreview: IResourcePreview, resource: URI, previewContent: string, token: CancellationToken): Promise<IResourcePreview> {
......@@ -427,27 +424,25 @@ export abstract class AbstractSynchroniser extends Disposable {
};
}
private async updateConflicts(resourcePreviews: IResourcePreview[], token: CancellationToken): Promise<void> {
const conflicts: Conflict[] = [];
for (const resourcePreview of resourcePreviews) {
if (resourcePreview.hasConflicts) {
conflicts.push({ local: resourcePreview.previewResource!, remote: resourcePreview.remoteResource! });
}
}
private async updateResourcePreviews(resourcePreviews: IResourcePreview[], token: CancellationToken): Promise<void> {
const oldConflicts = this.conflicts;
const oldPreviews = this._resourcePreviews;
this._resourcePreviews = resourcePreviews;
for (const conflict of this.conflicts) {
// clear obsolete conflicts
if (!conflicts.some(({ local }) => isEqual(local, conflict.local))) {
// clear obsolete previews
for (const resourcePreview of oldPreviews) {
if (!this._resourcePreviews.some(({ previewResource }) => isEqual(previewResource, resourcePreview.previewResource))) {
try {
await this.fileService.del(conflict.local);
} catch (error) {
// Ignore & log
this.logService.error(error);
}
await this.fileService.del(resourcePreview.previewResource);
} catch (error) { /* Ignore */ }
}
}
this.setConflicts(conflicts);
// update conflicts
const newConflicts = this.conflicts;
if (!equals(oldConflicts, newConflicts, (a, b) => isEqual(a.previewResource, b.previewResource))) {
this._onDidChangeConflicts.fire(newConflicts);
}
}
async hasPreviouslySynced(): Promise<boolean> {
......@@ -528,7 +523,11 @@ export abstract class AbstractSynchroniser extends Disposable {
// For preview, use remoteUserData if lastSyncUserData does not exists and last sync is from current machine
const lastSyncUserDataForPreview = lastSyncUserData === null && isLastSyncFromCurrentMachine ? remoteUserData : lastSyncUserData;
const resourcePreviews = await this.generateSyncPreview(remoteUserData, lastSyncUserDataForPreview, token);
await this.updateConflicts(resourcePreviews, token);
if (!token.isCancellationRequested) {
await this.updateResourcePreviews(resourcePreviews, token);
}
return { remoteUserData, lastSyncUserData, resourcePreviews, isLastSyncFromCurrentMachine };
}
......@@ -605,23 +604,20 @@ export abstract class AbstractSynchroniser extends Disposable {
}
async stop(): Promise<void> {
this.logService.info(`${this.syncResourceLogLabel}: Stopped synchronizing ${this.resource.toLowerCase()}.`);
if (this.status === SyncStatus.Idle) {
return;
}
this.logService.trace(`${this.syncResourceLogLabel}: Stopping synchronizing ${this.resource.toLowerCase()}.`);
if (this.syncPreviewPromise) {
this.syncPreviewPromise.cancel();
this.syncPreviewPromise = null;
}
if (this.conflicts.length) {
await Promise.all(this.conflicts.map(async ({ local }) => {
try {
this.fileService.del(local);
} catch (error) {
// Ignore & log
this.logService.error(error);
}
}));
this.setConflicts([]);
}
await this.updateResourcePreviews([], CancellationToken.None);
this.setStatus(SyncStatus.Idle);
this.logService.info(`${this.syncResourceLogLabel}: Stopped synchronizing ${this.resource.toLowerCase()}.`);
}
protected abstract readonly version: number;
......
......@@ -144,6 +144,7 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement
const formattingOptions = await this.getFormattingOptions();
const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData);
const lastSettingsSyncContent: ISettingsSyncContent | null = lastSyncUserData ? this.getSettingsSyncContent(lastSyncUserData) : null;
const ignoredSettings = await this.getIgnoredSettings();
let previewContent: string | null = null;
let hasLocalChanged: boolean = false;
......@@ -154,7 +155,6 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement
const localContent: string = fileContent ? fileContent.value.toString() : '{}';
this.validateContent(localContent);
this.logService.trace(`${this.syncResourceLogLabel}: Merging remote settings with local settings...`);
const ignoredSettings = await this.getIgnoredSettings();
const result = merge(localContent, remoteSettingsSyncContent.settings, lastSettingsSyncContent ? lastSettingsSyncContent.settings : null, ignoredSettings, [], formattingOptions);
previewContent = result.localContent || result.remoteContent;
hasLocalChanged = result.localContent !== null;
......@@ -171,7 +171,6 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement
if (previewContent && !token.isCancellationRequested) {
// Remove the ignored settings from the preview.
const ignoredSettings = await this.getIgnoredSettings();
const content = updateIgnoredSettings(previewContent, '{}', ignoredSettings, formattingOptions);
await this.fileService.writeFile(this.localPreviewResource, VSBuffer.fromString(content));
}
......
......@@ -281,8 +281,6 @@ export interface ISyncResourceHandle {
uri: URI;
}
export type Conflict = { remote: URI, local: URI };
export interface IRemoteUserData {
ref: string;
syncData: ISyncData | null;
......@@ -320,8 +318,9 @@ export interface IUserDataSynchroniser {
readonly resource: SyncResource;
readonly status: SyncStatus;
readonly onDidChangeStatus: Event<SyncStatus>;
readonly conflicts: Conflict[];
readonly onDidChangeConflicts: Event<Conflict[]>;
readonly resourcePreviews: IResourcePreview[];
readonly conflicts: IResourcePreview[];
readonly onDidChangeConflicts: Event<IResourcePreview[]>;
readonly onDidChangeLocal: Event<void>;
pull(): Promise<void>;
......@@ -336,7 +335,7 @@ export interface IUserDataSynchroniser {
resetLocal(): Promise<void>;
resolveContent(resource: URI): Promise<string | null>;
acceptConflict(conflictResource: URI, content: string): Promise<void>;
acceptPreviewContent(resource: URI, content: string): Promise<void>;
getRemoteSyncResourceHandles(): Promise<ISyncResourceHandle[]>;
getLocalSyncResourceHandles(): Promise<ISyncResourceHandle[]>;
......@@ -357,7 +356,7 @@ export interface IUserDataSyncResourceEnablementService {
setResourceEnablement(resource: SyncResource, enabled: boolean): void;
}
export type SyncResourceConflicts = { syncResource: SyncResource, conflicts: Conflict[] };
export type SyncResourceConflicts = { syncResource: SyncResource, conflicts: IResourcePreview[] };
export interface ISyncTask {
manifest: IUserDataManifest | null;
......@@ -393,7 +392,7 @@ export interface IUserDataSyncService {
isFirstTimeSyncingWithAnotherMachine(): Promise<boolean>;
hasPreviouslySynced(): Promise<boolean>;
resolveContent(resource: URI): Promise<string | null>;
acceptConflict(conflictResource: URI, content: string): Promise<void>;
acceptPreviewContent(conflictResource: URI, content: string): Promise<void>;
getLocalSyncResourceHandles(resource: SyncResource): Promise<ISyncResourceHandle[]>;
getRemoteSyncResourceHandles(resource: SyncResource): Promise<ISyncResourceHandle[]>;
......
......@@ -52,7 +52,7 @@ export class UserDataSyncChannel implements IServerChannel {
case 'resetLocal': return this.service.resetLocal();
case 'hasPreviouslySynced': return this.service.hasPreviouslySynced();
case 'isFirstTimeSyncingWithAnotherMachine': return this.service.isFirstTimeSyncingWithAnotherMachine();
case 'acceptConflict': return this.service.acceptConflict(URI.revive(args[0]), args[1]);
case 'acceptPreviewContent': return this.service.acceptPreviewContent(URI.revive(args[0]), args[1]);
case 'resolveContent': return this.service.resolveContent(URI.revive(args[0]));
case 'getLocalSyncResourceHandles': return this.service.getLocalSyncResourceHandles(args[0]);
case 'getRemoteSyncResourceHandles': return this.service.getRemoteSyncResourceHandles(args[0]);
......
......@@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IUserDataSyncService, SyncStatus, IUserDataSyncStoreService, SyncResource, IUserDataSyncLogService, IUserDataSynchroniser, UserDataSyncErrorCode, UserDataSyncError, SyncResourceConflicts, ISyncResourceHandle, IUserDataManifest, ISyncTask, Change } from 'vs/platform/userDataSync/common/userDataSync';
import { IUserDataSyncService, SyncStatus, IUserDataSyncStoreService, SyncResource, IUserDataSyncLogService, IUserDataSynchroniser, UserDataSyncErrorCode, UserDataSyncError, SyncResourceConflicts, ISyncResourceHandle, IUserDataManifest, ISyncTask, Change, IResourcePreview } from 'vs/platform/userDataSync/common/userDataSync';
import { Disposable } from 'vs/base/common/lifecycle';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { Emitter, Event } from 'vs/base/common/event';
......@@ -239,12 +239,12 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
}
async acceptConflict(conflict: URI, content: string): Promise<void> {
async acceptPreviewContent(resource: URI, content: string): Promise<void> {
await this.checkEnablement();
const syncResourceConflict = this.conflicts.filter(({ conflicts }) => conflicts.some(({ local, remote }) => isEqual(conflict, local) || isEqual(conflict, remote)))[0];
if (syncResourceConflict) {
const synchroniser = this.getSynchroniser(syncResourceConflict.syncResource);
await synchroniser.acceptConflict(conflict, content);
const synchroniser = this.synchronisers.find(synchroniser => synchroniser.resourcePreviews.some(({ localResource, previewResource, remoteResource }) =>
isEqual(resource, localResource) || isEqual(resource, previewResource) || isEqual(resource, remoteResource)));
if (synchroniser) {
await synchroniser.acceptPreviewContent(resource, content);
}
}
......@@ -365,7 +365,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
private updateConflicts(): void {
const conflicts = this.computeConflicts();
if (!equals(this._conflicts, conflicts, (a, b) => a.syncResource === b.syncResource && equals(a.conflicts, b.conflicts, (a, b) => isEqual(a.local, b.local) && isEqual(a.remote, b.remote)))) {
if (!equals(this._conflicts, conflicts, (a, b) => a.syncResource === b.syncResource && equals(a.conflicts, b.conflicts, (a, b) => isEqual(a.previewResource, b.previewResource)))) {
this._conflicts = this.computeConflicts();
this._onDidChangeConflicts.fire(conflicts);
}
......@@ -412,7 +412,18 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
private computeConflicts(): SyncResourceConflicts[] {
return this.synchronisers.filter(s => s.status === SyncStatus.HasConflicts)
.map(s => ({ syncResource: s.resource, conflicts: s.conflicts }));
.map(s => ({ syncResource: s.resource, conflicts: s.conflicts.map(r => this.toStrictResourcePreview(r)) }));
}
private toStrictResourcePreview(resourcePreview: IResourcePreview): IResourcePreview {
return {
localResource: resourcePreview.localResource,
previewResource: resourcePreview.previewResource,
remoteResource: resourcePreview.remoteResource,
localChange: resourcePreview.localChange,
remoteChange: resourcePreview.remoteChange,
hasConflicts: resourcePreview.hasConflicts,
};
}
getSynchroniser(source: SyncResource): IUserDataSynchroniser {
......
......@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { IUserDataSyncStoreService, IUserDataSyncService, SyncResource, SyncStatus, Conflict, USER_DATA_SYNC_SCHEME, PREVIEW_DIR_NAME, ISyncData } from 'vs/platform/userDataSync/common/userDataSync';
import { IUserDataSyncStoreService, IUserDataSyncService, SyncResource, SyncStatus, PREVIEW_DIR_NAME, ISyncData, IResourcePreview } from 'vs/platform/userDataSync/common/userDataSync';
import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userDataSync/test/common/userDataSyncClient';
import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService';
......@@ -14,6 +14,7 @@ import { VSBuffer } from 'vs/base/common/buffer';
import { SnippetsSynchroniser } from 'vs/platform/userDataSync/common/snippetsSync';
import { joinPath } from 'vs/base/common/resources';
import { IStringDictionary } from 'vs/base/common/collections';
import { URI } from 'vs/base/common/uri';
const tsSnippet1 = `{
......@@ -276,7 +277,7 @@ suite('SnippetsSync', () => {
assert.equal(testObject.status, SyncStatus.HasConflicts);
const environmentService = testClient.instantiationService.get(IEnvironmentService);
const local = joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json');
assertConflicts(testObject.conflicts, [{ local, remote: local.with({ scheme: USER_DATA_SYNC_SCHEME }) }]);
assertConflicts(testObject.conflicts, [local]);
});
test('first time sync when snippets exists - has conflicts and accept conflicts', async () => {
......@@ -286,12 +287,12 @@ suite('SnippetsSync', () => {
await updateSnippet('html.json', htmlSnippet2, testClient);
await testObject.sync(await testClient.manifest());
const conflicts = testObject.conflicts;
await testObject.acceptConflict(conflicts[0].local, htmlSnippet1);
await testObject.acceptPreviewContent(conflicts[0].previewResource, htmlSnippet1);
assert.equal(testObject.status, SyncStatus.Idle);
assert.deepEqual(testObject.conflicts, []);
const fileService = testClient.instantiationService.get(IFileService);
assert.ok(!await fileService.exists(conflicts[0].local));
assert.ok(!await fileService.exists(conflicts[0].previewResource));
const actual1 = await readSnippet('html.json', testClient);
assert.equal(actual1, htmlSnippet1);
......@@ -315,10 +316,7 @@ suite('SnippetsSync', () => {
const environmentService = testClient.instantiationService.get(IEnvironmentService);
const local1 = joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json');
const local2 = joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json');
assertConflicts(testObject.conflicts, [
{ local: local1, remote: local1.with({ scheme: USER_DATA_SYNC_SCHEME }) },
{ local: local2, remote: local2.with({ scheme: USER_DATA_SYNC_SCHEME }) }
]);
assertConflicts(testObject.conflicts, [local1, local2]);
});
test('first time sync when snippets exists - has multiple conflicts and accept one conflict', async () => {
......@@ -331,15 +329,13 @@ suite('SnippetsSync', () => {
await testObject.sync(await testClient.manifest());
let conflicts = testObject.conflicts;
await testObject.acceptConflict(conflicts[0].local, htmlSnippet2);
const fileService = testClient.instantiationService.get(IFileService);
assert.ok(!await fileService.exists(conflicts[0].local));
await testObject.acceptPreviewContent(conflicts[0].previewResource, htmlSnippet2);
conflicts = testObject.conflicts;
assert.equal(testObject.status, SyncStatus.HasConflicts);
const environmentService = testClient.instantiationService.get(IEnvironmentService);
const local = joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json');
assertConflicts(testObject.conflicts, [{ local, remote: local.with({ scheme: USER_DATA_SYNC_SCHEME }) }]);
assertConflicts(testObject.conflicts, [local]);
});
test('first time sync when snippets exists - has multiple conflicts and accept all conflicts', async () => {
......@@ -352,14 +348,14 @@ suite('SnippetsSync', () => {
await testObject.sync(await testClient.manifest());
const conflicts = testObject.conflicts;
await testObject.acceptConflict(conflicts[0].local, htmlSnippet2);
await testObject.acceptConflict(conflicts[1].local, tsSnippet1);
await testObject.acceptPreviewContent(conflicts[0].previewResource, htmlSnippet2);
await testObject.acceptPreviewContent(conflicts[1].previewResource, tsSnippet1);
assert.equal(testObject.status, SyncStatus.Idle);
assert.deepEqual(testObject.conflicts, []);
const fileService = testClient.instantiationService.get(IFileService);
assert.ok(!await fileService.exists(conflicts[0].local));
assert.ok(!await fileService.exists(conflicts[1].local));
assert.ok(!await fileService.exists(conflicts[0].previewResource));
assert.ok(!await fileService.exists(conflicts[1].previewResource));
const actual1 = await readSnippet('html.json', testClient);
assert.equal(actual1, htmlSnippet2);
......@@ -457,7 +453,7 @@ suite('SnippetsSync', () => {
assert.equal(testObject.status, SyncStatus.HasConflicts);
const environmentService = testClient.instantiationService.get(IEnvironmentService);
const local = joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json');
assertConflicts(testObject.conflicts, [{ local, remote: local.with({ scheme: USER_DATA_SYNC_SCHEME }) }]);
assertConflicts(testObject.conflicts, [local]);
});
test('sync updating a snippet - resolve conflict', async () => {
......@@ -470,7 +466,7 @@ suite('SnippetsSync', () => {
await updateSnippet('html.json', htmlSnippet3, testClient);
await testObject.sync(await testClient.manifest());
await testObject.acceptConflict(testObject.conflicts[0].local, htmlSnippet2);
await testObject.acceptPreviewContent(testObject.conflicts[0].previewResource, htmlSnippet2);
assert.equal(testObject.status, SyncStatus.Idle);
assert.deepEqual(testObject.conflicts, []);
......@@ -560,7 +556,7 @@ suite('SnippetsSync', () => {
assert.equal(testObject.status, SyncStatus.HasConflicts);
const environmentService = testClient.instantiationService.get(IEnvironmentService);
const local = joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json');
assertConflicts(testObject.conflicts, [{ local, remote: local.with({ scheme: USER_DATA_SYNC_SCHEME }) }]);
assertConflicts(testObject.conflicts, [local]);
});
test('sync removing a snippet - resolve conflict', async () => {
......@@ -574,7 +570,7 @@ suite('SnippetsSync', () => {
await updateSnippet('html.json', htmlSnippet2, testClient);
await testObject.sync(await testClient.manifest());
await testObject.acceptConflict(testObject.conflicts[0].local, htmlSnippet3);
await testObject.acceptPreviewContent(testObject.conflicts[0].previewResource, htmlSnippet3);
assert.equal(testObject.status, SyncStatus.Idle);
assert.deepEqual(testObject.conflicts, []);
......@@ -601,7 +597,7 @@ suite('SnippetsSync', () => {
await updateSnippet('html.json', htmlSnippet2, testClient);
await testObject.sync(await testClient.manifest());
await testObject.acceptConflict(testObject.conflicts[0].local, '');
await testObject.acceptPreviewContent(testObject.conflicts[0].previewResource, '');
assert.equal(testObject.status, SyncStatus.Idle);
assert.deepEqual(testObject.conflicts, []);
......@@ -689,6 +685,22 @@ suite('SnippetsSync', () => {
assert.deepEqual(actual, { 'typescript.json': tsSnippet1, 'global.code-snippets': globalSnippet });
});
test('previews are reset after all conflicts resolved', async () => {
await updateSnippet('html.json', htmlSnippet1, client2);
await updateSnippet('typescript.json', tsSnippet1, client2);
await client2.sync();
await updateSnippet('html.json', htmlSnippet2, testClient);
await testObject.sync(await testClient.manifest());
let conflicts = testObject.conflicts;
await testObject.acceptPreviewContent(conflicts[0].previewResource, htmlSnippet2);
assert.deepEqual(testObject.resourcePreviews, []);
const fileService = testClient.instantiationService.get(IFileService);
assert.ok(fileService.exists(conflicts[0].previewResource));
});
function parseSnippets(content: string): IStringDictionary<string> {
const syncData: ISyncData = JSON.parse(content);
return JSON.parse(syncData.content);
......@@ -719,8 +731,8 @@ suite('SnippetsSync', () => {
return null;
}
function assertConflicts(actual: Conflict[], expected: Conflict[]) {
assert.deepEqual(actual.map(({ local, remote }) => ({ local: local.toString(), remote: remote.toString() })), expected.map(({ local, remote }) => ({ local: local.toString(), remote: remote.toString() })));
function assertConflicts(actual: IResourcePreview[], expected: URI[]) {
assert.deepEqual(actual.map(({ previewResource }) => previewResource.toString()), expected.map(uri => uri.toString()));
}
});
......@@ -30,7 +30,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import {
IUserDataAutoSyncService, IUserDataSyncService, registerConfiguration,
SyncResource, SyncStatus, UserDataSyncError, UserDataSyncErrorCode, USER_DATA_SYNC_SCHEME, IUserDataSyncResourceEnablementService,
SyncResourceConflicts, Conflict, getSyncResourceFromLocalPreview
SyncResourceConflicts, getSyncResourceFromLocalPreview, IResourcePreview
} from 'vs/platform/userDataSync/common/userDataSync';
import { FloatingClickWidget } from 'vs/workbench/browser/parts/editor/editorWidgets';
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
......@@ -178,7 +178,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
// close stale conflicts editor previews
if (conflictsEditorInputs.length) {
conflictsEditorInputs.forEach(input => {
if (!conflicts.some(({ local }) => isEqual(local, input.primary.resource))) {
if (!conflicts.some(({ previewResource }) => isEqual(previewResource, input.primary.resource))) {
input.dispose();
}
});
......@@ -238,12 +238,12 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
}
}
private async acceptRemote(syncResource: SyncResource, conflicts: Conflict[]) {
private async acceptRemote(syncResource: SyncResource, conflicts: IResourcePreview[]) {
try {
for (const conflict of conflicts) {
const modelRef = await this.textModelResolverService.createModelReference(conflict.remote);
const modelRef = await this.textModelResolverService.createModelReference(conflict.remoteResource);
try {
await this.userDataSyncService.acceptConflict(conflict.remote, modelRef.object.textEditorModel.getValue());
await this.userDataSyncService.acceptPreviewContent(conflict.remoteResource, modelRef.object.textEditorModel.getValue());
} finally {
modelRef.dispose();
}
......@@ -253,12 +253,12 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
}
}
private async acceptLocal(syncResource: SyncResource, conflicts: Conflict[]): Promise<void> {
private async acceptLocal(syncResource: SyncResource, conflicts: IResourcePreview[]): Promise<void> {
try {
for (const conflict of conflicts) {
const modelRef = await this.textModelResolverService.createModelReference(conflict.local);
const modelRef = await this.textModelResolverService.createModelReference(conflict.previewResource);
try {
await this.userDataSyncService.acceptConflict(conflict.local, modelRef.object.textEditorModel.getValue());
await this.userDataSyncService.acceptPreviewContent(conflict.previewResource, modelRef.object.textEditorModel.getValue());
} finally {
modelRef.dispose();
}
......@@ -625,11 +625,11 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
} else if (syncResource === SyncResource.Keybindings) {
label = localize('keybindings conflicts preview', "Keybindings Conflicts (Remote ↔ Local)");
} else if (syncResource === SyncResource.Snippets) {
label = localize('snippets conflicts preview', "User Snippet Conflicts (Remote ↔ Local) - {0}", basename(conflict.local));
label = localize('snippets conflicts preview', "User Snippet Conflicts (Remote ↔ Local) - {0}", basename(conflict.previewResource));
}
await this.editorService.openEditor({
leftResource: conflict.remote,
rightResource: conflict.local,
leftResource: conflict.remoteResource,
rightResource: conflict.previewResource,
label,
options: {
preserveFocus: false,
......@@ -814,7 +814,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
private registerShowSnippetsConflictsAction(): void {
this._snippetsConflictsActionsDisposable.clear();
const resolveSnippetsConflictsWhenContext = ContextKeyExpr.regex(CONTEXT_CONFLICTS_SOURCES.keys()[0], /.*snippets.*/i);
const conflicts: Conflict[] | undefined = this.userDataSyncService.conflicts.filter(({ syncResource }) => syncResource === SyncResource.Snippets)[0]?.conflicts;
const conflicts: IResourcePreview[] | undefined = this.userDataSyncService.conflicts.filter(({ syncResource }) => syncResource === SyncResource.Snippets)[0]?.conflicts;
this._snippetsConflictsActionsDisposable.add(CommandsRegistry.registerCommand(resolveSnippetsConflictsCommand.id, () => this.handleSyncResourceConflicts(SyncResource.Snippets)));
this._snippetsConflictsActionsDisposable.add(MenuRegistry.appendMenuItem(MenuId.GlobalActivity, {
group: '5_sync',
......@@ -1127,11 +1127,11 @@ class AcceptChangesContribution extends Disposable implements IEditorContributio
return false;
}
if (syncResourceConflicts.conflicts.some(({ local }) => isEqual(local, model.uri))) {
if (syncResourceConflicts.conflicts.some(({ previewResource }) => isEqual(previewResource, model.uri))) {
return true;
}
if (syncResourceConflicts.conflicts.some(({ remote }) => isEqual(remote, model.uri))) {
if (syncResourceConflicts.conflicts.some(({ remoteResource }) => isEqual(remoteResource, model.uri))) {
return this.configurationService.getValue<boolean>('diffEditor.renderSideBySide');
}
......@@ -1142,7 +1142,7 @@ class AcceptChangesContribution extends Disposable implements IEditorContributio
if (!this.acceptChangesButton) {
const resource = this.editor.getModel()!.uri;
const syncResourceConflicts = this.getSyncResourceConflicts(resource)!;
const isRemote = syncResourceConflicts.conflicts.some(({ remote }) => isEqual(remote, resource));
const isRemote = syncResourceConflicts.conflicts.some(({ remoteResource }) => isEqual(remoteResource, resource));
const acceptRemoteLabel = localize('accept remote', "Accept Remote");
const acceptLocalLabel = localize('accept local', "Accept Local");
this.acceptChangesButton = this.instantiationService.createInstance(FloatingClickWidget, this.editor, isRemote ? acceptRemoteLabel : acceptLocalLabel, null);
......@@ -1163,11 +1163,11 @@ class AcceptChangesContribution extends Disposable implements IEditorContributio
});
if (result.confirmed) {
try {
await this.userDataSyncService.acceptConflict(model.uri, model.getValue());
await this.userDataSyncService.acceptPreviewContent(model.uri, model.getValue());
} catch (e) {
if (e instanceof UserDataSyncError && e.code === UserDataSyncErrorCode.LocalPreconditionFailed) {
const syncResourceCoflicts = this.userDataSyncService.conflicts.filter(({ syncResource }) => syncResource === syncResourceConflicts.syncResource)[0];
if (syncResourceCoflicts && syncResourceCoflicts.conflicts.some(conflict => isEqual(conflict.local, model.uri) || isEqual(conflict.remote, model.uri))) {
if (syncResourceCoflicts && syncResourceCoflicts.conflicts.some(conflict => isEqual(conflict.previewResource, model.uri) || isEqual(conflict.remoteResource, model.uri))) {
this.notificationService.warn(localize('update conflicts', "Could not resolve conflicts as there is new local version available. Please try again."));
}
} else {
......@@ -1183,7 +1183,7 @@ class AcceptChangesContribution extends Disposable implements IEditorContributio
}
private getSyncResourceConflicts(resource: URI): SyncResourceConflicts | undefined {
return this.userDataSyncService.conflicts.filter(({ conflicts }) => conflicts.some(({ local, remote }) => isEqual(local, resource) || isEqual(remote, resource)))[0];
return this.userDataSyncService.conflicts.filter(({ conflicts }) => conflicts.some(({ previewResource, remoteResource }) => isEqual(previewResource, resource) || isEqual(remoteResource, resource)))[0];
}
private disposeAcceptChangesWidgetRenderer(): void {
......
......@@ -103,8 +103,8 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
return this.channel.call('isFirstTimeSyncingWithAnotherMachine');
}
acceptConflict(conflict: URI, content: string): Promise<void> {
return this.channel.call('acceptConflict', [conflict, content]);
acceptPreviewContent(resource: URI, content: string): Promise<void> {
return this.channel.call('acceptPreviewContent', [resource, content]);
}
resolveContent(resource: URI): Promise<string | null> {
......@@ -140,8 +140,13 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
this._conflicts = conflicts.map(c =>
({
syncResource: c.syncResource,
conflicts: c.conflicts.map(({ local, remote }) =>
({ local: URI.revive(local), remote: URI.revive(remote) }))
conflicts: c.conflicts.map(r =>
({
...r,
localResource: URI.revive(r.localResource),
remoteResource: URI.revive(r.remoteResource),
previewResource: URI.revive(r.previewResource),
}))
}));
this._onDidChangeConflicts.fire(conflicts);
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册