diff --git a/src/vs/platform/environment/common/environment.ts b/src/vs/platform/environment/common/environment.ts index 5df93a4f3f2..ee908ae2ddd 100644 --- a/src/vs/platform/environment/common/environment.ts +++ b/src/vs/platform/environment/common/environment.ts @@ -128,8 +128,6 @@ export interface IEnvironmentService extends IUserHomeProvider { // sync resources userDataSyncLogResource: URI; userDataSyncHome: URI; - settingsSyncPreviewResource: URI; - keybindingsSyncPreviewResource: URI; machineSettingsResource: URI; diff --git a/src/vs/platform/environment/node/environmentService.ts b/src/vs/platform/environment/node/environmentService.ts index 4738addf8c7..07c266d77a1 100644 --- a/src/vs/platform/environment/node/environmentService.ts +++ b/src/vs/platform/environment/node/environmentService.ts @@ -114,12 +114,6 @@ export class EnvironmentService implements IEnvironmentService { @memoize get userDataSyncHome(): URI { return resources.joinPath(this.userRoamingDataHome, 'sync'); } - @memoize - get settingsSyncPreviewResource(): URI { return resources.joinPath(this.userDataSyncHome, 'settings.json'); } - - @memoize - get keybindingsSyncPreviewResource(): URI { return resources.joinPath(this.userDataSyncHome, 'keybindings.json'); } - @memoize get userDataSyncLogResource(): URI { return URI.file(path.join(this.logsPath, 'userDataSync.log')); } diff --git a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts index 2b6180c9ab2..3946c276a07 100644 --- a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts +++ b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts @@ -7,9 +7,9 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { IFileService, IFileContent, FileChangesEvent, FileSystemProviderError, FileSystemProviderErrorCode, FileOperationResult, FileOperationError } from 'vs/platform/files/common/files'; import { VSBuffer } from 'vs/base/common/buffer'; import { URI } from 'vs/base/common/uri'; -import { SyncResource, SyncStatus, IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, UserDataSyncError, IUserDataSyncLogService, IUserDataSyncUtilService, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSync'; +import { SyncResource, SyncStatus, IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, UserDataSyncError, IUserDataSyncLogService, IUserDataSyncUtilService, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, Conflict } from 'vs/platform/userDataSync/common/userDataSync'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { joinPath, dirname } from 'vs/base/common/resources'; +import { joinPath, dirname, isEqual } from 'vs/base/common/resources'; import { CancelablePromise } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -20,6 +20,7 @@ import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { isString } from 'vs/base/common/types'; import { uppercaseFirstLetter } from 'vs/base/common/strings'; +import { equals } from 'vs/base/common/arrays'; type SyncSourceClassification = { source?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; @@ -51,6 +52,11 @@ export abstract class AbstractSynchroniser extends Disposable { private _onDidChangStatus: Emitter = this._register(new Emitter()); readonly onDidChangeStatus: Event = this._onDidChangStatus.event; + private _conflicts: Conflict[] = []; + get conflicts(): Conflict[] { return this._conflicts; } + private _onDidChangeConflicts: Emitter = this._register(new Emitter()); + readonly onDidChangeConflicts: Event = this._onDidChangeConflicts.event; + protected readonly _onDidChangeLocal: Emitter = this._register(new Emitter()); readonly onDidChangeLocal: Event = this._onDidChangeLocal.event; @@ -87,6 +93,16 @@ export abstract class AbstractSynchroniser extends Disposable { // Log to telemetry when conflicts are resolved this.telemetryService.publicLog2<{ source: string }, SyncSourceClassification>('sync/conflictsResolved', { source: this.resource }); } + if (this.status !== SyncStatus.HasConflicts) { + this.setConflicts([]); + } + } + } + + protected 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); } } @@ -154,7 +170,7 @@ export abstract class AbstractSynchroniser extends Disposable { return !!lastSyncData; } - async getRemoteContentFromPreview(): Promise { + async getConflictContent(conflictResource: URI): Promise { return null; } @@ -285,15 +301,22 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser { this.cancel(); this.logService.trace(`${this.syncResourceLogLabel}: Stopped synchronizing ${this.resource.toLowerCase()}.`); try { - await this.fileService.del(this.conflictsPreviewResource); + await this.fileService.del(this.localPreviewResource); } catch (e) { /* ignore */ } this.setStatus(SyncStatus.Idle); } - async getRemoteContentFromPreview(): Promise { - if (this.syncPreviewResultPromise) { - const result = await this.syncPreviewResultPromise; - return result.remoteUserData && result.remoteUserData.syncData ? result.remoteUserData.syncData.content : null; + async getConflictContent(conflictResource: URI): Promise { + if (isEqual(this.remotePreviewResource, conflictResource) || isEqual(this.localPreviewResource, conflictResource)) { + if (this.syncPreviewResultPromise) { + const result = await this.syncPreviewResultPromise; + if (isEqual(this.remotePreviewResource, conflictResource)) { + return result.remoteUserData && result.remoteUserData.syncData ? result.remoteUserData.syncData.content : null; + } + if (isEqual(this.localPreviewResource, conflictResource)) { + return result.fileContent ? result.fileContent.value.toString() : null; + } + } } return null; } @@ -356,7 +379,8 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser { } } - protected abstract readonly conflictsPreviewResource: URI; + 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 ad91ac82436..01ad8d03ab0 100644 --- a/src/vs/platform/userDataSync/common/extensionsSync.ts +++ b/src/vs/platform/userDataSync/common/extensionsSync.ts @@ -11,11 +11,11 @@ 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 { localize } from 'vs/nls'; import { merge } from 'vs/platform/userDataSync/common/extensionsMerge'; import { isNonEmptyArray } from 'vs/base/common/arrays'; import { AbstractSynchroniser, IRemoteUserData, ISyncData } from 'vs/platform/userDataSync/common/abstractSynchronizer'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { URI } from 'vs/base/common/uri'; interface ISyncPreviewResult { readonly localExtensions: ISyncExtension[]; @@ -147,7 +147,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse return null; } - accept(content: string): Promise { + async acceptConflict(conflict: URI, content: string): Promise { throw new Error(`${this.syncResourceLogLabel}: Conflicts should not occur`); } @@ -230,9 +230,9 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse const installedExtensions = await this.extensionManagementService.getInstalled(ExtensionType.User); const extensionsToRemove = installedExtensions.filter(({ identifier }) => removed.some(r => areSameExtensions(identifier, r))); await Promise.all(extensionsToRemove.map(async extensionToRemove => { - this.logService.trace(`${this.syncResourceLogLabel}: Uninstalling local extension...', extensionToRemove.identifier.i`); + this.logService.trace(`${this.syncResourceLogLabel}: Uninstalling local extension...`, extensionToRemove.identifier.id); await this.extensionManagementService.uninstall(extensionToRemove); - this.logService.info(`${this.syncResourceLogLabel}: Uninstalled local extension.', extensionToRemove.identifier.i`); + this.logService.info(`${this.syncResourceLogLabel}: Uninstalled local extension.`, extensionToRemove.identifier.id); removeFromSkipped.push(extensionToRemove.identifier); })); } @@ -245,13 +245,13 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse // Builtin Extension: Sync only enablement state if (installedExtension && installedExtension.type === ExtensionType.System) { if (e.disabled) { - this.logService.trace(`${this.syncResourceLogLabel}: Disabling extension...', e.identifier.i`); + this.logService.trace(`${this.syncResourceLogLabel}: Disabling extension...`, e.identifier.id); await this.extensionEnablementService.disableExtension(e.identifier); - this.logService.info(`${this.syncResourceLogLabel}: Disabled extension', e.identifier.i`); + this.logService.info(`${this.syncResourceLogLabel}: Disabled extension`, e.identifier.id); } else { - this.logService.trace(`${this.syncResourceLogLabel}: Enabling extension...', e.identifier.i`); + this.logService.trace(`${this.syncResourceLogLabel}: Enabling extension...`, e.identifier.id); await this.extensionEnablementService.enableExtension(e.identifier); - this.logService.info(`${this.syncResourceLogLabel}: Enabled extension', e.identifier.i`); + this.logService.info(`${this.syncResourceLogLabel}: Enabled extension`, e.identifier.id); } removeFromSkipped.push(e.identifier); return; @@ -261,25 +261,25 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse if (extension) { try { if (e.disabled) { - this.logService.trace(`${this.syncResourceLogLabel}: Disabling extension...', e.identifier.id, extension.versio`); + this.logService.trace(`${this.syncResourceLogLabel}: Disabling extension...`, e.identifier.id, extension.version); await this.extensionEnablementService.disableExtension(extension.identifier); - this.logService.info(`${this.syncResourceLogLabel}: Disabled extension', e.identifier.id, extension.versio`); + this.logService.info(`${this.syncResourceLogLabel}: Disabled extension`, e.identifier.id, extension.version); } else { - this.logService.trace(`${this.syncResourceLogLabel}: Enabling extension...', e.identifier.id, extension.versio`); + this.logService.trace(`${this.syncResourceLogLabel}: Enabling extension...`, e.identifier.id, extension.version); await this.extensionEnablementService.enableExtension(extension.identifier); - this.logService.info(`${this.syncResourceLogLabel}: Enabled extension', e.identifier.id, extension.versio`); + this.logService.info(`${this.syncResourceLogLabel}: Enabled extension`, e.identifier.id, extension.version); } // Install only if the extension does not exist if (!installedExtension || installedExtension.manifest.version !== extension.version) { - this.logService.trace(`${this.syncResourceLogLabel}: Installing extension...', e.identifier.id, extension.versio`); + this.logService.trace(`${this.syncResourceLogLabel}: Installing extension...`, e.identifier.id, extension.version); await this.extensionManagementService.installFromGallery(extension); - this.logService.info(`${this.syncResourceLogLabel}: Installed extension.', e.identifier.id, extension.versio`); + this.logService.info(`${this.syncResourceLogLabel}: Installed extension.`, e.identifier.id, extension.version); removeFromSkipped.push(extension.identifier); } } catch (error) { addToSkipped.push(e); this.logService.error(error); - this.logService.info(localize('skip extension', "Skipped synchronizing extension {0}", extension.displayName || extension.identifier.id)); + this.logService.info(`${this.syncResourceLogLabel}: Skipped synchronizing extension`, extension.displayName || extension.identifier.id); } } else { addToSkipped.push(e); diff --git a/src/vs/platform/userDataSync/common/globalStateSync.ts b/src/vs/platform/userDataSync/common/globalStateSync.ts index 0be9b802764..66939041a9d 100644 --- a/src/vs/platform/userDataSync/common/globalStateSync.ts +++ b/src/vs/platform/userDataSync/common/globalStateSync.ts @@ -16,6 +16,7 @@ import { parse } from 'vs/base/common/json'; import { AbstractSynchroniser, IRemoteUserData } 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'; const argvProperties: string[] = ['locale']; @@ -131,7 +132,7 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs return null; } - accept(content: string): Promise { + async acceptConflict(conflict: URI, content: string): Promise { throw new Error(`${this.syncResourceLogLabel}: Conflicts should not occur`); } diff --git a/src/vs/platform/userDataSync/common/keybindingsSync.ts b/src/vs/platform/userDataSync/common/keybindingsSync.ts index c5248fe5a8b..0aa4c71114e 100644 --- a/src/vs/platform/userDataSync/common/keybindingsSync.ts +++ b/src/vs/platform/userDataSync/common/keybindingsSync.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IFileService, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; -import { UserDataSyncError, UserDataSyncErrorCode, SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService, SyncResource, IUserDataSynchroniser, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSync'; +import { UserDataSyncError, UserDataSyncErrorCode, SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService, SyncResource, IUserDataSynchroniser, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, USER_DATA_SYNC_SCHEME, PREVIEW_DIR_NAME } from 'vs/platform/userDataSync/common/userDataSync'; import { merge } from 'vs/platform/userDataSync/common/keybindingsMerge'; import { VSBuffer } from 'vs/base/common/buffer'; import { parse } from 'vs/base/common/json'; @@ -19,6 +19,7 @@ import { isNonEmptyArray } from 'vs/base/common/arrays'; import { IFileSyncPreviewResult, AbstractJsonFileSynchroniser, IRemoteUserData } from 'vs/platform/userDataSync/common/abstractSynchronizer'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { URI } from 'vs/base/common/uri'; +import { joinPath, isEqual } from 'vs/base/common/resources'; interface ISyncContent { mac?: string; @@ -29,8 +30,9 @@ interface ISyncContent { export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implements IUserDataSynchroniser { - protected get conflictsPreviewResource(): URI { return this.environmentService.keybindingsSyncPreviewResource; } protected readonly version: number = 1; + protected readonly localPreviewResource: URI = joinPath(this.syncFolder, PREVIEW_DIR_NAME, 'keybindings.json'); + protected readonly remotePreviewResource: URI = this.localPreviewResource.with({ scheme: USER_DATA_SYNC_SCHEME }); constructor( @IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService, @@ -39,7 +41,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem @IConfigurationService configurationService: IConfigurationService, @IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService, @IFileService fileService: IFileService, - @IEnvironmentService private readonly environmentService: IEnvironmentService, + @IEnvironmentService environmentService: IEnvironmentService, @IUserDataSyncUtilService userDataSyncUtilService: IUserDataSyncUtilService, @ITelemetryService telemetryService: ITelemetryService, ) { @@ -129,8 +131,10 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem } - async accept(content: string): Promise { - if (this.status === SyncStatus.HasConflicts) { + async acceptConflict(conflict: URI, content: string): Promise { + if (this.status === SyncStatus.HasConflicts + && (isEqual(this.localPreviewResource, conflict) || isEqual(this.remotePreviewResource, conflict)) + ) { const preview = await this.syncPreviewResultPromise!; this.cancel(); this.syncPreviewResultPromise = createCancelablePromise(async () => ({ ...preview, content })); @@ -156,8 +160,8 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem return false; } - async getRemoteContentFromPreview(): Promise { - const content = await super.getRemoteContentFromPreview(); + async getConflictContent(conflictResource: URI): Promise { + const content = await super.getConflictContent(conflictResource); return content !== null ? this.getKeybindingsContentFromSyncContent(content) : null; } @@ -224,7 +228,9 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem if (hasLocalChanged) { this.logService.trace(`${this.syncResourceLogLabel}: Updating local keybindings...`); - await this.backupLocal(this.toSyncContent(content, null)); + if (fileContent) { + await this.backupLocal(this.toSyncContent(fileContent.value.toString(), null)); + } await this.updateLocalFileContent(content, fileContent); this.logService.info(`${this.syncResourceLogLabel}: Updated local keybindings`); } @@ -238,7 +244,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem // Delete the preview try { - await this.fileService.del(this.conflictsPreviewResource); + await this.fileService.del(this.localPreviewResource); } catch (e) { /* ignore */ } } else { this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing keybindings.`); @@ -303,9 +309,11 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem } if (content && !token.isCancellationRequested) { - await this.fileService.writeFile(this.environmentService.keybindingsSyncPreviewResource, VSBuffer.fromString(content)); + await this.fileService.writeFile(this.localPreviewResource, VSBuffer.fromString(content)); } + this.setConflicts(hasConflicts && !token.isCancellationRequested ? [{ local: this.localPreviewResource, remote: this.remotePreviewResource }] : []); + return { fileContent, remoteUserData, lastSyncUserData, content, hasLocalChanged, hasRemoteChanged, hasConflicts }; } diff --git a/src/vs/platform/userDataSync/common/settingsSync.ts b/src/vs/platform/userDataSync/common/settingsSync.ts index 6704fa622c3..234496f2f4b 100644 --- a/src/vs/platform/userDataSync/common/settingsSync.ts +++ b/src/vs/platform/userDataSync/common/settingsSync.ts @@ -4,24 +4,23 @@ *--------------------------------------------------------------------------------------------*/ import { IFileService, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; -import { UserDataSyncError, UserDataSyncErrorCode, SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService, IConflictSetting, CONFIGURATION_SYNC_STORE_KEY, SyncResource, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSync'; +import { UserDataSyncError, UserDataSyncErrorCode, SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService, CONFIGURATION_SYNC_STORE_KEY, SyncResource, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, USER_DATA_SYNC_SCHEME, PREVIEW_DIR_NAME } from 'vs/platform/userDataSync/common/userDataSync'; import { VSBuffer } from 'vs/base/common/buffer'; import { parse } from 'vs/base/common/json'; import { localize } from 'vs/nls'; -import { Emitter, Event } from 'vs/base/common/event'; +import { Event } from 'vs/base/common/event'; import { createCancelablePromise } from 'vs/base/common/async'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { CancellationToken } from 'vs/base/common/cancellation'; import { updateIgnoredSettings, merge, getIgnoredSettings } from 'vs/platform/userDataSync/common/settingsMerge'; -import * as arrays from 'vs/base/common/arrays'; -import * as objects from 'vs/base/common/objects'; import { isEmptyObject } from 'vs/base/common/types'; import { edit } from 'vs/platform/userDataSync/common/content'; import { IFileSyncPreviewResult, AbstractJsonFileSynchroniser, IRemoteUserData } 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'; +import { joinPath, isEqual } from 'vs/base/common/resources'; export interface ISettingsSyncContent { settings: string; @@ -38,16 +37,12 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser { _serviceBrand: any; protected readonly version: number = 1; - protected get conflictsPreviewResource(): URI { return this.environmentService.settingsSyncPreviewResource; } - - private _conflicts: IConflictSetting[] = []; - get conflicts(): IConflictSetting[] { return this._conflicts; } - private _onDidChangeConflicts: Emitter = this._register(new Emitter()); - readonly onDidChangeConflicts: Event = this._onDidChangeConflicts.event; + protected readonly localPreviewResource: URI = joinPath(this.syncFolder, PREVIEW_DIR_NAME, 'settings.json'); + protected readonly remotePreviewResource: URI = this.localPreviewResource.with({ scheme: USER_DATA_SYNC_SCHEME }); constructor( @IFileService fileService: IFileService, - @IEnvironmentService private readonly environmentService: IEnvironmentService, + @IEnvironmentService environmentService: IEnvironmentService, @IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService, @IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService, @IUserDataSyncLogService logService: IUserDataSyncLogService, @@ -67,15 +62,6 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser { } } - private setConflicts(conflicts: IConflictSetting[]): void { - if (!arrays.equals(this.conflicts, conflicts, - (a, b) => a.key === b.key && objects.equals(a.localValue, b.localValue) && objects.equals(a.remoteValue, b.remoteValue)) - ) { - this._conflicts = conflicts; - this._onDidChangeConflicts.fire(conflicts); - } - } - async pull(): Promise { if (!this.isEnabled()) { this.logService.info(`${this.syncResourceLogLabel}: Skipped pulling settings as it is disabled.`); @@ -187,8 +173,8 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser { return false; } - async getRemoteContentFromPreview(): Promise { - let content = await super.getRemoteContentFromPreview(); + async getConflictContent(conflictResource: URI): Promise { + let content = await super.getConflictContent(conflictResource); if (content !== null) { const settingsSyncContent = this.parseSettingsSyncContent(content); content = settingsSyncContent ? settingsSyncContent.settings : null; @@ -232,8 +218,10 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser { return null; } - async accept(content: string): Promise { - if (this.status === SyncStatus.HasConflicts) { + async acceptConflict(conflict: URI, content: string): Promise { + if (this.status === SyncStatus.HasConflicts + && (isEqual(this.localPreviewResource, conflict) || isEqual(this.remotePreviewResource, conflict)) + ) { const preview = await this.syncPreviewResultPromise!; this.cancel(); const formatUtils = await this.getFormattingOptions(); @@ -289,7 +277,9 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser { if (hasLocalChanged) { this.logService.trace(`${this.syncResourceLogLabel}: Updating local settings...`); - await this.backupLocal(JSON.stringify(this.toSettingsSyncContent(content))); + if (fileContent) { + await this.backupLocal(JSON.stringify(this.toSettingsSyncContent(fileContent.value.toString()))); + } await this.updateLocalFileContent(content, fileContent); this.logService.info(`${this.syncResourceLogLabel}: Updated local settings`); } @@ -306,7 +296,7 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser { // Delete the preview try { - await this.fileService.del(this.conflictsPreviewResource); + await this.fileService.del(this.localPreviewResource); } catch (e) { /* ignore */ } } else { this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing settings.`); @@ -338,7 +328,6 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser { let hasLocalChanged: boolean = false; let hasRemoteChanged: boolean = false; let hasConflicts: boolean = false; - let conflictSettings: IConflictSetting[] = []; if (remoteSettingsSyncContent) { const localContent: string = fileContent ? fileContent.value.toString() : '{}'; @@ -350,7 +339,6 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser { hasLocalChanged = result.localContent !== null; hasRemoteChanged = result.remoteContent !== null; hasConflicts = result.hasConflicts; - conflictSettings = result.conflictsSettings; } // First time syncing to remote @@ -364,10 +352,11 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser { // Remove the ignored settings from the preview. const ignoredSettings = await this.getIgnoredSettings(); const previewContent = updateIgnoredSettings(content, '{}', ignoredSettings, formattingOptions); - await this.fileService.writeFile(this.environmentService.settingsSyncPreviewResource, VSBuffer.fromString(previewContent)); + await this.fileService.writeFile(this.localPreviewResource, VSBuffer.fromString(previewContent)); } - this.setConflicts(conflictSettings); + this.setConflicts(hasConflicts && !token.isCancellationRequested ? [{ local: this.localPreviewResource, remote: this.remotePreviewResource }] : []); + return { fileContent, remoteUserData, lastSyncUserData, content, hasLocalChanged, hasRemoteChanged, hasConflicts }; } diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index 260ca1554d8..4785ab9d216 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -18,7 +18,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IStringDictionary } from 'vs/base/common/collections'; import { FormattingOptions } from 'vs/base/common/jsonFormatter'; import { URI } from 'vs/base/common/uri'; -import { isEqual, joinPath, dirname, basename } from 'vs/base/common/resources'; +import { joinPath, dirname, basename, isEqualOrParent } from 'vs/base/common/resources'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IProductService } from 'vs/platform/product/common/productService'; import { distinct } from 'vs/base/common/arrays'; @@ -242,11 +242,15 @@ export const enum SyncStatus { HasConflicts = 'hasConflicts', } +export type Conflict = { remote: URI, local: URI }; + export interface IUserDataSynchroniser { readonly resource: SyncResource; readonly status: SyncStatus; readonly onDidChangeStatus: Event; + readonly conflicts: Conflict[]; + readonly onDidChangeConflicts: Event; readonly onDidChangeLocal: Event; pull(): Promise; @@ -258,10 +262,11 @@ export interface IUserDataSynchroniser { hasLocalData(): Promise; resetLocal(): Promise; - getRemoteContentFromPreview(): Promise; + getConflictContent(conflictResource: URI): Promise; + acceptConflict(conflictResource: URI, content: string): Promise; + getRemoteContent(ref?: string, fragment?: string): Promise; getLocalBackupContent(ref?: string, fragment?: string): Promise; - accept(content: string): Promise; } //#endregion @@ -282,6 +287,8 @@ export interface IUserDataSyncEnablementService { setResourceEnablement(resource: SyncResource, enabled: boolean): void; } +export type SyncResourceConflicts = { syncResource: SyncResource, conflicts: Conflict[] }; + export const IUserDataSyncService = createDecorator('IUserDataSyncService'); export interface IUserDataSyncService { _serviceBrand: any; @@ -289,8 +296,8 @@ export interface IUserDataSyncService { readonly status: SyncStatus; readonly onDidChangeStatus: Event; - readonly conflictsSources: SyncResource[]; - readonly onDidChangeConflicts: Event; + readonly conflicts: SyncResourceConflicts[]; + readonly onDidChangeConflicts: Event; readonly onDidChangeLocal: Event; readonly onSyncErrors: Event<[SyncResource, UserDataSyncError][]>; @@ -306,7 +313,7 @@ export interface IUserDataSyncService { isFirstTimeSyncWithMerge(): Promise; resolveContent(resource: URI): Promise; - accept(source: SyncResource, content: string): Promise; + acceptConflict(conflictResource: URI, content: string): Promise; } export const IUserDataAutoSyncService = createDecorator('IUserDataAutoSyncService'); @@ -335,36 +342,41 @@ export interface IConflictSetting { //#endregion +export const USER_DATA_SYNC_SCHEME = 'vscode-userdata-sync'; export const CONTEXT_SYNC_STATE = new RawContextKey('syncStatus', SyncStatus.Uninitialized); export const CONTEXT_SYNC_ENABLEMENT = new RawContextKey('syncEnabled', false); -export const USER_DATA_SYNC_SCHEME = 'vscode-userdata-sync'; -export const PREVIEW_QUERY = 'preview=true'; -export function toRemoteSyncResource(resource: SyncResource, ref?: string): URI { - return URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote', path: `/${resource}/${ref ? ref : 'latest'}` }); +export function toRemoteBackupSyncResource(resource: SyncResource, ref?: string): URI { + return URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote-backup', path: `/${resource}/${ref ? ref : 'latest'}` }); } export function toLocalBackupSyncResource(resource: SyncResource, ref?: string): URI { return URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local-backup', path: `/${resource}/${ref ? ref : 'latest'}` }); } - -export function resolveSyncResource(resource: URI): { remote: boolean, resource: SyncResource, ref?: string } | null { - if (resource.scheme === USER_DATA_SYNC_SCHEME) { - const remote = resource.authority === 'remote'; +export function resolveBackupSyncResource(resource: URI): { remote: boolean, resource: SyncResource, path: string } | null { + if (resource.scheme === USER_DATA_SYNC_SCHEME + && resource.authority === 'remote-backup' || resource.authority === 'local-backup') { const resourceKey: SyncResource = basename(dirname(resource)) as SyncResource; - const ref = basename(resource); - if (resourceKey && ref) { - return { remote, resource: resourceKey, ref: ref !== 'latest' ? ref : undefined }; + const path = resource.path.substring(resourceKey.length + 1); + if (resourceKey && path) { + const remote = resource.authority === 'remote-backup'; + return { remote, resource: resourceKey, path }; } } return null; } -export function getSyncSourceFromPreviewResource(uri: URI, environmentService: IEnvironmentService): SyncResource | undefined { - if (isEqual(uri, environmentService.settingsSyncPreviewResource)) { - return SyncResource.Settings; +export const PREVIEW_DIR_NAME = 'preview'; +export function getSyncResourceFromLocalPreview(localPreview: URI, environmentService: IEnvironmentService): SyncResource | undefined { + if (localPreview.scheme === USER_DATA_SYNC_SCHEME) { + return undefined; } - if (isEqual(uri, environmentService.keybindingsSyncPreviewResource)) { - return SyncResource.Keybindings; - } - return undefined; + localPreview = localPreview.with({ scheme: environmentService.userDataSyncHome.scheme }); + return ALL_SYNC_RESOURCES.filter(syncResource => isEqualOrParent(localPreview, joinPath(environmentService.userDataSyncHome, syncResource, PREVIEW_DIR_NAME)))[0]; +} +export function getSyncResourceFromRemotePreview(remotePreview: URI, environmentService: IEnvironmentService): SyncResource | undefined { + if (remotePreview.scheme !== USER_DATA_SYNC_SCHEME) { + return undefined; + } + remotePreview = remotePreview.with({ scheme: environmentService.userDataSyncHome.scheme }); + return ALL_SYNC_RESOURCES.filter(syncResource => isEqualOrParent(remotePreview, joinPath(environmentService.userDataSyncHome, syncResource, PREVIEW_DIR_NAME)))[0]; } diff --git a/src/vs/platform/userDataSync/common/userDataSyncIpc.ts b/src/vs/platform/userDataSync/common/userDataSyncIpc.ts index 6b8ef2b6710..ef68213366c 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncIpc.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncIpc.ts @@ -27,9 +27,9 @@ export class UserDataSyncChannel implements IServerChannel { call(context: any, command: string, args?: any): Promise { switch (command) { - case '_getInitialData': return Promise.resolve([this.service.status, this.service.conflictsSources, this.service.lastSyncTime]); + case '_getInitialData': return Promise.resolve([this.service.status, this.service.conflicts, this.service.lastSyncTime]); case 'sync': return this.service.sync(); - case 'accept': return this.service.accept(args[0], args[1]); + case 'acceptConflict': return this.service.acceptConflict(URI.revive(args[0]), args[1]); case 'pull': return this.service.pull(); case 'stop': this.service.stop(); return Promise.resolve(); case 'reset': return this.service.reset(); diff --git a/src/vs/platform/userDataSync/common/userDataSyncService.ts b/src/vs/platform/userDataSync/common/userDataSyncService.ts index fd8d3a93cea..4049ab3c30d 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, UserDataSyncStoreError, UserDataSyncErrorCode, UserDataSyncError, resolveSyncResource, PREVIEW_QUERY } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncService, SyncStatus, IUserDataSyncStoreService, SyncResource, IUserDataSyncLogService, IUserDataSynchroniser, UserDataSyncStoreError, UserDataSyncErrorCode, UserDataSyncError, resolveBackupSyncResource, SyncResourceConflicts } 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'; @@ -17,6 +17,7 @@ import { localize } from 'vs/nls'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { URI } from 'vs/base/common/uri'; import { SettingsSynchroniser } from 'vs/platform/userDataSync/common/settingsSync'; +import { isEqual } from 'vs/base/common/resources'; type SyncErrorClassification = { source: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; @@ -38,10 +39,10 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ readonly onDidChangeLocal: Event; - private _conflictsSources: SyncResource[] = []; - get conflictsSources(): SyncResource[] { return this._conflictsSources; } - private _onDidChangeConflicts: Emitter = this._register(new Emitter()); - readonly onDidChangeConflicts: Event = this._onDidChangeConflicts.event; + private _conflicts: SyncResourceConflicts[] = []; + get conflicts(): SyncResourceConflicts[] { return this._conflicts; } + private _onDidChangeConflicts: Emitter = this._register(new Emitter()); + readonly onDidChangeConflicts: Event = this._onDidChangeConflicts.event; private _syncErrors: [SyncResource, UserDataSyncError][] = []; private _onSyncErrors: Emitter<[SyncResource, UserDataSyncError][]> = this._register(new Emitter<[SyncResource, UserDataSyncError][]>()); @@ -62,7 +63,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ @IInstantiationService private readonly instantiationService: IInstantiationService, @IUserDataSyncLogService private readonly logService: IUserDataSyncLogService, @ITelemetryService private readonly telemetryService: ITelemetryService, - @IStorageService private readonly storageService: IStorageService, + @IStorageService private readonly storageService: IStorageService ) { super(); this.settingsSynchroniser = this._register(this.instantiationService.createInstance(SettingsSynchroniser)); @@ -74,6 +75,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ if (this.userDataSyncStoreService.userDataSyncStore) { this._register(Event.any(...this.synchronisers.map(s => Event.map(s.onDidChangeStatus, () => undefined)))(() => this.updateStatus())); + this._register(Event.any(...this.synchronisers.map(s => Event.map(s.onDidChangeConflicts, () => undefined)))(() => this.updateConflicts())); } this._lastSyncTime = this.storageService.getNumber(LAST_SYNC_TIME_KEY, StorageScope.GLOBAL, undefined); @@ -173,23 +175,32 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ } } - async accept(source: SyncResource, content: string): Promise { + async acceptConflict(conflict: URI, content: string): Promise { await this.checkEnablement(); - const synchroniser = this.getSynchroniser(source); - await synchroniser.accept(content); + const syncResourceConflict = this.conflicts.filter(({ conflicts }) => conflicts.some(({ local, remote }) => isEqual(conflict, local) || isEqual(conflict, remote)))[0]; + if (syncResourceConflict) { + const synchroniser = this.getSynchroniser(syncResourceConflict.syncResource); + await synchroniser.acceptConflict(conflict, content); + } } async resolveContent(resource: URI): Promise { - const result = resolveSyncResource(resource); + const result = resolveBackupSyncResource(resource); if (result) { const synchronizer = this.synchronisers.filter(s => s.resource === result.resource)[0]; if (synchronizer) { - if (PREVIEW_QUERY === resource.query) { - return result.remote ? synchronizer.getRemoteContentFromPreview() : null; - } - return result.remote ? synchronizer.getRemoteContent(result.ref, resource.fragment) : synchronizer.getLocalBackupContent(result.ref, resource.fragment); + const ref = result.path !== 'latest' ? result.path : undefined; + return result.remote ? synchronizer.getRemoteContent(ref, resource.fragment) : synchronizer.getLocalBackupContent(ref, resource.fragment); } } + + for (const synchronizer of this.synchronisers) { + const content = await synchronizer.getConflictContent(resource); + if (content !== null) { + return content; + } + } + return null; } @@ -263,15 +274,19 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ } private updateStatus(): void { - const conflictsSources = this.computeConflictsSources(); - if (!equals(this._conflictsSources, conflictsSources)) { - this._conflictsSources = this.computeConflictsSources(); - this._onDidChangeConflicts.fire(conflictsSources); - } + this.updateConflicts(); const status = this.computeStatus(); this.setStatus(status); } + private updateConflicts(): void { + const conflicts = this.computeConflicts(); + if (!equals(this._conflicts, conflicts, (a, b) => a.syncResource === b.syncResource && equals(a.conflicts, b.conflicts, (a, b) => isEqual(a.local, b.local) && isEqual(a.remote, b.remote)))) { + this._conflicts = this.computeConflicts(); + this._onDidChangeConflicts.fire(conflicts); + } + } + private computeStatus(): SyncStatus { if (!this.userDataSyncStoreService.userDataSyncStore) { return SyncStatus.Uninitialized; @@ -305,8 +320,9 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ this.logService.error(`${source}: ${toErrorMessage(e)}`); } - private computeConflictsSources(): SyncResource[] { - return this.synchronisers.filter(s => s.status === SyncStatus.HasConflicts).map(s => s.resource); + private computeConflicts(): SyncResourceConflicts[] { + return this.synchronisers.filter(s => s.status === SyncStatus.HasConflicts) + .map(s => ({ syncResource: s.resource, conflicts: s.conflicts })); } getSynchroniser(source: SyncResource): IUserDataSynchroniser { diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts index 7f6e210866e..a3c62e50951 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts @@ -52,9 +52,7 @@ export class UserDataSyncClient extends Disposable { const environmentService = this.instantiationService.stub(IEnvironmentService, >{ userDataSyncHome, settingsResource: joinPath(userDataDirectory, 'settings.json'), - settingsSyncPreviewResource: joinPath(userDataSyncHome, 'settings.json'), keybindingsResource: joinPath(userDataDirectory, 'keybindings.json'), - keybindingsSyncPreviewResource: joinPath(userDataSyncHome, 'keybindings.json'), argvResource: joinPath(userDataDirectory, 'argv.json'), args: {} }); diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts b/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts index 6c5e02511da..aadf8b52690 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts @@ -480,7 +480,7 @@ suite('UserDataSyncService', () => { await testObject.sync(); assert.deepEqual(testObject.status, SyncStatus.HasConflicts); - assert.deepEqual(testObject.conflictsSources, [SyncResource.Settings]); + assert.deepEqual(testObject.conflicts.map(({ syncResource }) => syncResource), [SyncResource.Settings]); }); test('test sync will sync other non conflicted areas', async () => { @@ -549,7 +549,7 @@ suite('UserDataSyncService', () => { await testObject.stop(); assert.deepEqual(testObject.status, SyncStatus.Idle); - assert.deepEqual(testObject.conflictsSources, []); + assert.deepEqual(testObject.conflicts, []); }); }); diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index c4a99be9b0b..9b778c41ba5 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -25,12 +25,15 @@ import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/c import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { IFileService } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { CONTEXT_SYNC_STATE, getUserDataSyncStore, ISyncConfiguration, IUserDataAutoSyncService, IUserDataSyncService, IUserDataSyncStore, registerConfiguration, SyncResource, SyncStatus, UserDataSyncError, UserDataSyncErrorCode, USER_DATA_SYNC_SCHEME, IUserDataSyncEnablementService, getSyncSourceFromPreviewResource, CONTEXT_SYNC_ENABLEMENT, PREVIEW_QUERY, resolveSyncResource, toRemoteSyncResource } from 'vs/platform/userDataSync/common/userDataSync'; +import { + CONTEXT_SYNC_STATE, getUserDataSyncStore, ISyncConfiguration, IUserDataAutoSyncService, IUserDataSyncService, IUserDataSyncStore, registerConfiguration, + SyncResource, SyncStatus, UserDataSyncError, UserDataSyncErrorCode, USER_DATA_SYNC_SCHEME, IUserDataSyncEnablementService, CONTEXT_SYNC_ENABLEMENT, + SyncResourceConflicts, Conflict, getSyncResourceFromLocalPreview, getSyncResourceFromRemotePreview +} from 'vs/platform/userDataSync/common/userDataSync'; import { FloatingClickWidget } from 'vs/workbench/browser/parts/editor/editorWidgets'; import { GLOBAL_ACTIVITY_ID } from 'vs/workbench/common/activity'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; @@ -133,10 +136,9 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo @IOutputService private readonly outputService: IOutputService, @IAuthenticationTokenService private readonly authTokenService: IAuthenticationTokenService, @IUserDataAutoSyncService userDataAutoSyncService: IUserDataAutoSyncService, - @ITextModelService textModelResolverService: ITextModelService, + @ITextModelService private readonly textModelResolverService: ITextModelService, @IPreferencesService private readonly preferencesService: IPreferencesService, @ITelemetryService private readonly telemetryService: ITelemetryService, - @IFileService private readonly fileService: IFileService, @IProductService private readonly productService: IProductService, @IStorageService private readonly storageService: IStorageService, @IOpenerService private readonly openerService: IOpenerService, @@ -150,10 +152,10 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo if (this.userDataSyncStore) { registerConfiguration(); this.onDidChangeSyncStatus(this.userDataSyncService.status); - this.onDidChangeConflicts(this.userDataSyncService.conflictsSources); + this.onDidChangeConflicts(this.userDataSyncService.conflicts); this.onDidChangeEnablement(this.userDataSyncEnablementService.isEnabled()); this._register(Event.debounce(userDataSyncService.onDidChangeStatus, () => undefined, 500)(() => this.onDidChangeSyncStatus(this.userDataSyncService.status))); - this._register(userDataSyncService.onDidChangeConflicts(() => this.onDidChangeConflicts(this.userDataSyncService.conflictsSources))); + this._register(userDataSyncService.onDidChangeConflicts(() => this.onDidChangeConflicts(this.userDataSyncService.conflicts))); this._register(userDataSyncService.onSyncErrors(errors => this.onSyncErrors(errors))); this._register(this.authTokenService.onTokenFailed(_ => this.onTokenFailed())); this._register(this.userDataSyncEnablementService.onDidChangeEnablement(enabled => this.onDidChangeEnablement(enabled))); @@ -284,44 +286,45 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } private readonly conflictsDisposables = new Map(); - private onDidChangeConflicts(conflicts: SyncResource[]) { + private onDidChangeConflicts(conflicts: SyncResourceConflicts[]) { this.updateBadge(); if (conflicts.length) { - this.conflictsSources.set(this.userDataSyncService.conflictsSources.join(',')); + const conflictsSources: SyncResource[] = conflicts.map(conflict => conflict.syncResource); + this.conflictsSources.set(conflictsSources.join(',')); // Clear and dispose conflicts those were cleared this.conflictsDisposables.forEach((disposable, conflictsSource) => { - if (this.userDataSyncService.conflictsSources.indexOf(conflictsSource) === -1) { + if (conflictsSources.indexOf(conflictsSource) === -1) { disposable.dispose(); this.conflictsDisposables.delete(conflictsSource); } }); - for (const conflictsSource of this.userDataSyncService.conflictsSources) { - const conflictsEditorInput = this.getConflictsEditorInput(conflictsSource); - if (!conflictsEditorInput && !this.conflictsDisposables.has(conflictsSource)) { - const conflictsArea = getSyncAreaLabel(conflictsSource); + for (const { syncResource, conflicts } of this.userDataSyncService.conflicts) { + const conflictsEditorInput = this.getConflictsEditorInput(syncResource); + if (!conflictsEditorInput && !this.conflictsDisposables.has(syncResource)) { + const conflictsArea = getSyncAreaLabel(syncResource); const handle = this.notificationService.prompt(Severity.Warning, localize('conflicts detected', "Unable to sync due to conflicts in {0}. Please resolve them to continue.", conflictsArea.toLowerCase()), [ { label: localize('accept remote', "Accept Remote"), run: () => { - this.telemetryService.publicLog2<{ source: string, action: string }, SyncConflictsClassification>('sync/handleConflicts', { source: conflictsSource, action: 'acceptRemote' }); - this.acceptRemote(conflictsSource); + this.telemetryService.publicLog2<{ source: string, action: string }, SyncConflictsClassification>('sync/handleConflicts', { source: syncResource, action: 'acceptRemote' }); + this.acceptRemote(syncResource, conflicts); } }, { label: localize('accept local', "Accept Local"), run: () => { - this.telemetryService.publicLog2<{ source: string, action: string }, SyncConflictsClassification>('sync/handleConflicts', { source: conflictsSource, action: 'acceptLocal' }); - this.acceptLocal(conflictsSource); + this.telemetryService.publicLog2<{ source: string, action: string }, SyncConflictsClassification>('sync/handleConflicts', { source: syncResource, action: 'acceptLocal' }); + this.acceptLocal(syncResource, conflicts); } }, { label: localize('show conflicts', "Show Conflicts"), run: () => { - this.telemetryService.publicLog2<{ source: string, action?: string }, SyncConflictsClassification>('sync/showConflicts', { source: conflictsSource }); - this.handleConflicts(conflictsSource); + this.telemetryService.publicLog2<{ source: string, action?: string }, SyncConflictsClassification>('sync/showConflicts', { source: syncResource }); + this.handleConflicts({ syncResource, conflicts }); } } ], @@ -329,18 +332,18 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo sticky: true } ); - this.conflictsDisposables.set(conflictsSource, toDisposable(() => { + this.conflictsDisposables.set(syncResource, toDisposable(() => { // close the conflicts warning notification handle.close(); // close opened conflicts editor previews - const conflictsEditorInput = this.getConflictsEditorInput(conflictsSource); + const conflictsEditorInput = this.getConflictsEditorInput(syncResource); if (conflictsEditorInput) { conflictsEditorInput.dispose(); } - this.conflictsDisposables.delete(conflictsSource); + this.conflictsDisposables.delete(syncResource); })); } } @@ -352,29 +355,24 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } } - private async acceptRemote(syncResource: SyncResource) { + private async acceptRemote(syncResource: SyncResource, conflicts: Conflict[]) { try { - const contents = await this.userDataSyncService.resolveContent(toRemoteSyncResource(syncResource).with({ query: PREVIEW_QUERY })); - if (contents) { - await this.userDataSyncService.accept(syncResource, contents); + for (const conflict of conflicts) { + const modelRef = await this.textModelResolverService.createModelReference(conflict.remote); + await this.userDataSyncService.acceptConflict(conflict.remote, modelRef.object.textEditorModel.getValue()); + modelRef.dispose(); } } catch (e) { this.notificationService.error(e); } } - private async acceptLocal(syncSource: SyncResource): Promise { + private async acceptLocal(syncResource: SyncResource, conflicts: Conflict[]): Promise { try { - const previewResource = syncSource === SyncResource.Settings - ? this.workbenchEnvironmentService.settingsSyncPreviewResource - : syncSource === SyncResource.Keybindings - ? this.workbenchEnvironmentService.keybindingsSyncPreviewResource - : null; - if (previewResource) { - const fileContent = await this.fileService.readFile(previewResource); - if (fileContent) { - this.userDataSyncService.accept(syncSource, fileContent.value.toString()); - } + for (const conflict of conflicts) { + const modelRef = await this.textModelResolverService.createModelReference(conflict.local); + await this.userDataSyncService.acceptConflict(conflict.local, modelRef.object.textEditorModel.getValue()); + modelRef.dispose(); } } catch (e) { this.notificationService.error(e); @@ -497,8 +495,8 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo if (this.userDataSyncService.status !== SyncStatus.Uninitialized && this.userDataSyncEnablementService.isEnabled() && this.authenticationState.get() === AuthStatus.SignedOut) { badge = new NumberBadge(1, () => localize('sign in to sync', "Sign in to Sync")); - } else if (this.userDataSyncService.conflictsSources.length) { - badge = new NumberBadge(this.userDataSyncService.conflictsSources.length, () => localize('has conflicts', "Sync: Conflicts Detected")); + } else if (this.userDataSyncService.conflicts.length) { + badge = new NumberBadge(this.userDataSyncService.conflicts.length, () => localize('has conflicts', "Sync: Conflicts Detected")); } if (badge) { @@ -729,35 +727,35 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } } - private getConflictsEditorInput(source: SyncResource): IEditorInput | undefined { - const previewResource = source === SyncResource.Settings ? this.workbenchEnvironmentService.settingsSyncPreviewResource - : source === SyncResource.Keybindings ? this.workbenchEnvironmentService.keybindingsSyncPreviewResource - : null; - return previewResource ? this.editorService.editors.filter(input => input instanceof DiffEditorInput && isEqual(previewResource, input.master.resource))[0] : undefined; + private getConflictsEditorInput(syncResource: SyncResource): IEditorInput | undefined { + return this.editorService.editors.filter(input => input instanceof DiffEditorInput && getSyncResourceFromLocalPreview(input.master.resource!, this.workbenchEnvironmentService) === syncResource)[0]; } private getAllConflictsEditorInputs(): IEditorInput[] { return this.editorService.editors.filter(input => { const resource = input instanceof DiffEditorInput ? input.master.resource : input.resource; - return isEqual(resource, this.workbenchEnvironmentService.settingsSyncPreviewResource) || isEqual(resource, this.workbenchEnvironmentService.keybindingsSyncPreviewResource); + return getSyncResourceFromLocalPreview(resource!, this.workbenchEnvironmentService) !== undefined; }); } - private async handleConflicts(resource: SyncResource): Promise { - let previewResource: URI | undefined = undefined; - let label: string = ''; - if (resource === SyncResource.Settings) { - previewResource = this.workbenchEnvironmentService.settingsSyncPreviewResource; - label = localize('settings conflicts preview', "Settings Conflicts (Remote ↔ Local)"); - } else if (resource === SyncResource.Keybindings) { - previewResource = this.workbenchEnvironmentService.keybindingsSyncPreviewResource; - label = localize('keybindings conflicts preview', "Keybindings Conflicts (Remote ↔ Local)"); + private async handleSyncResourceConflicts(resource: SyncResource): Promise { + const syncResourceCoflicts = this.userDataSyncService.conflicts.filter(({ syncResource }) => syncResource === resource)[0]; + if (syncResourceCoflicts) { + this.handleConflicts(syncResourceCoflicts); } - if (previewResource) { - const remoteContentResource = toRemoteSyncResource(resource).with({ query: PREVIEW_QUERY }); + } + + private async handleConflicts({ syncResource, conflicts }: SyncResourceConflicts): Promise { + for (const conflict of conflicts) { + let label: string | undefined = undefined; + if (syncResource === SyncResource.Settings) { + label = localize('settings conflicts preview', "Settings Conflicts (Remote ↔ Local)"); + } else if (syncResource === SyncResource.Keybindings) { + label = localize('keybindings conflicts preview', "Keybindings Conflicts (Remote ↔ Local)"); + } await this.editorService.openEditor({ - leftResource: remoteContentResource, - rightResource: previewResource, + leftResource: conflict.remote, + rightResource: conflict.local, label, options: { preserveFocus: false, @@ -846,7 +844,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo private registerShowSettingsConflictsAction(): void { const resolveSettingsConflictsWhenContext = ContextKeyExpr.regex(CONTEXT_CONFLICTS_SOURCES.keys()[0], /.*settings.*/i); - CommandsRegistry.registerCommand(resolveSettingsConflictsCommand.id, () => this.handleConflicts(SyncResource.Settings)); + CommandsRegistry.registerCommand(resolveSettingsConflictsCommand.id, () => this.handleSyncResourceConflicts(SyncResource.Settings)); MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { group: '5_sync', command: { @@ -873,7 +871,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo private registerShowKeybindingsConflictsAction(): void { const resolveKeybindingsConflictsWhenContext = ContextKeyExpr.regex(CONTEXT_CONFLICTS_SOURCES.keys()[0], /.*keybindings.*/i); - CommandsRegistry.registerCommand(resolveKeybindingsConflictsCommand.id, () => this.handleConflicts(SyncResource.Keybindings)); + CommandsRegistry.registerCommand(resolveKeybindingsConflictsCommand.id, () => this.handleSyncResourceConflicts(SyncResource.Keybindings)); MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { group: '5_sync', command: { @@ -931,9 +929,9 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo const quickPick = quickInputService.createQuickPick(); disposables.add(quickPick); const items: Array = []; - if (that.userDataSyncService.conflictsSources.length) { - for (const source of that.userDataSyncService.conflictsSources) { - switch (source) { + if (that.userDataSyncService.conflicts.length) { + for (const { syncResource } of that.userDataSyncService.conflicts) { + switch (syncResource) { case SyncResource.Settings: items.push({ id: resolveSettingsConflictsCommand.id, label: resolveSettingsConflictsCommand.title }); break; @@ -1109,11 +1107,11 @@ class AcceptChangesContribution extends Disposable implements IEditorContributio return false; // we need a model } - if (getSyncSourceFromPreviewResource(model.uri, this.environmentService) !== undefined) { + if (getSyncResourceFromLocalPreview(model.uri, this.environmentService) !== undefined) { return true; } - if (resolveSyncResource(model.uri) !== null && model.uri.query === PREVIEW_QUERY) { + if (getSyncResourceFromRemotePreview(model.uri, this.environmentService) !== undefined) { return this.configurationService.getValue('diffEditor.renderSideBySide'); } @@ -1123,14 +1121,14 @@ class AcceptChangesContribution extends Disposable implements IEditorContributio private createAcceptChangesWidgetRenderer(): void { if (!this.acceptChangesButton) { - const isRemote = resolveSyncResource(this.editor.getModel()!.uri) !== null; + const isRemote = getSyncResourceFromRemotePreview(this.editor.getModel()!.uri, this.environmentService) !== undefined; const acceptRemoteLabel = localize('accept remote', "Accept Remote"); const acceptLocalLabel = localize('accept local', "Accept Local"); this.acceptChangesButton = this.instantiationService.createInstance(FloatingClickWidget, this.editor, isRemote ? acceptRemoteLabel : acceptLocalLabel, null); this._register(this.acceptChangesButton.onClick(async () => { const model = this.editor.getModel(); if (model) { - const conflictsSource = (getSyncSourceFromPreviewResource(model.uri, this.environmentService) || resolveSyncResource(model.uri)!.resource)!; + const conflictsSource = (getSyncResourceFromLocalPreview(model.uri, this.environmentService) || getSyncResourceFromRemotePreview(model.uri, this.environmentService))!; this.telemetryService.publicLog2<{ source: string, action: string }, SyncConflictsClassification>('sync/handleConflicts', { source: conflictsSource, action: isRemote ? 'acceptRemote' : 'acceptLocal' }); const syncAreaLabel = getSyncAreaLabel(conflictsSource); const result = await this.dialogService.confirm({ @@ -1145,10 +1143,11 @@ class AcceptChangesContribution extends Disposable implements IEditorContributio }); if (result.confirmed) { try { - await this.userDataSyncService.accept(conflictsSource, model.getValue()); + await this.userDataSyncService.acceptConflict(model.uri, model.getValue()); } catch (e) { if (e instanceof UserDataSyncError && e.code === UserDataSyncErrorCode.LocalPreconditionFailed) { - if (this.userDataSyncService.conflictsSources.indexOf(conflictsSource) !== -1) { + const syncResourceCoflicts = this.userDataSyncService.conflicts.filter(({ syncResource }) => syncResource === conflictsSource)[0]; + if (syncResourceCoflicts && syncResourceCoflicts.conflicts.some(conflict => isEqual(conflict.local, model.uri) || isEqual(conflict.remote, model.uri))) { this.notificationService.warn(localize('update conflicts', "Could not resolve conflicts as there is new local version available. Please try again.")); } } else { diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncView.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncView.ts index be02cbc7149..bc59eeaff1d 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncView.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncView.ts @@ -10,7 +10,7 @@ import { localize } from 'vs/nls'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { TreeViewPane, TreeView } from 'vs/workbench/browser/parts/views/treeView'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { ALL_SYNC_RESOURCES, CONTEXT_SYNC_ENABLEMENT, IUserDataSyncStoreService, toRemoteSyncResource, resolveSyncResource, IUserDataSyncBackupStoreService, IResourceRefHandle, toLocalBackupSyncResource, SyncResource } from 'vs/platform/userDataSync/common/userDataSync'; +import { ALL_SYNC_RESOURCES, CONTEXT_SYNC_ENABLEMENT, IUserDataSyncStoreService, toRemoteBackupSyncResource, resolveBackupSyncResource, IUserDataSyncBackupStoreService, IResourceRefHandle, toLocalBackupSyncResource, SyncResource } from 'vs/platform/userDataSync/common/userDataSync'; import { registerAction2, Action2, MenuId } from 'vs/platform/actions/common/actions'; import { IContextKeyService, RawContextKey, ContextKeyExpr, ContextKeyEqualsExpr } from 'vs/platform/contextkey/common/contextkey'; import { URI } from 'vs/base/common/uri'; @@ -61,7 +61,7 @@ export class UserDataSyncViewContribution implements IWorkbenchContribution { disposable.dispose(); treeView.dataProvider = this.instantiationService.createInstance(UserDataSyncHistoryViewDataProvider, id, (resource: SyncResource) => remote ? this.userDataSyncStoreService.getAllRefs(resource) : this.userDataSyncBackupStoreService.getAllRefs(resource), - (resource: SyncResource, ref: string) => remote ? toRemoteSyncResource(resource, ref) : toLocalBackupSyncResource(resource, ref)); + (resource: SyncResource, ref: string) => remote ? toRemoteBackupSyncResource(resource, ref) : toLocalBackupSyncResource(resource, ref)); } }); const viewsRegistry = Registry.as(Extensions.ViewsRegistry); @@ -111,7 +111,7 @@ export class UserDataSyncViewContribution implements IWorkbenchContribution { async run(accessor: ServicesAccessor, handle: TreeViewItemHandleArg): Promise { const editorService = accessor.get(IEditorService); let resource = URI.parse(handle.$treeItemHandle); - const result = resolveSyncResource(resource); + const result = resolveBackupSyncResource(resource); if (result) { resource = resource.with({ fragment: result.resource }); await editorService.openEditor({ resource }); @@ -149,7 +149,7 @@ export class UserDataSyncViewContribution implements IWorkbenchContribution { const editorService = accessor.get(IEditorService); const environmentService = accessor.get(IEnvironmentService); const resource = URI.parse(handle.$treeItemHandle); - const result = resolveSyncResource(resource); + const result = resolveBackupSyncResource(resource); if (result) { const leftResource: URI = resource.with({ fragment: result.resource }); const rightResource: URI = result.resource === 'settings' ? environmentService.settingsResource : environmentService.keybindingsResource; diff --git a/src/vs/workbench/services/environment/browser/environmentService.ts b/src/vs/workbench/services/environment/browser/environmentService.ts index fa59b3513ef..a4ee8d92f87 100644 --- a/src/vs/workbench/services/environment/browser/environmentService.ts +++ b/src/vs/workbench/services/environment/browser/environmentService.ts @@ -105,12 +105,6 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment @memoize get userDataSyncHome(): URI { return joinPath(this.userRoamingDataHome, 'sync'); } - @memoize - get settingsSyncPreviewResource(): URI { return joinPath(this.userDataSyncHome, 'settings.json'); } - - @memoize - get keybindingsSyncPreviewResource(): URI { return joinPath(this.userDataSyncHome, 'keybindings.json'); } - @memoize get userDataSyncLogResource(): URI { return joinPath(this.options.logsPath, 'userDataSync.log'); } diff --git a/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncService.ts b/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncService.ts index d4838228ff0..c33de4a7cde 100644 --- a/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncService.ts +++ b/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { SyncStatus, SyncResource, IUserDataSyncService, UserDataSyncError } from 'vs/platform/userDataSync/common/userDataSync'; +import { SyncStatus, SyncResource, IUserDataSyncService, UserDataSyncError, SyncResourceConflicts } from 'vs/platform/userDataSync/common/userDataSync'; import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; import { Disposable } from 'vs/base/common/lifecycle'; import { Emitter, Event } from 'vs/base/common/event'; @@ -25,10 +25,10 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ get onDidChangeLocal(): Event { return this.channel.listen('onDidChangeLocal'); } - private _conflictsSources: SyncResource[] = []; - get conflictsSources(): SyncResource[] { return this._conflictsSources; } - private _onDidChangeConflicts: Emitter = this._register(new Emitter()); - readonly onDidChangeConflicts: Event = this._onDidChangeConflicts.event; + private _conflicts: SyncResourceConflicts[] = []; + get conflicts(): SyncResourceConflicts[] { return this._conflicts; } + private _onDidChangeConflicts: Emitter = this._register(new Emitter()); + readonly onDidChangeConflicts: Event = this._onDidChangeConflicts.event; private _lastSyncTime: number | undefined = undefined; get lastSyncTime(): number | undefined { return this._lastSyncTime; } @@ -52,7 +52,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ return userDataSyncChannel.listen(event, arg); } }; - this.channel.call<[SyncStatus, SyncResource[], number | undefined]>('_getInitialData').then(([status, conflicts, lastSyncTime]) => { + this.channel.call<[SyncStatus, SyncResourceConflicts[], number | undefined]>('_getInitialData').then(([status, conflicts, lastSyncTime]) => { this.updateStatus(status); this.updateConflicts(conflicts); if (lastSyncTime) { @@ -61,7 +61,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ this._register(this.channel.listen('onDidChangeStatus')(status => this.updateStatus(status))); this._register(this.channel.listen('onDidChangeLastSyncTime')(lastSyncTime => this.updateLastSyncTime(lastSyncTime))); }); - this._register(this.channel.listen('onDidChangeConflicts')(conflicts => this.updateConflicts(conflicts))); + this._register(this.channel.listen('onDidChangeConflicts')(conflicts => this.updateConflicts(conflicts))); this._register(this.channel.listen<[SyncResource, Error][]>('onSyncErrors')(errors => this._onSyncErrors.fire(errors.map(([source, error]) => ([source, UserDataSyncError.toUserDataSyncError(error)]))))); } @@ -73,8 +73,8 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ return this.channel.call('sync'); } - accept(source: SyncResource, content: string): Promise { - return this.channel.call('accept', [source, content]); + acceptConflict(conflict: URI, content: string): Promise { + return this.channel.call('acceptConflict', [conflict, content]); } reset(): Promise { @@ -102,8 +102,14 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ this._onDidChangeStatus.fire(status); } - private async updateConflicts(conflicts: SyncResource[]): Promise { - this._conflictsSources = conflicts; + private async updateConflicts(conflicts: SyncResourceConflicts[]): Promise { + // Revive URIs + this._conflicts = conflicts.map(c => + ({ + syncResource: c.syncResource, + conflicts: c.conflicts.map(({ local, remote }) => + ({ local: URI.revive(local), remote: URI.revive(remote) })) + })); this._onDidChangeConflicts.fire(conflicts); }