/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { Event } from 'vs/base/common/event'; import { IExtensionIdentifier, EXTENSION_IDENTIFIER_PATTERN } from 'vs/platform/extensionManagement/common/extensionManagement'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { Registry } from 'vs/platform/registry/common/platform'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope, allSettings } from 'vs/platform/configuration/common/configurationRegistry'; import { localize } from 'vs/nls'; import { IDisposable } from 'vs/base/common/lifecycle'; import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { ILogService } from 'vs/platform/log/common/log'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IStringDictionary } from 'vs/base/common/collections'; import { FormattingOptions } from 'vs/base/common/jsonFormatter'; import { URI } from 'vs/base/common/uri'; import { isEqual, joinPath } from 'vs/base/common/resources'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; export const CONFIGURATION_SYNC_STORE_KEY = 'configurationSync.store'; export interface ISyncConfiguration { sync: { enable: boolean, enableSettings: boolean, enableKeybindings: boolean, enableUIState: boolean, enableExtensions: boolean, keybindingsPerPlatform: boolean, ignoredExtensions: string[], ignoredSettings: string[] } } export function getDefaultIgnoredSettings(): string[] { const allSettings = Registry.as(ConfigurationExtensions.Configuration).getConfigurationProperties(); const machineSettings = Object.keys(allSettings).filter(setting => allSettings[setting].scope === ConfigurationScope.MACHINE || allSettings[setting].scope === ConfigurationScope.MACHINE_OVERRIDABLE); return [CONFIGURATION_SYNC_STORE_KEY, ...machineSettings]; } export function registerConfiguration(): IDisposable { const ignoredSettingsSchemaId = 'vscode://schemas/ignoredSettings'; const ignoredExtensionsSchemaId = 'vscode://schemas/ignoredExtensions'; const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); configurationRegistry.registerConfiguration({ id: 'sync', order: 30, title: localize('sync', "Sync"), type: 'object', properties: { 'sync.enable': { type: 'boolean', default: false, scope: ConfigurationScope.APPLICATION, deprecationMessage: 'deprecated' }, 'sync.enableSettings': { type: 'boolean', default: true, scope: ConfigurationScope.APPLICATION, deprecationMessage: 'deprecated' }, 'sync.enableKeybindings': { type: 'boolean', default: true, scope: ConfigurationScope.APPLICATION, deprecationMessage: 'Deprecated' }, 'sync.enableUIState': { type: 'boolean', default: true, scope: ConfigurationScope.APPLICATION, deprecationMessage: 'deprecated' }, 'sync.enableExtensions': { type: 'boolean', default: true, scope: ConfigurationScope.APPLICATION, deprecationMessage: 'deprecated' }, 'sync.keybindingsPerPlatform': { type: 'boolean', description: localize('sync.keybindingsPerPlatform', "Synchronize keybindings per platform."), default: true, scope: ConfigurationScope.APPLICATION, }, 'sync.ignoredExtensions': { 'type': 'array', 'description': localize('sync.ignoredExtensions', "List of extensions to be ignored while synchronizing. The identifier of an extension is always ${publisher}.${name}. For example: vscode.csharp."), $ref: ignoredExtensionsSchemaId, 'default': [], 'scope': ConfigurationScope.APPLICATION, uniqueItems: true, disallowSyncIgnore: true }, 'sync.ignoredSettings': { 'type': 'array', description: localize('sync.ignoredSettings', "Configure settings to be ignored while synchronizing."), 'default': [], 'scope': ConfigurationScope.APPLICATION, $ref: ignoredSettingsSchemaId, additionalProperties: true, uniqueItems: true, disallowSyncIgnore: true } } }); const jsonRegistry = Registry.as(JSONExtensions.JSONContribution); const registerIgnoredSettingsSchema = () => { const defaultIgnoreSettings = getDefaultIgnoredSettings().filter(s => s !== CONFIGURATION_SYNC_STORE_KEY); const ignoredSettingsSchema: IJSONSchema = { items: { type: 'string', enum: [...Object.keys(allSettings.properties).filter(setting => defaultIgnoreSettings.indexOf(setting) === -1), ...defaultIgnoreSettings.map(setting => `-${setting}`)] }, }; jsonRegistry.registerSchema(ignoredSettingsSchemaId, ignoredSettingsSchema); }; jsonRegistry.registerSchema(ignoredExtensionsSchemaId, { type: 'string', pattern: EXTENSION_IDENTIFIER_PATTERN, errorMessage: localize('app.extension.identifier.errorMessage', "Expected format '${publisher}.${name}'. Example: 'vscode.csharp'.") }); return configurationRegistry.onDidUpdateConfiguration(() => registerIgnoredSettingsSchema()); } // #region User Data Sync Store export interface IUserData { ref: string; content: string | null; } export interface IUserDataSyncStore { url: URI; authenticationProviderId: string; } export function getUserDataSyncStore(configurationService: IConfigurationService): IUserDataSyncStore | undefined { const value = configurationService.getValue<{ url: string, authenticationProviderId: string }>(CONFIGURATION_SYNC_STORE_KEY); if (value && value.url && value.authenticationProviderId) { return { url: joinPath(URI.parse(value.url), 'v1'), authenticationProviderId: value.authenticationProviderId }; } return undefined; } export const ALL_RESOURCE_KEYS: ResourceKey[] = ['settings', 'keybindings', 'extensions', 'globalState']; export type ResourceKey = 'settings' | 'keybindings' | 'extensions' | 'globalState'; export interface IUserDataManifest { latest?: Record session: string; } export const IUserDataSyncStoreService = createDecorator('IUserDataSyncStoreService'); export interface IUserDataSyncStoreService { _serviceBrand: undefined; readonly userDataSyncStore: IUserDataSyncStore | undefined; read(key: ResourceKey, oldValue: IUserData | null, source?: SyncSource): Promise; write(key: ResourceKey, content: string, ref: string | null, source?: SyncSource): Promise; manifest(): Promise; clear(): Promise; } //#endregion // #region User Data Sync Error export enum UserDataSyncErrorCode { // Server Errors Unauthorized = 'Unauthorized', Forbidden = 'Forbidden', ConnectionRefused = 'ConnectionRefused', RemotePreconditionFailed = 'RemotePreconditionFailed', TooLarge = 'TooLarge', NoRef = 'NoRef', TurnedOff = 'TurnedOff', SessionExpired = 'SessionExpired', // Local Errors LocalPreconditionFailed = 'LocalPreconditionFailed', LocalInvalidContent = 'LocalInvalidContent', LocalError = 'LocalError', Incompatible = 'Incompatible', Unknown = 'Unknown', } export class UserDataSyncError extends Error { constructor(message: string, public readonly code: UserDataSyncErrorCode, public readonly source?: SyncSource) { super(message); this.name = `${this.code} (UserDataSyncError) ${this.source}`; } static toUserDataSyncError(error: Error): UserDataSyncError { if (error instanceof UserDataSyncStoreError) { return error; } const match = /^(.+) \(UserDataSyncError\) (.+)?$/.exec(error.name); if (match && match[1]) { return new UserDataSyncError(error.message, match[1], match[2]); } return new UserDataSyncError(error.message, UserDataSyncErrorCode.Unknown); } } export class UserDataSyncStoreError extends UserDataSyncError { } //#endregion // #region User Data Synchroniser export interface ISyncExtension { identifier: IExtensionIdentifier; version?: string; disabled?: boolean; } export interface IGlobalState { argv: IStringDictionary; storage: IStringDictionary; } export const enum SyncSource { Settings = 'Settings', Keybindings = 'Keybindings', Extensions = 'Extensions', GlobalState = 'GlobalState' } export const enum SyncStatus { Uninitialized = 'uninitialized', Idle = 'idle', Syncing = 'syncing', HasConflicts = 'hasConflicts', } export interface IUserDataSynchroniser { readonly resourceKey: ResourceKey; readonly source: SyncSource; readonly status: SyncStatus; readonly onDidChangeStatus: Event; readonly onDidChangeLocal: Event; pull(): Promise; push(): Promise; sync(ref?: string): Promise; stop(): Promise; hasPreviouslySynced(): Promise hasLocalData(): Promise; resetLocal(): Promise; getRemoteContent(preivew?: boolean): Promise; accept(content: string): Promise; } //#endregion // #region User Data Sync Services export const IUserDataSyncEnablementService = createDecorator('IUserDataSyncEnablementService'); export interface IUserDataSyncEnablementService { _serviceBrand: any; readonly onDidChangeEnablement: Event; readonly onDidChangeResourceEnablement: Event<[ResourceKey, boolean]>; isEnabled(): boolean; setEnablement(enabled: boolean): void; isResourceEnabled(key: ResourceKey): boolean; setResourceEnablement(key: ResourceKey, enabled: boolean): void; } export const IUserDataSyncService = createDecorator('IUserDataSyncService'); export interface IUserDataSyncService { _serviceBrand: any; readonly status: SyncStatus; readonly onDidChangeStatus: Event; readonly conflictsSources: SyncSource[]; readonly onDidChangeConflicts: Event; readonly onDidChangeLocal: Event; readonly onSyncErrors: Event<[SyncSource, UserDataSyncError][]>; readonly lastSyncTime: number | undefined; readonly onDidChangeLastSyncTime: Event; pull(): Promise; sync(): Promise; stop(): Promise; reset(): Promise; resetLocal(): Promise; isFirstTimeSyncWithMerge(): Promise; getRemoteContent(source: SyncSource, preview: boolean): Promise; accept(source: SyncSource, content: string): Promise; } export const IUserDataAutoSyncService = createDecorator('IUserDataAutoSyncService'); export interface IUserDataAutoSyncService { _serviceBrand: any; readonly onError: Event; triggerAutoSync(): Promise; } export const IUserDataSyncUtilService = createDecorator('IUserDataSyncUtilService'); export interface IUserDataSyncUtilService { _serviceBrand: undefined; resolveUserBindings(userbindings: string[]): Promise>; resolveFormattingOptions(resource: URI): Promise; resolveDefaultIgnoredSettings(): Promise; } export const IUserDataSyncLogService = createDecorator('IUserDataSyncLogService'); export interface IUserDataSyncLogService extends ILogService { } export interface IConflictSetting { key: string; localValue: any | undefined; remoteValue: any | undefined; } export const ISettingsSyncService = createDecorator('ISettingsSyncService'); export interface ISettingsSyncService extends IUserDataSynchroniser { _serviceBrand: any; readonly onDidChangeConflicts: Event; readonly conflicts: IConflictSetting[]; resolveSettingsConflicts(resolvedConflicts: { key: string, value: any | undefined }[]): Promise; } //#endregion 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 function toRemoteContentResource(source: SyncSource): URI { return URI.from({ scheme: USER_DATA_SYNC_SCHEME, path: `${source}/remoteContent` }); } export function getSyncSourceFromRemoteContentResource(uri: URI): SyncSource | undefined { return [SyncSource.Settings, SyncSource.Keybindings, SyncSource.Extensions, SyncSource.GlobalState].filter(source => isEqual(uri, toRemoteContentResource(source)))[0]; } export function getSyncSourceFromPreviewResource(uri: URI, environmentService: IEnvironmentService): SyncSource | undefined { if (isEqual(uri, environmentService.settingsSyncPreviewResource)) { return SyncSource.Settings; } if (isEqual(uri, environmentService.keybindingsSyncPreviewResource)) { return SyncSource.Keybindings; } return undefined; }