提交 c26e198c 编写于 作者: S Sandeep Somavarapu

settings sync using remote user data service

上级 6706c179
......@@ -277,6 +277,10 @@
{
"name": "vs/workbench/services/notification",
"project": "vscode-workbench"
},
{
"name": "vs/workbench/services/userData",
"project": "vscode-workbench"
}
]
}
......@@ -1219,7 +1219,9 @@ declare module 'vscode' {
export interface UserDataProvider {
dataProvider: FileSystemProvider;
read(key: string): Promise<{ version: number, content: string } | null>;
write(key: string, version: number, content: string): Promise<void>;
}
......
......@@ -6,74 +6,36 @@
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';
import { IRemoteUserDataService, IUserData } from 'vs/workbench/services/userData/common/userData';
@extHostNamedCustomer(MainContext.MainThreadUserData)
export class MainThreadUserData extends Disposable implements MainThreadUserDataShape {
private readonly proxy: ExtHostUserDataShape;
private readonly loginProviders: Map<string, UserLoginProvider> = new Map<string, UserLoginProvider>();
constructor(
extHostContext: IExtHostContext,
@IUserIdentityService private readonly userIdentityService: IUserIdentityService,
@IUserDataProviderService private readonly userDataProviderService: IUserDataProviderService,
@IRemoteUserDataService private readonly remoteUserDataService: IRemoteUserDataService
) {
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();
}));
this._register(toDisposable(() => this.remoteUserDataService.deregisterRemoteUserDataProvider()));
}
$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(name: string): void {
const proxy = this.proxy;
this.remoteUserDataService.registerRemoteUserDataProvider(name, {
read(key: string): Promise<IUserData | null> {
return proxy.$read(key);
},
write(key: string, version: number, content: string): Promise<void> {
return proxy.$write(key, version, content);
}
});
}
$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<void> = this._register(new Emitter<void>());
readonly onDidChange: Event<void> = 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<void> {
return this.proxy.$logIn(this.identity);
}
logout(): Promise<void> {
return this.proxy.$logOut(this.identity);
$deregisterUserDataProvider(): void {
this.remoteUserDataService.deregisterRemoteUserDataProvider();
}
}
......@@ -47,7 +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';
import { IUserData } from 'vs/workbench/services/userData/common/userData';
export interface IEnvironment {
isExtensionDevelopmentDebug: boolean;
......@@ -142,9 +142,8 @@ export interface MainThreadConfigurationShape extends IDisposable {
}
export interface MainThreadUserDataShape extends IDisposable {
$registerUserLoginProvider(identitiy: string, loggedIn: boolean): void;
$updateLoggedIn(identitiy: string, loggedIn: boolean): void;
$registerUserDataProvider(identitiy: string, userDataProvider: IUserDataProvider): void;
$registerUserDataProvider(name: string): void;
$deregisterUserDataProvider(): void;
}
export interface MainThreadDiagnosticsShape extends IDisposable {
......@@ -753,8 +752,8 @@ export interface ExtHostConfigurationShape {
}
export interface ExtHostUserDataShape {
$logIn(identity: string): Promise<void>;
$logOut(identity: string): Promise<void>;
$read(key: string): Promise<IUserData | null>;
$write(key: string, version: number, content: string): Promise<void>;
}
export interface ExtHostDiagnosticsShape {
......
......@@ -4,50 +4,45 @@
*--------------------------------------------------------------------------------------------*/
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';
import { toDisposable, Disposable } from 'vs/base/common/lifecycle';
import { ILogService } from 'vs/platform/log/common/log';
import { IUserData } from 'vs/workbench/services/userData/common/userData';
export class ExtHostUserData implements ExtHostUserDataShape {
private readonly loginProviders: Map<string, vscode.UserLoginProvider> = new Map<string, vscode.UserLoginProvider>();
private name: string | null = null;
private userDataProvider: vscode.UserDataProvider | null = null;
constructor(
private readonly proxy: MainThreadUserDataShape,
private readonly extHostFileSystem: ExtHostFileSystem
private readonly logService: ILogService,
) {
}
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;
registerUserDataProvider(name: string, userDataProvider: vscode.UserDataProvider): vscode.Disposable {
if (this.userDataProvider) {
this.logService.warn(`A user data provider '${this.name}' already exists hence ignoring the remote user data provider '${name}'.`);
return Disposable.None;
}
this.userDataProvider = userDataProvider;
this.name = name;
this.proxy.$registerUserDataProvider(name);
return toDisposable(() => this.proxy.$deregisterUserDataProvider());
}
async $logIn(identity: string): Promise<void> {
const loginProvider = this.loginProviders.get(identity);
if (!loginProvider) {
return Promise.reject(new Error(`No login provider found for ${identity}`));
$read(key: string): Promise<IUserData | null> {
if (!this.userDataProvider) {
throw new Error('No remote user data provider exists.');
}
await loginProvider.login();
return this.userDataProvider.read(key);
}
$logOut(identity: string): Promise<void> {
const loginProvider = this.loginProviders.get(identity);
if (!loginProvider) {
return Promise.reject(new Error(`No login provider found for ${identity}`));
$write(key: string, version: number, content: string): Promise<void> {
if (!this.userDataProvider) {
throw new Error('No remote user data provider exists.');
}
return Promise.resolve();
return this.userDataProvider.write(key, version, content);
}
}
/*---------------------------------------------------------------------------------------------
* 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<IUserFriendlyUserIdentityDescriptor> = ExtensionsRegistry.registerExtensionPoint<IUserFriendlyUserIdentityDescriptor>({
extensionPoint: 'userData',
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<IUserFriendlyUserIdentityDescriptor>[]) {
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<IUserFriendlyUserIdentityDescriptor>[]) {
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<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench);
workbenchRegistry.registerWorkbenchContribution(UserIdentityExtensionHandler, LifecyclePhase.Starting);
......@@ -4,8 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions';
import { IUserIdentityService, IUserDataProviderService, IUserIdentity, IUserDataSyncService, SyncStatus } from 'vs/workbench/services/userData/common/userData';
import { IStatusbarService, StatusbarAlignment, IStatusbarEntryAccessor } from 'vs/platform/statusbar/common/statusbar';
import { IUserDataSyncService, IRemoteUserDataService } from 'vs/workbench/services/userData/common/userData';
import { localize } from 'vs/nls';
import { Disposable } from 'vs/base/common/lifecycle';
import { Action } from 'vs/base/common/actions';
......@@ -14,12 +13,12 @@ import { ICommandService, CommandsRegistry } from 'vs/platform/commands/common/c
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';
import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions';
import { RawContextKey, IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey';
const CONTEXT_SYNC_ENABLED = new RawContextKey<boolean>('syncEnabled', false);
Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration)
.registerConfiguration({
......@@ -41,149 +40,57 @@ class AutoSyncUserDataContribution extends Disposable implements IWorkbenchContr
constructor(
@IConfigurationService private readonly configurationService: IConfigurationService,
@IUserIdentityService private readonly userIdentityService: IUserIdentityService,
@IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService,
@IExtensionService private readonly extensionService: IExtensionService,
) {
super();
this.autoSync();
this._register(Event.any<any>(this.userIdentityService.onDidDeregisterUserIdentities, this.configurationService.onDidChangeConfiguration)(() => this.autoSync()));
}
private async autoSync(): Promise<void> {
if (this.configurationService.getValue<boolean>('userData.autoSync')) {
const userIdentity = this.userIdentityService.getUserIndetities()[0];
if (userIdentity) {
await this.extensionService.activateByEvent(`onUserData:${userIdentity.identity}`);
this.userDataSyncService.synchronise();
}
this.userDataSyncService.synchronise();
}
}
}
class UserDataSyncStatusContribution extends Disposable implements IWorkbenchContribution {
class UserDataSyncContextUpdateContribution extends Disposable implements IWorkbenchContribution {
private readonly userDataSyncStatusAccessor: IStatusbarEntryAccessor;
private syncEnablementContext: IContextKey<boolean>;
constructor(
@IUserIdentityService private userIdentityService: IUserIdentityService,
@IStatusbarService private statusbarService: IStatusbarService,
@IUserDataSyncService private userDataSyncService: IUserDataSyncService,
@IRemoteUserDataService remoteUserDataService: IRemoteUserDataService,
@IContextKeyService contextKeyService: IContextKeyService
) {
super();
this.userDataSyncStatusAccessor = this.statusbarService.addEntry({
text: '',
command: ShowUserDataSyncActions.ID
}, 'userDataSyncStatusEntry', localize('user data sync', "Sync User Data"), StatusbarAlignment.LEFT, 10);
this.updateUserDataSyncStatusAccessor();
this._register(Event.any<any>(
this.userIdentityService.onDidRegisterUserIdentities, this.userIdentityService.onDidDeregisterUserIdentities,
this.userIdentityService.onDidRegisterUserLoginProvider, this.userIdentityService.onDidDeregisterUserLoginProvider,
this.userDataSyncService.onDidChangeSyncStatus)
(() => this.updateUserDataSyncStatusAccessor()));
this._register(this.userIdentityService.onDidRegisterUserLoginProvider((identity => this.onDidRegisterUserLoginProvider(identity))));
this.syncEnablementContext = CONTEXT_SYNC_ENABLED.bindTo(contextKeyService);
this.syncEnablementContext.set(remoteUserDataService.isEnabled());
this._register(remoteUserDataService.onDidChangeEnablement(enabled => this.syncEnablementContext.set(enabled)));
}
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) {
const syncText = this.userDataSyncService.syncStatus === SyncStatus.Syncing ? localize('syncing', "{0}: Synchronosing...", userIdentity.title) : localize('sync user data', "{0}: Sync", userIdentity.title);
return `${userIdentity.iconText} ${syncText}`;
}
return userIdentity.title;
}
}
const workbenchRegistry = Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench);
workbenchRegistry.registerWorkbenchContribution(UserDataSyncStatusContribution, LifecyclePhase.Starting);
workbenchRegistry.registerWorkbenchContribution(UserDataSyncContextUpdateContribution, LifecyclePhase.Starting);
workbenchRegistry.registerWorkbenchContribution(AutoSyncUserDataContribution, LifecyclePhase.Ready);
export class ShowUserDataSyncActions extends Action {
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<void> {
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<boolean> {
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<void> {
const autoSync = this.configurationService.getValue<boolean>('userData.autoSync');
const picks = [];
......@@ -215,3 +122,13 @@ CommandsRegistry.registerCommand('workbench.userData.actions.startAutoSync', (se
const configurationService = serviceAccessor.get(IConfigurationService);
return configurationService.updateValue('userData.autoSync', true);
});
MenuRegistry.appendMenuItem(MenuId.GlobalActivity, {
group: '5_sync',
command: {
id: ShowUserDataSyncActions.ID,
title: localize('synchronise user data', "Sync...")
},
when: CONTEXT_SYNC_ENABLED,
order: 1
});
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable, } from 'vs/base/common/lifecycle';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { Emitter, Event } from 'vs/base/common/event';
import { IRemoteUserDataService, IRemoteUserDataProvider, IUserData } from 'vs/workbench/services/userData/common/userData';
import { ILogService } from 'vs/platform/log/common/log';
export class RemoteUserDataService extends Disposable implements IRemoteUserDataService {
_serviceBrand: any;
private remoteUserDataProvider: IRemoteUserDataProvider | null = null;
private name: string | null = null;
private readonly _onDidChangeEnablement: Emitter<boolean> = this._register(new Emitter<boolean>());
readonly onDidChangeEnablement: Event<boolean> = this._onDidChangeEnablement.event;
constructor(
@ILogService private logService: ILogService
) {
super();
}
registerRemoteUserDataProvider(name: string, remoteUserDataProvider: IRemoteUserDataProvider): void {
if (this.remoteUserDataProvider) {
this.logService.warn(`A remote user data provider '${this.name}' already exists hence ignoring the remote user data provider '${name}'.`);
return;
}
this.remoteUserDataProvider = remoteUserDataProvider;
this.name = name;
this._onDidChangeEnablement.fire(true);
}
deregisterRemoteUserDataProvider(): void {
this.remoteUserDataProvider = null;
this.name = null;
this._onDidChangeEnablement.fire(false);
}
getName(): string | null {
return this.name;
}
isEnabled(): boolean {
return !!this.remoteUserDataProvider;
}
read(key: string): Promise<IUserData | null> {
if (!this.remoteUserDataProvider) {
throw new Error('No remote user data provider exists.');
}
return this.remoteUserDataProvider.read(key);
}
write(key: string, version: number, content: string): Promise<void> {
if (!this.remoteUserDataProvider) {
throw new Error('No remote user data provider exists.');
}
return this.remoteUserDataProvider.write(key, version, content);
}
}
registerSingleton(IRemoteUserDataService, RemoteUserDataService);
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as objects from 'vs/base/common/objects';
import { Disposable } from 'vs/base/common/lifecycle';
import { IFileService, FileSystemProviderErrorCode, FileSystemProviderError, IFileContent } from 'vs/platform/files/common/files';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { IRemoteUserDataService, IUserData, RemoteUserDataError, RemoteUserDataErrorCode } from 'vs/workbench/services/userData/common/userData';
import { VSBuffer } from 'vs/base/common/buffer';
import { parse, JSONPath } from 'vs/base/common/json';
import { ITextModelService, ITextModelContentProvider } from 'vs/editor/common/services/resolverService';
import { URI } from 'vs/base/common/uri';
import { ITextModel } from 'vs/editor/common/model';
import { isEqual } from 'vs/base/common/resources';
import { IModelService } from 'vs/editor/common/services/modelService';
import { IModeService } from 'vs/editor/common/services/modeService';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { localize } from 'vs/nls';
import { Edit } from 'vs/base/common/jsonFormatter';
import { repeat } from 'vs/base/common/strings';
import { setProperty } from 'vs/base/common/jsonEdit';
import { Range } from 'vs/editor/common/core/range';
import { Selection } from 'vs/editor/common/core/selection';
import { EditOperation } from 'vs/editor/common/core/editOperation';
export interface ISettingsSyncService {
sync(): Promise<void>;
}
interface ISyncPreviewResult {
readonly fileContent: IFileContent | null;
readonly remoteUserData: IUserData | null;
readonly localSettingsPreviewModel: ITextModel;
readonly remoteSettingsPreviewModel: ITextModel;
readonly hasChanges: boolean;
}
export class SettingsSyncService extends Disposable implements ISettingsSyncService, ITextModelContentProvider {
private static LAST_SYNC_SETTINGS_STORAGE_KEY: string = 'LAST_SYNC_SETTINGS_CONTENTS';
private static EXTERNAL_USER_DATA_SETTINGS_KEY: string = 'settings';
private readonly remoteSettingsPreviewResource: URI;
private readonly localSettingsPreviewResource: URI;
private syncPreviewResultPromise: Promise<ISyncPreviewResult> | null = null;
constructor(
@IFileService private readonly fileService: IFileService,
@IWorkbenchEnvironmentService private readonly workbenchEnvironmentService: IWorkbenchEnvironmentService,
@IStorageService private readonly storageService: IStorageService,
@IRemoteUserDataService private readonly remoteUserDataService: IRemoteUserDataService,
@ITextModelService private readonly textModelResolverService: ITextModelService,
@IModelService private readonly modelService: IModelService,
@IModeService private readonly modeService: IModeService,
@IEditorService private readonly editorService: IEditorService
) {
super();
this.remoteSettingsPreviewResource = URI.file('remote').with({ scheme: 'vscode-settings-sync' });
this.localSettingsPreviewResource = this.workbenchEnvironmentService.settingsResource.with({ scheme: 'vscode-settings-sync' });
this.textModelResolverService.registerTextModelContentProvider('vscode-settings-sync', this);
}
provideTextContent(uri: URI): Promise<ITextModel> | null {
if (isEqual(this.remoteSettingsPreviewResource, uri, false)) {
return this.getPreview().then(({ remoteSettingsPreviewModel }) => remoteSettingsPreviewModel);
}
if (isEqual(this.localSettingsPreviewResource, uri, false)) {
return this.getPreview().then(({ localSettingsPreviewModel }) => localSettingsPreviewModel);
}
return null;
}
async sync(): Promise<void> {
const result = await this.getPreview();
if (result.localSettingsPreviewModel.getValue() === result.remoteSettingsPreviewModel.getValue()) {
// Ask to show preview?
await this.applySyncPreview(result);
this.syncPreviewResultPromise = null;
return;
}
await this.editorService.openEditor({
leftResource: this.remoteSettingsPreviewResource,
rightResource: this.localSettingsPreviewResource,
label: localize('fileReplaceChanges', "Remote Settings ↔ Local Settings (Settings Preview)"),
options: {
preserveFocus: false,
pinned: true,
revealIfVisible: true
}
});
}
private async applySyncPreview({ fileContent, remoteUserData, localSettingsPreviewModel, remoteSettingsPreviewModel }: ISyncPreviewResult): Promise<void> {
const syncedRemoteUserData = remoteUserData && remoteUserData.content === remoteSettingsPreviewModel.getValue() ? remoteUserData : { content: remoteSettingsPreviewModel.getValue(), version: remoteUserData ? remoteUserData.version + 1 : 1 };
if (!(remoteUserData && remoteUserData.version === syncedRemoteUserData.version)) {
await this.writeToRemote(syncedRemoteUserData);
}
await this.writeToLocal(localSettingsPreviewModel.getValue(), fileContent, syncedRemoteUserData);
}
private getPreview(): Promise<ISyncPreviewResult> {
if (!this.syncPreviewResultPromise) {
this.syncPreviewResultPromise = this.generatePreview();
}
return this.syncPreviewResultPromise;
}
private async generatePreview(): Promise<ISyncPreviewResult> {
const fileContent = await this.getLocalFileContent();
const remoteUserData = await this.remoteUserDataService.read(SettingsSyncService.EXTERNAL_USER_DATA_SETTINGS_KEY);
const remoteSettingsPreviewModel = this.modelService.getModel(this.remoteSettingsPreviewResource) || this.modelService.createModel('', this.modeService.create('jsonc'), this.remoteSettingsPreviewResource);
const localSettingsPreviewModel = this.modelService.getModel(this.localSettingsPreviewResource) || this.modelService.createModel('', this.modeService.create('jsonc'), this.localSettingsPreviewResource);
if (fileContent && !remoteUserData) {
// Remote does not exist, so sync with local.
const localContent = fileContent.value.toString();
localSettingsPreviewModel.setValue(localContent);
remoteSettingsPreviewModel.setValue(localContent);
return { fileContent, remoteUserData, localSettingsPreviewModel, remoteSettingsPreviewModel, hasChanges: true };
}
if (remoteUserData && !fileContent) {
// Settings file does not exist, so sync with remote contents.
const remoteContent = remoteUserData.content;
localSettingsPreviewModel.setValue(remoteContent);
remoteSettingsPreviewModel.setValue(remoteContent);
return { fileContent, remoteUserData, localSettingsPreviewModel, remoteSettingsPreviewModel, hasChanges: true };
}
if (fileContent && remoteUserData) {
const localContent: string = fileContent.value.toString();
const remoteContent: string = remoteUserData.content;
const lastSyncData = this.getLastSyncUserData();
// Already in Sync.
if (localContent === remoteUserData.content) {
localSettingsPreviewModel.setValue(localContent);
remoteSettingsPreviewModel.setValue(remoteContent);
return { fileContent, remoteUserData, localSettingsPreviewModel, remoteSettingsPreviewModel, hasChanges: false };
}
// Not synced till now
if (!lastSyncData) {
localSettingsPreviewModel.setValue(localContent);
remoteSettingsPreviewModel.setValue(remoteContent);
await this.mergeContents(localSettingsPreviewModel, remoteSettingsPreviewModel, null);
return { fileContent, remoteUserData, localSettingsPreviewModel, remoteSettingsPreviewModel, hasChanges: true };
}
// Remote data is newer than last synced data
if (remoteUserData.version > lastSyncData.version) {
// Local content is same as last synced. So, sync with remote content.
if (lastSyncData.content === localContent) {
localSettingsPreviewModel.setValue(remoteContent);
remoteSettingsPreviewModel.setValue(remoteContent);
return { fileContent, remoteUserData, localSettingsPreviewModel, remoteSettingsPreviewModel, hasChanges: true };
}
// Local content is diverged from last synced. Required merge and sync.
localSettingsPreviewModel.setValue(localContent);
remoteSettingsPreviewModel.setValue(remoteContent);
await this.mergeContents(localSettingsPreviewModel, remoteSettingsPreviewModel, null);
return { fileContent, remoteUserData, localSettingsPreviewModel, remoteSettingsPreviewModel, hasChanges: true };
}
// Remote data is same as last synced data
if (lastSyncData.version === remoteUserData.version) {
// Local contents are same as last synced data. No op.
if (lastSyncData.content === localContent) {
return { fileContent, remoteUserData, localSettingsPreviewModel, remoteSettingsPreviewModel, hasChanges: false };
}
// New local contents. Sync with Local.
localSettingsPreviewModel.setValue(localContent);
remoteSettingsPreviewModel.setValue(localContent);
return { fileContent, remoteUserData, localSettingsPreviewModel, remoteSettingsPreviewModel, hasChanges: true };
}
}
return { fileContent, remoteUserData, localSettingsPreviewModel, remoteSettingsPreviewModel, hasChanges: false };
}
private getLastSyncUserData(): IUserData | null {
const lastSyncStorageContents = this.storageService.get(SettingsSyncService.LAST_SYNC_SETTINGS_STORAGE_KEY, StorageScope.GLOBAL, undefined);
if (lastSyncStorageContents) {
return JSON.parse(lastSyncStorageContents);
}
return null;
}
private async getLocalFileContent(): Promise<IFileContent | null> {
try {
return await this.fileService.readFile(this.workbenchEnvironmentService.settingsResource);
} catch (error) {
if (error instanceof FileSystemProviderError && error.code !== FileSystemProviderErrorCode.FileNotFound) {
return null;
}
throw error;
}
}
private async mergeContents(localSettingsPreviewModel: ITextModel, remoteSettingsPreviewModel: ITextModel, lastSyncedContent: string | null): Promise<void> {
const local = parse(localSettingsPreviewModel.getValue());
const remote = parse(remoteSettingsPreviewModel.getValue());
const base = lastSyncedContent ? parse(lastSyncedContent) : null;
const baseToLocal = base ? this.compare(base, local) : { added: new Set<string>(), removed: new Set<string>(), updated: new Set<string>() };
const baseToRemote = base ? this.compare(base, remote) : { added: new Set<string>(), removed: new Set<string>(), updated: new Set<string>() };
const localToRemote = this.compare(local, remote);
const conflicts: Set<string> = new Set<string>();
// Removed settings in Local
for (const key of baseToLocal.removed.keys()) {
// Got updated in remote
if (baseToRemote.updated.has(key)) {
conflicts.add(key);
} else {
this.removeSetting(remoteSettingsPreviewModel, key);
}
}
// Removed settings in Remote
for (const key of baseToRemote.removed.keys()) {
// Got updated in local
if (baseToLocal.updated.has(key)) {
conflicts.add(key);
} else {
this.removeSetting(localSettingsPreviewModel, key);
}
}
// Added settings in Local
for (const key of baseToLocal.added.keys()) {
// Got added in remote
if (baseToRemote.added.has(key)) {
// Has different value
if (localToRemote.updated.has(key)) {
conflicts.add(key);
}
} else {
const edit = this.getEdit(remoteSettingsPreviewModel, [key], local[key]);
if (edit) {
this.applyEditsToBuffer(edit, remoteSettingsPreviewModel);
}
}
}
// Added settings in remote
for (const key of baseToRemote.added.keys()) {
// Got added in local
if (baseToLocal.added.has(key)) {
// Has different value
if (localToRemote.updated.has(key)) {
conflicts.add(key);
}
} else {
const edit = this.getEdit(localSettingsPreviewModel, [key], remote[key]);
if (edit) {
this.applyEditsToBuffer(edit, localSettingsPreviewModel);
}
}
}
// Updated settings in Local
for (const key of baseToLocal.updated.keys()) {
// Got updated in remote
if (baseToRemote.updated.has(key)) {
// Has different value
if (localToRemote.updated.has(key)) {
conflicts.add(key);
}
} else {
const edit = this.getEdit(remoteSettingsPreviewModel, [key], local[key]);
if (edit) {
this.applyEditsToBuffer(edit, remoteSettingsPreviewModel);
}
}
}
// Updated settings in Remote
for (const key of baseToRemote.updated.keys()) {
// Got updated in local
if (baseToLocal.updated.has(key)) {
// Has different value
if (localToRemote.updated.has(key)) {
conflicts.add(key);
}
} else {
const edit = this.getEdit(localSettingsPreviewModel, [key], remote[key]);
if (edit) {
this.applyEditsToBuffer(edit, localSettingsPreviewModel);
}
}
}
}
private compare(from: { [key: string]: any }, to: { [key: string]: any }): { added: Set<string>, removed: Set<string>, updated: Set<string> } {
const fromKeys = Object.keys(from);
const toKeys = Object.keys(to);
const added = toKeys.filter(key => fromKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
const removed = fromKeys.filter(key => toKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
const updated: Set<string> = new Set<string>();
for (const key of fromKeys) {
const value1 = from[key];
const value2 = to[key];
if (!objects.equals(value1, value2)) {
updated.add(key);
}
}
return { added, removed, updated };
}
private removeSetting(model: ITextModel, key: string): void {
const edit = this.getEdit(model, [key], undefined);
if (edit) {
this.applyEditsToBuffer(edit, model);
}
}
private applyEditsToBuffer(edit: Edit, model: ITextModel): void {
const startPosition = model.getPositionAt(edit.offset);
const endPosition = model.getPositionAt(edit.offset + edit.length);
const range = new Range(startPosition.lineNumber, startPosition.column, endPosition.lineNumber, endPosition.column);
let currentText = model.getValueInRange(range);
if (edit.content !== currentText) {
const editOperation = currentText ? EditOperation.replace(range, edit.content) : EditOperation.insert(startPosition, edit.content);
model.pushEditOperations([new Selection(startPosition.lineNumber, startPosition.column, startPosition.lineNumber, startPosition.column)], [editOperation], () => []);
}
}
private getEdit(model: ITextModel, jsonPath: JSONPath, value: any): Edit | undefined {
const { tabSize, insertSpaces } = model.getOptions();
const eol = model.getEOL();
// Without jsonPath, the entire configuration file is being replaced, so we just use JSON.stringify
if (!jsonPath.length) {
const content = JSON.stringify(value, null, insertSpaces ? repeat(' ', tabSize) : '\t');
return {
content,
length: model.getValue().length,
offset: 0
};
}
return setProperty(model.getValue(), jsonPath, value, { tabSize, insertSpaces, eol })[0];
}
private async writeToRemote(userData: IUserData): Promise<void> {
try {
await this.remoteUserDataService.write(SettingsSyncService.EXTERNAL_USER_DATA_SETTINGS_KEY, userData.version, userData.content);
} catch (e) {
if (e instanceof RemoteUserDataError && e.code === RemoteUserDataErrorCode.InvalidVersion) {
// Rejected as there is a new version. Sync again
return this.sync();
}
// An unknown error
throw e;
}
}
private async writeToLocal(newContent: string, oldContent: IFileContent | null, remoteUserData: IUserData): Promise<void> {
if (oldContent) {
try {
// file exists before
await this.fileService.writeFile(this.workbenchEnvironmentService.settingsResource, VSBuffer.fromString(newContent), oldContent);
} catch (error) {
// Error to check for dirty to sync again
throw error;
}
} else {
try {
// file does not exist before
await this.fileService.createFile(this.workbenchEnvironmentService.settingsResource, VSBuffer.fromString(newContent), { overwrite: false });
} catch (error) {
if (error instanceof FileSystemProviderError && error.code === FileSystemProviderErrorCode.FileExists) {
return this.sync();
}
throw error;
}
}
this.storageService.store(SettingsSyncService.LAST_SYNC_SETTINGS_STORAGE_KEY, JSON.stringify(remoteUserData), StorageScope.GLOBAL);
}
}
......@@ -4,89 +4,53 @@
*--------------------------------------------------------------------------------------------*/
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<void>;
login(): Promise<void>;
logout(): Promise<void>;
export interface IUserData {
version: number;
content: string;
}
export interface IUserIdentity {
identity: string;
title: string;
iconText?: string;
export enum RemoteUserDataErrorCode {
InvalidVersion = 'InvalidVersion'
}
export const IUserIdentityService = createDecorator<IUserIdentityService>('IUserIdentityService');
export interface IUserIdentityService {
_serviceBrand: any;
readonly onDidRegisterUserIdentities: Event<IUserIdentity[]>;
readonly onDidDeregisterUserIdentities: Event<IUserIdentity[]>;
readonly onDidRegisterUserLoginProvider: Event<string>;
readonly onDidDeregisterUserLoginProvider: Event<string>;
export class RemoteUserDataError extends Error {
registerUserIdentities(userIdentities: IUserIdentity[]): void;
constructor(message: string, public readonly code: RemoteUserDataErrorCode) {
super(message);
}
deregisterUserIdentities(identities: string[]): void;
registerUserLoginProvider(identity: string, userLoginProvider: IUserLoginProvider): void;
deregisterUserLoginProvider(identity: string): void;
getUserIndetities(): ReadonlyArray<IUserIdentity>;
getUserIdentity(identity: string): IUserIdentity | null;
getUserLoginProvider(identity: string): IUserLoginProvider | null;
}
export interface IUserDataProvider {
export interface IRemoteUserDataProvider {
read(key: string): Promise<IUserData | null>;
userDataScheme: string;
write(key: string, version: number, content: string): Promise<void>;
}
export const IUserDataProviderService = createDecorator<IUserDataProviderService>('IUserDataProviderService');
export const IRemoteUserDataService = createDecorator<IRemoteUserDataService>('IRemoteUserDataService');
export interface IUserDataProviderService {
export interface IRemoteUserDataService {
_serviceBrand: any;
_serviceBrand: undefined;
registerUserDataProvider(identity: string, userDataProvider: IUserDataProvider): void;
readonly onDidChangeEnablement: Event<boolean>;
deregisterAll(): void;
isEnabled(): boolean;
getUserDataProvider(identity: string): IUserDataProvider | null;
registerRemoteUserDataProvider(name: string, remoteUserDataProvider: IRemoteUserDataProvider): void;
}
deregisterRemoteUserDataProvider(): void;
export const IUserDataSyncService = createDecorator<IUserDataSyncService>('IUserDataSyncService');
getName(): string | null;
read(key: string): Promise<IUserData | null>;
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 });
write(key: string, version: number, content: string): Promise<void>;
export interface IUserDataExtension {
identifier: IExtensionIdentifier;
version?: string;
}
export enum SyncStatus {
......@@ -94,6 +58,8 @@ export enum SyncStatus {
SyncDone
}
export const IUserDataSyncService = createDecorator<IUserDataSyncService>('IUserDataSyncService');
export interface IUserDataSyncService {
_serviceBrand: any;
......@@ -104,6 +70,4 @@ export interface IUserDataSyncService {
synchronise(): Promise<void>;
getExtensions(): Promise<IUserDataExtension[]>;
}
/*---------------------------------------------------------------------------------------------
* 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<string, IUserDataProvider> = new Map<string, IUserDataProvider>();
private activeUserDataProvider: IUserDataProvider | null = null;
private readonly _ondDidChangeActiveUserDataProvider: Emitter<void> = this._register(new Emitter<void>());
readonly ondDidChangeActiveUserDataProvider: Event<void> = 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);
/*---------------------------------------------------------------------------------------------
* 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<string, IUserIdentity> = new Map<string, IUserIdentity>();
private readonly userLoginProviders: Map<string, IUserLoginProvider> = new Map<string, IUserLoginProvider>();
private readonly _onDidRegisterUserIdentities: Emitter<IUserIdentity[]> = this._register(new Emitter<IUserIdentity[]>());
readonly onDidRegisterUserIdentities: Event<IUserIdentity[]> = this._onDidRegisterUserIdentities.event;
private readonly _onDidDeregisterUserIdentities: Emitter<IUserIdentity[]> = this._register(new Emitter<IUserIdentity[]>());
readonly onDidDeregisterUserIdentities: Event<IUserIdentity[]> = this._onDidDeregisterUserIdentities.event;
private readonly _onDidRegisterUserLoginProvider: Emitter<string> = this._register(new Emitter<string>());
readonly onDidRegisterUserLoginProvider: Event<string> = this._onDidRegisterUserLoginProvider.event;
private readonly _onDidDeregisterUserLoginProvider: Emitter<string> = this._register(new Emitter<string>());
readonly onDidDeregisterUserLoginProvider: Event<string> = 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<IUserIdentity> {
return values(this.userIdentities);
}
getUserLoginProvider(identity: string): IUserLoginProvider | null {
return this.userLoginProviders.get(identity) || null;
}
}
registerSingleton(IUserIdentityService, UserIdentityService);
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册