diff --git a/src/vs/platform/userDataSync/common/extensionsSync.ts b/src/vs/platform/userDataSync/common/extensionsSync.ts index 98beb932ced..d8d83228bbd 100644 --- a/src/vs/platform/userDataSync/common/extensionsSync.ts +++ b/src/vs/platform/userDataSync/common/extensionsSync.ts @@ -19,11 +19,13 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { localize } from 'vs/nls'; import { merge } from 'vs/platform/userDataSync/common/extensionsMerge'; -export interface ISyncPreviewResult { +interface ISyncPreviewResult { readonly added: ISyncExtension[]; - readonly removed: ISyncExtension[]; + readonly removed: IExtensionIdentifier[]; readonly updated: ISyncExtension[]; readonly remote: ISyncExtension[] | null; + readonly remoteUserData: IUserData | null; + readonly skippedExtensions: ISyncExtension[]; } interface ILastSyncUserData extends IUserData { @@ -72,6 +74,61 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser } } + async pull(): Promise { + if (!this.configurationService.getValue('sync.enableExtensions')) { + this.logService.info('Extensions: Skipped pulling extensions as it is disabled.'); + return; + } + + this.stop(); + + try { + this.logService.info('Extensions: Started pulling extensions...'); + this.setStatus(SyncStatus.Syncing); + + const remoteUserData = await this.getRemoteUserData(); + + if (remoteUserData.content !== null) { + const localExtensions = await this.getLocalExtensions(); + const remoteExtensions: ISyncExtension[] = JSON.parse(remoteUserData.content); + const { added, updated, remote } = merge(localExtensions, remoteExtensions, [], [], this.getIgnoredExtensions()); + await this.apply({ added, removed: [], updated, remote, remoteUserData, skippedExtensions: [] }); + } + + // No remote exists to pull + else { + this.logService.info('Extensions: Remote extensions does not exist.'); + } + + this.logService.info('Extensions: Finished pulling extensions.'); + } finally { + this.setStatus(SyncStatus.Idle); + } + } + + async push(): Promise { + if (!this.configurationService.getValue('sync.enableExtensions')) { + this.logService.info('Extensions: Skipped pushing extensions as it is disabled.'); + return; + } + + this.stop(); + + try { + this.logService.info('Extensions: Started pushing extensions...'); + this.setStatus(SyncStatus.Syncing); + + const localExtensions = await this.getLocalExtensions(); + const { added, removed, updated, remote } = merge(localExtensions, null, null, [], this.getIgnoredExtensions()); + await this.apply({ added, removed, updated, remote, remoteUserData: null, skippedExtensions: [] }); + + this.logService.info('Extensions: Finished pulling extensions.'); + } finally { + this.setStatus(SyncStatus.Idle); + } + + } + async sync(): Promise { if (!this.configurationService.getValue('sync.enableExtensions')) { this.logService.trace('Extensions: Skipping synchronizing extensions as it is disabled.'); @@ -90,7 +147,8 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser this.setStatus(SyncStatus.Syncing); try { - await this.doSync(); + const previewResult = await this.getPreview(); + await this.apply(previewResult); } catch (e) { this.setStatus(SyncStatus.Idle); if (e instanceof UserDataSyncStoreError && e.code === UserDataSyncStoreErrorCode.Rejected) { @@ -114,7 +172,7 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser } async hasRemote(): Promise { - const remoteUserData = await this.userDataSyncStoreService.read(ExtensionsSynchroniser.EXTERNAL_USER_DATA_EXTENSIONS_KEY, null); + const remoteUserData = await this.getRemoteUserData(); return remoteUserData.content !== null; } @@ -134,13 +192,13 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser }); } - private async doSync(): Promise { + private async getPreview(): Promise { const lastSyncData = await this.getLastSyncUserData(); const lastSyncExtensions: ISyncExtension[] | null = lastSyncData ? JSON.parse(lastSyncData.content!) : null; - let skippedExtensions: ISyncExtension[] = lastSyncData ? lastSyncData.skippedExtensions || [] : []; + const skippedExtensions: ISyncExtension[] = lastSyncData ? lastSyncData.skippedExtensions || [] : []; - let remoteData = await this.userDataSyncStoreService.read(ExtensionsSynchroniser.EXTERNAL_USER_DATA_EXTENSIONS_KEY, lastSyncData); - const remoteExtensions: ISyncExtension[] = remoteData.content ? JSON.parse(remoteData.content) : null; + const remoteUserData = await this.getRemoteUserData(lastSyncData); + const remoteExtensions: ISyncExtension[] = remoteUserData.content ? JSON.parse(remoteUserData.content) : null; const localExtensions = await this.getLocalExtensions(); @@ -150,9 +208,16 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser this.logService.info('Extensions: Remote extensions does not exist. Synchronizing extensions for the first time.'); } - const ignoredExtensions = this.configurationService.getValue('sync.ignoredExtensions') || []; - const { added, removed, updated, remote } = merge(localExtensions, remoteExtensions, lastSyncExtensions, skippedExtensions, ignoredExtensions); + const { added, removed, updated, remote } = merge(localExtensions, remoteExtensions, lastSyncExtensions, skippedExtensions, this.getIgnoredExtensions()); + return { added, removed, updated, remote, skippedExtensions, remoteUserData }; + } + + private getIgnoredExtensions() { + return this.configurationService.getValue('sync.ignoredExtensions') || []; + } + + private async apply({ added, removed, updated, remote, remoteUserData, skippedExtensions }: ISyncPreviewResult): Promise { if (!added.length && !removed.length && !updated.length && !remote) { this.logService.trace('Extensions: No changes found during synchronizing extensions.'); } @@ -165,15 +230,13 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser if (remote) { // update remote this.logService.info('Extensions: Updating remote extensions...'); - remoteData = await this.writeToRemote(remote, remoteData.ref); + remoteUserData = await this.writeToRemote(remote, remoteUserData ? remoteUserData.ref : null); } - if (remoteData.content - && (!lastSyncData || lastSyncData.ref !== remoteData.ref) - ) { + if (remoteUserData?.content) { // update last sync this.logService.info('Extensions: Updating last synchronised extensions...'); - await this.updateLastSyncValue({ ...remoteData, skippedExtensions }); + await this.updateLastSyncValue({ ...remoteUserData, skippedExtensions }); } } @@ -243,6 +306,10 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser await this.fileService.writeFile(this.lastSyncExtensionsResource, VSBuffer.fromString(JSON.stringify(lastSyncUserData))); } + private getRemoteUserData(lastSyncData?: IUserData | null): Promise { + return this.userDataSyncStoreService.read(ExtensionsSynchroniser.EXTERNAL_USER_DATA_EXTENSIONS_KEY, lastSyncData || null); + } + private async writeToRemote(extensions: ISyncExtension[], ref: string | null): Promise { const content = JSON.stringify(extensions); ref = await this.userDataSyncStoreService.write(ExtensionsSynchroniser.EXTERNAL_USER_DATA_EXTENSIONS_KEY, content, ref); diff --git a/src/vs/platform/userDataSync/common/globalStateSync.ts b/src/vs/platform/userDataSync/common/globalStateSync.ts index 5677baaf1f1..36c81f2e038 100644 --- a/src/vs/platform/userDataSync/common/globalStateSync.ts +++ b/src/vs/platform/userDataSync/common/globalStateSync.ts @@ -19,6 +19,12 @@ import { parse } from 'vs/base/common/json'; const argvProperties: string[] = ['locale']; +interface ISyncPreviewResult { + readonly local: IGlobalState | undefined; + readonly remote: IGlobalState | undefined; + readonly remoteUserData: IUserData | null; +} + export class GlobalStateSynchroniser extends Disposable implements ISynchroniser { private static EXTERNAL_USER_DATA_GLOBAL_STATE_KEY: string = 'globalState'; @@ -53,6 +59,58 @@ export class GlobalStateSynchroniser extends Disposable implements ISynchroniser } } + async pull(): Promise { + if (!this.configurationService.getValue('sync.enableUIState')) { + this.logService.info('UI State: Skipped pulling ui state as it is disabled.'); + return; + } + + this.stop(); + + try { + this.logService.info('UI State: Started pulling ui state...'); + this.setStatus(SyncStatus.Syncing); + + const remoteUserData = await this.getRemoteUserData(); + + if (remoteUserData.content !== null) { + const local: IGlobalState = JSON.parse(remoteUserData.content); + await this.apply({ local, remote: undefined, remoteUserData }); + } + + // No remote exists to pull + else { + this.logService.info('UI State: Remote UI state does not exist.'); + } + + this.logService.info('UI State: Finished pulling UI state.'); + } finally { + this.setStatus(SyncStatus.Idle); + } + } + + async push(): Promise { + if (!this.configurationService.getValue('sync.enableUIState')) { + this.logService.info('UI State: Skipped pushing UI State as it is disabled.'); + return; + } + + this.stop(); + + try { + this.logService.info('UI State: Started pushing UI State...'); + this.setStatus(SyncStatus.Syncing); + + const remote = await this.getLocalGlobalState(); + await this.apply({ local: undefined, remote, remoteUserData: null }); + + this.logService.info('UI State: Finished pulling UI State.'); + } finally { + this.setStatus(SyncStatus.Idle); + } + + } + async sync(): Promise { if (!this.configurationService.getValue('sync.enableUIState')) { this.logService.trace('UI State: Skipping synchronizing UI state as it is disabled.'); @@ -68,9 +126,9 @@ export class GlobalStateSynchroniser extends Disposable implements ISynchroniser this.setStatus(SyncStatus.Syncing); try { - await this.doSync(); + const result = await this.getPreview(); + await this.apply(result); this.logService.trace('UI State: Finised synchronizing ui state.'); - this.setStatus(SyncStatus.Idle); return true; } catch (e) { this.setStatus(SyncStatus.Idle); @@ -80,6 +138,8 @@ export class GlobalStateSynchroniser extends Disposable implements ISynchroniser return this.sync(); } throw e; + } finally { + this.setStatus(SyncStatus.Idle); } } @@ -91,21 +151,25 @@ export class GlobalStateSynchroniser extends Disposable implements ISynchroniser } async hasRemote(): Promise { - const remoteUserData = await this.userDataSyncStoreService.read(GlobalStateSynchroniser.EXTERNAL_USER_DATA_GLOBAL_STATE_KEY, null); + const remoteUserData = await this.getRemoteUserData(); return remoteUserData.content !== null; } - private async doSync(): Promise { + private async getPreview(): Promise { const lastSyncData = await this.getLastSyncUserData(); const lastSyncGlobalState = lastSyncData && lastSyncData.content ? JSON.parse(lastSyncData.content) : null; - let remoteData = await this.userDataSyncStoreService.read(GlobalStateSynchroniser.EXTERNAL_USER_DATA_GLOBAL_STATE_KEY, lastSyncData); - const remoteGlobalState: IGlobalState = remoteData.content ? JSON.parse(remoteData.content) : null; + const remoteUserData = await this.getRemoteUserData(); + const remoteGlobalState: IGlobalState = remoteUserData.content ? JSON.parse(remoteUserData.content) : null; const localGloablState = await this.getLocalGlobalState(); const { local, remote } = merge(localGloablState, remoteGlobalState, lastSyncGlobalState); + return { local, remote, remoteUserData }; + } + + private async apply({ local, remote, remoteUserData }: ISyncPreviewResult): Promise { if (local) { // update local this.logService.info('UI State: Updating local ui state...'); @@ -115,15 +179,13 @@ export class GlobalStateSynchroniser extends Disposable implements ISynchroniser if (remote) { // update remote this.logService.info('UI State: Updating remote ui state...'); - remoteData = await this.writeToRemote(remote, remoteData.ref); + remoteUserData = await this.writeToRemote(remote, remoteUserData ? remoteUserData.ref : null); } - if (remoteData.content - && (!lastSyncData || lastSyncData.ref !== remoteData.ref) - ) { + if (remoteUserData?.content) { // update last sync this.logService.info('UI State: Updating last synchronised ui state...'); - await this.updateLastSyncValue(remoteData); + await this.updateLastSyncValue(remoteUserData); } } @@ -166,6 +228,10 @@ export class GlobalStateSynchroniser extends Disposable implements ISynchroniser await this.fileService.writeFile(this.lastSyncGlobalStateResource, VSBuffer.fromString(JSON.stringify(remoteUserData))); } + private getRemoteUserData(lastSyncData?: IUserData | null): Promise { + return this.userDataSyncStoreService.read(GlobalStateSynchroniser.EXTERNAL_USER_DATA_GLOBAL_STATE_KEY, lastSyncData || null); + } + private async writeToRemote(globalState: IGlobalState, ref: string | null): Promise { const content = JSON.stringify(globalState); ref = await this.userDataSyncStoreService.write(GlobalStateSynchroniser.EXTERNAL_USER_DATA_GLOBAL_STATE_KEY, content, ref); diff --git a/src/vs/platform/userDataSync/common/keybindingsSync.ts b/src/vs/platform/userDataSync/common/keybindingsSync.ts index 1b92064f972..bea6e5671c8 100644 --- a/src/vs/platform/userDataSync/common/keybindingsSync.ts +++ b/src/vs/platform/userDataSync/common/keybindingsSync.ts @@ -30,7 +30,7 @@ interface ISyncContent { interface ISyncPreviewResult { readonly fileContent: IFileContent | null; - readonly remoteUserData: IUserData; + readonly remoteUserData: IUserData | null; readonly hasLocalChanged: boolean; readonly hasRemoteChanged: boolean; readonly hasConflicts: boolean; @@ -73,6 +73,79 @@ export class KeybindingsSynchroniser extends Disposable implements ISynchroniser } } + async pull(): Promise { + if (!this.configurationService.getValue('sync.enableKeybindings')) { + this.logService.info('Keybindings: Skipped pulling keybindings as it is disabled.'); + return; + } + + this.stop(); + + try { + this.logService.info('Keybindings: Started pulling keybindings...'); + this.setStatus(SyncStatus.Syncing); + + const remoteUserData = await this.getRemoteUserData(); + const remoteContent = remoteUserData.content !== null ? this.getKeybindingsContentFromSyncContent(remoteUserData.content) : null; + + if (remoteContent !== null) { + await this.fileService.writeFile(this.environmentService.settingsSyncPreviewResource, VSBuffer.fromString(remoteContent)); + this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve({ + fileContent: null, + hasConflicts: false, + hasLocalChanged: true, + hasRemoteChanged: false, + remoteUserData + })); + await this.apply(); + } + + // No remote exists to pull + else { + this.logService.info('Keybindings: Remote keybindings does not exist.'); + } + } finally { + this.setStatus(SyncStatus.Idle); + } + + } + + async push(): Promise { + if (!this.configurationService.getValue('sync.enableKeybindings')) { + this.logService.info('Keybindings: Skipped pushing keybindings as it is disabled.'); + return; + } + + this.stop(); + + try { + this.logService.info('Keybindings: Started pushing keybindings...'); + this.setStatus(SyncStatus.Syncing); + + const fileContent = await this.getLocalFileContent(); + + if (fileContent !== null) { + await this.fileService.writeFile(this.environmentService.settingsSyncPreviewResource, fileContent.value); + this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve({ + fileContent, + hasConflicts: false, + hasLocalChanged: false, + hasRemoteChanged: true, + remoteUserData: null + })); + await this.apply(); + } + + // No local exists to push + else { + this.logService.info('Keybindings: Local keybindings does not exist.'); + } + } finally { + this.setStatus(SyncStatus.Idle); + } + + } + async sync(_continue?: boolean): Promise { if (!this.configurationService.getValue('sync.enableKeybindings')) { this.logService.trace('Keybindings: Skipping synchronizing keybindings as it is disabled.'); @@ -115,6 +188,8 @@ export class KeybindingsSynchroniser extends Disposable implements ISynchroniser return this.sync(); } throw e; + } finally { + this.setStatus(SyncStatus.Idle); } } @@ -134,7 +209,7 @@ export class KeybindingsSynchroniser extends Disposable implements ISynchroniser } async hasRemote(): Promise { - const remoteUserData = await this.userDataSyncStoreService.read(KeybindingsSynchroniser.EXTERNAL_USER_DATA_KEYBINDINGS_KEY, null); + const remoteUserData = await this.getRemoteUserData(); return remoteUserData.content !== null; } @@ -143,6 +218,7 @@ export class KeybindingsSynchroniser extends Disposable implements ISynchroniser return false; } await this.apply(); + this.setStatus(SyncStatus.Idle); return true; } @@ -170,11 +246,12 @@ export class KeybindingsSynchroniser extends Disposable implements ISynchroniser } if (hasRemoteChanged) { this.logService.info('Keybindings: Updating remote keybindings'); - const remoteContents = this.updateSyncContent(content, remoteUserData.content); - const ref = await this.updateRemoteUserData(remoteContents, remoteUserData.ref); + let remoteContents = remoteUserData ? remoteUserData.content : (await this.getRemoteUserData()).content; + remoteContents = this.updateSyncContent(content, remoteContents); + const ref = await this.updateRemoteUserData(remoteContents, remoteUserData ? remoteUserData.ref : null); remoteUserData = { ref, content: remoteContents }; } - if (remoteUserData.content) { + if (remoteUserData?.content) { this.logService.info('Keybindings: Updating last synchronised keybindings'); const lastSyncContent = this.updateSyncContent(content, null); await this.updateLastSyncUserData({ ref: remoteUserData.ref, content: lastSyncContent }); @@ -188,7 +265,6 @@ export class KeybindingsSynchroniser extends Disposable implements ISynchroniser this.logService.trace('Keybindings: Finised synchronizing keybindings.'); this.syncPreviewResultPromise = null; - this.setStatus(SyncStatus.Idle); } private hasErrors(content: string): boolean { @@ -210,7 +286,7 @@ export class KeybindingsSynchroniser extends Disposable implements ISynchroniser const remoteUserData = await this.getRemoteUserData(lastSyncData); const remoteContent = remoteUserData.content ? this.getKeybindingsContentFromSyncContent(remoteUserData.content) : null; // Get file content last to get the latest - const fileContent = await this.getLocalContent(); + const fileContent = await this.getLocalFileContent(); let hasLocalChanged: boolean = false; let hasRemoteChanged: boolean = false; let hasConflicts: boolean = false; @@ -262,7 +338,7 @@ export class KeybindingsSynchroniser extends Disposable implements ISynchroniser return this._formattingOptions; } - private async getLocalContent(): Promise { + private async getLocalFileContent(): Promise { try { return await this.fileService.readFile(this.environmentService.keybindingsResource); } catch (error) { @@ -293,8 +369,8 @@ export class KeybindingsSynchroniser extends Disposable implements ISynchroniser await this.fileService.writeFile(this.lastSyncKeybindingsResource, VSBuffer.fromString(JSON.stringify(remoteUserData))); } - private async getRemoteUserData(lastSyncData: IUserData | null): Promise { - return this.userDataSyncStoreService.read(KeybindingsSynchroniser.EXTERNAL_USER_DATA_KEYBINDINGS_KEY, lastSyncData); + private async getRemoteUserData(lastSyncData?: IUserData | null): Promise { + return this.userDataSyncStoreService.read(KeybindingsSynchroniser.EXTERNAL_USER_DATA_KEYBINDINGS_KEY, lastSyncData || null); } private async updateRemoteUserData(content: string, ref: string | null): Promise { diff --git a/src/vs/platform/userDataSync/common/settingsMerge.ts b/src/vs/platform/userDataSync/common/settingsMerge.ts index 1b710e133bd..c00080dbcb1 100644 --- a/src/vs/platform/userDataSync/common/settingsMerge.ts +++ b/src/vs/platform/userDataSync/common/settingsMerge.ts @@ -12,17 +12,14 @@ import { FormattingOptions } from 'vs/base/common/jsonFormatter'; import * as contentUtil from 'vs/platform/userDataSync/common/content'; import { IConflictSetting } from 'vs/platform/userDataSync/common/userDataSync'; -export function computeRemoteContent(localContent: string, remoteContent: string, ignoredSettings: string[], formattingOptions: FormattingOptions): string { +export function updateIgnoredSettings(targetContent: string, sourceContent: string, ignoredSettings: string[], formattingOptions: FormattingOptions): string { if (ignoredSettings.length) { - const remote = parse(remoteContent); - const ignored = ignoredSettings.reduce((set, key) => { set.add(key); return set; }, new Set()); + const source = parse(sourceContent); for (const key of ignoredSettings) { - if (ignored.has(key)) { - localContent = contentUtil.edit(localContent, [key], remote[key], formattingOptions); - } + targetContent = contentUtil.edit(targetContent, [key], source[key], formattingOptions); } } - return localContent; + return targetContent; } export function merge(localContent: string, remoteContent: string, baseContent: string | null, ignoredSettings: string[], resolvedConflicts: { key: string, value: any | undefined }[], formattingOptions: FormattingOptions): { mergeContent: string, hasChanges: boolean, conflicts: IConflictSetting[] } { diff --git a/src/vs/platform/userDataSync/common/settingsSync.ts b/src/vs/platform/userDataSync/common/settingsSync.ts index 89dc8c0ae81..813826aa728 100644 --- a/src/vs/platform/userDataSync/common/settingsSync.ts +++ b/src/vs/platform/userDataSync/common/settingsSync.ts @@ -17,14 +17,14 @@ import { joinPath, dirname } from 'vs/base/common/resources'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { startsWith } from 'vs/base/common/strings'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { computeRemoteContent, merge } from 'vs/platform/userDataSync/common/settingsMerge'; +import { updateIgnoredSettings, merge } from 'vs/platform/userDataSync/common/settingsMerge'; import { FormattingOptions } from 'vs/base/common/jsonFormatter'; import * as arrays from 'vs/base/common/arrays'; import * as objects from 'vs/base/common/objects'; interface ISyncPreviewResult { readonly fileContent: IFileContent | null; - readonly remoteUserData: IUserData; + readonly remoteUserData: IUserData | null; readonly hasLocalChanged: boolean; readonly hasRemoteChanged: boolean; readonly conflicts: IConflictSetting[]; @@ -86,6 +86,87 @@ export class SettingsSynchroniser extends Disposable implements ISettingsSyncSer } } + async pull(): Promise { + if (!this.configurationService.getValue('sync.enableSettings')) { + this.logService.info('Settings: Skipped pulling settings as it is disabled.'); + return; + } + + this.stop(); + + try { + this.logService.info('Settings: Started pulling settings...'); + this.setStatus(SyncStatus.Syncing); + + const remoteUserData = await this.getRemoteUserData(); + + if (remoteUserData.content !== null) { + const fileContent = await this.getLocalFileContent(); + const formatUtils = await this.getFormattingOptions(); + // Update ignored settings + const content = updateIgnoredSettings(remoteUserData.content, fileContent ? fileContent.value.toString() : '{}', getIgnoredSettings(this.configurationService), formatUtils); + await this.fileService.writeFile(this.environmentService.settingsSyncPreviewResource, VSBuffer.fromString(content)); + + this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve({ + conflicts: [], + fileContent, + hasLocalChanged: true, + hasRemoteChanged: false, + remoteUserData + })); + + await this.apply(); + } + + // No remote exists to pull + else { + this.logService.info('Settings: Remote settings does not exist.'); + } + } finally { + this.setStatus(SyncStatus.Idle); + } + } + + async push(): Promise { + if (!this.configurationService.getValue('sync.enableSettings')) { + this.logService.info('Settings: Skipped pushing settings as it is disabled.'); + return; + } + + this.stop(); + + try { + this.logService.info('Settings: Started pushing settings...'); + this.setStatus(SyncStatus.Syncing); + + const fileContent = await this.getLocalFileContent(); + + if (fileContent !== null) { + const formatUtils = await this.getFormattingOptions(); + // Remove ignored settings + const content = updateIgnoredSettings(fileContent.value.toString(), '{}', getIgnoredSettings(this.configurationService), formatUtils); + await this.fileService.writeFile(this.environmentService.settingsSyncPreviewResource, VSBuffer.fromString(content)); + + this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve({ + conflicts: [], + fileContent, + hasLocalChanged: false, + hasRemoteChanged: true, + remoteUserData: null + })); + + await this.apply(); + } + + // No local exists to push + else { + this.logService.info('Settings: Local settings does not exist.'); + } + } finally { + this.setStatus(SyncStatus.Idle); + } + } + async sync(_continue?: boolean): Promise { if (!this.configurationService.getValue('sync.enableSettings')) { this.logService.trace('Settings: Skipping synchronizing settings as it is disabled.'); @@ -123,7 +204,7 @@ export class SettingsSynchroniser extends Disposable implements ISettingsSyncSer } async hasRemote(): Promise { - const remoteUserData = await this.userDataSyncStoreService.read(SettingsSynchroniser.EXTERNAL_USER_DATA_SETTINGS_KEY, null); + const remoteUserData = await this.getRemoteUserData(); return remoteUserData.content !== null; } @@ -159,12 +240,15 @@ export class SettingsSynchroniser extends Disposable implements ISettingsSyncSer return this.sync(); } throw e; + } finally { + this.setStatus(SyncStatus.Idle); } } private async continueSync(): Promise { if (this.status === SyncStatus.HasConflicts) { await this.apply(); + this.setStatus(SyncStatus.Idle); } return true; } @@ -193,12 +277,12 @@ export class SettingsSynchroniser extends Disposable implements ISettingsSyncSer } if (hasRemoteChanged) { const formatUtils = await this.getFormattingOptions(); - const remoteContent = remoteUserData.content ? computeRemoteContent(content, remoteUserData.content, getIgnoredSettings(this.configurationService, content), formatUtils) : content; + const remoteContent = remoteUserData?.content ? updateIgnoredSettings(content, remoteUserData.content, getIgnoredSettings(this.configurationService, content), formatUtils) : content; this.logService.info('Settings: Updating remote settings'); - const ref = await this.writeToRemote(remoteContent, remoteUserData.ref); + const ref = await this.writeToRemote(remoteContent, remoteUserData ? remoteUserData.ref : null); remoteUserData = { ref, content }; } - if (remoteUserData.content) { + if (remoteUserData?.content) { this.logService.info('Settings: Updating last synchronised sttings'); await this.updateLastSyncValue(remoteUserData); } @@ -211,7 +295,6 @@ export class SettingsSynchroniser extends Disposable implements ISettingsSyncSer this.logService.trace('Settings: Finised synchronizing settings.'); this.syncPreviewResultPromise = null; - this.setStatus(SyncStatus.Idle); } private hasErrors(content: string): boolean { @@ -229,7 +312,7 @@ export class SettingsSynchroniser extends Disposable implements ISettingsSyncSer private async generatePreview(resolvedConflicts: { key: string, value: any }[], token: CancellationToken): Promise { const lastSyncData = await this.getLastSyncUserData(); - const remoteUserData = await this.userDataSyncStoreService.read(SettingsSynchroniser.EXTERNAL_USER_DATA_SETTINGS_KEY, lastSyncData); + const remoteUserData = await this.getRemoteUserData(lastSyncData); const remoteContent: string | null = remoteUserData.content; // Get file content last to get the latest const fileContent = await this.getLocalFileContent(); @@ -303,6 +386,10 @@ export class SettingsSynchroniser extends Disposable implements ISettingsSyncSer } } + private getRemoteUserData(lastSyncData?: IUserData | null): Promise { + return this.userDataSyncStoreService.read(SettingsSynchroniser.EXTERNAL_USER_DATA_SETTINGS_KEY, lastSyncData || null); + } + private async writeToRemote(content: string, ref: string | null): Promise { return this.userDataSyncStoreService.write(SettingsSynchroniser.EXTERNAL_USER_DATA_SETTINGS_KEY, content, ref); } diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index 3558afcb8e8..eacd383b650 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -184,6 +184,8 @@ export interface ISynchroniser { readonly status: SyncStatus; readonly onDidChangeStatus: Event; readonly onDidChangeLocal: Event; + pull(): Promise; + push(): Promise; sync(_continue?: boolean): Promise; stop(): void; hasPreviouslySynced(): Promise diff --git a/src/vs/platform/userDataSync/common/userDataSyncIpc.ts b/src/vs/platform/userDataSync/common/userDataSyncIpc.ts index 05f4d99128f..61f634471ba 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncIpc.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncIpc.ts @@ -25,6 +25,8 @@ export class UserDataSyncChannel implements IServerChannel { call(context: any, command: string, args?: any): Promise { switch (command) { case 'sync': return this.service.sync(args[0]); + case 'pull': return this.service.pull(); + case 'push': return this.service.push(); case '_getInitialStatus': return Promise.resolve(this.service.status); case 'getConflictsSource': return Promise.resolve(this.service.conflictsSource); case 'removeExtension': return this.service.removeExtension(args[0]); @@ -52,6 +54,8 @@ export class SettingsSyncChannel implements IServerChannel { call(context: any, command: string, args?: any): Promise { switch (command) { case 'sync': return this.service.sync(args[0]); + case 'pull': return this.service.pull(); + case 'push': return this.service.push(); case '_getInitialStatus': return Promise.resolve(this.service.status); case '_getInitialConflicts': return Promise.resolve(this.service.conflicts); case 'stop': this.service.stop(); return Promise.resolve(); diff --git a/src/vs/platform/userDataSync/common/userDataSyncService.ts b/src/vs/platform/userDataSync/common/userDataSyncService.ts index 5a23f9d01c4..9c4aaf3cb0c 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncService.ts @@ -56,6 +56,38 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ this.onDidChangeLocal = Event.any(...this.synchronisers.map(s => s.onDidChangeLocal)); } + async pull(): Promise { + if (!this.userDataSyncStoreService.userDataSyncStore) { + throw new Error('Not enabled'); + } + if (this.authTokenService.status === AuthTokenStatus.SignedOut) { + throw new Error('Not Authenticated. Please sign in to start sync.'); + } + for (const synchroniser of this.synchronisers) { + try { + await synchroniser.pull(); + } catch (e) { + this.logService.error(`${this.getSyncSource(synchroniser)}: ${toErrorMessage(e)}`); + } + } + } + + async push(): Promise { + if (!this.userDataSyncStoreService.userDataSyncStore) { + throw new Error('Not enabled'); + } + if (this.authTokenService.status === AuthTokenStatus.SignedOut) { + throw new Error('Not Authenticated. Please sign in to start sync.'); + } + for (const synchroniser of this.synchronisers) { + try { + await synchroniser.push(); + } catch (e) { + this.logService.error(`${this.getSyncSource(synchroniser)}: ${toErrorMessage(e)}`); + } + } + } + async sync(_continue?: boolean): Promise { if (!this.userDataSyncStoreService.userDataSyncStore) { throw new Error('Not enabled'); diff --git a/src/vs/platform/userDataSync/test/common/settingsMerge.test.ts b/src/vs/platform/userDataSync/test/common/settingsMerge.test.ts index 343c99b38d6..4f51f87c2e3 100644 --- a/src/vs/platform/userDataSync/test/common/settingsMerge.test.ts +++ b/src/vs/platform/userDataSync/test/common/settingsMerge.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { merge, computeRemoteContent } from 'vs/platform/userDataSync/common/settingsMerge'; +import { merge, updateIgnoredSettings } from 'vs/platform/userDataSync/common/settingsMerge'; import { IConflictSetting } from 'vs/platform/userDataSync/common/userDataSync'; const formattingOptions = { eol: '\n', insertSpaces: false, tabSize: 4 }; @@ -567,7 +567,7 @@ suite('SettingsMerge - Compute Remote Content', () => { 'd': 4, 'e': 6, }); - const actual = computeRemoteContent(localContent, remoteContent, [], formattingOptions); + const actual = updateIgnoredSettings(localContent, remoteContent, [], formattingOptions); assert.equal(actual, localContent); }); @@ -588,7 +588,7 @@ suite('SettingsMerge - Compute Remote Content', () => { 'b': 2, 'c': 3, }); - const actual = computeRemoteContent(localContent, remoteContent, ['a'], formattingOptions); + const actual = updateIgnoredSettings(localContent, remoteContent, ['a'], formattingOptions); assert.equal(actual, expected); }); diff --git a/src/vs/workbench/services/userDataSync/electron-browser/settingsSyncService.ts b/src/vs/workbench/services/userDataSync/electron-browser/settingsSyncService.ts index 51c3a999ff6..345482f53b1 100644 --- a/src/vs/workbench/services/userDataSync/electron-browser/settingsSyncService.ts +++ b/src/vs/workbench/services/userDataSync/electron-browser/settingsSyncService.ts @@ -45,6 +45,14 @@ export class SettingsSyncService extends Disposable implements ISettingsSyncServ }); } + pull(): Promise { + return this.channel.call('pull'); + } + + push(): Promise { + return this.channel.call('push'); + } + sync(_continue?: boolean): Promise { return this.channel.call('sync', [_continue]); } diff --git a/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncService.ts b/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncService.ts index fe09e70eec6..a8b04ea9302 100644 --- a/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncService.ts +++ b/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncService.ts @@ -38,6 +38,14 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ }); } + pull(): Promise { + return this.channel.call('pull'); + } + + push(): Promise { + return this.channel.call('push'); + } + sync(_continue?: boolean): Promise { return this.channel.call('sync', [_continue]); }