diff --git a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts index 252970b8c27..d0d293cabf5 100644 --- a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts +++ b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts @@ -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 = this._register(new Emitter()); readonly onDidChangeStatus: Event = this._onDidChangStatus.event; - private _conflicts: Conflict[] = []; - get conflicts(): Conflict[] { return this._conflicts; } - private _onDidChangeConflicts: Emitter = this._register(new Emitter()); - readonly onDidChangeConflicts: Event = this._onDidChangeConflicts.event; + private _resourcePreviews: IResourcePreview[] = []; + get resourcePreviews(): IResourcePreview[] { return this._resourcePreviews; } + get conflicts(): IResourcePreview[] { return this._resourcePreviews.filter(({ hasConflicts }) => hasConflicts); } + private _onDidChangeConflicts: Emitter = this._register(new Emitter()); + readonly onDidChangeConflicts: Event = this._onDidChangeConflicts.event; private readonly localChangeTriggerScheduler = new RunOnceScheduler(() => this.doTriggerLocalChange(), 50); private readonly _onDidChangeLocal: Emitter = this._register(new Emitter()); @@ -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 { - let preview = this.syncPreviewPromise ? await this.syncPreviewPromise : null; - - if (!preview || !preview.resourcePreviews.some(({ hasConflicts }) => hasConflicts)) { + async acceptPreviewContent(resource: URI, content: string): Promise { + 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 { - const conflict = this.conflicts.find(({ local, remote }) => isEqual(local, conflictResource) || isEqual(remote, conflictResource)); - if (!conflict) { - return preview; + private async updateSyncResourcePreviewContent(preview: ISyncResourcePreview, resource: URI, previewContent: string, token: CancellationToken): Promise { + 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 index = preview.resourcePreviews.findIndex(({ previewResource }) => previewResource && isEqual(previewResource, conflict.local)); - if (index === -1) { - return preview; - } - 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 { @@ -427,27 +424,25 @@ export abstract class AbstractSynchroniser extends Disposable { }; } - private async updateConflicts(resourcePreviews: IResourcePreview[], token: CancellationToken): Promise { - 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 { + 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 { @@ -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 { - 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; diff --git a/src/vs/platform/userDataSync/common/settingsSync.ts b/src/vs/platform/userDataSync/common/settingsSync.ts index feb30518b99..4d8af146c62 100644 --- a/src/vs/platform/userDataSync/common/settingsSync.ts +++ b/src/vs/platform/userDataSync/common/settingsSync.ts @@ -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)); } diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index cf6b03f0b22..a92a028b8c1 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -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; - readonly conflicts: Conflict[]; - readonly onDidChangeConflicts: Event; + readonly resourcePreviews: IResourcePreview[]; + readonly conflicts: IResourcePreview[]; + readonly onDidChangeConflicts: Event; readonly onDidChangeLocal: Event; pull(): Promise; @@ -336,7 +335,7 @@ export interface IUserDataSynchroniser { resetLocal(): Promise; resolveContent(resource: URI): Promise; - acceptConflict(conflictResource: URI, content: string): Promise; + acceptPreviewContent(resource: URI, content: string): Promise; getRemoteSyncResourceHandles(): Promise; getLocalSyncResourceHandles(): Promise; @@ -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; hasPreviouslySynced(): Promise; resolveContent(resource: URI): Promise; - acceptConflict(conflictResource: URI, content: string): Promise; + acceptPreviewContent(conflictResource: URI, content: string): Promise; getLocalSyncResourceHandles(resource: SyncResource): Promise; getRemoteSyncResourceHandles(resource: SyncResource): Promise; diff --git a/src/vs/platform/userDataSync/common/userDataSyncIpc.ts b/src/vs/platform/userDataSync/common/userDataSyncIpc.ts index 33a87aa165c..dd6ee86bd1e 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncIpc.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncIpc.ts @@ -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]); diff --git a/src/vs/platform/userDataSync/common/userDataSyncService.ts b/src/vs/platform/userDataSync/common/userDataSyncService.ts index 4c87152e11a..8548e162d09 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncService.ts @@ -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 { + async acceptPreviewContent(resource: URI, content: string): Promise { 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 { diff --git a/src/vs/platform/userDataSync/test/common/snippetsSync.test.ts b/src/vs/platform/userDataSync/test/common/snippetsSync.test.ts index d1ad340d577..25e37483de4 100644 --- a/src/vs/platform/userDataSync/test/common/snippetsSync.test.ts +++ b/src/vs/platform/userDataSync/test/common/snippetsSync.test.ts @@ -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 { 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())); } }); diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index 586b7f4f541..df9541e4ca4 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -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 { + private async acceptLocal(syncResource: SyncResource, conflicts: IResourcePreview[]): Promise { 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('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 { diff --git a/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncService.ts b/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncService.ts index 7f1084593fe..e6af654e7c8 100644 --- a/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncService.ts +++ b/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncService.ts @@ -103,8 +103,8 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ return this.channel.call('isFirstTimeSyncingWithAnotherMachine'); } - acceptConflict(conflict: URI, content: string): Promise { - return this.channel.call('acceptConflict', [conflict, content]); + acceptPreviewContent(resource: URI, content: string): Promise { + return this.channel.call('acceptPreviewContent', [resource, content]); } resolveContent(resource: URI): Promise { @@ -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); }