diff --git a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts index fa19ec45df4..88e4f41c68d 100644 --- a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts +++ b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts @@ -9,7 +9,8 @@ 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, IUserDataManifest, ISyncData, IRemoteUserData, PREVIEW_DIR_NAME + IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService, Conflict, 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'; import { joinPath, dirname, isEqual, basename } from 'vs/base/common/resources'; @@ -55,6 +56,13 @@ function isSyncData(thing: any): thing is ISyncData { export interface ISyncResourcePreview extends IBaseSyncResourcePreview { readonly remoteUserData: IRemoteUserData; readonly lastSyncUserData: IRemoteUserData | null; + readonly resourcePreviews: IResourcePreview[]; +} + +export interface IResourcePreview extends IBaseResourcePreview { + readonly remoteContent: string | null; + readonly localContent: string | null; + readonly previewContent: string | null; } export abstract class AbstractSynchroniser extends Disposable { @@ -127,7 +135,7 @@ export abstract class AbstractSynchroniser extends Disposable { else { this.logService.trace(`${this.syncResourceLogLabel}: Checking for local changes...`); const lastSyncUserData = await this.getLastSyncUserData(); - const hasRemoteChanged = lastSyncUserData ? (await this.generatePreview(lastSyncUserData, lastSyncUserData, CancellationToken.None)).hasRemoteChanged : true; + const hasRemoteChanged = lastSyncUserData ? (await this.doGenerateSyncResourcePreview(lastSyncUserData, lastSyncUserData, CancellationToken.None)).resourcePreviews.some(({ remoteChange }) => remoteChange !== Change.None) : true; if (hasRemoteChanged) { this._onDidChangeLocal.fire(); } @@ -153,7 +161,7 @@ export abstract class AbstractSynchroniser extends Disposable { } } - protected setConflicts(conflicts: Conflict[]) { + 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); @@ -176,7 +184,7 @@ export abstract class AbstractSynchroniser extends Disposable { const remoteUserData = await this.getRemoteUserData(lastSyncUserData); const preview = await this.generatePullPreview(remoteUserData, lastSyncUserData, CancellationToken.None); - await this.applyPreview(preview, false); + await this.applyPreview(remoteUserData, lastSyncUserData, preview, false); this.logService.info(`${this.syncResourceLogLabel}: Finished pulling ${this.syncResourceLogLabel.toLowerCase()}.`); } finally { this.setStatus(SyncStatus.Idle); @@ -199,7 +207,7 @@ export abstract class AbstractSynchroniser extends Disposable { const remoteUserData = await this.getRemoteUserData(lastSyncUserData); const preview = await this.generatePushPreview(remoteUserData, lastSyncUserData, CancellationToken.None); - await this.applyPreview(preview, true); + await this.applyPreview(remoteUserData, lastSyncUserData, preview, true); this.logService.info(`${this.syncResourceLogLabel}: Finished pushing ${this.syncResourceLogLabel.toLowerCase()}.`); } finally { @@ -267,7 +275,7 @@ export abstract class AbstractSynchroniser extends Disposable { const lastSyncUserData = await this.getLastSyncUserData(); const remoteUserData = await this.getLatestRemoteUserData(null, lastSyncUserData); const preview = await this.generateReplacePreview(syncData, remoteUserData, lastSyncUserData); - await this.applyPreview(preview, false); + await this.applyPreview(remoteUserData, lastSyncUserData, preview, false); this.logService.info(`${this.syncResourceLogLabel}: Finished resetting ${this.resource.toLowerCase()}.`); } finally { this.setStatus(SyncStatus.Idle); @@ -294,11 +302,11 @@ export abstract class AbstractSynchroniser extends Disposable { return this.getRemoteUserData(lastSyncUserData); } - async generateSyncPreview(): Promise { + async generateSyncResourcePreview(): Promise { if (this.isEnabled()) { const lastSyncUserData = await this.getLastSyncUserData(); const remoteUserData = await this.getRemoteUserData(lastSyncUserData); - return this.generatePreview(remoteUserData, lastSyncUserData, CancellationToken.None); + return this.doGenerateSyncResourcePreview(remoteUserData, lastSyncUserData, CancellationToken.None); } return null; } @@ -343,16 +351,16 @@ export abstract class AbstractSynchroniser extends Disposable { try { // generate or use existing preview if (!this.syncPreviewPromise) { - this.syncPreviewPromise = createCancelablePromise(token => this.generatePreview(remoteUserData, lastSyncUserData, token)); + this.syncPreviewPromise = createCancelablePromise(token => this.doGenerateSyncResourcePreview(remoteUserData, lastSyncUserData, token)); } const preview = await this.syncPreviewPromise; - if (preview.hasConflicts) { + if (preview.resourcePreviews.some(({ hasConflicts }) => hasConflicts)) { return SyncStatus.HasConflicts; } // apply preview - await this.applyPreview(preview, false); + await this.applyPreview(remoteUserData, lastSyncUserData, preview.resourcePreviews, false); // reset preview this.syncPreviewPromise = null; @@ -367,32 +375,80 @@ export abstract class AbstractSynchroniser extends Disposable { } } - protected async getSyncPreviewInProgress(): Promise { - return this.syncPreviewPromise ? this.syncPreviewPromise : null; - } - async acceptConflict(conflictUri: URI, conflictContent: string): Promise { - let preview = await this.getSyncPreviewInProgress(); + let preview = this.syncPreviewPromise ? await this.syncPreviewPromise : null; - if (!preview || !preview.hasConflicts) { + if (!preview || !preview.resourcePreviews.some(({ hasConflicts }) => hasConflicts)) { return; } - - this.syncPreviewPromise = createCancelablePromise(token => this.updatePreviewWithConflict(preview!, conflictUri, conflictContent, token)); + this.syncPreviewPromise = createCancelablePromise(async token => { + const newPreview = await this.updateSyncResourcePreviewWithConflict(preview!, conflictUri, conflictContent, token); + await this.updateConflicts(newPreview.resourcePreviews, token); + return newPreview; + }); preview = await this.syncPreviewPromise; - if (!preview.hasConflicts) { - + if (!preview.resourcePreviews.some(({ hasConflicts }) => hasConflicts)) { // apply preview - await this.applyPreview(preview, false); + await this.applyPreview(preview.remoteUserData, preview.lastSyncUserData, preview.resourcePreviews, false); // reset preview this.syncPreviewPromise = null; 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; + } + 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 + }; + } + + protected async updateResourcePreviewContent(resourcePreview: IResourcePreview, resource: URI, previewContent: string, token: CancellationToken): Promise { + return { + ...resourcePreview, + previewContent, + hasConflicts: false, + localChange: Change.Modified, + remoteChange: Change.Modified, + }; + } + + 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! }); + } + } + + for (const conflict of this.conflicts) { + // clear obsolete conflicts + if (!conflicts.some(({ local }) => isEqual(local, conflict.local))) { + try { + await this.fileService.del(conflict.local); + } catch (error) { + // Ignore & log + this.logService.error(error); + } + } + } + + this.setConflicts(conflicts); } async hasPreviouslySynced(): Promise { @@ -400,11 +456,6 @@ export abstract class AbstractSynchroniser extends Disposable { return !!lastSyncData; } - protected async isLastSyncFromCurrentMachine(remoteUserData: IRemoteUserData): Promise { - const machineId = await this.currentMachineIdPromise; - return !!remoteUserData.syncData?.machineId && remoteUserData.syncData.machineId === machineId; - } - async getRemoteSyncResourceHandles(): Promise { const handles = await this.userDataSyncStoreService.getAllRefs(this.resource); return handles.map(({ created, ref }) => ({ created, uri: this.toRemoteBackupResource(ref) })); @@ -447,12 +498,41 @@ export abstract class AbstractSynchroniser extends Disposable { return null; } + protected async resolvePreviewContent(uri: URI): Promise { + const syncPreview = this.syncPreviewPromise ? await this.syncPreviewPromise : null; + if (syncPreview) { + for (const resourcePreview of syncPreview.resourcePreviews) { + if (resourcePreview.previewResource && isEqual(resourcePreview.previewResource, uri)) { + return resourcePreview.previewContent || ''; + } + if (resourcePreview.remoteResource && isEqual(resourcePreview.remoteResource, uri)) { + return resourcePreview.remoteContent || ''; + } + if (resourcePreview.localResource && isEqual(resourcePreview.localResource, uri)) { + return resourcePreview.localContent || ''; + } + } + } + return null; + } + async resetLocal(): Promise { try { await this.fileService.del(this.lastSyncResource); } catch (e) { /* ignore */ } } + private async doGenerateSyncResourcePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise { + const machineId = await this.currentMachineIdPromise; + const isLastSyncFromCurrentMachine = !!remoteUserData.syncData?.machineId && remoteUserData.syncData.machineId === machineId; + + // 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); + return { remoteUserData, lastSyncUserData, resourcePreviews, isLastSyncFromCurrentMachine }; + } + async getLastSyncUserData(): Promise { try { const content = await this.fileService.readFile(this.lastSyncResource); @@ -531,21 +611,30 @@ export abstract class AbstractSynchroniser extends Disposable { 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([]); + } this.setStatus(SyncStatus.Idle); } protected abstract readonly version: number; - protected abstract generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise; - protected abstract generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise; - protected abstract generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise; - protected abstract generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise; - protected abstract updatePreviewWithConflict(preview: ISyncResourcePreview, conflictResource: URI, content: string, token: CancellationToken): Promise; - protected abstract applyPreview(preview: ISyncResourcePreview, forcePush: boolean): Promise; + protected abstract generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise; + protected abstract generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise; + protected abstract generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise; + protected abstract generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise; + protected abstract applyPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: IResourcePreview[], forcePush: boolean): Promise; } -export interface IFileSyncPreview extends ISyncResourcePreview { +export interface IFileResourcePreview extends IResourcePreview { readonly fileContent: IFileContent | null; - readonly content: string | null; } export abstract class AbstractFileSynchroniser extends AbstractSynchroniser { @@ -568,28 +657,6 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser { this._register(this.fileService.onDidFilesChange(e => this.onFileChanges(e))); } - async stop(): Promise { - await super.stop(); - try { - await this.fileService.del(this.localPreviewResource); - } catch (e) { /* ignore */ } - } - - protected async resolvePreviewContent(conflictResource: URI): Promise { - if (isEqual(this.remotePreviewResource, conflictResource) || isEqual(this.localPreviewResource, conflictResource)) { - const syncPreview = await this.getSyncPreviewInProgress(); - if (syncPreview) { - if (isEqual(this.remotePreviewResource, conflictResource)) { - return syncPreview.remoteUserData && syncPreview.remoteUserData.syncData ? syncPreview.remoteUserData.syncData.content : null; - } - if (isEqual(this.localPreviewResource, conflictResource)) { - return (syncPreview as IFileSyncPreview).fileContent ? (syncPreview as IFileSyncPreview).fileContent!.value.toString() : null; - } - } - } - return null; - } - protected async getLocalFileContent(): Promise { try { return await this.fileService.readFile(this.file); @@ -624,8 +691,6 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser { this.triggerLocalChange(); } - protected abstract readonly localPreviewResource: URI; - protected abstract readonly remotePreviewResource: URI; } export abstract class AbstractJsonFileSynchroniser extends AbstractFileSynchroniser { diff --git a/src/vs/platform/userDataSync/common/extensionsSync.ts b/src/vs/platform/userDataSync/common/extensionsSync.ts index 4cce10a6179..60eb144bc0c 100644 --- a/src/vs/platform/userDataSync/common/extensionsSync.ts +++ b/src/vs/platform/userDataSync/common/extensionsSync.ts @@ -5,7 +5,7 @@ import { IUserDataSyncStoreService, ISyncExtension, IUserDataSyncLogService, IUserDataSynchroniser, SyncResource, IUserDataSyncResourceEnablementService, - IUserDataSyncBackupStoreService, ISyncResourceHandle, USER_DATA_SYNC_SCHEME, IRemoteUserData, ISyncData, IResourcePreview + IUserDataSyncBackupStoreService, ISyncResourceHandle, USER_DATA_SYNC_SCHEME, IRemoteUserData, ISyncData, Change } from 'vs/platform/userDataSync/common/userDataSync'; import { Event } from 'vs/base/common/event'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -14,8 +14,8 @@ import { ExtensionType, IExtensionIdentifier } from 'vs/platform/extensions/comm import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IFileService } from 'vs/platform/files/common/files'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { merge, getIgnoredExtensions, IMergeResult } from 'vs/platform/userDataSync/common/extensionsMerge'; -import { AbstractSynchroniser, ISyncResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer'; +import { merge, getIgnoredExtensions } from 'vs/platform/userDataSync/common/extensionsMerge'; +import { AbstractSynchroniser, IResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { URI } from 'vs/base/common/uri'; import { joinPath, dirname, basename, isEqual } from 'vs/base/common/resources'; @@ -25,9 +25,8 @@ import { compare } from 'vs/base/common/strings'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { CancellationToken } from 'vs/base/common/cancellation'; -interface IExtensionsSyncPreview extends ISyncResourcePreview { +export interface IExtensionResourcePreview extends IResourcePreview { readonly localExtensions: ISyncExtension[]; - readonly lastSyncUserData: ILastSyncUserData | null; readonly added: ISyncExtension[]; readonly removed: IExtensionIdentifier[]; readonly updated: ISyncExtension[]; @@ -74,84 +73,111 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse () => undefined, 500)(() => this.triggerLocalChange())); } - protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise { + protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise { const installedExtensions = await this.extensionManagementService.getInstalled(); const localExtensions = this.getLocalExtensions(installedExtensions); const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService); + const localResource = ExtensionsSynchroniser.EXTENSIONS_DATA_URI; + const localContent = this.format(localExtensions); + const remoteResource = this.remotePreviewResource; + const previewResource = this.localPreviewResource; + const previewContent = null; + const resourcePreviews: IExtensionResourcePreview[] = []; if (remoteUserData.syncData !== null) { const remoteExtensions = await this.parseAndMigrateExtensions(remoteUserData.syncData); const mergeResult = merge(localExtensions, remoteExtensions, localExtensions, [], ignoredExtensions); const { added, removed, updated, remote } = mergeResult; - const resourcePreviews: IResourcePreview[] = this.getResourcePreviews(mergeResult); - return { - remoteUserData, lastSyncUserData, - added, removed, updated, remote, localExtensions, skippedExtensions: [], - hasLocalChanged: resourcePreviews.some(({ hasLocalChanged }) => hasLocalChanged), - hasRemoteChanged: resourcePreviews.some(({ hasRemoteChanged }) => hasRemoteChanged), + resourcePreviews.push({ + localResource, + localContent, + remoteResource, + remoteContent: this.format(remoteExtensions), + previewResource, + previewContent, + added, + removed, + updated, + remote, + localExtensions, + skippedExtensions: [], + localChange: added.length > 0 || removed.length > 0 || updated.length > 0 ? Change.Modified : Change.None, + remoteChange: remote !== null ? Change.Modified : Change.None, hasConflicts: false, - isLastSyncFromCurrentMachine: false, - resourcePreviews, - }; + }); } else { - return { - remoteUserData, lastSyncUserData, + resourcePreviews.push({ + localResource, + localContent, + remoteResource, + remoteContent: null, + previewResource, + previewContent, added: [], removed: [], updated: [], remote: null, localExtensions, skippedExtensions: [], - hasLocalChanged: false, - hasRemoteChanged: false, + localChange: Change.None, + remoteChange: Change.None, hasConflicts: false, - isLastSyncFromCurrentMachine: false, - resourcePreviews: [], - }; + }); } + return resourcePreviews; } - protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise { + protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise { const installedExtensions = await this.extensionManagementService.getInstalled(); const localExtensions = this.getLocalExtensions(installedExtensions); + const remoteExtensions = remoteUserData.syncData ? await this.parseAndMigrateExtensions(remoteUserData.syncData) : null; const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService); const mergeResult = merge(localExtensions, null, null, [], ignoredExtensions); const { added, removed, updated, remote } = mergeResult; - const resourcePreviews: IResourcePreview[] = this.getResourcePreviews(mergeResult); - return { - added, removed, updated, remote, remoteUserData, localExtensions, skippedExtensions: [], lastSyncUserData, - hasLocalChanged: resourcePreviews.some(({ hasLocalChanged }) => hasLocalChanged), - hasRemoteChanged: resourcePreviews.some(({ hasRemoteChanged }) => hasRemoteChanged), - isLastSyncFromCurrentMachine: false, + return [{ + localResource: ExtensionsSynchroniser.EXTENSIONS_DATA_URI, + localContent: this.format(localExtensions), + remoteResource: this.remotePreviewResource, + remoteContent: remoteExtensions ? this.format(remoteExtensions) : null, + previewResource: this.localPreviewResource, + previewContent: null, + added, + removed, + updated, + remote, + localExtensions, + skippedExtensions: [], + localChange: added.length > 0 || removed.length > 0 || updated.length > 0 ? Change.Modified : Change.None, + remoteChange: remote !== null ? Change.Modified : Change.None, hasConflicts: false, - resourcePreviews - }; + }]; } - protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise { + protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise { const installedExtensions = await this.extensionManagementService.getInstalled(); const localExtensions = this.getLocalExtensions(installedExtensions); + const remoteExtensions = remoteUserData.syncData ? await this.parseAndMigrateExtensions(remoteUserData.syncData) : null; const syncExtensions = await this.parseAndMigrateExtensions(syncData); const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService); const mergeResult = merge(localExtensions, syncExtensions, localExtensions, [], ignoredExtensions); const { added, removed, updated } = mergeResult; - const resourcePreviews: IResourcePreview[] = this.getResourcePreviews(mergeResult); - return { - added, removed, updated, remote: syncExtensions, remoteUserData, localExtensions, skippedExtensions: [], lastSyncUserData, - hasLocalChanged: resourcePreviews.some(({ hasLocalChanged }) => hasLocalChanged), - hasRemoteChanged: true, - isLastSyncFromCurrentMachine: false, + return [{ + localResource: ExtensionsSynchroniser.EXTENSIONS_DATA_URI, + localContent: this.format(localExtensions), + remoteResource: this.remotePreviewResource, + remoteContent: remoteExtensions ? this.format(remoteExtensions) : null, + previewResource: this.localPreviewResource, + previewContent: null, + added, + removed, + updated, + remote: syncExtensions, + localExtensions, + skippedExtensions: [], + localChange: added.length > 0 || removed.length > 0 || updated.length > 0 ? Change.Modified : Change.None, + remoteChange: Change.Modified, hasConflicts: false, - resourcePreviews - }; + }]; } - protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise { + protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise { const remoteExtensions: ISyncExtension[] | null = remoteUserData.syncData ? await this.parseAndMigrateExtensions(remoteUserData.syncData) : null; const skippedExtensions: ISyncExtension[] = lastSyncUserData ? lastSyncUserData.skippedExtensions || [] : []; - const isLastSyncFromCurrentMachine = await this.isLastSyncFromCurrentMachine(remoteUserData); - let lastSyncExtensions: ISyncExtension[] | null = null; - if (lastSyncUserData === null) { - if (isLastSyncFromCurrentMachine) { - lastSyncExtensions = await this.parseAndMigrateExtensions(remoteUserData.syncData!); - } - } else { - lastSyncExtensions = await this.parseAndMigrateExtensions(lastSyncUserData.syncData!); - } + const lastSyncExtensions: ISyncExtension[] | null = lastSyncUserData ? await this.parseAndMigrateExtensions(lastSyncUserData.syncData!) : null; const installedExtensions = await this.extensionManagementService.getInstalled(); const localExtensions = this.getLocalExtensions(installedExtensions); @@ -165,36 +191,34 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse const mergeResult = merge(localExtensions, remoteExtensions, lastSyncExtensions, skippedExtensions, ignoredExtensions); const { added, removed, updated, remote } = mergeResult; - const resourcePreviews: IResourcePreview[] = this.getResourcePreviews(mergeResult); - return { + return [{ + localResource: ExtensionsSynchroniser.EXTENSIONS_DATA_URI, + localContent: this.format(localExtensions), + remoteResource: this.remotePreviewResource, + remoteContent: remoteExtensions ? this.format(remoteExtensions) : null, + previewResource: this.localPreviewResource, + previewContent: null, added, removed, updated, remote, - skippedExtensions, - remoteUserData, localExtensions, - lastSyncUserData, - hasLocalChanged: resourcePreviews.some(({ hasLocalChanged }) => hasLocalChanged), - hasRemoteChanged: resourcePreviews.some(({ hasRemoteChanged }) => hasRemoteChanged), - isLastSyncFromCurrentMachine, + skippedExtensions, + localChange: added.length > 0 || removed.length > 0 || updated.length > 0 ? Change.Modified : Change.None, + remoteChange: remote !== null ? Change.Modified : Change.None, hasConflicts: false, - resourcePreviews - }; + }]; } - protected async updatePreviewWithConflict(preview: IExtensionsSyncPreview, conflictResource: URI, content: string, token: CancellationToken): Promise { - throw new Error(`${this.syncResourceLogLabel}: Conflicts should not occur`); - } + protected async applyPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, resourcePreviews: IExtensionResourcePreview[], forcePush: boolean): Promise { + let { added, removed, updated, remote, skippedExtensions, localExtensions, localChange, remoteChange } = resourcePreviews[0]; - protected async applyPreview({ added, removed, updated, remote, remoteUserData, skippedExtensions, lastSyncUserData, localExtensions, hasLocalChanged, hasRemoteChanged }: IExtensionsSyncPreview, forcePush: boolean): Promise { - - if (!hasLocalChanged && !hasRemoteChanged) { + if (localChange === Change.None && remoteChange === Change.None) { this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing extensions.`); } - if (hasLocalChanged) { + if (localChange !== Change.None) { await this.backupLocal(JSON.stringify(localExtensions)); skippedExtensions = await this.updateLocalExtensions(added, removed, updated, skippedExtensions); } @@ -215,18 +239,6 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse } } - private getResourcePreviews({ added, removed, updated, remote }: IMergeResult): IResourcePreview[] { - const hasLocalChanged = added.length > 0 || removed.length > 0 || updated.length > 0; - const hasRemoteChanged = remote !== null; - return [{ - hasLocalChanged, - hasConflicts: false, - hasRemoteChanged, - localResouce: ExtensionsSynchroniser.EXTENSIONS_DATA_URI, - remoteResource: this.remotePreviewResource - }]; - } - async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> { return [{ resource: joinPath(uri, 'extensions.json'), comparableResource: ExtensionsSynchroniser.EXTENSIONS_DATA_URI }]; } @@ -238,6 +250,10 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse return this.format(localExtensions); } + if (isEqual(this.remotePreviewResource, uri) || isEqual(this.localPreviewResource, uri)) { + return this.resolvePreviewContent(uri); + } + let content = await super.resolveContent(uri); if (content) { return content; diff --git a/src/vs/platform/userDataSync/common/globalStateSync.ts b/src/vs/platform/userDataSync/common/globalStateSync.ts index 77dc7dee3a0..76a2360abfe 100644 --- a/src/vs/platform/userDataSync/common/globalStateSync.ts +++ b/src/vs/platform/userDataSync/common/globalStateSync.ts @@ -5,7 +5,7 @@ import { IUserDataSyncStoreService, IUserDataSyncLogService, IGlobalState, SyncResource, IUserDataSynchroniser, IUserDataSyncResourceEnablementService, - IUserDataSyncBackupStoreService, ISyncResourceHandle, IStorageValue, USER_DATA_SYNC_SCHEME, IRemoteUserData, ISyncData, IResourcePreview + IUserDataSyncBackupStoreService, ISyncResourceHandle, IStorageValue, USER_DATA_SYNC_SCHEME, IRemoteUserData, ISyncData, Change } from 'vs/platform/userDataSync/common/userDataSync'; import { VSBuffer } from 'vs/base/common/buffer'; import { Event } from 'vs/base/common/event'; @@ -14,9 +14,9 @@ import { dirname, joinPath, basename, isEqual } from 'vs/base/common/resources'; import { IFileService } from 'vs/platform/files/common/files'; import { IStringDictionary } from 'vs/base/common/collections'; import { edit } from 'vs/platform/userDataSync/common/content'; -import { merge, IMergeResult } from 'vs/platform/userDataSync/common/globalStateMerge'; +import { merge } from 'vs/platform/userDataSync/common/globalStateMerge'; import { parse } from 'vs/base/common/json'; -import { AbstractSynchroniser, ISyncResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer'; +import { AbstractSynchroniser, IResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { URI } from 'vs/base/common/uri'; @@ -30,12 +30,11 @@ import { CancellationToken } from 'vs/base/common/cancellation'; const argvStoragePrefx = 'globalState.argv.'; const argvProperties: string[] = ['locale']; -interface IGlobalStateSyncPreview extends ISyncResourcePreview { +export interface IGlobalStateResourcePreview extends IResourcePreview { readonly local: { added: IStringDictionary, removed: string[], updated: IStringDictionary }; readonly remote: IStringDictionary | null; readonly skippedStorageKeys: string[]; readonly localUserData: IGlobalState; - readonly lastSyncUserData: ILastSyncUserData | null; } interface ILastSyncUserData extends IRemoteUserData { @@ -75,76 +74,99 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs ); } - protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise { + protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise { const localGlobalState = await this.getLocalGlobalState(); + const resourcePreviews: IGlobalStateResourcePreview[] = []; + const localResource = GlobalStateSynchroniser.GLOBAL_STATE_DATA_URI; + const localContent = this.format(localGlobalState); + const remoteResource = this.remotePreviewResource; + const previewResource = this.localPreviewResource; + const previewContent = null; if (remoteUserData.syncData !== null) { const remoteGlobalState: IGlobalState = JSON.parse(remoteUserData.syncData.content); const mergeResult = merge(localGlobalState.storage, remoteGlobalState.storage, null, this.getSyncStorageKeys(), lastSyncUserData?.skippedStorageKeys || [], this.logService); const { local, remote, skipped } = mergeResult; - const resourcePreviews: IResourcePreview[] = this.getResourcePreviews(mergeResult); - return { - remoteUserData, lastSyncUserData, - local, remote, localUserData: localGlobalState, skippedStorageKeys: skipped, - hasLocalChanged: resourcePreviews.some(({ hasLocalChanged }) => hasLocalChanged), - hasRemoteChanged: resourcePreviews.some(({ hasRemoteChanged }) => hasRemoteChanged), + resourcePreviews.push({ + localResource, + localContent, + remoteResource, + remoteContent: this.format(remoteGlobalState), + previewResource, + previewContent, + local, + remote, + localUserData: localGlobalState, + skippedStorageKeys: skipped, + localChange: Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0 ? Change.Modified : Change.None, + remoteChange: remote !== null ? Change.Modified : Change.None, hasConflicts: false, - isLastSyncFromCurrentMachine: false, - resourcePreviews - }; + }); } else { - return { - remoteUserData, lastSyncUserData, - local: { added: {}, removed: [], updated: {} }, remote: null, localUserData: localGlobalState, skippedStorageKeys: [], - hasLocalChanged: false, - hasRemoteChanged: false, + resourcePreviews.push({ + localResource, + localContent, + remoteResource, + remoteContent: null, + previewResource, + previewContent, + local: { added: {}, removed: [], updated: {} }, + remote: null, + localUserData: localGlobalState, + skippedStorageKeys: [], + localChange: Change.None, + remoteChange: Change.None, hasConflicts: false, - isLastSyncFromCurrentMachine: false, - resourcePreviews: [] - }; + }); } + return resourcePreviews; } - protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise { + protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise { const localUserData = await this.getLocalGlobalState(); - return { - local: { added: {}, removed: [], updated: {} }, remote: localUserData.storage, remoteUserData, localUserData, lastSyncUserData, + const remoteGlobalState: IGlobalState = remoteUserData.syncData ? JSON.parse(remoteUserData.syncData.content) : null; + return [{ + localResource: GlobalStateSynchroniser.GLOBAL_STATE_DATA_URI, + localContent: this.format(localUserData), + remoteResource: this.remotePreviewResource, + remoteContent: remoteGlobalState ? this.format(remoteGlobalState) : null, + previewResource: this.localPreviewResource, + previewContent: null, + local: { added: {}, removed: [], updated: {} }, + remote: localUserData.storage, + localUserData, skippedStorageKeys: [], - hasLocalChanged: false, - hasRemoteChanged: true, - isLastSyncFromCurrentMachine: false, + localChange: Change.None, + remoteChange: Change.Modified, hasConflicts: false, - resourcePreviews: this.getResourcePreviews({ local: { added: {}, removed: [], updated: {} }, remote: localUserData.storage, skipped: [] }) - }; + }]; } - protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise { + protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise { const localUserData = await this.getLocalGlobalState(); const syncGlobalState: IGlobalState = JSON.parse(syncData.content); + const remoteGlobalState: IGlobalState = remoteUserData.syncData ? JSON.parse(remoteUserData.syncData.content) : null; const mergeResult = merge(localUserData.storage, syncGlobalState.storage, localUserData.storage, this.getSyncStorageKeys(), lastSyncUserData?.skippedStorageKeys || [], this.logService); const { local, skipped } = mergeResult; - const resourcePreviews: IResourcePreview[] = this.getResourcePreviews(mergeResult); - return { - local, remote: syncGlobalState.storage, remoteUserData, localUserData, lastSyncUserData, + return [{ + localResource: GlobalStateSynchroniser.GLOBAL_STATE_DATA_URI, + localContent: this.format(localUserData), + remoteResource: this.remotePreviewResource, + remoteContent: remoteGlobalState ? this.format(remoteGlobalState) : null, + previewResource: this.localPreviewResource, + previewContent: null, + local, + remote: syncGlobalState.storage, + localUserData, skippedStorageKeys: skipped, - hasLocalChanged: resourcePreviews.some(({ hasLocalChanged }) => hasLocalChanged), - hasRemoteChanged: true, - isLastSyncFromCurrentMachine: false, + localChange: Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0 ? Change.Modified : Change.None, + remoteChange: Change.Modified, hasConflicts: false, - resourcePreviews: [], - }; + }]; } - protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise { + protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise { const remoteGlobalState: IGlobalState = remoteUserData.syncData ? JSON.parse(remoteUserData.syncData.content) : null; - const isLastSyncFromCurrentMachine = await this.isLastSyncFromCurrentMachine(remoteUserData); - let lastSyncGlobalState: IGlobalState | null = null; - if (lastSyncUserData === null) { - if (isLastSyncFromCurrentMachine) { - lastSyncGlobalState = remoteUserData.syncData ? JSON.parse(remoteUserData.syncData.content) : null; - } - } else { - lastSyncGlobalState = lastSyncUserData.syncData ? JSON.parse(lastSyncUserData.syncData.content) : null; - } + const lastSyncGlobalState: IGlobalState | null = lastSyncUserData && lastSyncUserData.syncData ? JSON.parse(lastSyncUserData.syncData.content) : null; const localGloablState = await this.getLocalGlobalState(); @@ -156,30 +178,32 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs const mergeResult = merge(localGloablState.storage, remoteGlobalState ? remoteGlobalState.storage : null, lastSyncGlobalState ? lastSyncGlobalState.storage : null, this.getSyncStorageKeys(), lastSyncUserData?.skippedStorageKeys || [], this.logService); const { local, remote, skipped } = mergeResult; - const resourcePreviews: IResourcePreview[] = this.getResourcePreviews(mergeResult); - return { - local, remote, remoteUserData, localUserData: localGloablState, lastSyncUserData, + return [{ + localResource: GlobalStateSynchroniser.GLOBAL_STATE_DATA_URI, + localContent: this.format(localGloablState), + remoteResource: this.remotePreviewResource, + remoteContent: remoteGlobalState ? this.format(remoteGlobalState) : null, + previewResource: this.localPreviewResource, + previewContent: null, + local, + remote, + localUserData: localGloablState, skippedStorageKeys: skipped, - hasLocalChanged: resourcePreviews.some(({ hasLocalChanged }) => hasLocalChanged), - hasRemoteChanged: resourcePreviews.some(({ hasRemoteChanged }) => hasRemoteChanged), - isLastSyncFromCurrentMachine, + localChange: Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0 ? Change.Modified : Change.None, + remoteChange: remote !== null ? Change.Modified : Change.None, hasConflicts: false, - resourcePreviews - }; + }]; } - protected async updatePreviewWithConflict(preview: IGlobalStateSyncPreview, conflictResource: URI, content: string, token: CancellationToken): Promise { - throw new Error(`${this.syncResourceLogLabel}: Conflicts should not occur`); - } + protected async applyPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, resourcePreviews: IGlobalStateResourcePreview[], forcePush: boolean): Promise { + let { local, remote, localUserData, localChange, remoteChange, skippedStorageKeys } = resourcePreviews[0]; - protected async applyPreview({ local, remote, remoteUserData, lastSyncUserData, localUserData, hasLocalChanged, hasRemoteChanged, skippedStorageKeys }: IGlobalStateSyncPreview, forcePush: boolean): Promise { - - if (!hasLocalChanged && !hasRemoteChanged) { + if (localChange === Change.None && remoteChange === Change.None) { this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing ui state.`); } - if (hasLocalChanged) { + if (localChange !== Change.None) { // update local this.logService.trace(`${this.syncResourceLogLabel}: Updating local ui state...`); await this.backupLocal(JSON.stringify(localUserData)); @@ -187,7 +211,7 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs this.logService.info(`${this.syncResourceLogLabel}: Updated local ui state`); } - if (hasRemoteChanged) { + if (remoteChange !== Change.None) { // update remote this.logService.trace(`${this.syncResourceLogLabel}: Updating remote ui state...`); const content = JSON.stringify({ storage: remote }); @@ -203,18 +227,6 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs } } - private getResourcePreviews({ local, remote }: IMergeResult): IResourcePreview[] { - const hasLocalChanged = Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0; - const hasRemoteChanged = remote !== null; - return [{ - hasLocalChanged, - hasConflicts: false, - hasRemoteChanged, - localResouce: GlobalStateSynchroniser.GLOBAL_STATE_DATA_URI, - remoteResource: this.remotePreviewResource - }]; - } - async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> { return [{ resource: joinPath(uri, 'globalState.json'), comparableResource: GlobalStateSynchroniser.GLOBAL_STATE_DATA_URI }]; } @@ -225,6 +237,10 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs return this.format(localGlobalState); } + if (isEqual(this.remotePreviewResource, uri) || isEqual(this.localPreviewResource, uri)) { + return this.resolvePreviewContent(uri); + } + let content = await super.resolveContent(uri); if (content) { return content; diff --git a/src/vs/platform/userDataSync/common/keybindingsSync.ts b/src/vs/platform/userDataSync/common/keybindingsSync.ts index ba5c85f34c1..62d101a8ec7 100644 --- a/src/vs/platform/userDataSync/common/keybindingsSync.ts +++ b/src/vs/platform/userDataSync/common/keybindingsSync.ts @@ -7,7 +7,7 @@ import { IFileService, FileOperationError, FileOperationResult } from 'vs/platfo import { UserDataSyncError, UserDataSyncErrorCode, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService, SyncResource, IUserDataSynchroniser, IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService, USER_DATA_SYNC_SCHEME, ISyncResourceHandle, - IRemoteUserData, ISyncData, IResourcePreview + IRemoteUserData, ISyncData, Change } from 'vs/platform/userDataSync/common/userDataSync'; import { merge } from 'vs/platform/userDataSync/common/keybindingsMerge'; import { VSBuffer } from 'vs/base/common/buffer'; @@ -19,7 +19,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { OS, OperatingSystem } from 'vs/base/common/platform'; import { isUndefined } from 'vs/base/common/types'; import { isNonEmptyArray } from 'vs/base/common/arrays'; -import { IFileSyncPreview, AbstractJsonFileSynchroniser } from 'vs/platform/userDataSync/common/abstractSynchronizer'; +import { AbstractJsonFileSynchroniser, IFileResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { URI } from 'vs/base/common/uri'; import { joinPath, isEqual, dirname, basename } from 'vs/base/common/resources'; @@ -53,105 +53,69 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem super(environmentService.keybindingsResource, SyncResource.Keybindings, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncResourceEnablementService, telemetryService, logService, userDataSyncUtilService, configurationService); } - protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise { + protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise { const fileContent = await this.getLocalFileContent(); - const content = remoteUserData.syncData !== null ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null; - const hasLocalChanged = content !== null; - const hasRemoteChanged = false; - const hasConflicts = false; + const previewContent = remoteUserData.syncData !== null ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null; - const resourcePreviews: IResourcePreview[] = [{ - hasConflicts, - hasLocalChanged, - hasRemoteChanged, - localResouce: this.file, - remoteResource: this.remotePreviewResource, - }]; - - return { + return [{ + localResource: this.file, fileContent, - remoteUserData, - lastSyncUserData, - content, - hasConflicts, - hasLocalChanged, - hasRemoteChanged, - isLastSyncFromCurrentMachine: false, - resourcePreviews - }; + localContent: fileContent ? fileContent.value.toString() : null, + remoteResource: this.remotePreviewResource, + remoteContent: previewContent, + previewResource: this.localPreviewResource, + previewContent, + localChange: previewContent !== null ? Change.Modified : Change.None, + remoteChange: Change.None, + hasConflicts: false, + }]; } - protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise { + protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise { const fileContent = await this.getLocalFileContent(); - const content: string | null = fileContent ? fileContent.value.toString() : null; - const hasLocalChanged = false; - const hasRemoteChanged = content !== null; - const hasConflicts = false; + const previewContent: string | null = fileContent ? fileContent.value.toString() : null; - const resourcePreviews: IResourcePreview[] = [{ - hasConflicts, - hasLocalChanged, - hasRemoteChanged, - localResouce: this.file, - remoteResource: this.remotePreviewResource, - }]; - return { + return [{ + localResource: this.file, fileContent, - remoteUserData, - lastSyncUserData, - content, - hasLocalChanged, - hasRemoteChanged, - hasConflicts, - isLastSyncFromCurrentMachine: false, - resourcePreviews - }; + localContent: fileContent ? fileContent.value.toString() : null, + remoteResource: this.remotePreviewResource, + remoteContent: remoteUserData.syncData !== null ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null, + previewResource: this.localPreviewResource, + previewContent, + localChange: Change.None, + remoteChange: previewContent !== null ? Change.Modified : Change.None, + hasConflicts: false, + }]; } - protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise { + protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise { const fileContent = await this.getLocalFileContent(); - const content = this.getKeybindingsContentFromSyncContent(syncData.content); - const hasLocalChanged = content !== null; - const hasRemoteChanged = content !== null; - const hasConflicts = false; + const previewContent = this.getKeybindingsContentFromSyncContent(syncData.content); - const resourcePreviews: IResourcePreview[] = [{ - hasConflicts, - hasLocalChanged, - hasRemoteChanged, - localResouce: this.file, - remoteResource: this.remotePreviewResource, - }]; - return { + return [{ + localResource: this.file, fileContent, - remoteUserData, - lastSyncUserData, - content, - hasConflicts, - hasLocalChanged, - hasRemoteChanged, - isLastSyncFromCurrentMachine: false, - resourcePreviews - }; + localContent: fileContent ? fileContent.value.toString() : null, + remoteResource: this.remotePreviewResource, + remoteContent: remoteUserData.syncData !== null ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null, + previewResource: this.localPreviewResource, + previewContent, + localChange: previewContent !== null ? Change.Modified : Change.None, + remoteChange: previewContent !== null ? Change.Modified : Change.None, + hasConflicts: false, + }]; } - protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken = CancellationToken.None): Promise { + protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise { const remoteContent = remoteUserData.syncData ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null; - const isLastSyncFromCurrentMachine = await this.isLastSyncFromCurrentMachine(remoteUserData); - let lastSyncContent: string | null = null; - if (lastSyncUserData === null) { - if (isLastSyncFromCurrentMachine) { - lastSyncContent = remoteUserData.syncData ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null; - } - } else { - lastSyncContent = lastSyncUserData.syncData ? this.getKeybindingsContentFromSyncContent(lastSyncUserData.syncData.content) : null; - } + const lastSyncContent: string | null = lastSyncUserData && lastSyncUserData.syncData ? this.getKeybindingsContentFromSyncContent(lastSyncUserData.syncData.content) : null; // Get file content last to get the latest const fileContent = await this.getLocalFileContent(); const formattingOptions = await this.getFormattingOptions(); - let content: string | null = null; + let previewContent: string | null = null; let hasLocalChanged: boolean = false; let hasRemoteChanged: boolean = false; let hasConflicts: boolean = false; @@ -170,7 +134,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem const result = await merge(localContent, remoteContent, lastSyncContent, formattingOptions, this.userDataSyncUtilService); // Sync only if there are changes if (result.hasChanges) { - content = result.mergeContent; + previewContent = result.mergeContent; hasConflicts = result.hasConflicts; hasLocalChanged = hasConflicts || result.mergeContent !== localContent; hasRemoteChanged = hasConflicts || result.mergeContent !== remoteContent; @@ -181,43 +145,37 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem // First time syncing to remote else if (fileContent) { this.logService.trace(`${this.syncResourceLogLabel}: Remote keybindings does not exist. Synchronizing keybindings for the first time.`); - content = fileContent.value.toString(); + previewContent = fileContent.value.toString(); hasRemoteChanged = true; } - if (content && !token.isCancellationRequested) { - await this.fileService.writeFile(this.localPreviewResource, VSBuffer.fromString(content)); + if (previewContent && !token.isCancellationRequested) { + await this.fileService.writeFile(this.localPreviewResource, VSBuffer.fromString(previewContent)); } - this.setConflicts(hasConflicts && !token.isCancellationRequested ? [{ local: this.localPreviewResource, remote: this.remotePreviewResource }] : []); - const resourcePreviews: IResourcePreview[] = [{ - hasConflicts, - hasLocalChanged, - hasRemoteChanged, - localResouce: this.file, + return [{ + localResource: this.file, + fileContent, + localContent: fileContent ? fileContent.value.toString() : null, remoteResource: this.remotePreviewResource, - previewResource: this.localPreviewResource + remoteContent, + previewResource: this.localPreviewResource, + previewContent, + hasConflicts, + localChange: hasLocalChanged ? Change.Modified : Change.None, + remoteChange: hasRemoteChanged ? Change.Modified : Change.None, }]; - - return { fileContent, remoteUserData, lastSyncUserData, content, hasLocalChanged, hasRemoteChanged, hasConflicts, isLastSyncFromCurrentMachine, resourcePreviews }; } - protected async updatePreviewWithConflict(preview: IFileSyncPreview, conflictResource: URI, conflictContent: string, token: CancellationToken): Promise { - if (isEqual(this.localPreviewResource, conflictResource) || isEqual(this.remotePreviewResource, conflictResource)) { - preview = { ...preview, content: conflictContent, hasConflicts: false }; - } - return preview; - } - - protected async applyPreview(preview: IFileSyncPreview, forcePush: boolean): Promise { - let { fileContent, remoteUserData, lastSyncUserData, content, hasLocalChanged, hasRemoteChanged } = preview; + protected async applyPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: IFileResourcePreview[], forcePush: boolean): Promise { + let { fileContent, previewContent: content, localChange, remoteChange } = resourcePreviews[0]; if (content !== null) { if (this.hasErrors(content)) { throw new UserDataSyncError(localize('errorInvalidSettings', "Unable to sync keybindings because the content in the file is not valid. Please open the file and correct it."), UserDataSyncErrorCode.LocalInvalidContent, this.resource); } - if (hasLocalChanged) { + if (localChange !== Change.None) { this.logService.trace(`${this.syncResourceLogLabel}: Updating local keybindings...`); if (fileContent) { await this.backupLocal(this.toSyncContent(fileContent.value.toString(), null)); @@ -226,7 +184,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem this.logService.info(`${this.syncResourceLogLabel}: Updated local keybindings`); } - if (hasRemoteChanged) { + if (remoteChange !== Change.None) { this.logService.trace(`${this.syncResourceLogLabel}: Updating remote keybindings...`); const remoteContents = this.toSyncContent(content, remoteUserData.syncData ? remoteUserData.syncData.content : null); remoteUserData = await this.updateRemoteUserData(remoteContents, forcePush ? null : remoteUserData.ref); @@ -272,7 +230,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem } async resolveContent(uri: URI): Promise { - if (isEqual(this.remotePreviewResource, uri)) { + if (isEqual(this.remotePreviewResource, uri) || isEqual(this.localPreviewResource, uri)) { return this.resolvePreviewContent(uri); } let content = await super.resolveContent(uri); @@ -292,11 +250,6 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem return null; } - protected async resolvePreviewContent(conflictResource: URI): Promise { - const content = await super.resolvePreviewContent(conflictResource); - return content !== null ? this.getKeybindingsContentFromSyncContent(content) : null; - } - getKeybindingsContentFromSyncContent(syncContent: string): string | null { try { const parsed = JSON.parse(syncContent); diff --git a/src/vs/platform/userDataSync/common/settingsSync.ts b/src/vs/platform/userDataSync/common/settingsSync.ts index f5dea9f8686..feb30518b99 100644 --- a/src/vs/platform/userDataSync/common/settingsSync.ts +++ b/src/vs/platform/userDataSync/common/settingsSync.ts @@ -7,7 +7,7 @@ import { IFileService, FileOperationError, FileOperationResult } from 'vs/platfo import { UserDataSyncError, UserDataSyncErrorCode, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService, CONFIGURATION_SYNC_STORE_KEY, SyncResource, IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService, USER_DATA_SYNC_SCHEME, ISyncResourceHandle, IUserDataSynchroniser, - IRemoteUserData, ISyncData, IResourcePreview + IRemoteUserData, ISyncData, Change } from 'vs/platform/userDataSync/common/userDataSync'; import { VSBuffer } from 'vs/base/common/buffer'; import { localize } from 'vs/nls'; @@ -17,7 +17,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { CancellationToken } from 'vs/base/common/cancellation'; import { updateIgnoredSettings, merge, getIgnoredSettings, isEmpty } from 'vs/platform/userDataSync/common/settingsMerge'; import { edit } from 'vs/platform/userDataSync/common/content'; -import { IFileSyncPreview, AbstractJsonFileSynchroniser } from 'vs/platform/userDataSync/common/abstractSynchronizer'; +import { AbstractJsonFileSynchroniser, IFileResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { URI } from 'vs/base/common/uri'; import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; @@ -58,133 +58,94 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement super(environmentService.settingsResource, SyncResource.Settings, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncResourceEnablementService, telemetryService, logService, userDataSyncUtilService, configurationService); } - protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise { + protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise { const fileContent = await this.getLocalFileContent(); const formatUtils = await this.getFormattingOptions(); const ignoredSettings = await this.getIgnoredSettings(); const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData); - let content: string | null = null; + let previewContent: string | null = null; if (remoteSettingsSyncContent !== null) { // Update ignored settings from local file content - content = updateIgnoredSettings(remoteSettingsSyncContent.settings, fileContent ? fileContent.value.toString() : '{}', ignoredSettings, formatUtils); + previewContent = updateIgnoredSettings(remoteSettingsSyncContent.settings, fileContent ? fileContent.value.toString() : '{}', ignoredSettings, formatUtils); } - const hasLocalChanged = content !== null; - const hasRemoteChanged = false; - const hasConflicts = false; - - const resourcePreviews: IResourcePreview[] = [{ - hasConflicts, - hasLocalChanged, - hasRemoteChanged, - localResouce: this.file, - remoteResource: this.remotePreviewResource, - }]; - - return { + return [{ + localResource: this.file, fileContent, - remoteUserData, - lastSyncUserData, - content, - hasLocalChanged, - hasRemoteChanged, - hasConflicts, - isLastSyncFromCurrentMachine: false, - resourcePreviews - }; + localContent: fileContent ? fileContent.value.toString() : null, + remoteResource: this.remotePreviewResource, + remoteContent: remoteSettingsSyncContent ? remoteSettingsSyncContent.settings : null, + previewResource: this.localPreviewResource, + previewContent, + localChange: previewContent !== null ? Change.Modified : Change.None, + remoteChange: Change.None, + hasConflicts: false, + }]; } - protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise { + protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise { const fileContent = await this.getLocalFileContent(); const formatUtils = await this.getFormattingOptions(); const ignoredSettings = await this.getIgnoredSettings(); + const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData); - let content: string | null = null; + let previewContent: string | null = null; if (fileContent !== null) { // Remove ignored settings - content = updateIgnoredSettings(fileContent.value.toString(), '{}', ignoredSettings, formatUtils); + previewContent = updateIgnoredSettings(fileContent.value.toString(), '{}', ignoredSettings, formatUtils); } - const hasLocalChanged = false; - const hasRemoteChanged = content !== null; - const hasConflicts = false; - - const resourcePreviews: IResourcePreview[] = [{ - hasConflicts, - hasLocalChanged, - hasRemoteChanged, - localResouce: this.file, - remoteResource: this.remotePreviewResource, - }]; - - return { + return [{ + localResource: this.file, fileContent, - remoteUserData, - lastSyncUserData, - content, - hasLocalChanged, - hasRemoteChanged, - hasConflicts, - isLastSyncFromCurrentMachine: false, - resourcePreviews - }; + localContent: fileContent ? fileContent.value.toString() : null, + remoteResource: this.remotePreviewResource, + remoteContent: remoteSettingsSyncContent ? remoteSettingsSyncContent.settings : null, + previewResource: this.localPreviewResource, + previewContent, + localChange: Change.None, + remoteChange: previewContent !== null ? Change.Modified : Change.None, + hasConflicts: false, + }]; } - protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise { + protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise { const fileContent = await this.getLocalFileContent(); const formatUtils = await this.getFormattingOptions(); const ignoredSettings = await this.getIgnoredSettings(); - let content: string | null = null; + let previewContent: string | null = null; const settingsSyncContent = this.parseSettingsSyncContent(syncData.content); + const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData); if (settingsSyncContent) { - content = updateIgnoredSettings(settingsSyncContent.settings, fileContent ? fileContent.value.toString() : '{}', ignoredSettings, formatUtils); + previewContent = updateIgnoredSettings(settingsSyncContent.settings, fileContent ? fileContent.value.toString() : '{}', ignoredSettings, formatUtils); } - const hasLocalChanged = content !== null; - const hasRemoteChanged = content !== null; - const hasConflicts = false; - - const resourcePreviews: IResourcePreview[] = [{ - hasConflicts, - hasLocalChanged, - hasRemoteChanged, - localResouce: this.file, - remoteResource: this.remotePreviewResource, - }]; - - return { + return [{ + localResource: this.file, fileContent, - remoteUserData, - lastSyncUserData, - content, - hasLocalChanged, - hasRemoteChanged, - hasConflicts, - resourcePreviews, - isLastSyncFromCurrentMachine: false - }; + localContent: fileContent ? fileContent.value.toString() : null, + remoteResource: this.remotePreviewResource, + remoteContent: remoteSettingsSyncContent ? remoteSettingsSyncContent.settings : null, + previewResource: this.localPreviewResource, + previewContent, + localChange: previewContent !== null ? Change.Modified : Change.None, + remoteChange: previewContent !== null ? Change.Modified : Change.None, + hasConflicts: false, + }]; } - protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken = CancellationToken.None): Promise { + protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise { const fileContent = await this.getLocalFileContent(); const formattingOptions = await this.getFormattingOptions(); const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData); - const isLastSyncFromCurrentMachine = await this.isLastSyncFromCurrentMachine(remoteUserData); - let lastSettingsSyncContent: ISettingsSyncContent | null = null; - if (lastSyncUserData === null) { - if (isLastSyncFromCurrentMachine) { - lastSettingsSyncContent = this.getSettingsSyncContent(remoteUserData); - } - } else { - lastSettingsSyncContent = this.getSettingsSyncContent(lastSyncUserData); - } + const lastSettingsSyncContent: ISettingsSyncContent | null = lastSyncUserData ? this.getSettingsSyncContent(lastSyncUserData) : null; - let content: string | null = null; + let previewContent: string | null = null; let hasLocalChanged: boolean = false; let hasRemoteChanged: boolean = false; let hasConflicts: boolean = false; @@ -195,7 +156,7 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement 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); - content = result.localContent || result.remoteContent; + previewContent = result.localContent || result.remoteContent; hasLocalChanged = result.localContent !== null; hasRemoteChanged = result.remoteContent !== null; hasConflicts = result.hasConflicts; @@ -204,49 +165,47 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement // First time syncing to remote else if (fileContent) { this.logService.trace(`${this.syncResourceLogLabel}: Remote settings does not exist. Synchronizing settings for the first time.`); - content = fileContent.value.toString(); + previewContent = fileContent.value.toString(); hasRemoteChanged = true; } - if (content && !token.isCancellationRequested) { + if (previewContent && !token.isCancellationRequested) { // Remove the ignored settings from the preview. const ignoredSettings = await this.getIgnoredSettings(); - const previewContent = updateIgnoredSettings(content, '{}', ignoredSettings, formattingOptions); - await this.fileService.writeFile(this.localPreviewResource, VSBuffer.fromString(previewContent)); + const content = updateIgnoredSettings(previewContent, '{}', ignoredSettings, formattingOptions); + await this.fileService.writeFile(this.localPreviewResource, VSBuffer.fromString(content)); } - this.setConflicts(hasConflicts && !token.isCancellationRequested ? [{ local: this.localPreviewResource, remote: this.remotePreviewResource }] : []); - const resourcePreviews: IResourcePreview[] = [{ - hasConflicts, - hasLocalChanged, - hasRemoteChanged, - localResouce: this.file, + return [{ + localResource: this.file, + fileContent, + localContent: fileContent ? fileContent.value.toString() : null, remoteResource: this.remotePreviewResource, - previewResource: this.localPreviewResource + remoteContent: remoteSettingsSyncContent ? remoteSettingsSyncContent.settings : null, + previewResource: this.localPreviewResource, + previewContent, + localChange: hasLocalChanged ? Change.Modified : Change.None, + remoteChange: hasRemoteChanged ? Change.Modified : Change.None, + hasConflicts, }]; - - return { fileContent, remoteUserData, lastSyncUserData, content, hasLocalChanged, hasRemoteChanged, hasConflicts, isLastSyncFromCurrentMachine, resourcePreviews }; } - protected async updatePreviewWithConflict(preview: IFileSyncPreview, conflictResource: URI, conflictContent: string, token: CancellationToken): Promise { - if (isEqual(this.localPreviewResource, conflictResource) || isEqual(this.remotePreviewResource, conflictResource)) { - const formatUtils = await this.getFormattingOptions(); - // Add ignored settings from local file content - const ignoredSettings = await this.getIgnoredSettings(); - const content = updateIgnoredSettings(conflictContent, preview.fileContent ? preview.fileContent.value.toString() : '{}', ignoredSettings, formatUtils); - preview = { ...preview, content, hasConflicts: false }; - } - return preview; + protected async updateResourcePreviewContent(resourcePreview: IFileResourcePreview, resource: URI, previewContent: string, token: CancellationToken): Promise { + const formatUtils = await this.getFormattingOptions(); + // Add ignored settings from local file content + const ignoredSettings = await this.getIgnoredSettings(); + previewContent = updateIgnoredSettings(previewContent, resourcePreview.fileContent ? resourcePreview.fileContent.value.toString() : '{}', ignoredSettings, formatUtils); + return super.updateResourcePreviewContent(resourcePreview, resource, previewContent, token) as Promise; } - protected async applyPreview(preview: IFileSyncPreview, forcePush: boolean): Promise { - let { fileContent, remoteUserData, lastSyncUserData, content, hasLocalChanged, hasRemoteChanged } = preview; + protected async applyPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: IFileResourcePreview[], forcePush: boolean): Promise { + let { fileContent, previewContent: content, localChange, remoteChange } = resourcePreviews[0]; if (content !== null) { this.validateContent(content); - if (hasLocalChanged) { + if (localChange !== Change.None) { this.logService.trace(`${this.syncResourceLogLabel}: Updating local settings...`); if (fileContent) { await this.backupLocal(JSON.stringify(this.toSettingsSyncContent(fileContent.value.toString()))); @@ -254,7 +213,7 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement await this.updateLocalFileContent(content, fileContent); this.logService.info(`${this.syncResourceLogLabel}: Updated local settings`); } - if (hasRemoteChanged) { + if (remoteChange !== Change.None) { const formatUtils = await this.getFormattingOptions(); // Update ignored settings from remote const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData); @@ -302,7 +261,7 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement } async resolveContent(uri: URI): Promise { - if (isEqual(this.remotePreviewResource, uri)) { + if (isEqual(this.remotePreviewResource, uri) || isEqual(this.localPreviewResource, uri)) { return this.resolvePreviewContent(uri); } let content = await super.resolveContent(uri); @@ -327,13 +286,9 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement protected async resolvePreviewContent(conflictResource: URI): Promise { let content = await super.resolvePreviewContent(conflictResource); - if (content !== null) { - const settingsSyncContent = this.parseSettingsSyncContent(content); - content = settingsSyncContent ? settingsSyncContent.settings : null; - } if (content !== null) { const formatUtils = await this.getFormattingOptions(); - // remove ignored settings from the remote content for preview + // remove ignored settings from the preview content const ignoredSettings = await this.getIgnoredSettings(); content = updateIgnoredSettings(content, '{}', ignoredSettings, formatUtils); } diff --git a/src/vs/platform/userDataSync/common/snippetsMerge.ts b/src/vs/platform/userDataSync/common/snippetsMerge.ts index 07c944c53cd..a8d7107ba18 100644 --- a/src/vs/platform/userDataSync/common/snippetsMerge.ts +++ b/src/vs/platform/userDataSync/common/snippetsMerge.ts @@ -5,28 +5,31 @@ import { values } from 'vs/base/common/map'; import { IStringDictionary } from 'vs/base/common/collections'; -import { deepClone } from 'vs/base/common/objects'; export interface IMergeResult { - added: IStringDictionary; - updated: IStringDictionary; - removed: string[]; + local: { + added: IStringDictionary; + updated: IStringDictionary; + removed: string[]; + }; + remote: { + added: IStringDictionary; + updated: IStringDictionary; + removed: string[]; + }; conflicts: string[]; - remote: IStringDictionary | null; } -export function merge(local: IStringDictionary, remote: IStringDictionary | null, base: IStringDictionary | null, resolvedConflicts: IStringDictionary = {}): IMergeResult { - const added: IStringDictionary = {}; - const updated: IStringDictionary = {}; - const removed: Set = new Set(); +export function merge(local: IStringDictionary, remote: IStringDictionary | null, base: IStringDictionary | null): IMergeResult { + const localAdded: IStringDictionary = {}; + const localUpdated: IStringDictionary = {}; + const localRemoved: Set = new Set(); if (!remote) { return { - added, - removed: values(removed), - updated, - conflicts: [], - remote: Object.keys(local).length > 0 ? local : null + local: { added: localAdded, updated: localUpdated, removed: values(localRemoved) }, + remote: { added: local, updated: {}, removed: [] }, + conflicts: [] }; } @@ -34,145 +37,118 @@ export function merge(local: IStringDictionary, remote: IStringDictionar if (localToRemote.added.size === 0 && localToRemote.removed.size === 0 && localToRemote.updated.size === 0) { // No changes found between local and remote. return { - added, - removed: values(removed), - updated, - conflicts: [], - remote: null + local: { added: localAdded, updated: localUpdated, removed: values(localRemoved) }, + remote: { added: {}, updated: {}, removed: [] }, + conflicts: [] }; } const baseToLocal = compare(base, local); const baseToRemote = compare(base, remote); - const remoteContent: IStringDictionary = deepClone(remote); + + const remoteAdded: IStringDictionary = {}; + const remoteUpdated: IStringDictionary = {}; + const remoteRemoved: Set = new Set(); + const conflicts: Set = new Set(); - const handledConflicts: Set = new Set(); - const handleConflict = (key: string): void => { - if (handledConflicts.has(key)) { - return; - } - handledConflicts.add(key); - const conflictContent = resolvedConflicts[key]; - - // add to conflicts - if (conflictContent === undefined) { - conflicts.add(key); - } - - // remove the snippet - else if (conflictContent === null) { - delete remote[key]; - if (local[key]) { - removed.add(key); - } - } - - // add/update the snippet - else { - if (local[key]) { - if (local[key] !== conflictContent) { - updated[key] = conflictContent; - } - } else { - added[key] = conflictContent; - } - remoteContent[key] = conflictContent; - } - }; // Removed snippets in Local for (const key of values(baseToLocal.removed)) { // Conflict - Got updated in remote. if (baseToRemote.updated.has(key)) { // Add to local - added[key] = remote[key]; + localAdded[key] = remote[key]; } // Remove it in remote else { - delete remoteContent[key]; + remoteRemoved.add(key); } } // Removed snippets in Remote for (const key of values(baseToRemote.removed)) { - if (handledConflicts.has(key)) { + if (conflicts.has(key)) { continue; } // Conflict - Got updated in local if (baseToLocal.updated.has(key)) { - handleConflict(key); + conflicts.add(key); } // Also remove in Local else { - removed.add(key); + localRemoved.add(key); } } // Updated snippets in Local for (const key of values(baseToLocal.updated)) { - if (handledConflicts.has(key)) { + if (conflicts.has(key)) { continue; } // Got updated in remote if (baseToRemote.updated.has(key)) { // Has different value if (localToRemote.updated.has(key)) { - handleConflict(key); + conflicts.add(key); } } else { - remoteContent[key] = local[key]; + remoteUpdated[key] = local[key]; } } // Updated snippets in Remote for (const key of values(baseToRemote.updated)) { - if (handledConflicts.has(key)) { + if (conflicts.has(key)) { continue; } // Got updated in local if (baseToLocal.updated.has(key)) { // Has different value if (localToRemote.updated.has(key)) { - handleConflict(key); + conflicts.add(key); } } else if (local[key] !== undefined) { - updated[key] = remote[key]; + localUpdated[key] = remote[key]; } } // Added snippets in Local for (const key of values(baseToLocal.added)) { - if (handledConflicts.has(key)) { + if (conflicts.has(key)) { continue; } // Got added in remote if (baseToRemote.added.has(key)) { // Has different value if (localToRemote.updated.has(key)) { - handleConflict(key); + conflicts.add(key); } } else { - remoteContent[key] = local[key]; + remoteAdded[key] = local[key]; } } // Added snippets in remote for (const key of values(baseToRemote.added)) { - if (handledConflicts.has(key)) { + if (conflicts.has(key)) { continue; } // Got added in local if (baseToLocal.added.has(key)) { // Has different value if (localToRemote.updated.has(key)) { - handleConflict(key); + conflicts.add(key); } } else { - added[key] = remote[key]; + localAdded[key] = remote[key]; } } - return { added, removed: values(removed), updated, conflicts: values(conflicts), remote: areSame(remote, remoteContent) ? null : remoteContent }; + return { + local: { added: localAdded, removed: values(localRemoved), updated: localUpdated }, + remote: { added: remoteAdded, removed: values(remoteRemoved), updated: remoteUpdated }, + conflicts: values(conflicts), + }; } function compare(from: IStringDictionary | null, to: IStringDictionary | null): { added: Set, removed: Set, updated: Set } { @@ -196,7 +172,7 @@ function compare(from: IStringDictionary | null, to: IStringDictionary, b: IStringDictionary): boolean { +export function areSame(a: IStringDictionary, b: IStringDictionary): boolean { const { added, removed, updated } = compare(a, b); return added.size === 0 && removed.size === 0 && updated.size === 0; } diff --git a/src/vs/platform/userDataSync/common/snippetsSync.ts b/src/vs/platform/userDataSync/common/snippetsSync.ts index e73a65f5411..38120e538bf 100644 --- a/src/vs/platform/userDataSync/common/snippetsSync.ts +++ b/src/vs/platform/userDataSync/common/snippetsSync.ts @@ -5,30 +5,23 @@ import { IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSynchroniser, SyncResource, IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService, - Conflict, USER_DATA_SYNC_SCHEME, ISyncResourceHandle, IRemoteUserData, ISyncData, IResourcePreview + USER_DATA_SYNC_SCHEME, ISyncResourceHandle, IRemoteUserData, ISyncData, UserDataSyncError, UserDataSyncErrorCode, Change } from 'vs/platform/userDataSync/common/userDataSync'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IFileService, FileChangesEvent, IFileStat, IFileContent, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { AbstractSynchroniser, ISyncResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer'; +import { AbstractSynchroniser, IFileResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IStringDictionary } from 'vs/base/common/collections'; import { URI } from 'vs/base/common/uri'; -import { joinPath, extname, relativePath, isEqualOrParent, isEqual, basename, dirname } from 'vs/base/common/resources'; +import { joinPath, extname, relativePath, isEqualOrParent, basename, dirname } from 'vs/base/common/resources'; import { VSBuffer } from 'vs/base/common/buffer'; -import { merge, IMergeResult } from 'vs/platform/userDataSync/common/snippetsMerge'; +import { merge, IMergeResult, areSame } from 'vs/platform/userDataSync/common/snippetsMerge'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IStorageService } from 'vs/platform/storage/common/storage'; - -interface ISinppetsSyncPreview extends ISyncResourcePreview { - readonly local: IStringDictionary; - readonly added: IStringDictionary; - readonly updated: IStringDictionary; - readonly removed: string[]; - readonly conflicts: Conflict[]; - readonly resolvedConflicts: IStringDictionary; - readonly remote: IStringDictionary | null; -} +import { deepClone } from 'vs/base/common/objects'; +import { localize } from 'vs/nls'; +import { values } from 'vs/base/common/map'; export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser { @@ -60,83 +53,40 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD this.triggerLocalChange(); } - protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise { + protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise { + const resourcePreviews: IFileResourcePreview[] = []; if (remoteUserData.syncData !== null) { const local = await this.getSnippetsFileContents(); const localSnippets = this.toSnippetsContents(local); const remoteSnippets = this.parseSnippets(remoteUserData.syncData); const mergeResult = merge(localSnippets, remoteSnippets, localSnippets); - const { added, updated, remote, removed } = mergeResult; - return { - remoteUserData, lastSyncUserData, - added, removed, updated, remote, local, - hasLocalChanged: Object.keys(added).length > 0 || removed.length > 0 || Object.keys(updated).length > 0, - hasRemoteChanged: remote !== null, - conflicts: [], resolvedConflicts: {}, hasConflicts: false, - isLastSyncFromCurrentMachine: false, - resourcePreviews: this.getResourcePreviews(mergeResult) - }; - } else { - return { - remoteUserData, lastSyncUserData, - added: {}, removed: [], updated: {}, remote: null, local: {}, - hasLocalChanged: false, - hasRemoteChanged: false, - conflicts: [], resolvedConflicts: {}, hasConflicts: false, - isLastSyncFromCurrentMachine: false, - resourcePreviews: [] - }; + resourcePreviews.push(...this.getResourcePreviews(mergeResult, local, remoteSnippets)); } + return resourcePreviews; } - protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise { + protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise { const local = await this.getSnippetsFileContents(); const localSnippets = this.toSnippetsContents(local); const mergeResult = merge(localSnippets, null, null); - const { added, updated, remote, removed } = mergeResult; - return { - added, removed, updated, remote, remoteUserData, local, lastSyncUserData, conflicts: [], resolvedConflicts: {}, - hasLocalChanged: Object.keys(added).length > 0 || removed.length > 0 || Object.keys(updated).length > 0, - hasRemoteChanged: remote !== null, - isLastSyncFromCurrentMachine: false, - hasConflicts: false, - resourcePreviews: this.getResourcePreviews(mergeResult) - }; + const resourcePreviews = this.getResourcePreviews(mergeResult, local, {}); + return resourcePreviews; } - protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise { + protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise { const local = await this.getSnippetsFileContents(); const localSnippets = this.toSnippetsContents(local); const snippets = this.parseSnippets(syncData); const mergeResult = merge(localSnippets, snippets, localSnippets); - const { added, updated, removed } = mergeResult; - return { - added, removed, updated, remote: snippets, remoteUserData, local, lastSyncUserData, conflicts: [], resolvedConflicts: {}, hasConflicts: false, - hasLocalChanged: Object.keys(added).length > 0 || removed.length > 0 || Object.keys(updated).length > 0, - hasRemoteChanged: true, - isLastSyncFromCurrentMachine: false, - resourcePreviews: this.getResourcePreviews(mergeResult) - }; + const resourcePreviews = this.getResourcePreviews(mergeResult, local, snippets); + return resourcePreviews; } - protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken = CancellationToken.None): Promise { + protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise { const local = await this.getSnippetsFileContents(); - return this.doGeneratePreview(local, remoteUserData, lastSyncUserData, {}, token); - } - - private async doGeneratePreview(local: IStringDictionary, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resolvedConflicts: IStringDictionary = {}, token: CancellationToken = CancellationToken.None): Promise { const localSnippets = this.toSnippetsContents(local); const remoteSnippets: IStringDictionary | null = remoteUserData.syncData ? this.parseSnippets(remoteUserData.syncData) : null; - const isLastSyncFromCurrentMachine = await this.isLastSyncFromCurrentMachine(remoteUserData); - - let lastSyncSnippets: IStringDictionary | null = null; - if (lastSyncUserData === null) { - if (isLastSyncFromCurrentMachine) { - lastSyncSnippets = remoteUserData.syncData ? this.parseSnippets(remoteUserData.syncData) : null; - } - } else { - lastSyncSnippets = lastSyncUserData.syncData ? this.parseSnippets(lastSyncUserData.syncData) : null; - } + const lastSyncSnippets: IStringDictionary | null = lastSyncUserData && lastSyncUserData.syncData ? this.parseSnippets(lastSyncUserData.syncData) : null; if (remoteSnippets) { this.logService.trace(`${this.syncResourceLogLabel}: Merging remote snippets with local snippets...`); @@ -144,79 +94,47 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD this.logService.trace(`${this.syncResourceLogLabel}: Remote snippets does not exist. Synchronizing snippets for the first time.`); } - const mergeResult = merge(localSnippets, remoteSnippets, lastSyncSnippets, resolvedConflicts); - const resourcePreviews = this.getResourcePreviews(mergeResult); + const mergeResult = merge(localSnippets, remoteSnippets, lastSyncSnippets); + const resourcePreviews = this.getResourcePreviews(mergeResult, local, remoteSnippets || {}); - const conflicts: Conflict[] = []; - for (const key of mergeResult.conflicts) { - const localPreview = joinPath(this.syncPreviewFolder, key); - conflicts.push({ local: localPreview, remote: localPreview.with({ scheme: USER_DATA_SYNC_SCHEME }) }); - const content = local[key]; - if (!token.isCancellationRequested) { - await this.fileService.writeFile(localPreview, content ? content.value : VSBuffer.fromString('')); - } - } - - for (const conflict of this.conflicts) { - // clear obsolete conflicts - if (!conflicts.some(({ local }) => isEqual(local, conflict.local))) { - try { - await this.fileService.del(conflict.local); - } catch (error) { - // Ignore & log - this.logService.error(error); + for (const resourcePreview of resourcePreviews) { + if (resourcePreview.hasConflicts) { + if (!token.isCancellationRequested) { + await this.fileService.writeFile(resourcePreview.previewResource!, VSBuffer.fromString(resourcePreview.previewContent || '')); } } } - this.setConflicts(conflicts); + return resourcePreviews; + } + protected async updateResourcePreviewContent(resourcePreview: IFileResourcePreview, resource: URI, previewContent: string, token: CancellationToken): Promise { return { - remoteUserData, local, - lastSyncUserData, - added: mergeResult.added, - removed: mergeResult.removed, - updated: mergeResult.updated, - conflicts, - hasConflicts: conflicts.length > 0, - remote: mergeResult.remote, - resolvedConflicts, - hasLocalChanged: Object.keys(mergeResult.added).length > 0 || mergeResult.removed.length > 0 || Object.keys(mergeResult.updated).length > 0, - hasRemoteChanged: mergeResult.remote !== null, - isLastSyncFromCurrentMachine, - resourcePreviews + ...resourcePreview, + previewContent: previewContent || null, + hasConflicts: false, + localChange: previewContent ? Change.Modified : Change.Deleted, + remoteChange: previewContent ? Change.Modified : Change.Deleted, }; } - protected async updatePreviewWithConflict(preview: ISinppetsSyncPreview, conflictResource: URI, content: string, token: CancellationToken): Promise { - const conflict = this.conflicts.filter(({ local, remote }) => isEqual(local, conflictResource) || isEqual(remote, conflictResource))[0]; - if (conflict) { - const key = relativePath(this.syncPreviewFolder, conflict.local)!; - preview.resolvedConflicts[key] = content || null; - preview = await this.doGeneratePreview(preview.local, preview.remoteUserData, preview.lastSyncUserData, preview.resolvedConflicts, token); + protected async applyPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: IFileResourcePreview[], forcePush: boolean): Promise { + if (resourcePreviews.some(({ hasConflicts }) => hasConflicts)) { + throw new UserDataSyncError(localize('unresolved conflicts', "Error while syncing {0}. Please resolve conflicts first.", this.syncResourceLogLabel), UserDataSyncErrorCode.UnresolvedConflicts, this.resource); } - return preview; - } - protected async applyPreview(preview: ISinppetsSyncPreview, forcePush: boolean): Promise { - let { added, removed, updated, local, remote, remoteUserData, lastSyncUserData, hasLocalChanged, hasRemoteChanged } = preview; - - if (!hasLocalChanged && !hasRemoteChanged) { + if (resourcePreviews.every(({ localChange, remoteChange }) => localChange === Change.None && remoteChange === Change.None)) { this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing snippets.`); } - if (hasLocalChanged) { + if (resourcePreviews.some(({ localChange }) => localChange !== Change.None)) { // back up all snippets - await this.backupLocal(JSON.stringify(this.toSnippetsContents(local))); - await this.updateLocalSnippets(added, removed, updated, local); + await this.updateLocalBackup(resourcePreviews); + await this.updateLocalSnippets(resourcePreviews); } - if (remote) { - // update remote - this.logService.trace(`${this.syncResourceLogLabel}: Updating remote snippets...`); - const content = JSON.stringify(remote); - remoteUserData = await this.updateRemoteUserData(content, forcePush ? null : remoteUserData.ref); - this.logService.info(`${this.syncResourceLogLabel}: Updated remote snippets`); + if (resourcePreviews.some(({ remoteChange }) => remoteChange !== Change.None)) { + remoteUserData = await this.updateRemoteSnippets(resourcePreviews, remoteUserData, forcePush); } if (lastSyncUserData?.ref !== remoteUserData.ref) { @@ -226,52 +144,149 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized snippets`); } + for (const { previewResource } of resourcePreviews) { + // Delete the preview + try { + await this.fileService.del(previewResource); + } catch (e) { /* ignore */ } + } + } - private getResourcePreviews(mergeResult: IMergeResult): IResourcePreview[] { - const resourcePreviews: IResourcePreview[] = []; - for (const key of Object.keys(mergeResult.added)) { - resourcePreviews.push({ - remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME }), - hasConflicts: false, - hasLocalChanged: true, - hasRemoteChanged: false - }); - } - for (const key of Object.keys(mergeResult.updated)) { - resourcePreviews.push({ - remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME }), - localResouce: joinPath(this.snippetsFolder, key), - hasConflicts: false, - hasLocalChanged: true, - hasRemoteChanged: true - }); - } - for (const key of mergeResult.removed) { - resourcePreviews.push({ - localResouce: joinPath(this.snippetsFolder, key), - hasConflicts: false, - hasLocalChanged: true, - hasRemoteChanged: false - }); - } - for (const key of mergeResult.conflicts) { - resourcePreviews.push({ - localResouce: joinPath(this.snippetsFolder, key), + private getResourcePreviews(mergeResult: IMergeResult, localFileContent: IStringDictionary, remoteSnippets: IStringDictionary): IFileResourcePreview[] { + const resourcePreviews: Map = new Map(); + + /* Snippets added remotely -> add locally */ + for (const key of Object.keys(mergeResult.local.added)) { + resourcePreviews.set(key, { + localResource: joinPath(this.snippetsFolder, key), + fileContent: null, + localContent: null, remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME }), + remoteContent: remoteSnippets[key], previewResource: joinPath(this.syncPreviewFolder, key), - hasConflicts: true, - hasLocalChanged: true, - hasRemoteChanged: true + previewContent: mergeResult.local.added[key], + hasConflicts: false, + localChange: Change.Added, + remoteChange: Change.None }); } - return resourcePreviews; - } + /* Snippets updated remotely -> update locally */ + for (const key of Object.keys(mergeResult.local.updated)) { + resourcePreviews.set(key, { + localResource: joinPath(this.snippetsFolder, key), + fileContent: localFileContent[key], + localContent: localFileContent[key].value.toString(), + remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME }), + remoteContent: remoteSnippets[key], + previewResource: joinPath(this.syncPreviewFolder, key), + previewContent: mergeResult.local.updated[key], + hasConflicts: false, + localChange: Change.Modified, + remoteChange: Change.None + }); + } - async stop(): Promise { - await this.clearConflicts(); - return super.stop(); + /* Snippets removed remotely -> remove locally */ + for (const key of mergeResult.local.removed) { + resourcePreviews.set(key, { + localResource: joinPath(this.snippetsFolder, key), + fileContent: localFileContent[key], + localContent: localFileContent[key].value.toString(), + remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME }), + remoteContent: null, + previewResource: joinPath(this.syncPreviewFolder, key), + previewContent: null, + hasConflicts: false, + localChange: Change.Deleted, + remoteChange: Change.None + }); + } + + /* Snippets added locally -> add remotely */ + for (const key of Object.keys(mergeResult.remote.added)) { + resourcePreviews.set(key, { + localResource: joinPath(this.snippetsFolder, key), + fileContent: localFileContent[key], + localContent: localFileContent[key].value.toString(), + remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME }), + remoteContent: null, + previewResource: joinPath(this.syncPreviewFolder, key), + previewContent: mergeResult.remote.added[key], + hasConflicts: false, + localChange: Change.None, + remoteChange: Change.Added + }); + } + + /* Snippets updated locally -> update remotely */ + for (const key of Object.keys(mergeResult.remote.updated)) { + resourcePreviews.set(key, { + localResource: joinPath(this.snippetsFolder, key), + fileContent: localFileContent[key], + localContent: localFileContent[key].value.toString(), + remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME }), + remoteContent: remoteSnippets[key], + previewResource: joinPath(this.syncPreviewFolder, key), + previewContent: mergeResult.remote.updated[key], + hasConflicts: false, + localChange: Change.None, + remoteChange: Change.Modified + }); + } + + /* Snippets removed locally -> remove remotely */ + for (const key of mergeResult.remote.removed) { + resourcePreviews.set(key, { + localResource: joinPath(this.snippetsFolder, key), + fileContent: null, + localContent: null, + remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME }), + remoteContent: remoteSnippets[key], + previewResource: joinPath(this.syncPreviewFolder, key), + previewContent: null, + hasConflicts: false, + localChange: Change.None, + remoteChange: Change.Deleted + }); + } + + /* Snippets with conflicts */ + for (const key of mergeResult.conflicts) { + resourcePreviews.set(key, { + localResource: joinPath(this.snippetsFolder, key), + fileContent: localFileContent[key] || null, + localContent: localFileContent[key] ? localFileContent[key].value.toString() : null, + remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME }), + remoteContent: remoteSnippets[key] || null, + previewResource: joinPath(this.syncPreviewFolder, key), + previewContent: localFileContent[key] ? localFileContent[key].value.toString() : null, + hasConflicts: true, + localChange: localFileContent[key] ? Change.Modified : Change.Added, + remoteChange: remoteSnippets[key] ? Change.Modified : Change.Added + }); + } + + /* Unmodified Snippets */ + for (const key of Object.keys(localFileContent)) { + if (!resourcePreviews.has(key)) { + resourcePreviews.set(key, { + localResource: joinPath(this.snippetsFolder, key), + fileContent: localFileContent[key] || null, + localContent: localFileContent[key] ? localFileContent[key].value.toString() : null, + remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME }), + remoteContent: remoteSnippets[key] || null, + previewResource: joinPath(this.syncPreviewFolder, key), + previewContent: localFileContent[key] ? localFileContent[key].value.toString() : null, + hasConflicts: false, + localChange: Change.None, + remoteChange: Change.None + }); + } + } + + return values(resourcePreviews); } async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> { @@ -294,13 +309,16 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD } async resolveContent(uri: URI): Promise { - if (isEqualOrParent(uri.with({ scheme: this.syncFolder.scheme }), this.syncPreviewFolder)) { + if (isEqualOrParent(uri.with({ scheme: this.syncPreviewFolder.scheme }), this.syncPreviewFolder) + || isEqualOrParent(uri, this.syncPreviewFolder.with({ scheme: USER_DATA_SYNC_SCHEME }))) { return this.resolvePreviewContent(uri); } + let content = await super.resolveContent(uri); if (content) { return content; } + content = await super.resolveContent(dirname(uri)); if (content) { const syncData = this.parseSyncData(content); @@ -309,20 +327,7 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD return snippets[basename(uri)] || null; } } - return null; - } - private async resolvePreviewContent(conflictResource: URI): Promise { - const syncPreview = await this.getSyncPreviewInProgress(); - if (syncPreview) { - const key = relativePath(this.syncPreviewFolder, conflictResource.with({ scheme: this.syncPreviewFolder.scheme }))!; - if (conflictResource.scheme === this.syncPreviewFolder.scheme) { - return (syncPreview as ISinppetsSyncPreview).local[key] ? (syncPreview as ISinppetsSyncPreview).local[key].value.toString() : null; - } else if (syncPreview.remoteUserData && syncPreview.remoteUserData.syncData) { - const snippets = this.parseSnippets(syncPreview.remoteUserData.syncData); - return snippets[key] || null; - } - } return null; } @@ -338,34 +343,78 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD return false; } - private async clearConflicts(): Promise { - if (this.conflicts.length) { - await Promise.all(this.conflicts.map(({ local }) => this.fileService.del(local))); - this.setConflicts([]); + private async updateLocalBackup(resourcePreviews: IFileResourcePreview[]): Promise { + const local: IStringDictionary = {}; + for (const resourcePreview of resourcePreviews) { + if (resourcePreview.fileContent) { + local[basename(resourcePreview.localResource!)] = resourcePreview.fileContent; + } + } + await this.backupLocal(JSON.stringify(this.toSnippetsContents(local))); + } + + private async updateLocalSnippets(resourcePreviews: IFileResourcePreview[]): Promise { + if (resourcePreviews.some(({ hasConflicts }) => hasConflicts)) { + // Do not update if there are conflicts + return; + } + + for (const { fileContent, previewContent: content, localResource, remoteResource, localChange } of resourcePreviews) { + if (localChange !== Change.None) { + const key = remoteResource ? basename(remoteResource) : basename(localResource!); + const resource = joinPath(this.snippetsFolder, key); + + // Removed + if (localChange === Change.Deleted) { + this.logService.trace(`${this.syncResourceLogLabel}: Deleting snippet...`, basename(resource)); + await this.fileService.del(resource); + this.logService.info(`${this.syncResourceLogLabel}: Deleted snippet`, basename(resource)); + } + + // Added + else if (localChange === Change.Added) { + this.logService.trace(`${this.syncResourceLogLabel}: Creating snippet...`, basename(resource)); + await this.fileService.createFile(resource, VSBuffer.fromString(content!), { overwrite: false }); + this.logService.info(`${this.syncResourceLogLabel}: Created snippet`, basename(resource)); + } + + // Updated + else { + this.logService.trace(`${this.syncResourceLogLabel}: Updating snippet...`, basename(resource)); + await this.fileService.writeFile(resource, VSBuffer.fromString(content!), fileContent!); + this.logService.info(`${this.syncResourceLogLabel}: Updated snippet`, basename(resource)); + } + } } } - private async updateLocalSnippets(added: IStringDictionary, removed: string[], updated: IStringDictionary, local: IStringDictionary): Promise { - for (const key of removed) { - const resource = joinPath(this.snippetsFolder, key); - this.logService.trace(`${this.syncResourceLogLabel}: Deleting snippet...`, basename(resource)); - await this.fileService.del(resource); - this.logService.info(`${this.syncResourceLogLabel}: Deleted snippet`, basename(resource)); + private async updateRemoteSnippets(resourcePreviews: IFileResourcePreview[], remoteUserData: IRemoteUserData, forcePush: boolean): Promise { + if (resourcePreviews.some(({ hasConflicts }) => hasConflicts)) { + // Do not update if there are conflicts + return remoteUserData; } - for (const key of Object.keys(added)) { - const resource = joinPath(this.snippetsFolder, key); - this.logService.trace(`${this.syncResourceLogLabel}: Creating snippet...`, basename(resource)); - await this.fileService.createFile(resource, VSBuffer.fromString(added[key]), { overwrite: false }); - this.logService.info(`${this.syncResourceLogLabel}: Created snippet`, basename(resource)); + const currentSnippets: IStringDictionary = remoteUserData.syncData ? this.parseSnippets(remoteUserData.syncData) : {}; + const newSnippets: IStringDictionary = deepClone(currentSnippets); + + for (const { previewContent: content, localResource, remoteResource, remoteChange } of resourcePreviews) { + if (remoteChange !== Change.None) { + const key = localResource ? basename(localResource) : basename(remoteResource!); + if (remoteChange === Change.Deleted) { + delete newSnippets[key]; + } else { + newSnippets[key] = content!; + } + } } - for (const key of Object.keys(updated)) { - const resource = joinPath(this.snippetsFolder, key); - this.logService.trace(`${this.syncResourceLogLabel}: Updating snippet...`, basename(resource)); - await this.fileService.writeFile(resource, VSBuffer.fromString(updated[key]), local[key]); - this.logService.info(`${this.syncResourceLogLabel}: Updated snippet`, basename(resource)); + if (!areSame(currentSnippets, newSnippets)) { + // update remote + this.logService.trace(`${this.syncResourceLogLabel}: Updating remote snippets...`); + remoteUserData = await this.updateRemoteUserData(JSON.stringify(newSnippets), forcePush ? null : remoteUserData.ref); + this.logService.info(`${this.syncResourceLogLabel}: Updated remote snippets`); } + return remoteUserData; } private parseSnippets(syncData: ISyncData): IStringDictionary { diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index 2183252b50d..cf6b03f0b22 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -212,6 +212,7 @@ export enum UserDataSyncErrorCode { LocalInvalidContent = 'LocalInvalidContent', LocalError = 'LocalError', Incompatible = 'Incompatible', + UnresolvedConflicts = 'UnresolvedConflicts', Unknown = 'Unknown', } @@ -293,20 +294,24 @@ export interface ISyncData { content: string; } +export const enum Change { + None, + Added, + Modified, + Deleted, +} + export interface IResourcePreview { - readonly remoteResource?: URI; - readonly localResouce?: URI; - readonly previewResource?: URI; - readonly hasLocalChanged: boolean; - readonly hasRemoteChanged: boolean; + readonly remoteResource: URI; + readonly localResource: URI; + readonly previewResource: URI; + readonly localChange: Change; + readonly remoteChange: Change; readonly hasConflicts: boolean; } export interface ISyncResourcePreview { readonly isLastSyncFromCurrentMachine: boolean; - readonly hasLocalChanged: boolean; - readonly hasRemoteChanged: boolean; - readonly hasConflicts: boolean; readonly resourcePreviews: IResourcePreview[]; } @@ -325,7 +330,7 @@ export interface IUserDataSynchroniser { replace(uri: URI): Promise; stop(): Promise; - generateSyncPreview(): Promise + generateSyncResourcePreview(): Promise hasPreviouslySynced(): Promise hasLocalData(): Promise; resetLocal(): Promise; diff --git a/src/vs/platform/userDataSync/common/userDataSyncService.ts b/src/vs/platform/userDataSync/common/userDataSyncService.ts index 24d128981dc..4c87152e11a 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 } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncService, SyncStatus, IUserDataSyncStoreService, SyncResource, IUserDataSyncLogService, IUserDataSynchroniser, UserDataSyncErrorCode, UserDataSyncError, SyncResourceConflicts, ISyncResourceHandle, IUserDataManifest, ISyncTask, Change } 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'; @@ -297,8 +297,9 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ } for (const synchroniser of synchronizers) { - const preview = await synchroniser.generateSyncPreview(); - if (preview && !preview.isLastSyncFromCurrentMachine && (preview.hasLocalChanged || preview.hasRemoteChanged)) { + const preview = await synchroniser.generateSyncResourcePreview(); + if (preview && !preview.isLastSyncFromCurrentMachine + && (preview.resourcePreviews.some(({ localChange }) => localChange !== Change.None) || preview.resourcePreviews.some(({ remoteChange }) => remoteChange !== Change.None))) { return true; } } diff --git a/src/vs/platform/userDataSync/test/common/snippetsMerge.test.ts b/src/vs/platform/userDataSync/test/common/snippetsMerge.test.ts index 5a396c4a6da..55b33d2d8ce 100644 --- a/src/vs/platform/userDataSync/test/common/snippetsMerge.test.ts +++ b/src/vs/platform/userDataSync/test/common/snippetsMerge.test.ts @@ -116,11 +116,13 @@ suite('SnippetsMerge', () => { const actual = merge(local, remote, null); - assert.deepEqual(actual.added, {}); - assert.deepEqual(actual.updated, {}); - assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.local.added, {}); + assert.deepEqual(actual.local.updated, {}); + assert.deepEqual(actual.local.removed, []); assert.deepEqual(actual.conflicts, []); - assert.equal(actual.remote, null); + assert.deepEqual(actual.remote.added, {}); + assert.deepEqual(actual.remote.updated, {}); + assert.deepEqual(actual.remote.removed, []); }); test('merge when local and remote are same with multiple entries', async () => { @@ -129,11 +131,13 @@ suite('SnippetsMerge', () => { const actual = merge(local, remote, null); - assert.deepEqual(actual.added, {}); - assert.deepEqual(actual.updated, {}); - assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.local.added, {}); + assert.deepEqual(actual.local.updated, {}); + assert.deepEqual(actual.local.removed, []); assert.deepEqual(actual.conflicts, []); - assert.equal(actual.remote, null); + assert.deepEqual(actual.remote.added, {}); + assert.deepEqual(actual.remote.updated, {}); + assert.deepEqual(actual.remote.removed, []); }); test('merge when local and remote are same with multiple entries in different order', async () => { @@ -142,11 +146,13 @@ suite('SnippetsMerge', () => { const actual = merge(local, remote, null); - assert.deepEqual(actual.added, {}); - assert.deepEqual(actual.updated, {}); - assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.local.added, {}); + assert.deepEqual(actual.local.updated, {}); + assert.deepEqual(actual.local.removed, []); assert.deepEqual(actual.conflicts, []); - assert.equal(actual.remote, null); + assert.deepEqual(actual.remote.added, {}); + assert.deepEqual(actual.remote.updated, {}); + assert.deepEqual(actual.remote.removed, []); }); test('merge when local and remote are same with different base content', async () => { @@ -156,11 +162,13 @@ suite('SnippetsMerge', () => { const actual = merge(local, remote, base); - assert.deepEqual(actual.added, {}); - assert.deepEqual(actual.updated, {}); - assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.local.added, {}); + assert.deepEqual(actual.local.updated, {}); + assert.deepEqual(actual.local.removed, []); assert.deepEqual(actual.conflicts, []); - assert.equal(actual.remote, null); + assert.deepEqual(actual.remote.added, {}); + assert.deepEqual(actual.remote.updated, {}); + assert.deepEqual(actual.remote.removed, []); }); test('merge when a new entry is added to remote', async () => { @@ -169,11 +177,13 @@ suite('SnippetsMerge', () => { const actual = merge(local, remote, null); - assert.deepEqual(actual.added, { 'typescript.json': tsSnippet1 }); - assert.deepEqual(actual.updated, {}); - assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.local.added, { 'typescript.json': tsSnippet1 }); + assert.deepEqual(actual.local.updated, {}); + assert.deepEqual(actual.local.removed, []); assert.deepEqual(actual.conflicts, []); - assert.equal(actual.remote, null); + assert.deepEqual(actual.remote.added, {}); + assert.deepEqual(actual.remote.updated, {}); + assert.deepEqual(actual.remote.removed, []); }); test('merge when multiple new entries are added to remote', async () => { @@ -182,11 +192,13 @@ suite('SnippetsMerge', () => { const actual = merge(local, remote, null); - assert.deepEqual(actual.added, remote); - assert.deepEqual(actual.updated, {}); - assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.local.added, remote); + assert.deepEqual(actual.local.updated, {}); + assert.deepEqual(actual.local.removed, []); assert.deepEqual(actual.conflicts, []); - assert.equal(actual.remote, null); + assert.deepEqual(actual.remote.added, {}); + assert.deepEqual(actual.remote.updated, {}); + assert.deepEqual(actual.remote.removed, []); }); test('merge when new entry is added to remote from base and local has not changed', async () => { @@ -195,11 +207,13 @@ suite('SnippetsMerge', () => { const actual = merge(local, remote, local); - assert.deepEqual(actual.added, { 'typescript.json': tsSnippet1 }); - assert.deepEqual(actual.updated, {}); - assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.local.added, { 'typescript.json': tsSnippet1 }); + assert.deepEqual(actual.local.updated, {}); + assert.deepEqual(actual.local.removed, []); assert.deepEqual(actual.conflicts, []); - assert.equal(actual.remote, null); + assert.deepEqual(actual.remote.added, {}); + assert.deepEqual(actual.remote.updated, {}); + assert.deepEqual(actual.remote.removed, []); }); test('merge when an entry is removed from remote from base and local has not changed', async () => { @@ -208,11 +222,13 @@ suite('SnippetsMerge', () => { const actual = merge(local, remote, local); - assert.deepEqual(actual.added, {}); - assert.deepEqual(actual.updated, {}); - assert.deepEqual(actual.removed, ['typescript.json']); + assert.deepEqual(actual.local.added, {}); + assert.deepEqual(actual.local.updated, {}); + assert.deepEqual(actual.local.removed, ['typescript.json']); assert.deepEqual(actual.conflicts, []); - assert.equal(actual.remote, null); + assert.deepEqual(actual.remote.added, {}); + assert.deepEqual(actual.remote.updated, {}); + assert.deepEqual(actual.remote.removed, []); }); test('merge when all entries are removed from base and local has not changed', async () => { @@ -221,11 +237,13 @@ suite('SnippetsMerge', () => { const actual = merge(local, remote, local); - assert.deepEqual(actual.added, {}); - assert.deepEqual(actual.updated, {}); - assert.deepEqual(actual.removed, ['html.json', 'typescript.json']); + assert.deepEqual(actual.local.added, {}); + assert.deepEqual(actual.local.updated, {}); + assert.deepEqual(actual.local.removed, ['html.json', 'typescript.json']); assert.deepEqual(actual.conflicts, []); - assert.equal(actual.remote, null); + assert.deepEqual(actual.remote.added, {}); + assert.deepEqual(actual.remote.updated, {}); + assert.deepEqual(actual.remote.removed, []); }); test('merge when an entry is updated in remote from base and local has not changed', async () => { @@ -234,11 +252,13 @@ suite('SnippetsMerge', () => { const actual = merge(local, remote, local); - assert.deepEqual(actual.added, {}); - assert.deepEqual(actual.updated, { 'html.json': htmlSnippet2 }); - assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.local.added, {}); + assert.deepEqual(actual.local.updated, { 'html.json': htmlSnippet2 }); + assert.deepEqual(actual.local.removed, []); assert.deepEqual(actual.conflicts, []); - assert.equal(actual.remote, null); + assert.deepEqual(actual.remote.added, {}); + assert.deepEqual(actual.remote.updated, {}); + assert.deepEqual(actual.remote.removed, []); }); test('merge when remote has moved forwarded with multiple changes and local stays with base', async () => { @@ -247,11 +267,13 @@ suite('SnippetsMerge', () => { const actual = merge(local, remote, local); - assert.deepEqual(actual.added, { 'c.json': cSnippet }); - assert.deepEqual(actual.updated, { 'html.json': htmlSnippet2 }); - assert.deepEqual(actual.removed, ['typescript.json']); + assert.deepEqual(actual.local.added, { 'c.json': cSnippet }); + assert.deepEqual(actual.local.updated, { 'html.json': htmlSnippet2 }); + assert.deepEqual(actual.local.removed, ['typescript.json']); assert.deepEqual(actual.conflicts, []); - assert.deepEqual(actual.remote, null); + assert.deepEqual(actual.remote.added, {}); + assert.deepEqual(actual.remote.updated, {}); + assert.deepEqual(actual.remote.removed, []); }); test('merge when a new entries are added to local', async () => { @@ -260,11 +282,13 @@ suite('SnippetsMerge', () => { const actual = merge(local, remote, null); - assert.deepEqual(actual.added, {}); - assert.deepEqual(actual.updated, {}); - assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.local.added, {}); + assert.deepEqual(actual.local.updated, {}); + assert.deepEqual(actual.local.removed, []); assert.deepEqual(actual.conflicts, []); - assert.deepEqual(actual.remote, local); + assert.deepEqual(actual.remote.added, { 'c.json': cSnippet }); + assert.deepEqual(actual.remote.updated, {}); + assert.deepEqual(actual.remote.removed, []); }); test('merge when multiple new entries are added to local from base and remote is not changed', async () => { @@ -273,11 +297,13 @@ suite('SnippetsMerge', () => { const actual = merge(local, remote, remote); - assert.deepEqual(actual.added, {}); - assert.deepEqual(actual.updated, {}); - assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.local.added, {}); + assert.deepEqual(actual.local.updated, {}); + assert.deepEqual(actual.local.removed, []); assert.deepEqual(actual.conflicts, []); - assert.deepEqual(actual.remote, { 'typescript.json': tsSnippet1, 'html.json': htmlSnippet1, 'c.json': cSnippet }); + assert.deepEqual(actual.remote.added, { 'html.json': htmlSnippet1, 'c.json': cSnippet }); + assert.deepEqual(actual.remote.updated, {}); + assert.deepEqual(actual.remote.removed, []); }); test('merge when an entry is removed from local from base and remote has not changed', async () => { @@ -286,11 +312,13 @@ suite('SnippetsMerge', () => { const actual = merge(local, remote, remote); - assert.deepEqual(actual.added, {}); - assert.deepEqual(actual.updated, {}); - assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.local.added, {}); + assert.deepEqual(actual.local.updated, {}); + assert.deepEqual(actual.local.removed, []); assert.deepEqual(actual.conflicts, []); - assert.deepEqual(actual.remote, local); + assert.deepEqual(actual.remote.added, {}); + assert.deepEqual(actual.remote.updated, {}); + assert.deepEqual(actual.remote.removed, ['typescript.json']); }); test('merge when an entry is updated in local from base and remote has not changed', async () => { @@ -299,11 +327,13 @@ suite('SnippetsMerge', () => { const actual = merge(local, remote, remote); - assert.deepEqual(actual.added, {}); - assert.deepEqual(actual.updated, {}); - assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.local.added, {}); + assert.deepEqual(actual.local.updated, {}); + assert.deepEqual(actual.local.removed, []); assert.deepEqual(actual.conflicts, []); - assert.deepEqual(actual.remote, local); + assert.deepEqual(actual.remote.added, {}); + assert.deepEqual(actual.remote.updated, { 'html.json': htmlSnippet2 }); + assert.deepEqual(actual.remote.removed, []); }); test('merge when local has moved forwarded with multiple changes and remote stays with base', async () => { @@ -312,11 +342,13 @@ suite('SnippetsMerge', () => { const actual = merge(local, remote, remote); - assert.deepEqual(actual.added, {}); - assert.deepEqual(actual.updated, {}); - assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.local.added, {}); + assert.deepEqual(actual.local.updated, {}); + assert.deepEqual(actual.local.removed, []); assert.deepEqual(actual.conflicts, []); - assert.deepEqual(actual.remote, local); + assert.deepEqual(actual.remote.added, { 'c.json': cSnippet }); + assert.deepEqual(actual.remote.updated, { 'html.json': htmlSnippet2 }); + assert.deepEqual(actual.remote.removed, ['typescript.json']); }); test('merge when local and remote with one entry but different value', async () => { @@ -325,11 +357,13 @@ suite('SnippetsMerge', () => { const actual = merge(local, remote, null); - assert.deepEqual(actual.added, {}); - assert.deepEqual(actual.updated, {}); - assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.local.added, {}); + assert.deepEqual(actual.local.updated, {}); + assert.deepEqual(actual.local.removed, []); assert.deepEqual(actual.conflicts, ['html.json']); - assert.deepEqual(actual.remote, null); + assert.deepEqual(actual.remote.added, {}); + assert.deepEqual(actual.remote.updated, {}); + assert.deepEqual(actual.remote.removed, []); }); test('merge when the entry is removed in remote but updated in local and a new entry is added in remote', async () => { @@ -339,11 +373,13 @@ suite('SnippetsMerge', () => { const actual = merge(local, remote, base); - assert.deepEqual(actual.added, { 'typescript.json': tsSnippet1 }); - assert.deepEqual(actual.updated, {}); - assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.local.added, { 'typescript.json': tsSnippet1 }); + assert.deepEqual(actual.local.updated, {}); + assert.deepEqual(actual.local.removed, []); assert.deepEqual(actual.conflicts, ['html.json']); - assert.deepEqual(actual.remote, null); + assert.deepEqual(actual.remote.added, {}); + assert.deepEqual(actual.remote.updated, {}); + assert.deepEqual(actual.remote.removed, []); }); test('merge with single entry and local is empty', async () => { @@ -353,11 +389,13 @@ suite('SnippetsMerge', () => { const actual = merge(local, remote, base); - assert.deepEqual(actual.added, { 'html.json': htmlSnippet2 }); - assert.deepEqual(actual.updated, {}); - assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.local.added, { 'html.json': htmlSnippet2 }); + assert.deepEqual(actual.local.updated, {}); + assert.deepEqual(actual.local.removed, []); assert.deepEqual(actual.conflicts, []); - assert.deepEqual(actual.remote, null); + assert.deepEqual(actual.remote.added, {}); + assert.deepEqual(actual.remote.updated, {}); + assert.deepEqual(actual.remote.removed, []); }); test('merge when local and remote has moved forwareded with conflicts', async () => { @@ -367,41 +405,13 @@ suite('SnippetsMerge', () => { const actual = merge(local, remote, base); - assert.deepEqual(actual.added, { 'typescript.json': tsSnippet2 }); - assert.deepEqual(actual.updated, {}); - assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.local.added, { 'typescript.json': tsSnippet2 }); + assert.deepEqual(actual.local.updated, {}); + assert.deepEqual(actual.local.removed, []); assert.deepEqual(actual.conflicts, ['html.json']); - assert.deepEqual(actual.remote, { 'typescript.json': tsSnippet2, 'c.json': cSnippet }); - }); - - test('merge when local and remote has moved forwareded with resolved conflicts - update', async () => { - const base = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 }; - const local = { 'html.json': htmlSnippet2, 'c.json': cSnippet }; - const remote = { 'typescript.json': tsSnippet2 }; - const resolvedConflicts = { 'html.json': htmlSnippet2 }; - - const actual = merge(local, remote, base, resolvedConflicts); - - assert.deepEqual(actual.added, { 'typescript.json': tsSnippet2 }); - assert.deepEqual(actual.updated, {}); - assert.deepEqual(actual.removed, []); - assert.deepEqual(actual.conflicts, []); - assert.deepEqual(actual.remote, { 'typescript.json': tsSnippet2, 'html.json': htmlSnippet2, 'c.json': cSnippet }); - }); - - test('merge when local and remote has moved forwareded with resolved conflicts - remove', async () => { - const base = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 }; - const local = { 'html.json': htmlSnippet2, 'c.json': cSnippet }; - const remote = { 'typescript.json': tsSnippet2 }; - const resolvedConflicts = { 'html.json': null }; - - const actual = merge(local, remote, base, resolvedConflicts); - - assert.deepEqual(actual.added, { 'typescript.json': tsSnippet2 }); - assert.deepEqual(actual.updated, {}); - assert.deepEqual(actual.removed, ['html.json']); - assert.deepEqual(actual.conflicts, []); - assert.deepEqual(actual.remote, { 'typescript.json': tsSnippet2, 'c.json': cSnippet }); + assert.deepEqual(actual.remote.added, { 'c.json': cSnippet }); + assert.deepEqual(actual.remote.updated, {}); + assert.deepEqual(actual.remote.removed, []); }); test('merge when local and remote has moved forwareded with multiple conflicts', async () => { @@ -411,26 +421,13 @@ suite('SnippetsMerge', () => { const actual = merge(local, remote, base); - assert.deepEqual(actual.added, {}); - assert.deepEqual(actual.updated, {}); - assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.local.added, {}); + assert.deepEqual(actual.local.updated, {}); + assert.deepEqual(actual.local.removed, []); assert.deepEqual(actual.conflicts, ['html.json', 'typescript.json']); - assert.deepEqual(actual.remote, null); - }); - - test('merge when local and remote has moved forwareded with multiple conflicts and resolving one conflict', async () => { - const base = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 }; - const local = { 'html.json': htmlSnippet2, 'typescript.json': tsSnippet2, 'c.json': cSnippet }; - const remote = { 'c.json': cSnippet }; - const resolvedConflicts = { 'html.json': htmlSnippet1 }; - - const actual = merge(local, remote, base, resolvedConflicts); - - assert.deepEqual(actual.added, {}); - assert.deepEqual(actual.updated, { 'html.json': htmlSnippet1 }); - assert.deepEqual(actual.removed, []); - assert.deepEqual(actual.conflicts, ['typescript.json']); - assert.deepEqual(actual.remote, { 'c.json': cSnippet, 'html.json': htmlSnippet1 }); + assert.deepEqual(actual.remote.added, {}); + assert.deepEqual(actual.remote.updated, {}); + assert.deepEqual(actual.remote.removed, []); }); }); diff --git a/src/vs/platform/userDataSync/test/common/synchronizer.test.ts b/src/vs/platform/userDataSync/test/common/synchronizer.test.ts index df32970dea9..3782f86ae82 100644 --- a/src/vs/platform/userDataSync/test/common/synchronizer.test.ts +++ b/src/vs/platform/userDataSync/test/common/synchronizer.test.ts @@ -4,19 +4,21 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { IUserDataSyncStoreService, SyncResource, SyncStatus, IUserDataSyncResourceEnablementService, IRemoteUserData, ISyncData } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncStoreService, SyncResource, SyncStatus, IUserDataSyncResourceEnablementService, IRemoteUserData, ISyncData, Change, USER_DATA_SYNC_SCHEME } 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 { AbstractSynchroniser, ISyncResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer'; +import { AbstractSynchroniser, ISyncResourcePreview, IResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer'; import { Barrier } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; import { CancellationToken } from 'vs/base/common/cancellation'; import { URI } from 'vs/base/common/uri'; -interface ITestSyncPreview extends ISyncResourcePreview { +interface ITestResourcePreview extends IResourcePreview { ref?: string; } +const resource = URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'testResource', path: `/current.json` }); + class TestSynchroniser extends AbstractSynchroniser { syncBarrier: Barrier = new Barrier(); @@ -40,32 +42,32 @@ class TestSynchroniser extends AbstractSynchroniser { return super.doSync(remoteUserData, lastSyncUserData); } - protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise { - return { hasLocalChanged: false, hasRemoteChanged: false, isLastSyncFromCurrentMachine: false, hasConflicts: this.syncResult.hasConflicts, remoteUserData, lastSyncUserData, resourcePreviews: [] }; + protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise { + return [{ localContent: null, localResource: resource, remoteContent: null, remoteResource: resource, previewContent: null, previewResource: resource, localChange: Change.None, remoteChange: Change.None, hasConflicts: this.syncResult.hasConflicts }]; } - protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise { - return { hasLocalChanged: false, hasRemoteChanged: false, isLastSyncFromCurrentMachine: false, hasConflicts: this.syncResult.hasConflicts, remoteUserData, lastSyncUserData, resourcePreviews: [] }; + protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise { + return [{ localContent: null, localResource: resource, remoteContent: null, remoteResource: resource, previewContent: null, previewResource: resource, localChange: Change.None, remoteChange: Change.None, hasConflicts: this.syncResult.hasConflicts }]; } - protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise { - return { hasLocalChanged: false, hasRemoteChanged: false, isLastSyncFromCurrentMachine: false, hasConflicts: this.syncResult.hasConflicts, remoteUserData, lastSyncUserData, resourcePreviews: [] }; + protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise { + return [{ localContent: null, localResource: resource, remoteContent: null, remoteResource: resource, previewContent: null, previewResource: resource, localChange: Change.None, remoteChange: Change.None, hasConflicts: this.syncResult.hasConflicts }]; } - protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise { + protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise { if (this.syncResult.hasError) { throw new Error('failed'); } - return { ref: remoteUserData.ref, hasLocalChanged: false, hasRemoteChanged: false, isLastSyncFromCurrentMachine: false, hasConflicts: this.syncResult.hasConflicts, remoteUserData, lastSyncUserData, resourcePreviews: [] }; + return [{ localContent: null, localResource: resource, remoteContent: null, remoteResource: resource, previewContent: null, previewResource: resource, localChange: Change.None, remoteChange: Change.None, hasConflicts: this.syncResult.hasConflicts, ref: remoteUserData.ref }]; } protected async updatePreviewWithConflict(preview: ISyncResourcePreview, conflictResource: URI, conflictContent: string): Promise { return preview; } - protected async applyPreview({ ref }: ITestSyncPreview, forcePush: boolean): Promise { - if (ref) { - await this.apply(ref); + protected async applyPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, preview: ITestResourcePreview[], forcePush: boolean): Promise { + if (preview[0]?.ref) { + await this.apply(preview[0].ref); } } diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts index 7fb10cb47a8..b13435f84e3 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts @@ -402,7 +402,7 @@ abstract class UserDataSyncActivityViewDataProvider implements ITreeViewDataProv handle, collapsibleState: TreeItemCollapsibleState.None, resourceUri: resource, - command: { id: `workbench.actions.sync.commpareWithLocal`, title: '', arguments: [{ $treeViewId: '', $treeItemHandle: handle }] }, + command: { id: `workbench.actions.sync.compareWithLocal`, title: '', arguments: [{ $treeViewId: '', $treeItemHandle: handle }] }, contextValue: `sync-associatedResource-${(element).syncResourceHandle.syncResource}` }; });