User data sync: Implement starting flow

- user identity
- user login provider
- user data provider
- activation event
- status bar UI
This commit is contained in:
Sandeep Somavarapu
2019-08-25 20:25:16 +02:00
parent 366e1b275c
commit bb1866ab4e
15 changed files with 790 additions and 1 deletions

View File

@@ -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<any>[] = 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

View File

@@ -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<void>;
}
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<void>;
$logOut(identity: string): Promise<void>;
}
export interface ExtHostDiagnosticsShape {
$acceptMarkersChange(data: [UriComponents, IMarkerData[]][]): void;
}
@@ -1316,6 +1328,7 @@ export const MainContext = {
MainThreadCommands: createMainId<MainThreadCommandsShape>('MainThreadCommands'),
MainThreadComments: createMainId<MainThreadCommentsShape>('MainThreadComments'),
MainThreadConfiguration: createMainId<MainThreadConfigurationShape>('MainThreadConfiguration'),
MainThreadUserData: createMainId<MainThreadUserDataShape>('MainThreadUserData'),
MainThreadConsole: createMainId<MainThreadConsoleShape>('MainThreadConsole'),
MainThreadDebugService: createMainId<MainThreadDebugServiceShape>('MainThreadDebugService'),
MainThreadDecorations: createMainId<MainThreadDecorationsShape>('MainThreadDecorations'),
@@ -1355,6 +1368,7 @@ export const MainContext = {
export const ExtHostContext = {
ExtHostCommands: createExtId<ExtHostCommandsShape>('ExtHostCommands'),
ExtHostConfiguration: createExtId<ExtHostConfigurationShape>('ExtHostConfiguration'),
ExtHostUserData: createExtId<ExtHostUserDataShape>('ExtHostUserData'),
ExtHostDiagnostics: createExtId<ExtHostDiagnosticsShape>('ExtHostDiagnostics'),
ExtHostDebugService: createExtId<ExtHostDebugServiceShape>('ExtHostDebugService'),
ExtHostDecorations: createExtId<ExtHostDecorationsShape>('ExtHostDecorations'),

View File

@@ -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<string, vscode.UserLoginProvider> = new Map<string, vscode.UserLoginProvider>();
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<void> {
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<void> {
const loginProvider = this.loginProviders.get(identity);
if (!loginProvider) {
return Promise.reject(new Error(`No login provider found for ${identity}`));
}
return Promise.resolve();
}
}

View File

@@ -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<IUserFriendlyUserIdentityDescriptor> = ExtensionsRegistry.registerExtensionPoint<IUserFriendlyUserIdentityDescriptor>({
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<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);