diff --git a/src/vs/platform/userDataSync/common/tasksMerge.ts b/src/vs/platform/userDataSync/common/tasksMerge.ts new file mode 100644 index 00000000000..fd492e1a0e8 --- /dev/null +++ b/src/vs/platform/userDataSync/common/tasksMerge.ts @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export interface IMergeResult { + localContent: string | null; + remoteContent: string | null; + hasConflicts: boolean; +} + +export function merge(originalLocalContent: string, originalRemoteContent: string, baseContent: string | null): IMergeResult { + + const localForwarded = baseContent !== originalLocalContent; + const remoteForwarded = baseContent !== originalRemoteContent; + + /* no changes */ + if (!localForwarded && !remoteForwarded) { + return { localContent: null, remoteContent: null, hasConflicts: false }; + } + + /* local has changed and remote has not */ + if (localForwarded && !remoteForwarded) { + return { localContent: null, remoteContent: originalLocalContent, hasConflicts: false }; + } + + /* remote has changed and local has not */ + if (remoteForwarded && !localForwarded) { + return { localContent: originalRemoteContent, remoteContent: null, hasConflicts: false }; + } + + return { localContent: originalLocalContent, remoteContent: originalRemoteContent, hasConflicts: true }; +} + diff --git a/src/vs/platform/userDataSync/common/tasksSync.ts b/src/vs/platform/userDataSync/common/tasksSync.ts new file mode 100644 index 00000000000..2b0db639516 --- /dev/null +++ b/src/vs/platform/userDataSync/common/tasksSync.ts @@ -0,0 +1,332 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { VSBuffer } from 'vs/base/common/buffer'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { URI } from 'vs/base/common/uri'; +import { localize } from 'vs/nls'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IFileService } from 'vs/platform/files/common/files'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; +import { AbstractInitializer, AbstractJsonFileSynchroniser, IAcceptResult, IFileResourcePreview, IMergeResult } from 'vs/platform/userDataSync/common/abstractSynchronizer'; +import { Change, IRemoteUserData, ISyncResourceHandle, IUserDataSyncBackupStoreService, IUserDataSyncConfiguration, IUserDataSynchroniser, IUserDataSyncLogService, IUserDataSyncEnablementService, IUserDataSyncStoreService, IUserDataSyncUtilService, SyncResource, UserDataSyncError, UserDataSyncErrorCode, USER_DATA_SYNC_SCHEME } from 'vs/platform/userDataSync/common/userDataSync'; + +interface ITasksSyncContent { + tasks: string; +} + +interface ITasksResourcePreview extends IFileResourcePreview { + previewResult: IMergeResult; +} + +export function getTasksContentFromSyncContent(syncContent: string, logService: ILogService): string | null { + try { + const parsed = JSON.parse(syncContent); + return parsed.tasks; + } catch (e) { + logService.error(e); + return null; + } +} + +export class TasksSynchroniser extends AbstractJsonFileSynchroniser implements IUserDataSynchroniser { + + protected readonly version: number = 1; + private readonly previewResource: URI = this.extUri.joinPath(this.syncPreviewFolder, 'tasks.json'); + private readonly localResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' }); + private readonly remoteResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' }); + private readonly acceptedResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' }); + + constructor( + @IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService, + @IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService, + @IUserDataSyncLogService logService: IUserDataSyncLogService, + @IConfigurationService configurationService: IConfigurationService, + @IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService, + @IFileService fileService: IFileService, + @IEnvironmentService environmentService: IEnvironmentService, + @IStorageService storageService: IStorageService, + @IUserDataSyncUtilService userDataSyncUtilService: IUserDataSyncUtilService, + @ITelemetryService telemetryService: ITelemetryService, + @IUriIdentityService uriIdentityService: IUriIdentityService, + ) { + super(uriIdentityService.extUri.joinPath(uriIdentityService.extUri.dirname(environmentService.settingsResource), 'tasks.json'), SyncResource.Tasks, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, userDataSyncUtilService, configurationService, uriIdentityService); + } + + protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, isRemoteDataFromCurrentMachine: boolean, userDataSyncConfiguration: IUserDataSyncConfiguration): Promise { + const remoteContent = remoteUserData.syncData ? getTasksContentFromSyncContent(remoteUserData.syncData.content, this.logService) : null; + + // Use remote data as last sync data if last sync data does not exist and remote data is from same machine + lastSyncUserData = lastSyncUserData === null && isRemoteDataFromCurrentMachine ? remoteUserData : lastSyncUserData; + const lastSyncContent: string | null = lastSyncUserData?.syncData ? getTasksContentFromSyncContent(lastSyncUserData.syncData.content, this.logService) : null; + + // Get file content last to get the latest + const fileContent = await this.getLocalFileContent(); + + let content: string | null = null; + let hasLocalChanged: boolean = false; + let hasRemoteChanged: boolean = false; + let hasConflicts: boolean = false; + + if (remoteContent) { + const localContent = fileContent ? fileContent.value.toString() : null; + if (localContent !== null && this.hasErrors(localContent)) { + throw new UserDataSyncError(localize('errorInvalidTasks', "Unable to sync tasks because the content in the file is not valid. Please open the file and correct it."), UserDataSyncErrorCode.LocalInvalidContent, this.resource); + } + + if (!lastSyncContent // First time sync + || lastSyncContent !== localContent // Local has forwarded + || lastSyncContent !== remoteContent // Remote has forwarded + ) { + this.logService.trace(`${this.syncResourceLogLabel}: Merging remote tasks with local tasks...`); + const result = merge(localContent, remoteContent, lastSyncContent); + content = result.content; + hasConflicts = result.hasConflicts; + hasLocalChanged = result.hasLocalChanged; + hasRemoteChanged = result.hasRemoteChanged; + } + } + + // First time syncing to remote + else if (fileContent) { + this.logService.trace(`${this.syncResourceLogLabel}: Remote tasks does not exist. Synchronizing tasks for the first time.`); + content = fileContent.value.toString(); + hasRemoteChanged = true; + } + + const previewResult: IMergeResult = { + content, + localChange: hasLocalChanged ? fileContent ? Change.Modified : Change.Added : Change.None, + remoteChange: hasRemoteChanged ? Change.Modified : Change.None, + hasConflicts + }; + + return [{ + fileContent, + localResource: this.localResource, + localContent: fileContent ? fileContent.value.toString() : null, + localChange: previewResult.localChange, + + remoteResource: this.remoteResource, + remoteContent, + remoteChange: previewResult.remoteChange, + + previewResource: this.previewResource, + previewResult, + acceptedResource: this.acceptedResource, + }]; + + } + + protected async hasRemoteChanged(lastSyncUserData: IRemoteUserData): Promise { + const lastSyncContent: string | null = lastSyncUserData?.syncData ? getTasksContentFromSyncContent(lastSyncUserData.syncData.content, this.logService) : null; + if (lastSyncContent === null) { + return true; + } + + const fileContent = await this.getLocalFileContent(); + const localContent = fileContent ? fileContent.value.toString() : null; + const result = merge(localContent, lastSyncContent, lastSyncContent); + return result.hasLocalChanged || result.hasRemoteChanged; + } + + protected async getMergeResult(resourcePreview: ITasksResourcePreview, token: CancellationToken): Promise { + return resourcePreview.previewResult; + } + + protected async getAcceptResult(resourcePreview: ITasksResourcePreview, resource: URI, content: string | null | undefined, token: CancellationToken): Promise { + + /* Accept local resource */ + if (this.extUri.isEqual(resource, this.localResource)) { + return { + content: resourcePreview.fileContent ? resourcePreview.fileContent.value.toString() : null, + localChange: Change.None, + remoteChange: Change.Modified, + }; + } + + /* Accept remote resource */ + if (this.extUri.isEqual(resource, this.remoteResource)) { + return { + content: resourcePreview.remoteContent, + localChange: Change.Modified, + remoteChange: Change.None, + }; + } + + /* Accept preview resource */ + if (this.extUri.isEqual(resource, this.previewResource)) { + if (content === undefined) { + return { + content: resourcePreview.previewResult.content, + localChange: resourcePreview.previewResult.localChange, + remoteChange: resourcePreview.previewResult.remoteChange, + }; + } else { + return { + content, + localChange: Change.Modified, + remoteChange: Change.Modified, + }; + } + } + + throw new Error(`Invalid Resource: ${resource.toString()}`); + } + + protected async applyResult(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: [ITasksResourcePreview, IAcceptResult][], force: boolean): Promise { + const { fileContent } = resourcePreviews[0][0]; + let { content, localChange, remoteChange } = resourcePreviews[0][1]; + + if (localChange === Change.None && remoteChange === Change.None) { + this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing tasks.`); + } + + if (content !== null) { + content = content.trim(); + content = content || '{}'; + if (this.hasErrors(content)) { + throw new UserDataSyncError(localize('errorInvalidTasks', "Unable to sync tasks because the content in the file is not valid. Please open the file and correct it."), UserDataSyncErrorCode.LocalInvalidContent, this.resource); + } + } + + if (localChange !== Change.None) { + this.logService.trace(`${this.syncResourceLogLabel}: Updating local tasks...`); + if (fileContent) { + await this.backupLocal(JSON.stringify(this.toTasksSyncContent(fileContent.value.toString()))); + } + await this.updateLocalFileContent(content || '{}', fileContent, force); + this.logService.info(`${this.syncResourceLogLabel}: Updated local tasks`); + } + + if (remoteChange !== Change.None) { + this.logService.trace(`${this.syncResourceLogLabel}: Updating remote tasks...`); + const remoteContents = JSON.stringify(this.toTasksSyncContent(content || '{}')); + remoteUserData = await this.updateRemoteUserData(remoteContents, force ? null : remoteUserData.ref); + this.logService.info(`${this.syncResourceLogLabel}: Updated remote tasks`); + } + + // Delete the preview + try { + await this.fileService.del(this.previewResource); + } catch (e) { /* ignore */ } + + if (lastSyncUserData?.ref !== remoteUserData.ref) { + this.logService.trace(`${this.syncResourceLogLabel}: Updating last synchronized tasks...`); + await this.updateLastSyncUserData(remoteUserData); + this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized tasks`); + } + + } + + async hasLocalData(): Promise { + return this.fileService.exists(this.file); + } + + async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource: URI }[]> { + const comparableResource = (await this.fileService.exists(this.file)) ? this.file : this.localResource; + return [{ resource: this.extUri.joinPath(uri, 'tasks.json'), comparableResource }]; + } + + override async resolveContent(uri: URI): Promise { + if (this.extUri.isEqual(this.remoteResource, uri) || this.extUri.isEqual(this.localResource, uri) || this.extUri.isEqual(this.acceptedResource, uri)) { + return this.resolvePreviewContent(uri); + } + let content = await super.resolveContent(uri); + if (content) { + return content; + } + content = await super.resolveContent(this.extUri.dirname(uri)); + if (content) { + const syncData = this.parseSyncData(content); + if (syncData) { + switch (this.extUri.basename(uri)) { + case 'tasks.json': + return getTasksContentFromSyncContent(syncData.content, this.logService); + } + } + } + return null; + } + + private toTasksSyncContent(tasks: string): ITasksSyncContent { + return { tasks }; + } + +} + +export class TasksInitializer extends AbstractInitializer { + + private tasksResource = this.uriIdentityService.extUri.joinPath(this.uriIdentityService.extUri.dirname(this.environmentService.settingsResource), 'tasks.json'); + + constructor( + @IFileService fileService: IFileService, + @IEnvironmentService environmentService: IEnvironmentService, + @IUserDataSyncLogService logService: IUserDataSyncLogService, + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, + ) { + super(SyncResource.Tasks, environmentService, logService, fileService, uriIdentityService); + } + + async doInitialize(remoteUserData: IRemoteUserData): Promise { + const tasksContent = remoteUserData.syncData ? getTasksContentFromSyncContent(remoteUserData.syncData.content, this.logService) : null; + if (!tasksContent) { + this.logService.info('Skipping initializing tasks because remote tasks does not exist.'); + return; + } + + const isEmpty = await this.isEmpty(); + if (!isEmpty) { + this.logService.info('Skipping initializing tasks because local tasks exist.'); + return; + } + + await this.fileService.writeFile(this.tasksResource, VSBuffer.fromString(tasksContent)); + + await this.updateLastSyncUserData(remoteUserData); + } + + private async isEmpty(): Promise { + return this.fileService.exists(this.tasksResource); + } + +} + +function merge(originalLocalContent: string | null, originalRemoteContent: string | null, baseContent: string | null): { + content: string | null; + hasLocalChanged: boolean; + hasRemoteChanged: boolean; + hasConflicts: boolean; +} { + + /* no changes */ + if (originalLocalContent === null && originalRemoteContent === null && baseContent === null) { + return { content: null, hasLocalChanged: false, hasRemoteChanged: false, hasConflicts: false }; + } + + const localForwarded = baseContent !== originalLocalContent; + const remoteForwarded = baseContent !== originalRemoteContent; + + /* no changes */ + if (!localForwarded && !remoteForwarded) { + return { content: null, hasLocalChanged: false, hasRemoteChanged: false, hasConflicts: false }; + } + + /* local has changed and remote has not */ + if (localForwarded && !remoteForwarded) { + return { content: originalLocalContent, hasRemoteChanged: true, hasLocalChanged: false, hasConflicts: false }; + } + + /* remote has changed and local has not */ + if (remoteForwarded && !localForwarded) { + return { content: originalRemoteContent, hasLocalChanged: true, hasRemoteChanged: false, hasConflicts: false }; + } + + return { content: originalLocalContent, hasLocalChanged: true, hasRemoteChanged: true, hasConflicts: true }; +} diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index 9db405d5e18..47593641517 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -135,10 +135,11 @@ export const enum SyncResource { Settings = 'settings', Keybindings = 'keybindings', Snippets = 'snippets', + Tasks = 'tasks', Extensions = 'extensions', - GlobalState = 'globalState' + GlobalState = 'globalState', } -export const ALL_SYNC_RESOURCES: SyncResource[] = [SyncResource.Settings, SyncResource.Keybindings, SyncResource.Snippets, SyncResource.Extensions, SyncResource.GlobalState]; +export const ALL_SYNC_RESOURCES: SyncResource[] = [SyncResource.Settings, SyncResource.Keybindings, SyncResource.Snippets, SyncResource.Tasks, SyncResource.Extensions, SyncResource.GlobalState]; export function getLastSyncResourceUri(syncResource: SyncResource, environmentService: IEnvironmentService, extUri: IExtUri): URI { return extUri.joinPath(environmentService.userDataSyncHome, syncResource, `lastSync${syncResource}.json`); diff --git a/src/vs/platform/userDataSync/common/userDataSyncService.ts b/src/vs/platform/userDataSync/common/userDataSyncService.ts index fd3b6f17b7a..700fc1f901f 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncService.ts @@ -24,6 +24,7 @@ import { GlobalStateSynchroniser } from 'vs/platform/userDataSync/common/globalS import { KeybindingsSynchroniser } from 'vs/platform/userDataSync/common/keybindingsSync'; import { SettingsSynchroniser } from 'vs/platform/userDataSync/common/settingsSync'; import { SnippetsSynchroniser } from 'vs/platform/userDataSync/common/snippetsSync'; +import { TasksSynchroniser } from 'vs/platform/userDataSync/common/tasksSync'; import { ALL_SYNC_RESOURCES, Change, createSyncHeaders, IManualSyncTask, IResourcePreview, ISyncResourceHandle, ISyncResourcePreview, ISyncTask, IUserDataManifest, IUserDataSyncConfiguration, IUserDataSyncEnablementService, IUserDataSynchroniser, IUserDataSyncLogService, IUserDataSyncService, IUserDataSyncStoreManagementService, IUserDataSyncStoreService, MergeState, SyncResource, SyncStatus, UserDataSyncError, UserDataSyncErrorCode, UserDataSyncStoreError, USER_DATA_SYNC_CONFIGURATION_SCOPE } from 'vs/platform/userDataSync/common/userDataSync'; type SyncErrorClassification = { @@ -877,6 +878,7 @@ class Synchronizers extends Disposable { case SyncResource.Settings: return this.instantiationService.createInstance(SettingsSynchroniser); case SyncResource.Keybindings: return this.instantiationService.createInstance(KeybindingsSynchroniser); case SyncResource.Snippets: return this.instantiationService.createInstance(SnippetsSynchroniser); + case SyncResource.Tasks: return this.instantiationService.createInstance(TasksSynchroniser); case SyncResource.GlobalState: return this.instantiationService.createInstance(GlobalStateSynchroniser); case SyncResource.Extensions: return this.instantiationService.createInstance(ExtensionsSynchroniser); } @@ -887,8 +889,9 @@ class Synchronizers extends Disposable { case SyncResource.Settings: return 0; case SyncResource.Keybindings: return 1; case SyncResource.Snippets: return 2; - case SyncResource.GlobalState: return 3; - case SyncResource.Extensions: return 4; + case SyncResource.Tasks: return 3; + case SyncResource.GlobalState: return 4; + case SyncResource.Extensions: return 5; } } diff --git a/src/vs/platform/userDataSync/test/common/tasksSync.test.ts b/src/vs/platform/userDataSync/test/common/tasksSync.test.ts new file mode 100644 index 00000000000..ad3fd61bb3e --- /dev/null +++ b/src/vs/platform/userDataSync/test/common/tasksSync.test.ts @@ -0,0 +1,445 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IFileService } from 'vs/platform/files/common/files'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; +import { getTasksContentFromSyncContent, TasksSynchroniser } from 'vs/platform/userDataSync/common/tasksSync'; +import { Change, IUserDataSyncStoreService, MergeState, SyncResource, SyncStatus } from 'vs/platform/userDataSync/common/userDataSync'; +import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userDataSync/test/common/userDataSyncClient'; + +suite('TasksSync', () => { + + const disposableStore = new DisposableStore(); + const server = new UserDataSyncTestServer(); + let client: UserDataSyncClient; + + let testObject: TasksSynchroniser; + + setup(async () => { + client = disposableStore.add(new UserDataSyncClient(server)); + await client.setUp(true); + testObject = client.getSynchronizer(SyncResource.Tasks) as TasksSynchroniser; + disposableStore.add(toDisposable(() => client.instantiationService.get(IUserDataSyncStoreService).clear())); + }); + + teardown(() => disposableStore.clear()); + + test('when tasks file does not exist', async () => { + const fileService = client.instantiationService.get(IFileService); + const uriIdentityService = client.instantiationService.get(IUriIdentityService); + const tasksResource = uriIdentityService.extUri.joinPath(uriIdentityService.extUri.dirname(client.instantiationService.get(IEnvironmentService).settingsResource), 'tasks.json'); + + assert.deepStrictEqual(await testObject.getLastSyncUserData(), null); + let manifest = await client.manifest(); + server.reset(); + await testObject.sync(manifest); + + assert.deepStrictEqual(server.requests, [ + { type: 'GET', url: `${server.url}/v1/resource/${testObject.resource}/latest`, headers: {} }, + ]); + assert.ok(!await fileService.exists(tasksResource)); + + const lastSyncUserData = await testObject.getLastSyncUserData(); + const remoteUserData = await testObject.getRemoteUserData(null); + assert.deepStrictEqual(lastSyncUserData!.ref, remoteUserData.ref); + assert.deepStrictEqual(lastSyncUserData!.syncData, remoteUserData.syncData); + assert.strictEqual(lastSyncUserData!.syncData, null); + + manifest = await client.manifest(); + server.reset(); + await testObject.sync(manifest); + assert.deepStrictEqual(server.requests, []); + + manifest = await client.manifest(); + server.reset(); + await testObject.sync(manifest); + assert.deepStrictEqual(server.requests, []); + }); + + test('when tasks file does not exist and remote has changes', async () => { + const client2 = disposableStore.add(new UserDataSyncClient(server)); + await client2.setUp(true); + const content = JSON.stringify({ + 'version': '2.0.0', + 'tasks': [{ + 'type': 'npm', + 'script': 'watch', + 'label': 'Watch' + }] + }); + const uriIdentityService2 = client2.instantiationService.get(IUriIdentityService); + const tasksResource2 = uriIdentityService2.extUri.joinPath(uriIdentityService2.extUri.dirname(client2.instantiationService.get(IEnvironmentService).settingsResource), 'tasks.json'); + await client2.instantiationService.get(IFileService).writeFile(tasksResource2, VSBuffer.fromString(content)); + await client2.sync(); + + const fileService = client.instantiationService.get(IFileService); + const uriIdentityService = client.instantiationService.get(IUriIdentityService); + const tasksResource = uriIdentityService.extUri.joinPath(uriIdentityService.extUri.dirname(client.instantiationService.get(IEnvironmentService).settingsResource), 'tasks.json'); + + await testObject.sync(await client.manifest()); + + assert.deepStrictEqual(testObject.status, SyncStatus.Idle); + const lastSyncUserData = await testObject.getLastSyncUserData(); + const remoteUserData = await testObject.getRemoteUserData(null); + assert.strictEqual(getTasksContentFromSyncContent(lastSyncUserData!.syncData!.content!, client.instantiationService.get(ILogService)), content); + assert.strictEqual(getTasksContentFromSyncContent(remoteUserData!.syncData!.content!, client.instantiationService.get(ILogService)), content); + assert.strictEqual((await fileService.readFile(tasksResource)).value.toString(), content); + }); + + test('when tasks file exists locally and remote has no tasks', async () => { + const fileService = client.instantiationService.get(IFileService); + const uriIdentityService = client.instantiationService.get(IUriIdentityService); + const tasksResource = uriIdentityService.extUri.joinPath(uriIdentityService.extUri.dirname(client.instantiationService.get(IEnvironmentService).settingsResource), 'tasks.json'); + const content = JSON.stringify({ + 'version': '2.0.0', + 'tasks': [{ + 'type': 'npm', + 'script': 'watch', + 'label': 'Watch' + }] + }); + fileService.writeFile(tasksResource, VSBuffer.fromString(content)); + + await testObject.sync(await client.manifest()); + + assert.deepStrictEqual(testObject.status, SyncStatus.Idle); + const lastSyncUserData = await testObject.getLastSyncUserData(); + const remoteUserData = await testObject.getRemoteUserData(null); + assert.strictEqual(getTasksContentFromSyncContent(lastSyncUserData!.syncData!.content!, client.instantiationService.get(ILogService)), content); + assert.strictEqual(getTasksContentFromSyncContent(remoteUserData!.syncData!.content!, client.instantiationService.get(ILogService)), content); + }); + + test('when tasks file locally has moved forward', async () => { + const fileService = client.instantiationService.get(IFileService); + const uriIdentityService = client.instantiationService.get(IUriIdentityService); + const tasksResource = uriIdentityService.extUri.joinPath(uriIdentityService.extUri.dirname(client.instantiationService.get(IEnvironmentService).settingsResource), 'tasks.json'); + fileService.writeFile(tasksResource, VSBuffer.fromString(JSON.stringify({ + 'version': '2.0.0', + 'tasks': [] + }))); + + await testObject.sync(await client.manifest()); + + const content = JSON.stringify({ + 'version': '2.0.0', + 'tasks': [{ + 'type': 'npm', + 'script': 'watch', + 'label': 'Watch' + }] + }); + fileService.writeFile(tasksResource, VSBuffer.fromString(content)); + + await testObject.sync(await client.manifest()); + + assert.deepStrictEqual(testObject.status, SyncStatus.Idle); + const lastSyncUserData = await testObject.getLastSyncUserData(); + const remoteUserData = await testObject.getRemoteUserData(null); + assert.strictEqual(getTasksContentFromSyncContent(lastSyncUserData!.syncData!.content!, client.instantiationService.get(ILogService)), content); + assert.strictEqual(getTasksContentFromSyncContent(remoteUserData!.syncData!.content!, client.instantiationService.get(ILogService)), content); + }); + + test('when tasks file remotely has moved forward', async () => { + const client2 = disposableStore.add(new UserDataSyncClient(server)); + await client2.setUp(true); + const uriIdentityService2 = client2.instantiationService.get(IUriIdentityService); + const tasksResource2 = uriIdentityService2.extUri.joinPath(uriIdentityService2.extUri.dirname(client2.instantiationService.get(IEnvironmentService).settingsResource), 'tasks.json'); + const fileService2 = client2.instantiationService.get(IFileService); + await fileService2.writeFile(tasksResource2, VSBuffer.fromString(JSON.stringify({ + 'version': '2.0.0', + 'tasks': [] + }))); + + const fileService = client.instantiationService.get(IFileService); + const uriIdentityService = client.instantiationService.get(IUriIdentityService); + const tasksResource = uriIdentityService.extUri.joinPath(uriIdentityService.extUri.dirname(client.instantiationService.get(IEnvironmentService).settingsResource), 'tasks.json'); + + await client2.sync(); + await testObject.sync(await client.manifest()); + + const content = JSON.stringify({ + 'version': '2.0.0', + 'tasks': [{ + 'type': 'npm', + 'script': 'watch', + 'label': 'Watch' + }] + }); + fileService2.writeFile(tasksResource2, VSBuffer.fromString(content)); + + await client2.sync(); + await testObject.sync(await client.manifest()); + + assert.deepStrictEqual(testObject.status, SyncStatus.Idle); + const lastSyncUserData = await testObject.getLastSyncUserData(); + const remoteUserData = await testObject.getRemoteUserData(null); + assert.strictEqual(getTasksContentFromSyncContent(lastSyncUserData!.syncData!.content!, client.instantiationService.get(ILogService)), content); + assert.strictEqual(getTasksContentFromSyncContent(remoteUserData!.syncData!.content!, client.instantiationService.get(ILogService)), content); + assert.strictEqual((await fileService.readFile(tasksResource)).value.toString(), content); + }); + + test('when tasks file has moved forward locally and remotely - accept preview', async () => { + const client2 = disposableStore.add(new UserDataSyncClient(server)); + await client2.setUp(true); + const uriIdentityService2 = client2.instantiationService.get(IUriIdentityService); + const tasksResource2 = uriIdentityService2.extUri.joinPath(uriIdentityService2.extUri.dirname(client2.instantiationService.get(IEnvironmentService).settingsResource), 'tasks.json'); + const fileService2 = client2.instantiationService.get(IFileService); + await fileService2.writeFile(tasksResource2, VSBuffer.fromString(JSON.stringify({ + 'version': '2.0.0', + 'tasks': [] + }))); + + const fileService = client.instantiationService.get(IFileService); + const uriIdentityService = client.instantiationService.get(IUriIdentityService); + const tasksResource = uriIdentityService.extUri.joinPath(uriIdentityService.extUri.dirname(client.instantiationService.get(IEnvironmentService).settingsResource), 'tasks.json'); + + await client2.sync(); + await testObject.sync(await client.manifest()); + + fileService2.writeFile(tasksResource2, VSBuffer.fromString(JSON.stringify({ + 'version': '2.0.0', + 'tasks': [{ + 'type': 'npm', + 'script': 'watch', + }] + }))); + await client2.sync(); + + const content = JSON.stringify({ + 'version': '2.0.0', + 'tasks': [{ + 'type': 'npm', + 'script': 'watch', + 'label': 'Watch' + }] + }); + fileService.writeFile(tasksResource, VSBuffer.fromString(content)); + await testObject.sync(await client.manifest()); + + assert.deepStrictEqual(testObject.status, SyncStatus.HasConflicts); + assert.deepStrictEqual(testObject.conflicts.length, 1); + assert.deepStrictEqual(testObject.conflicts[0].mergeState, MergeState.Conflict); + assert.deepStrictEqual(testObject.conflicts[0].localChange, Change.Modified); + assert.deepStrictEqual(testObject.conflicts[0].remoteChange, Change.Modified); + assert.deepStrictEqual((await fileService.readFile(testObject.conflicts[0].previewResource)).value.toString(), content); + + await testObject.accept(testObject.conflicts[0].previewResource); + await testObject.apply(false); + assert.deepStrictEqual(testObject.status, SyncStatus.Idle); + const lastSyncUserData = await testObject.getLastSyncUserData(); + const remoteUserData = await testObject.getRemoteUserData(null); + assert.strictEqual(getTasksContentFromSyncContent(lastSyncUserData!.syncData!.content!, client.instantiationService.get(ILogService)), content); + assert.strictEqual(getTasksContentFromSyncContent(remoteUserData!.syncData!.content!, client.instantiationService.get(ILogService)), content); + assert.strictEqual((await fileService.readFile(tasksResource)).value.toString(), content); + }); + + test('when tasks file has moved forward locally and remotely - accept modified preview', async () => { + const client2 = disposableStore.add(new UserDataSyncClient(server)); + await client2.setUp(true); + const uriIdentityService2 = client2.instantiationService.get(IUriIdentityService); + const tasksResource2 = uriIdentityService2.extUri.joinPath(uriIdentityService2.extUri.dirname(client2.instantiationService.get(IEnvironmentService).settingsResource), 'tasks.json'); + const fileService2 = client2.instantiationService.get(IFileService); + await fileService2.writeFile(tasksResource2, VSBuffer.fromString(JSON.stringify({ + 'version': '2.0.0', + 'tasks': [] + }))); + + const fileService = client.instantiationService.get(IFileService); + const uriIdentityService = client.instantiationService.get(IUriIdentityService); + const tasksResource = uriIdentityService.extUri.joinPath(uriIdentityService.extUri.dirname(client.instantiationService.get(IEnvironmentService).settingsResource), 'tasks.json'); + + await client2.sync(); + await testObject.sync(await client.manifest()); + + fileService2.writeFile(tasksResource2, VSBuffer.fromString(JSON.stringify({ + 'version': '2.0.0', + 'tasks': [{ + 'type': 'npm', + 'script': 'watch', + }] + }))); + await client2.sync(); + + fileService.writeFile(tasksResource, VSBuffer.fromString(JSON.stringify({ + 'version': '2.0.0', + 'tasks': [{ + 'type': 'npm', + 'script': 'watch', + 'label': 'Watch' + }] + }))); + await testObject.sync(await client.manifest()); + + const content = JSON.stringify({ + 'version': '2.0.0', + 'tasks': [{ + 'type': 'npm', + 'script': 'watch', + 'label': 'Watch 2' + }] + }); + await testObject.accept(testObject.conflicts[0].previewResource, content); + await testObject.apply(false); + assert.deepStrictEqual(testObject.status, SyncStatus.Idle); + const lastSyncUserData = await testObject.getLastSyncUserData(); + const remoteUserData = await testObject.getRemoteUserData(null); + assert.strictEqual(getTasksContentFromSyncContent(lastSyncUserData!.syncData!.content!, client.instantiationService.get(ILogService)), content); + assert.strictEqual(getTasksContentFromSyncContent(remoteUserData!.syncData!.content!, client.instantiationService.get(ILogService)), content); + assert.strictEqual((await fileService.readFile(tasksResource)).value.toString(), content); + }); + + test('when tasks file has moved forward locally and remotely - accept remote', async () => { + const client2 = disposableStore.add(new UserDataSyncClient(server)); + await client2.setUp(true); + const uriIdentityService2 = client2.instantiationService.get(IUriIdentityService); + const tasksResource2 = uriIdentityService2.extUri.joinPath(uriIdentityService2.extUri.dirname(client2.instantiationService.get(IEnvironmentService).settingsResource), 'tasks.json'); + const fileService2 = client2.instantiationService.get(IFileService); + await fileService2.writeFile(tasksResource2, VSBuffer.fromString(JSON.stringify({ + 'version': '2.0.0', + 'tasks': [] + }))); + + const fileService = client.instantiationService.get(IFileService); + const uriIdentityService = client.instantiationService.get(IUriIdentityService); + const tasksResource = uriIdentityService.extUri.joinPath(uriIdentityService.extUri.dirname(client.instantiationService.get(IEnvironmentService).settingsResource), 'tasks.json'); + + await client2.sync(); + await testObject.sync(await client.manifest()); + + const content = JSON.stringify({ + 'version': '2.0.0', + 'tasks': [{ + 'type': 'npm', + 'script': 'watch', + }] + }); + fileService2.writeFile(tasksResource2, VSBuffer.fromString(content)); + await client2.sync(); + + fileService.writeFile(tasksResource, VSBuffer.fromString(JSON.stringify({ + 'version': '2.0.0', + 'tasks': [{ + 'type': 'npm', + 'script': 'watch', + 'label': 'Watch' + }] + }))); + await testObject.sync(await client.manifest()); + assert.deepStrictEqual(testObject.status, SyncStatus.HasConflicts); + + await testObject.accept(testObject.conflicts[0].remoteResource); + await testObject.apply(false); + assert.deepStrictEqual(testObject.status, SyncStatus.Idle); + const lastSyncUserData = await testObject.getLastSyncUserData(); + const remoteUserData = await testObject.getRemoteUserData(null); + assert.strictEqual(getTasksContentFromSyncContent(lastSyncUserData!.syncData!.content!, client.instantiationService.get(ILogService)), content); + assert.strictEqual(getTasksContentFromSyncContent(remoteUserData!.syncData!.content!, client.instantiationService.get(ILogService)), content); + assert.strictEqual((await fileService.readFile(tasksResource)).value.toString(), content); + }); + + test('when tasks file has moved forward locally and remotely - accept local', async () => { + const client2 = disposableStore.add(new UserDataSyncClient(server)); + await client2.setUp(true); + const uriIdentityService2 = client2.instantiationService.get(IUriIdentityService); + const tasksResource2 = uriIdentityService2.extUri.joinPath(uriIdentityService2.extUri.dirname(client2.instantiationService.get(IEnvironmentService).settingsResource), 'tasks.json'); + const fileService2 = client2.instantiationService.get(IFileService); + await fileService2.writeFile(tasksResource2, VSBuffer.fromString(JSON.stringify({ + 'version': '2.0.0', + 'tasks': [] + }))); + + const fileService = client.instantiationService.get(IFileService); + const uriIdentityService = client.instantiationService.get(IUriIdentityService); + const tasksResource = uriIdentityService.extUri.joinPath(uriIdentityService.extUri.dirname(client.instantiationService.get(IEnvironmentService).settingsResource), 'tasks.json'); + + await client2.sync(); + await testObject.sync(await client.manifest()); + + fileService2.writeFile(tasksResource2, VSBuffer.fromString(JSON.stringify({ + 'version': '2.0.0', + 'tasks': [{ + 'type': 'npm', + 'script': 'watch', + }] + }))); + await client2.sync(); + + const content = JSON.stringify({ + 'version': '2.0.0', + 'tasks': [{ + 'type': 'npm', + 'script': 'watch', + 'label': 'Watch' + }] + }); + fileService.writeFile(tasksResource, VSBuffer.fromString(content)); + await testObject.sync(await client.manifest()); + assert.deepStrictEqual(testObject.status, SyncStatus.HasConflicts); + + await testObject.accept(testObject.conflicts[0].localResource); + await testObject.apply(false); + assert.deepStrictEqual(testObject.status, SyncStatus.Idle); + const lastSyncUserData = await testObject.getLastSyncUserData(); + const remoteUserData = await testObject.getRemoteUserData(null); + assert.strictEqual(getTasksContentFromSyncContent(lastSyncUserData!.syncData!.content!, client.instantiationService.get(ILogService)), content); + assert.strictEqual(getTasksContentFromSyncContent(remoteUserData!.syncData!.content!, client.instantiationService.get(ILogService)), content); + assert.strictEqual((await fileService.readFile(tasksResource)).value.toString(), content); + }); + + test('when tasks file is created after first sync', async () => { + const fileService = client.instantiationService.get(IFileService); + const uriIdentityService = client.instantiationService.get(IUriIdentityService); + const tasksResource = uriIdentityService.extUri.joinPath(uriIdentityService.extUri.dirname(client.instantiationService.get(IEnvironmentService).settingsResource), 'tasks.json'); + await testObject.sync(await client.manifest()); + + const content = JSON.stringify({ + 'version': '2.0.0', + 'tasks': [{ + 'type': 'npm', + 'script': 'watch', + 'label': 'Watch' + }] + }); + await fileService.createFile(tasksResource, VSBuffer.fromString(content)); + + let lastSyncUserData = await testObject.getLastSyncUserData(); + const manifest = await client.manifest(); + server.reset(); + await testObject.sync(manifest); + + assert.deepStrictEqual(server.requests, [ + { type: 'POST', url: `${server.url}/v1/resource/${testObject.resource}`, headers: { 'If-Match': lastSyncUserData?.ref } }, + ]); + + lastSyncUserData = await testObject.getLastSyncUserData(); + const remoteUserData = await testObject.getRemoteUserData(null); + assert.deepStrictEqual(lastSyncUserData!.ref, remoteUserData.ref); + assert.deepStrictEqual(lastSyncUserData!.syncData, remoteUserData.syncData); + assert.strictEqual(getTasksContentFromSyncContent(lastSyncUserData!.syncData!.content!, client.instantiationService.get(ILogService)), content); + }); + + test('apply remote when tasks file does not exist', async () => { + const fileService = client.instantiationService.get(IFileService); + const uriIdentityService = client.instantiationService.get(IUriIdentityService); + const tasksResource = uriIdentityService.extUri.joinPath(uriIdentityService.extUri.dirname(client.instantiationService.get(IEnvironmentService).settingsResource), 'tasks.json'); + if (await fileService.exists(tasksResource)) { + await fileService.del(tasksResource); + } + + const preview = (await testObject.preview(await client.manifest(), {}))!; + + server.reset(); + const content = await testObject.resolveContent(preview.resourcePreviews[0].remoteResource); + await testObject.accept(preview.resourcePreviews[0].remoteResource, content); + await testObject.apply(false); + assert.deepStrictEqual(server.requests, []); + }); + +}); diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index 6dd1c5f1161..721d4dcb8c7 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -76,6 +76,7 @@ const configureSyncCommand = { id: CONFIGURE_SYNC_COMMAND_ID, title: localize('c const resolveSettingsConflictsCommand = { id: 'workbench.userDataSync.actions.resolveSettingsConflicts', title: localize('showConflicts', "{0}: Show Settings Conflicts", SYNC_TITLE) }; const resolveKeybindingsConflictsCommand = { id: 'workbench.userDataSync.actions.resolveKeybindingsConflicts', title: localize('showKeybindingsConflicts', "{0}: Show Keybindings Conflicts", SYNC_TITLE) }; const resolveSnippetsConflictsCommand = { id: 'workbench.userDataSync.actions.resolveSnippetsConflicts', title: localize('showSnippetsConflicts', "{0}: Show User Snippets Conflicts", SYNC_TITLE) }; +const resolveTasksConflictsCommand = { id: 'workbench.userDataSync.actions.resolveTasksConflicts', title: localize('showTasksConflicts', "{0}: Show User Tasks Conflicts", SYNC_TITLE) }; const syncNowCommand = { id: 'workbench.userDataSync.actions.syncNow', title: localize('sync now', "{0}: Sync Now", SYNC_TITLE), @@ -327,7 +328,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo }); break; case UserDataSyncErrorCode.TooLarge: - if (error.resource === SyncResource.Keybindings || error.resource === SyncResource.Settings) { + if (error.resource === SyncResource.Keybindings || error.resource === SyncResource.Settings || error.resource === SyncResource.Tasks) { this.disableSync(error.resource); const sourceArea = getSyncAreaLabel(error.resource); this.handleTooLargeError(error.resource, localize('too large', "Disabled syncing {0} because size of the {1} file to sync is larger than {2}. Please open the file and reduce the size and enable sync", sourceArea.toLowerCase(), sourceArea.toLowerCase(), '100kb'), error); @@ -429,7 +430,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo if (this.invalidContentErrorDisposables.has(source)) { return; } - if (source !== SyncResource.Settings && source !== SyncResource.Keybindings) { + if (source !== SyncResource.Settings && source !== SyncResource.Keybindings && source !== SyncResource.Tasks) { return; } if (!this.hostService.hasFocus) { @@ -537,7 +538,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo if (e instanceof UserDataSyncError) { switch (e.code) { case UserDataSyncErrorCode.TooLarge: - if (e.resource === SyncResource.Keybindings || e.resource === SyncResource.Settings) { + if (e.resource === SyncResource.Keybindings || e.resource === SyncResource.Settings || e.resource === SyncResource.Tasks) { this.handleTooLargeError(e.resource, localize('too large while starting sync', "Settings sync cannot be turned on because size of the {0} file to sync is larger than {1}. Please open the file and reduce the size and turn on sync", getSyncAreaLabel(e.resource).toLowerCase(), '100kb'), e); return; } @@ -626,6 +627,9 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo }, { id: SyncResource.Snippets, label: getSyncAreaLabel(SyncResource.Snippets) + }, { + id: SyncResource.Tasks, + label: getSyncAreaLabel(SyncResource.Tasks) }, { id: SyncResource.Extensions, label: getSyncAreaLabel(SyncResource.Extensions) @@ -692,6 +696,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo case SyncResource.Settings: return this.userDataSyncEnablementService.setResourceEnablement(SyncResource.Settings, false); case SyncResource.Keybindings: return this.userDataSyncEnablementService.setResourceEnablement(SyncResource.Keybindings, false); case SyncResource.Snippets: return this.userDataSyncEnablementService.setResourceEnablement(SyncResource.Snippets, false); + case SyncResource.Tasks: return this.userDataSyncEnablementService.setResourceEnablement(SyncResource.Tasks, false); case SyncResource.Extensions: return this.userDataSyncEnablementService.setResourceEnablement(SyncResource.Extensions, false); case SyncResource.GlobalState: return this.userDataSyncEnablementService.setResourceEnablement(SyncResource.GlobalState, false); } @@ -794,6 +799,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo this.registerShowSettingsConflictsAction(); this.registerShowKeybindingsConflictsAction(); this.registerShowSnippetsConflictsAction(); + this.registerShowTasksConflictsAction(); this.registerEnableSyncViewsAction(); this.registerManageSyncAction(); @@ -979,6 +985,33 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo }); } + private registerShowTasksConflictsAction(): void { + const resolveTasksConflictsWhenContext = ContextKeyExpr.regex(CONTEXT_CONFLICTS_SOURCES.keys()[0], /.*tasks.*/i); + CommandsRegistry.registerCommand(resolveTasksConflictsCommand.id, () => this.handleSyncResourceConflicts(SyncResource.Tasks)); + MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { + group: '5_sync', + command: { + id: resolveTasksConflictsCommand.id, + title: localize('resolveTasksConflicts_global', "{0}: Show User Tasks Conflicts (1)", SYNC_TITLE), + }, + when: resolveTasksConflictsWhenContext, + order: 2 + }); + MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { + group: '5_sync', + command: { + id: resolveKeybindingsConflictsCommand.id, + title: localize('resolveTasksConflicts_global', "{0}: Show User Tasks Conflicts (1)", SYNC_TITLE), + }, + when: resolveTasksConflictsWhenContext, + order: 2 + }); + MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: resolveTasksConflictsCommand, + when: resolveTasksConflictsWhenContext, + }); + } + private _snippetsConflictsActionsDisposable: DisposableStore = new DisposableStore(); private registerShowSnippetsConflictsAction(): void { this._snippetsConflictsActionsDisposable.clear(); @@ -1058,6 +1091,9 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo case SyncResource.Snippets: items.push({ id: resolveSnippetsConflictsCommand.id, label: resolveSnippetsConflictsCommand.title }); break; + case SyncResource.Tasks: + items.push({ id: resolveTasksConflictsCommand.id, label: resolveTasksConflictsCommand.title }); + break; } } items.push({ type: 'separator' }); diff --git a/src/vs/workbench/services/userData/browser/userDataInit.ts b/src/vs/workbench/services/userData/browser/userDataInit.ts index 95b22469344..0a04a172de6 100644 --- a/src/vs/workbench/services/userData/browser/userDataInit.ts +++ b/src/vs/workbench/services/userData/browser/userDataInit.ts @@ -36,6 +36,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { IExtensionStorageService } from 'vs/platform/extensionManagement/common/extensionStorage'; import { ICredentialsService } from 'vs/platform/credentials/common/credentials'; +import { TasksInitializer } from 'vs/platform/userDataSync/common/tasksSync'; export const IUserDataInitializationService = createDecorator('IUserDataInitializationService'); export interface IUserDataInitializationService { @@ -185,7 +186,7 @@ export class UserDataInitializationService implements IUserDataInitializationSer async initializeOtherResources(instantiationService: IInstantiationService): Promise { try { this.logService.trace(`UserDataInitializationService#initializeOtherResources`); - await Promise.allSettled([this.initialize([SyncResource.Keybindings, SyncResource.Snippets]), this.initializeExtensions(instantiationService)]); + await Promise.allSettled([this.initialize([SyncResource.Keybindings, SyncResource.Snippets, SyncResource.Tasks]), this.initializeExtensions(instantiationService)]); } finally { this.initializationFinished.open(); } @@ -271,6 +272,7 @@ export class UserDataInitializationService implements IUserDataInitializationSer switch (syncResource) { case SyncResource.Settings: return new SettingsInitializer(this.fileService, this.environmentService, this.logService, this.uriIdentityService); case SyncResource.Keybindings: return new KeybindingsInitializer(this.fileService, this.environmentService, this.logService, this.uriIdentityService); + case SyncResource.Tasks: return new TasksInitializer(this.fileService, this.environmentService, this.logService, this.uriIdentityService); case SyncResource.Snippets: return new SnippetsInitializer(this.fileService, this.environmentService, this.logService, this.uriIdentityService); case SyncResource.GlobalState: return new GlobalStateInitializer(this.storageService, this.fileService, this.environmentService, this.logService, this.uriIdentityService); } diff --git a/src/vs/workbench/services/userDataSync/common/userDataSync.ts b/src/vs/workbench/services/userDataSync/common/userDataSync.ts index fcf13eae627..a6e289f706d 100644 --- a/src/vs/workbench/services/userDataSync/common/userDataSync.ts +++ b/src/vs/workbench/services/userDataSync/common/userDataSync.ts @@ -74,6 +74,7 @@ export function getSyncAreaLabel(source: SyncResource): string { case SyncResource.Settings: return localize('settings', "Settings"); case SyncResource.Keybindings: return localize('keybindings', "Keyboard Shortcuts"); case SyncResource.Snippets: return localize('snippets', "User Snippets"); + case SyncResource.Tasks: return localize('tasks', "User Tasks"); case SyncResource.Extensions: return localize('extensions', "Extensions"); case SyncResource.GlobalState: return localize('ui state label', "UI State"); }