diff --git a/build/lib/i18n.resources.json b/build/lib/i18n.resources.json index 570d97669c7..05b56dbb65d 100644 --- a/build/lib/i18n.resources.json +++ b/build/lib/i18n.resources.json @@ -106,6 +106,10 @@ "name": "vs/workbench/contrib/quickopen", "project": "vscode-workbench" }, + { + "name": "vs/workbench/contrib/userData", + "project": "vscode-workbench" + }, { "name": "vs/workbench/contrib/remote", "project": "vscode-workbench" diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 665eb1130c1..dbe1de14c9e 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -1194,4 +1194,32 @@ declare module 'vscode' { } //#endregion + + // #region Sandy - User data synchronization + + export namespace window { + + export function registerUserLoginProvider(identity: string, userLoginProvider: UserLoginProvider): Disposable; + + export function registerUserDataProvider(identity: string, userDataProvider: UserDataProvider): Disposable; + + } + + export interface UserDataProvider { + + dataProvider: FileSystemProvider; + + } + + export interface UserLoginProvider { + + isLoggedin(): boolean; + + readonly onDidChange: Event; + + login(): Promise; + + } + + //#endregion } diff --git a/src/vs/workbench/api/browser/extensionHost.contribution.ts b/src/vs/workbench/api/browser/extensionHost.contribution.ts index 2905c524113..3201bfc181d 100644 --- a/src/vs/workbench/api/browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/browser/extensionHost.contribution.ts @@ -18,6 +18,7 @@ import './mainThreadCodeInsets'; import './mainThreadClipboard'; import './mainThreadCommands'; import './mainThreadConfiguration'; +import './mainThreadUserData'; import './mainThreadConsole'; import './mainThreadDebugService'; import './mainThreadDecorations'; diff --git a/src/vs/workbench/api/browser/mainThreadUserData.ts b/src/vs/workbench/api/browser/mainThreadUserData.ts new file mode 100644 index 00000000000..af63864ffb0 --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadUserData.ts @@ -0,0 +1,79 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { MainContext, ExtHostContext, IExtHostContext, MainThreadUserDataShape, ExtHostUserDataShape } from '../common/extHost.protocol'; +import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; +import { IUserDataProviderService, IUserIdentityService, IUserDataProvider, IUserLoginProvider } from 'vs/workbench/services/userData/common/userData'; +import { Emitter, Event } from 'vs/base/common/event'; + +@extHostNamedCustomer(MainContext.MainThreadUserData) +export class MainThreadUserData extends Disposable implements MainThreadUserDataShape { + + private readonly proxy: ExtHostUserDataShape; + private readonly loginProviders: Map = new Map(); + + constructor( + extHostContext: IExtHostContext, + @IUserIdentityService private readonly userIdentityService: IUserIdentityService, + @IUserDataProviderService private readonly userDataProviderService: IUserDataProviderService, + ) { + super(); + this.proxy = extHostContext.getProxy(ExtHostContext.ExtHostUserData); + this._register(toDisposable(() => { + this.userDataProviderService.deregisterAll(); + this.loginProviders.forEach((loginProvider, identity) => this.userIdentityService.deregisterUserLoginProvider(identity)); + this.loginProviders.clear(); + })); + } + + $registerUserLoginProvider(identity: string, loggedIn: boolean): void { + const userLoginProvider = new UserLoginProvider(identity, loggedIn, this.proxy); + this.loginProviders.set(identity, userLoginProvider); + this.userIdentityService.registerUserLoginProvider(identity, userLoginProvider); + } + + $registerUserDataProvider(identity: string, userDataProvider: IUserDataProvider): void { + this.userDataProviderService.registerUserDataProvider(identity, userDataProvider); + } + + $updateLoggedIn(identity: string, loggedIn: boolean): void { + const loginProvider = this.loginProviders.get(identity); + if (loginProvider) { + loginProvider.loggedIn = loggedIn; + } + } + +} + + +class UserLoginProvider extends Disposable implements IUserLoginProvider { + + private _onDidChange: Emitter = this._register(new Emitter()); + readonly onDidChange: Event = this._onDidChange.event; + + private _loggedIn: boolean; + get loggedIn(): boolean { return this._loggedIn; } + set loggedIn(loggedIn: boolean) { + if (this._loggedIn !== loggedIn) { + this._loggedIn = loggedIn; + this._onDidChange.fire(); + } + } + + constructor(private readonly identity: string, loggedIn: boolean, private readonly proxy: ExtHostUserDataShape) { + super(); + this._loggedIn = loggedIn; + } + + login(): Promise { + return this.proxy.$logIn(this.identity); + } + + logout(): Promise { + return this.proxy.$logOut(this.identity); + } + +} diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 5b4a69da168..cafd0e18e9a 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -68,6 +68,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IURITransformerService } from 'vs/workbench/api/common/extHostUriTransformerService'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService'; +import { ExtHostUserData } from 'vs/workbench/api/common/extHostUserData'; export interface IExtensionApiFactory { (extension: IExtensionDescription, registry: ExtensionDescriptionRegistry, configProvider: ExtHostConfigProvider): typeof vscode; @@ -132,6 +133,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostWindow = rpcProtocol.set(ExtHostContext.ExtHostWindow, new ExtHostWindow(rpcProtocol)); const extHostProgress = rpcProtocol.set(ExtHostContext.ExtHostProgress, new ExtHostProgress(rpcProtocol.getProxy(MainContext.MainThreadProgress))); const extHostLabelService = rpcProtocol.set(ExtHostContext.ExtHosLabelService, new ExtHostLabelService(rpcProtocol)); + const extHostUserData = rpcProtocol.set(ExtHostContext.ExtHostUserData, new ExtHostUserData(rpcProtocol.getProxy(MainContext.MainThreadUserData), extHostFileSystem)); // Check that no named customers are missing const expected: ProxyIdentifier[] = values(ExtHostContext); @@ -541,7 +543,13 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I }, createInputBox(): vscode.InputBox { return extHostQuickOpen.createInputBox(extension.identifier); - } + }, + registerUserDataProvider: proposedApiFunction(extension, (identity: string, userDataProvider: vscode.UserDataProvider): vscode.Disposable => { + return extHostUserData.registerUserDataProvider(identity, userDataProvider); + }), + registerUserLoginProvider: proposedApiFunction(extension, (identity: string, userLoginProvider: vscode.UserLoginProvider): vscode.Disposable => { + return extHostUserData.registerUserLoginProvider(identity, userLoginProvider); + }) }; // namespace: workspace diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index d6ca16a738c..60cbdb012f4 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -47,6 +47,7 @@ import { ExtensionActivationError } from 'vs/workbench/services/extensions/commo import { createExtHostContextProxyIdentifier as createExtId, createMainContextProxyIdentifier as createMainId, IRPCProtocol } from 'vs/workbench/services/extensions/common/proxyIdentifier'; import * as search from 'vs/workbench/services/search/common/search'; import { SaveReason } from 'vs/workbench/services/textfile/common/textfiles'; +import { IUserDataProvider } from 'vs/workbench/services/userData/common/userData'; export interface IEnvironment { isExtensionDevelopmentDebug: boolean; @@ -140,6 +141,12 @@ export interface MainThreadConfigurationShape extends IDisposable { $removeConfigurationOption(target: ConfigurationTarget | null, key: string, resource: UriComponents | undefined): Promise; } +export interface MainThreadUserDataShape extends IDisposable { + $registerUserLoginProvider(identitiy: string, loggedIn: boolean): void; + $updateLoggedIn(identitiy: string, loggedIn: boolean): void; + $registerUserDataProvider(identitiy: string, userDataProvider: IUserDataProvider): void; +} + export interface MainThreadDiagnosticsShape extends IDisposable { $changeMany(owner: string, entries: [UriComponents, IMarkerData[] | undefined][]): void; $clear(owner: string): void; @@ -743,6 +750,11 @@ export interface ExtHostConfigurationShape { $acceptConfigurationChanged(data: IConfigurationInitData, eventData: IWorkspaceConfigurationChangeEventData): void; } +export interface ExtHostUserDataShape { + $logIn(identity: string): Promise; + $logOut(identity: string): Promise; +} + export interface ExtHostDiagnosticsShape { $acceptMarkersChange(data: [UriComponents, IMarkerData[]][]): void; } @@ -1316,6 +1328,7 @@ export const MainContext = { MainThreadCommands: createMainId('MainThreadCommands'), MainThreadComments: createMainId('MainThreadComments'), MainThreadConfiguration: createMainId('MainThreadConfiguration'), + MainThreadUserData: createMainId('MainThreadUserData'), MainThreadConsole: createMainId('MainThreadConsole'), MainThreadDebugService: createMainId('MainThreadDebugService'), MainThreadDecorations: createMainId('MainThreadDecorations'), @@ -1355,6 +1368,7 @@ export const MainContext = { export const ExtHostContext = { ExtHostCommands: createExtId('ExtHostCommands'), ExtHostConfiguration: createExtId('ExtHostConfiguration'), + ExtHostUserData: createExtId('ExtHostUserData'), ExtHostDiagnostics: createExtId('ExtHostDiagnostics'), ExtHostDebugService: createExtId('ExtHostDebugService'), ExtHostDecorations: createExtId('ExtHostDecorations'), diff --git a/src/vs/workbench/api/common/extHostUserData.ts b/src/vs/workbench/api/common/extHostUserData.ts new file mode 100644 index 00000000000..5472a0542b5 --- /dev/null +++ b/src/vs/workbench/api/common/extHostUserData.ts @@ -0,0 +1,53 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ExtHostUserDataShape, MainThreadUserDataShape } from './extHost.protocol'; +import { ExtHostFileSystem } from 'vs/workbench/api/common/extHostFileSystem'; +import * as vscode from 'vscode'; +import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; + +export class ExtHostUserData implements ExtHostUserDataShape { + + private readonly loginProviders: Map = new Map(); + + constructor( + private readonly proxy: MainThreadUserDataShape, + private readonly extHostFileSystem: ExtHostFileSystem + ) { + } + + registerUserDataProvider(identity: string, userDataProvider: vscode.UserDataProvider): vscode.Disposable { + const userDataScheme = `vscode-userdata-${identity}`; + const disposable = this.extHostFileSystem.registerFileSystemProvider(userDataScheme, userDataProvider.dataProvider); + this.proxy.$registerUserDataProvider(identity, { userDataScheme }); + return disposable; + } + + registerUserLoginProvider(identity: string, loginProvider: vscode.UserLoginProvider): vscode.Disposable { + this.loginProviders.set(identity, loginProvider); + this.proxy.$registerUserLoginProvider(identity, loginProvider.isLoggedin()); + const disposable = new DisposableStore(); + disposable.add(loginProvider.onDidChange(() => this.proxy.$updateLoggedIn(identity, loginProvider.isLoggedin()))); + disposable.add(toDisposable(() => this.loginProviders.delete(identity))); + return disposable; + } + + async $logIn(identity: string): Promise { + const loginProvider = this.loginProviders.get(identity); + if (!loginProvider) { + return Promise.reject(new Error(`No login provider found for ${identity}`)); + } + await loginProvider.login(); + } + + $logOut(identity: string): Promise { + const loginProvider = this.loginProviders.get(identity); + if (!loginProvider) { + return Promise.reject(new Error(`No login provider found for ${identity}`)); + } + return Promise.resolve(); + } + +} diff --git a/src/vs/workbench/api/common/userDataExtensionPoint.ts b/src/vs/workbench/api/common/userDataExtensionPoint.ts new file mode 100644 index 00000000000..25a507428ed --- /dev/null +++ b/src/vs/workbench/api/common/userDataExtensionPoint.ts @@ -0,0 +1,113 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; +import { IJSONSchema } from 'vs/base/common/jsonSchema'; +import { ExtensionMessageCollector, ExtensionsRegistry, IExtensionPoint, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IUserIdentityService, IUserIdentity } from 'vs/workbench/services/userData/common/userData'; + +export interface IUserFriendlyUserIdentityDescriptor { + id: string; + title: string; + iconText: string; +} + +export const userIdentityContribution: IJSONSchema = { + description: localize('vscode.extension.contributes.userIdentity', 'Contributes user identity to the editor'), + type: 'object', + properties: { + id: { + description: localize({ key: 'vscode.extension.contributes.user.identity.id', comment: ['Contribution refers to those that an extension contributes to VS Code through an extension/contribution point. '] }, "Unique id to identify the user user identity"), + type: 'string', + pattern: '^[a-zA-Z0-9_-]+$' + }, + title: { + description: localize('vscode.extension.contributes.views.containers.title', 'Human readable string used to render the user identity'), + type: 'string' + }, + iconText: { + description: localize('vscode.extension.contributes.views.containers.icon', "Path to the user identity icon."), + type: 'string' + } + } +}; + + +const viewsContainersExtensionPoint: IExtensionPoint = ExtensionsRegistry.registerExtensionPoint({ + extensionPoint: 'userIdentity', + jsonSchema: userIdentityContribution +}); + +class UserIdentityExtensionHandler implements IWorkbenchContribution { + + constructor( + @IUserIdentityService private readonly userIdentityService: IUserIdentityService + ) { + this.handleAndRegisterUserIdentities(); + } + + private handleAndRegisterUserIdentities() { + viewsContainersExtensionPoint.setHandler((extensions, { added, removed }) => { + if (removed.length) { + this.removeUserIdentities(removed); + } + if (added.length) { + this.addUserIdentities(added); + } + }); + } + + private addUserIdentities(extensionPoints: readonly IExtensionPointUser[]) { + const userIdentities: IUserIdentity[] = []; + for (let { value, collector } of extensionPoints) { + if (!this.isValidUserIdentity(value, collector)) { + return; + } + userIdentities.push({ + identity: value.id, + title: value.title, + iconText: `$(${value.iconText})` + }); + } + if (userIdentities.length) { + this.userIdentityService.registerUserIdentities(userIdentities); + } + } + + private removeUserIdentities(extensionPoints: readonly IExtensionPointUser[]) { + const identities = extensionPoints.map(({ value }) => value.id); + if (identities.length) { + this.userIdentityService.deregisterUserIdentities(identities); + } + } + + + private isValidUserIdentity(userIdentityDescriptor: IUserFriendlyUserIdentityDescriptor, collector: ExtensionMessageCollector): boolean { + if (typeof userIdentityDescriptor.id !== 'string') { + collector.error(localize('requireidstring', "property `{0}` is mandatory and must be of type `string`. Only alphanumeric characters, '_', and '-' are allowed.", 'id')); + return false; + } + if (!(/^[a-z0-9_-]+$/i.test(userIdentityDescriptor.id))) { + collector.error(localize('requireidstring', "property `{0}` is mandatory and must be of type `string`. Only alphanumeric characters, '_', and '-' are allowed.", 'id')); + return false; + } + if (typeof userIdentityDescriptor.title !== 'string') { + collector.error(localize('requirestring', "property `{0}` is mandatory and must be of type `string`", 'title')); + return false; + } + if (typeof userIdentityDescriptor.iconText !== 'string') { + collector.error(localize('requirestring', "property `{0}` is mandatory and must be of type `string`", 'icon')); + return false; + } + return true; + } + +} + +const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); +workbenchRegistry.registerWorkbenchContribution(UserIdentityExtensionHandler, LifecyclePhase.Starting); diff --git a/src/vs/workbench/contrib/userData/common/userData.contribution.ts b/src/vs/workbench/contrib/userData/common/userData.contribution.ts new file mode 100644 index 00000000000..5a02c216206 --- /dev/null +++ b/src/vs/workbench/contrib/userData/common/userData.contribution.ts @@ -0,0 +1,209 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { IUserIdentityService, IUserDataProviderService, IUserIdentity } from 'vs/workbench/services/userData/common/userData'; +import { IStatusbarService, StatusbarAlignment, IStatusbarEntryAccessor } from 'vs/platform/statusbar/common/statusbar'; +import { localize } from 'vs/nls'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { Action } from 'vs/base/common/actions'; +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { ICommandService, CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { Event } from 'vs/base/common/event'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; + +Registry.as(ConfigurationExtensions.Configuration) + .registerConfiguration({ + id: 'userData', + order: 30, + title: localize('user data', "User Data"), + type: 'object', + properties: { + 'userData.autoSync': { + type: 'boolean', + description: localize('userData.autoSync', "When enabled, automatically gets user data. User data is fetched from the installed user data sync extension."), + default: false, + scope: ConfigurationScope.APPLICATION + } + } + }); + +class UserDataExtensionActivationContribution extends Disposable implements IWorkbenchContribution { + + constructor( + @IConfigurationService private readonly configurationService: IConfigurationService, + @IUserIdentityService private readonly userIdentityService: IUserIdentityService, + @IExtensionService private readonly extensionService: IExtensionService, + ) { + super(); + this.activateUserDataExtensionsOnAutoSync(); + this._register(Event.any(this.userIdentityService.onDidDeregisterUserIdentities, this.configurationService.onDidChangeConfiguration)(() => this.activateUserDataExtensionsOnAutoSync())); + } + + private activateUserDataExtensionsOnAutoSync(): void { + if (this.configurationService.getValue('userData.autoSync')) { + this.userIdentityService.getUserIndetities().forEach(({ identity }) => this.extensionService.activateByEvent(`onUserData:${identity}`)); + } + } + +} + +class UserDataSyncStatusContribution extends Disposable implements IWorkbenchContribution { + + private readonly userDataSyncStatusAccessor: IStatusbarEntryAccessor; + + constructor( + @IUserIdentityService private userIdentityService: IUserIdentityService, + @IStatusbarService private statusbarService: IStatusbarService + ) { + super(); + this.userDataSyncStatusAccessor = this.statusbarService.addEntry({ + text: '', + command: ShowUserDataSyncActions.ID + }, 'userDataSyncStatusEntry', '', StatusbarAlignment.LEFT, 1); + this.updateUserDataSyncStatusAccessor(); + this._register(Event.any( + this.userIdentityService.onDidRegisterUserIdentities, this.userIdentityService.onDidDeregisterUserIdentities, + this.userIdentityService.onDidRegisterUserLoginProvider, this.userIdentityService.onDidDeregisterUserLoginProvider) + (() => this.updateUserDataSyncStatusAccessor())); + this._register(this.userIdentityService.onDidRegisterUserLoginProvider((identity => this.onDidRegisterUserLoginProvider(identity)))); + } + + private onDidRegisterUserLoginProvider(identity: string): void { + const userLoginProvider = this.userIdentityService.getUserLoginProvider(identity); + if (userLoginProvider) { + this._register(userLoginProvider.onDidChange(() => this.updateUserDataSyncStatusAccessor())); + } + } + + private updateUserDataSyncStatusAccessor(): void { + const userIdentity = this.userIdentityService.getUserIndetities()[0]; + if (userIdentity) { + const loginProvider = this.userIdentityService.getUserLoginProvider(userIdentity.identity); + const neededSignIn = loginProvider && !loginProvider.loggedIn; + const text = this.getText(userIdentity, !!neededSignIn); + this.userDataSyncStatusAccessor.update({ text, command: ShowUserDataSyncActions.ID }); + this.statusbarService.updateEntryVisibility('userDataSyncStatusEntry', true); + } else { + this.statusbarService.updateEntryVisibility('userDataSyncStatusEntry', false); + } + } + + private getText(userIdentity: IUserIdentity, neededSignIn: boolean): string { + if (neededSignIn) { + const signinText = localize('sign in', "{0}: Sign in to sync", userIdentity.title); + return userIdentity.iconText ? `${userIdentity.iconText} ${signinText}` : signinText; + } + if (userIdentity.iconText) { + return `${userIdentity.iconText} ${localize('sync user data', "{0}: Sync", userIdentity.title)}`; + } + return userIdentity.title; + } +} + +const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); +workbenchRegistry.registerWorkbenchContribution(UserDataSyncStatusContribution, LifecyclePhase.Starting); +workbenchRegistry.registerWorkbenchContribution(UserDataExtensionActivationContribution, LifecyclePhase.Ready); + +export class ShowUserDataSyncActions extends Action { + + public static ID: string = 'workbench.userData.actions.showUserDataSyncActions'; + public static LABEL: string = localize('workbench.userData.actions.showUserDataSyncActions.label', "Show User Data Sync Actions"); + + constructor( + @IQuickInputService private readonly quickInputService: IQuickInputService, + @IUserDataProviderService private readonly userDataProviderService: IUserDataProviderService, + @IUserIdentityService private userIdentityService: IUserIdentityService, + @ICommandService private commandService: ICommandService, + @IExtensionService private extensionService: IExtensionService, + @IStorageService private storageService: IStorageService, + @IDialogService private dialogService: IDialogService, + @IConfigurationService private configurationService: IConfigurationService, + ) { + super(ShowUserDataSyncActions.ID, ShowUserDataSyncActions.LABEL); + } + + async run(): Promise { + const userIdentity = this.userIdentityService.getUserIndetities()[0]; + if (!userIdentity) { + return; + } + + await this.extensionService.activateByEvent(`onUserData:${userIdentity.identity}`); + const userDataProvider = this.userDataProviderService.getUserDataProvider(userIdentity.identity); + if (!userDataProvider) { + return; + } + + const loginProvider = this.userIdentityService.getUserLoginProvider(userIdentity.identity); + if (loginProvider && !loginProvider.loggedIn) { + if (!await this.promptToSigin(userIdentity)) { + return; + } + await loginProvider.login(); + if (!loginProvider.loggedIn) { + return; + } + } + return this.showSyncActions(); + } + + private async promptToSigin(userIdentity: IUserIdentity): Promise { + if (!this.storageService.getBoolean(`workbench.userData.${userIdentity.identity}.donotAskSignIn`, StorageScope.GLOBAL, false)) { + const result = await this.dialogService.confirm({ + message: localize('sign in to start sync', "Sign In to Start Sync"), + detail: localize('sign in deatils', "Sign in to {0} to get your settings, keybindings, snippets and extensions.", userIdentity.title), + primaryButton: localize('sign in primary', "Sign In"), + secondaryButton: localize('no thanks', "No Thanks"), + checkbox: { + label: localize('doNotAskAgain', "Do not ask me again") + }, + }); + if (result.confirmed && result.checkboxChecked) { + this.storageService.store(`workbench.userData.${userIdentity.identity}.donotAskSignIn`, true, StorageScope.GLOBAL); + } + return result.confirmed; + } + return true; + } + + private async showSyncActions(): Promise { + const autoSync = this.configurationService.getValue('userData.autoSync'); + const picks = []; + if (autoSync) { + picks.push({ label: localize('turn off sync', "Sync: Turn Off Sync"), id: 'workbench.userData.actions.stopAutoSync' }); + } else { + picks.push({ label: localize('sync', "Sync: Start Sync"), id: 'workbench.userData.actions.startSync' }); + picks.push({ label: localize('turn on sync', "Sync: Turn On Auto Sync"), id: 'workbench.userData.actions.startAutoSync' }); + } + picks.push({ label: localize('customise', "Sync: Settings"), id: 'workbench.userData.actions.syncSettings' }); + const result = await this.quickInputService.pick(picks, { canPickMany: false }); + if (result && result.id) { + return this.commandService.executeCommand(result.id); + } + } +} + +CommandsRegistry.registerCommand(ShowUserDataSyncActions.ID, (serviceAccessor) => { + const instantiationService = serviceAccessor.get(IInstantiationService); + return instantiationService.createInstance(ShowUserDataSyncActions).run(); +}); + +CommandsRegistry.registerCommand('workbench.userData.actions.stopAutoSync', (serviceAccessor) => { + const configurationService = serviceAccessor.get(IConfigurationService); + return configurationService.updateValue('userData.autoSync', false); +}); + +CommandsRegistry.registerCommand('workbench.userData.actions.startAutoSync', (serviceAccessor) => { + const configurationService = serviceAccessor.get(IConfigurationService); + return configurationService.updateValue('userData.autoSync', true); +}); diff --git a/src/vs/workbench/services/extensions/common/extensionsRegistry.ts b/src/vs/workbench/services/extensions/common/extensionsRegistry.ts index f5fd3137e47..4f2765ac997 100644 --- a/src/vs/workbench/services/extensions/common/extensionsRegistry.ts +++ b/src/vs/workbench/services/extensions/common/extensionsRegistry.ts @@ -267,6 +267,11 @@ export const schema = { body: 'onView:${5:viewId}', description: nls.localize('vscode.extension.activationEvents.onView', 'An activation event emitted whenever the specified view is expanded.'), }, + { + label: 'onIdentity', + body: 'onIdentity:${8:identity}', + description: nls.localize('vscode.extension.activationEvents.onIdentity', 'An activation event emitted whenever the specified user identity.'), + }, { label: 'onUri', body: 'onUri', diff --git a/src/vs/workbench/services/userData/common/userData.ts b/src/vs/workbench/services/userData/common/userData.ts new file mode 100644 index 00000000000..80dc4ae571c --- /dev/null +++ b/src/vs/workbench/services/userData/common/userData.ts @@ -0,0 +1,100 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { URI } from 'vs/base/common/uri'; +import { Event } from 'vs/base/common/event'; +import { IExtensionIdentifier } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { Schemas } from 'vs/base/common/network'; + +export interface IUserLoginProvider { + + readonly loggedIn: boolean; + + readonly onDidChange: Event; + + login(): Promise; + + logout(): Promise; + +} + +export interface IUserIdentity { + identity: string; + title: string; + iconText?: string; +} + +export const IUserIdentityService = createDecorator('IUserIdentityService'); + +export interface IUserIdentityService { + + _serviceBrand: any; + + readonly onDidRegisterUserIdentities: Event; + + readonly onDidDeregisterUserIdentities: Event; + + readonly onDidRegisterUserLoginProvider: Event; + + readonly onDidDeregisterUserLoginProvider: Event; + + registerUserIdentities(userIdentities: IUserIdentity[]): void; + + deregisterUserIdentities(identities: string[]): void; + + registerUserLoginProvider(identity: string, userLoginProvider: IUserLoginProvider): void; + + deregisterUserLoginProvider(identity: string): void; + + getUserIndetities(): ReadonlyArray; + + getUserIdentity(identity: string): IUserIdentity | null; + + getUserLoginProvider(identity: string): IUserLoginProvider | null; +} + +export interface IUserDataProvider { + + userDataScheme: string; + +} + +export const IUserDataProviderService = createDecorator('IUserDataProviderService'); + +export interface IUserDataProviderService { + + _serviceBrand: any; + + registerUserDataProvider(identity: string, userDataProvider: IUserDataProvider): void; + + deregisterAll(): void; + + getUserDataProvider(identity: string): IUserDataProvider | null; + +} + +export const IUserDataSyncService = createDecorator('IUserDataSyncService'); + + +export const USER_DATA_SETTINGS_RESOURCE = URI.file('settings.json').with({ scheme: Schemas.userData }); +export const USER_DATA_KEYBINDINGS_RESOURCE = URI.file('keybindings.json').with({ scheme: Schemas.userData }); +export const USER_DATA_SNIPPETS_RESOURCE = URI.file('snippets').with({ scheme: Schemas.userData }); +export const USER_DATA_EXTENSIONS_RESOURCE = URI.file('extensions.json').with({ scheme: Schemas.userData }); + +export interface IUserDataExtension { + identifier: IExtensionIdentifier; + version?: string; +} + +export interface IUserDataSyncService { + + _serviceBrand: any; + + synchronise(): Promise; + + getExtensions(): Promise; + +} diff --git a/src/vs/workbench/services/userData/common/userDataProviderService.ts b/src/vs/workbench/services/userData/common/userDataProviderService.ts new file mode 100644 index 00000000000..330705482b8 --- /dev/null +++ b/src/vs/workbench/services/userData/common/userDataProviderService.ts @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IUserDataProviderService, IUserDataProvider } from 'vs/workbench/services/userData/common/userData'; +import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { Emitter, Event } from 'vs/base/common/event'; + +export class UserDataProviderService extends Disposable implements IUserDataProviderService { + + _serviceBrand: any; + + private readonly userDataProviders: Map = new Map(); + private activeUserDataProvider: IUserDataProvider | null = null; + + private readonly _ondDidChangeActiveUserDataProvider: Emitter = this._register(new Emitter()); + readonly ondDidChangeActiveUserDataProvider: Event = this._ondDidChangeActiveUserDataProvider.event; + + constructor() { + super(); + this._register(toDisposable(() => this.userDataProviders.clear())); + } + + registerUserDataProvider(identity: string, userDataProvider: IUserDataProvider): void { + this.userDataProviders.set(identity, userDataProvider); + this.activeUserDataProvider = userDataProvider; + this._ondDidChangeActiveUserDataProvider.fire(); + } + + deregisterAll(): void { + this.userDataProviders.clear(); + this.activeUserDataProvider = null; + this._ondDidChangeActiveUserDataProvider.fire(); + } + + getUserDataProvider(identity: string): IUserDataProvider | null { + return this.userDataProviders.get(identity) || null; + } + + getActiveUserDataProvider(): IUserDataProvider | null { + return this.activeUserDataProvider; + } + +} + +registerSingleton(IUserDataProviderService, UserDataProviderService); diff --git a/src/vs/workbench/services/userData/common/userDataSyncService.ts b/src/vs/workbench/services/userData/common/userDataSyncService.ts new file mode 100644 index 00000000000..da54862db48 --- /dev/null +++ b/src/vs/workbench/services/userData/common/userDataSyncService.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IUserDataSyncService, IUserDataProviderService, IUserDataExtension } from 'vs/workbench/services/userData/common/userData'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; + +export class UserDataSyncService extends Disposable implements IUserDataSyncService { + + _serviceBrand: any; + + constructor( + @IUserDataProviderService private readonly userDataProviderService: IUserDataProviderService + ) { + super(); + } + + synchronise(): Promise { + return Promise.resolve(); + } + + getExtensions(): Promise { + return Promise.resolve([]); + } + +} + +registerSingleton(IUserDataSyncService, UserDataSyncService); diff --git a/src/vs/workbench/services/userData/common/userIdentityService.ts b/src/vs/workbench/services/userData/common/userIdentityService.ts new file mode 100644 index 00000000000..8de526bb91d --- /dev/null +++ b/src/vs/workbench/services/userData/common/userIdentityService.ts @@ -0,0 +1,90 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IUserIdentityService, IUserIdentity, IUserLoginProvider } from 'vs/workbench/services/userData/common/userData'; +import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { values } from 'vs/base/common/map'; +import { Emitter, Event } from 'vs/base/common/event'; + +export class UserIdentityService extends Disposable implements IUserIdentityService { + + _serviceBrand: any; + + private readonly userIdentities: Map = new Map(); + private readonly userLoginProviders: Map = new Map(); + + private readonly _onDidRegisterUserIdentities: Emitter = this._register(new Emitter()); + readonly onDidRegisterUserIdentities: Event = this._onDidRegisterUserIdentities.event; + + private readonly _onDidDeregisterUserIdentities: Emitter = this._register(new Emitter()); + readonly onDidDeregisterUserIdentities: Event = this._onDidDeregisterUserIdentities.event; + + private readonly _onDidRegisterUserLoginProvider: Emitter = this._register(new Emitter()); + readonly onDidRegisterUserLoginProvider: Event = this._onDidRegisterUserLoginProvider.event; + + private readonly _onDidDeregisterUserLoginProvider: Emitter = this._register(new Emitter()); + readonly onDidDeregisterUserLoginProvider: Event = this._onDidDeregisterUserLoginProvider.event; + + constructor() { + super(); + this._register(toDisposable(() => { + this.userIdentities.clear(); + this.userLoginProviders.clear(); + })); + } + + registerUserIdentities(userIdentities: IUserIdentity[]): void { + const registered: IUserIdentity[] = []; + for (const userIdentity of userIdentities) { + if (!this.userIdentities.has(userIdentity.identity)) { + this.userIdentities.set(userIdentity.identity, userIdentity); + registered.push(userIdentity); + } + } + this._onDidRegisterUserIdentities.fire(registered); + } + + deregisterUserIdentities(identities: string[]): void { + const deregistered: IUserIdentity[] = []; + for (const identity of identities) { + const userIdentity = this.userIdentities.get(identity); + if (userIdentity) { + this.userIdentities.delete(identity); + deregistered.push(userIdentity); + } + } + this._onDidDeregisterUserIdentities.fire(deregistered); + } + + registerUserLoginProvider(identity: string, userLoginProvider: IUserLoginProvider): void { + if (!this.userLoginProviders.has(identity)) { + this.userLoginProviders.set(identity, userLoginProvider); + this._onDidRegisterUserLoginProvider.fire(identity); + } + } + + deregisterUserLoginProvider(identity: string): void { + if (this.userLoginProviders.has(identity)) { + this.userLoginProviders.delete(identity); + this._onDidDeregisterUserLoginProvider.fire(identity); + } + } + + getUserIdentity(identity: string): IUserIdentity | null { + return this.userIdentities.get(identity) || null; + } + + getUserIndetities(): ReadonlyArray { + return values(this.userIdentities); + } + + getUserLoginProvider(identity: string): IUserLoginProvider | null { + return this.userLoginProviders.get(identity) || null; + } + +} + +registerSingleton(IUserIdentityService, UserIdentityService); diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index 2fdb06fe058..c8c4b433ce8 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -30,6 +30,7 @@ import 'vs/workbench/browser/parts/quickinput/quickInputActions'; import 'vs/workbench/api/common/menusExtensionPoint'; import 'vs/workbench/api/common/configurationExtensionPoint'; +import 'vs/workbench/api/common/userDataExtensionPoint'; import 'vs/workbench/api/browser/viewsExtensionPoint'; //#endregion @@ -74,6 +75,9 @@ import 'vs/workbench/services/themes/browser/workbenchThemeService'; import 'vs/workbench/services/label/common/labelService'; import 'vs/workbench/services/extensionManagement/common/extensionEnablementService'; import 'vs/workbench/services/notification/common/notificationService'; +import 'vs/workbench/services/userData/common/userIdentityService'; +import 'vs/workbench/services/userData/common/userDataProviderService'; +import 'vs/workbench/services/userData/common/userDataSyncService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionGalleryService'; @@ -236,4 +240,7 @@ import 'vs/workbench/contrib/experiments/browser/experiments.contribution'; // Send a Smile import 'vs/workbench/contrib/feedback/browser/feedback.contribution'; +// User Data +import 'vs/workbench/contrib/userData/common/userData.contribution'; + //#endregion