data workspace extension batch 2 (#12208)

* work in progress

* load projects in view and test cases

* update scope

* make the sql proj menu available in workspace view

* add extension unit test

* address comments

* fix errors
This commit is contained in:
Alan Ren
2020-09-10 17:17:57 -07:00
committed by GitHub
parent cd8102535b
commit 7df132b307
20 changed files with 645 additions and 32 deletions

View File

@@ -0,0 +1,70 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IProjectProvider, IProjectType } from 'dataworkspace';
import * as vscode from 'vscode';
/**
* Defines the project provider registry
*/
export interface IProjectProviderRegistry {
/**
* Registers a new project provider
* @param provider The project provider
*/
registerProvider(provider: IProjectProvider): vscode.Disposable;
/**
* Clear the providers
*/
clear(): void;
/**
* Gets all the registered providers
*/
readonly providers: IProjectProvider[];
/**
* Gets the project provider for the specified project type
* @param projectType The project type, file extension of the project
*/
getProviderByProjectType(projectType: string): IProjectProvider | undefined;
}
/**
* Defines the project service
*/
export interface IWorkspaceService {
/**
* Gets all supported project types
*/
getAllProjectTypes(): Promise<IProjectType[]>;
/**
* Gets the project files in current workspace
*/
getProjectsInWorkspace(): Promise<string[]>;
/**
* Gets the project provider by project file
* @param projectFilePath The full path of the project file
*/
getProjectProvider(projectFilePath: string): Promise<IProjectProvider | undefined>;
}
/**
* Represents the item for the workspace tree
*/
export interface WorkspaceTreeItem {
/**
* Gets the tree data provider
*/
treeDataProvider: vscode.TreeDataProvider<any>;
/**
* Gets the raw element returned by the tree data provider
*/
element: any;
}

View File

@@ -0,0 +1,12 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export class Log {
error(msg: string): void {
console.error(msg);
}
}
const Logger = new Log();
export default Logger;

View File

@@ -0,0 +1,45 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IProjectProvider } from 'dataworkspace';
import * as vscode from 'vscode';
import { IProjectProviderRegistry } from './interfaces';
export const ProjectProviderRegistry: IProjectProviderRegistry = new class implements IProjectProviderRegistry {
private _providers = new Array<IProjectProvider>();
private _providerMapping: { [key: string]: IProjectProvider } = {};
registerProvider(provider: IProjectProvider): vscode.Disposable {
this.validateProvider(provider);
this._providers.push(provider);
provider.supportedProjectTypes.forEach(projectType => {
this._providerMapping[projectType.projectFileExtension.toUpperCase()] = provider;
});
return new vscode.Disposable(() => {
const idx = this._providers.indexOf(provider);
if (idx >= 0) {
this._providers.splice(idx, 1);
provider.supportedProjectTypes.forEach(projectType => {
delete this._providerMapping[projectType.projectFileExtension.toUpperCase()];
});
}
});
}
get providers(): IProjectProvider[] {
return this._providers.slice(0);
}
clear(): void {
this._providers.length = 0;
}
validateProvider(provider: IProjectProvider): void {
}
getProviderByProjectType(projectType: string): IProjectProvider | undefined {
return projectType ? this._providerMapping[projectType.toUpperCase()] : undefined;
}
};

View File

@@ -0,0 +1,66 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { IWorkspaceService, WorkspaceTreeItem as WorkspaceTreeItem } from './interfaces';
import * as nls from 'vscode-nls';
import { EOL } from 'os';
const localize = nls.loadMessageBundle();
/**
* Tree data provider for the workspace main view
*/
export class WorkspaceTreeDataProvider implements vscode.TreeDataProvider<WorkspaceTreeItem>{
constructor(private _workspaceService: IWorkspaceService) { }
private _onDidChangeTreeData: vscode.EventEmitter<void | WorkspaceTreeItem | null | undefined> | undefined = new vscode.EventEmitter<WorkspaceTreeItem | undefined | void>();
readonly onDidChangeTreeData?: vscode.Event<void | WorkspaceTreeItem | null | undefined> | undefined = this._onDidChangeTreeData?.event;
refresh(): void {
this._onDidChangeTreeData?.fire();
}
getTreeItem(element: WorkspaceTreeItem): vscode.TreeItem | Thenable<vscode.TreeItem> {
return element.treeDataProvider.getTreeItem(element.element);
}
async getChildren(element?: WorkspaceTreeItem | undefined): Promise<WorkspaceTreeItem[]> {
if (element) {
const items = await element.treeDataProvider.getChildren(element.element);
return items ? items.map(item => <WorkspaceTreeItem>{ treeDataProvider: element.treeDataProvider, element: item }) : [];
}
else {
// if the element is undefined return the project tree items
const projects = await this._workspaceService.getProjectsInWorkspace();
const unknownProjects: string[] = [];
const treeItems: WorkspaceTreeItem[] = [];
let project: string;
for (project of projects) {
const projectProvider = await this._workspaceService.getProjectProvider(project);
if (projectProvider === undefined) {
unknownProjects.push(project);
continue;
}
const treeDataProvider = await projectProvider.getProjectTreeDataProvider(project);
if (treeDataProvider.onDidChangeTreeData) {
treeDataProvider.onDidChangeTreeData((e: any) => {
this._onDidChangeTreeData?.fire(e);
});
}
const children = await treeDataProvider.getChildren(element);
children?.forEach(child => {
treeItems.push({
treeDataProvider: treeDataProvider,
element: child
});
});
}
if (unknownProjects.length > 0) {
vscode.window.showErrorMessage(localize('UnknownProjectsError', "No provider was found for the following projects: {0}", unknownProjects.join(EOL)));
}
return treeItems;
}
}
}

View File

@@ -0,0 +1,14 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as dataworkspace from 'dataworkspace';
import { ProjectProviderRegistry } from './common/projectProviderRegistry';
export class DataWorkspaceExtension implements dataworkspace.IExtension {
registerProjectProvider(provider: dataworkspace.IProjectProvider): vscode.Disposable {
return ProjectProviderRegistry.registerProvider(provider);
}
}

View File

@@ -0,0 +1,59 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
declare module 'dataworkspace' {
import * as vscode from 'vscode';
export const enum extension {
name = 'Microsoft.data-workspace'
}
/**
* dataworkspace extension
*/
export interface IExtension {
/**
* register a project provider
* @param provider new project provider
* @requires a disposable object, upon disposal, the provider will be unregistered.
*/
registerProjectProvider(provider: IProjectProvider): vscode.Disposable;
}
/**
* Defines the capabilities of project provider
*/
export interface IProjectProvider {
/**
* Gets the tree data provider for the given project file
* @param projectFilePath The full path of the project file
*/
getProjectTreeDataProvider(projectFilePath: string): Promise<vscode.TreeDataProvider<any>>;
/**
* Gets the supported project types
*/
readonly supportedProjectTypes: IProjectType[];
}
/**
* Defines the project type
*/
export interface IProjectType {
/**
* display name of the project type
*/
readonly displayName: string;
/**
* project file extension, e.g. sqlproj
*/
readonly projectFileExtension: string;
/**
* Gets the icon path of the project type
*/
readonly icon: string | vscode.Uri | { light: string | vscode.Uri, dark: string | vscode.Uri }
}
}

View File

@@ -4,10 +4,23 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as dataworkspace from 'dataworkspace';
import { WorkspaceTreeDataProvider } from './common/workspaceTreeDataProvider';
import { WorkspaceService } from './services/workspaceService';
import { DataWorkspaceExtension } from './dataWorkspaceExtension';
export async function activate(context: vscode.ExtensionContext): Promise<void> {
vscode.commands.registerCommand('projects.addProject', () => {
});
export async function activate(context: vscode.ExtensionContext): Promise<dataworkspace.IExtension> {
const workspaceService = new WorkspaceService();
const workspaceTreeDataProvider = new WorkspaceTreeDataProvider(workspaceService);
context.subscriptions.push(vscode.window.registerTreeDataProvider('dataworkspace.views.main', workspaceTreeDataProvider));
context.subscriptions.push(vscode.commands.registerCommand('projects.addProject', () => {
}));
context.subscriptions.push(vscode.commands.registerCommand('dataworkspace.refresh', () => {
workspaceTreeDataProvider.refresh();
}));
return new DataWorkspaceExtension();
}
export function deactivate(): void {

View File

@@ -0,0 +1,76 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as dataworkspace from 'dataworkspace';
import * as path from 'path';
import { IWorkspaceService } from '../common/interfaces';
import { ProjectProviderRegistry } from '../common/projectProviderRegistry';
import * as nls from 'vscode-nls';
import Logger from '../common/logger';
const localize = nls.loadMessageBundle();
const WorkspaceConfigurationName = 'dataworkspace';
const ProjectsConfigurationName = 'projects';
export class WorkspaceService implements IWorkspaceService {
async getAllProjectTypes(): Promise<dataworkspace.IProjectType[]> {
await this.ensureProviderExtensionLoaded();
const projectTypes: dataworkspace.IProjectType[] = [];
ProjectProviderRegistry.providers.forEach(provider => {
projectTypes.push(...provider.supportedProjectTypes);
});
return projectTypes;
}
async getProjectsInWorkspace(): Promise<string[]> {
if (vscode.workspace.workspaceFile) {
const projects = <string[]>vscode.workspace.getConfiguration(WorkspaceConfigurationName).get(ProjectsConfigurationName);
return projects.map(project => path.isAbsolute(project) ? project : path.join(vscode.workspace.rootPath!, project));
}
return [];
}
async getProjectProvider(projectFilePath: string): Promise<dataworkspace.IProjectProvider | undefined> {
const projectType = path.extname(projectFilePath).replace(/\./g, '');
let provider = ProjectProviderRegistry.getProviderByProjectType(projectType);
if (!provider) {
await this.ensureProviderExtensionLoaded(projectType);
}
return ProjectProviderRegistry.getProviderByProjectType(projectType);
}
/**
* Ensure the project provider extension for the specified project is loaded
* @param projectType The file extension of the project, if not specified, all project provider extensions will be loaded.
*/
private async ensureProviderExtensionLoaded(projectType: string | undefined = undefined): Promise<void> {
const inactiveExtensions = vscode.extensions.all.filter(ext => !ext.isActive);
const projType = projectType ? projectType.toUpperCase() : undefined;
let extension: vscode.Extension<any>;
for (extension of inactiveExtensions) {
const projectTypes = extension.packageJSON.contributes && extension.packageJSON.contributes.projects as string[];
// Process only when this extension is contributing project providers
if (projectTypes && projectTypes.length > 0) {
if (projType) {
if (projectTypes.findIndex((proj: string) => proj.toUpperCase() === projType) !== -1) {
await this.activateExtension(extension);
break;
}
} else {
await this.activateExtension(extension);
}
}
}
}
private async activateExtension(extension: vscode.Extension<any>): Promise<void> {
try {
await extension.activate();
} catch (err) {
Logger.error(localize('activateExtensionFailed', "Failed to load the project provider extension '{0}'. Error message: {1}", extension.id, err.message ?? err));
}
}
}

View File

@@ -0,0 +1,27 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'mocha';
import * as should from 'should';
import { DataWorkspaceExtension } from '../dataWorkspaceExtension';
import { createProjectProvider } from './projectProviderRegistry.test';
import { ProjectProviderRegistry } from '../common/projectProviderRegistry';
suite('DataWorkspaceExtension Tests', function (): void {
test('register and unregister project provider through the extension api', async () => {
const extension = new DataWorkspaceExtension();
const provider = createProjectProvider([
{
projectFileExtension: 'testproj',
icon: '',
displayName: 'test project'
}
]);
const disposable = extension.registerProjectProvider(provider);
should.strictEqual(ProjectProviderRegistry.providers.length, 1, 'project provider should have been registered');
disposable.dispose();
should.strictEqual(ProjectProviderRegistry.providers.length, 0, 'there should be nothing in the ProjectProviderRegistry');
});
});

View File

@@ -0,0 +1,48 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'path';
const testRunner = require('vscodetestcover');
const suite = 'Data Workspace Extension Tests';
const mochaOptions: any = {
ui: 'tdd',
useColors: true,
timeout: 10000
};
// set relevant mocha options from the environment
if (process.env.ADS_TEST_GREP) {
mochaOptions.grep = process.env.ADS_TEST_GREP;
console.log(`setting options.grep to: ${mochaOptions.grep}`);
}
if (process.env.ADS_TEST_INVERT_GREP) {
mochaOptions.invert = parseInt(process.env.ADS_TEST_INVERT_GREP);
console.log(`setting options.invert to: ${mochaOptions.invert}`);
}
if (process.env.ADS_TEST_TIMEOUT) {
mochaOptions.timeout = parseInt(process.env.ADS_TEST_TIMEOUT);
console.log(`setting options.timeout to: ${mochaOptions.timeout}`);
}
if (process.env.ADS_TEST_RETRIES) {
mochaOptions.retries = parseInt(process.env.ADS_TEST_RETRIES);
console.log(`setting options.retries to: ${mochaOptions.retries}`);
}
if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) {
mochaOptions.reporter = 'mocha-multi-reporters';
mochaOptions.reporterOptions = {
reporterEnabled: 'spec, mocha-junit-reporter',
mochaJunitReporterReporterOptions: {
testsuitesTitle: `${suite} ${process.platform}`,
mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`)
}
};
}
testRunner.configure(mochaOptions, { coverConfig: '../../coverConfig.json' });
export = testRunner;

View File

@@ -0,0 +1,100 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'mocha';
import * as vscode from 'vscode';
import * as should from 'should';
import { ProjectProviderRegistry } from '../common/projectProviderRegistry';
import { IProjectProvider, IProjectType } from 'dataworkspace';
export class MockTreeDataProvider implements vscode.TreeDataProvider<any>{
onDidChangeTreeData?: vscode.Event<any> | undefined;
getTreeItem(element: any): vscode.TreeItem | Thenable<vscode.TreeItem> {
throw new Error('Method not implemented.');
}
getChildren(element?: any): vscode.ProviderResult<any[]> {
throw new Error('Method not implemented.');
}
}
export function createProjectProvider(projectTypes: IProjectType[]): IProjectProvider {
const treeDataProvider = new MockTreeDataProvider();
const projectProvider: IProjectProvider = {
supportedProjectTypes: projectTypes,
getProjectTreeDataProvider: (projectFile: string): Promise<vscode.TreeDataProvider<any>> => {
return Promise.resolve(treeDataProvider);
}
};
return projectProvider;
}
suite('ProjectProviderRegistry Tests', function (): void {
test('register and unregister project providers', async () => {
const provider1 = createProjectProvider([
{
projectFileExtension: 'testproj',
icon: '',
displayName: 'test project'
}, {
projectFileExtension: 'testproj1',
icon: '',
displayName: 'test project 1'
}
]);
const provider2 = createProjectProvider([
{
projectFileExtension: 'sqlproj',
icon: '',
displayName: 'sql project'
}
]);
should.strictEqual(ProjectProviderRegistry.providers.length, 0, 'there should be no project provider at the beginning of the test');
const disposable1 = ProjectProviderRegistry.registerProvider(provider1);
let providerResult = ProjectProviderRegistry.getProviderByProjectType('testproj');
should.equal(providerResult, provider1, 'provider1 should be returned for testproj project type');
// make sure the project type is case-insensitive for getProviderByProjectType method
providerResult = ProjectProviderRegistry.getProviderByProjectType('TeStProJ');
should.equal(providerResult, provider1, 'provider1 should be returned for testproj project type');
providerResult = ProjectProviderRegistry.getProviderByProjectType('testproj1');
should.equal(providerResult, provider1, 'provider1 should be returned for testproj1 project type');
should.strictEqual(ProjectProviderRegistry.providers.length, 1, 'there should be only one project provider at this time');
const disposable2 = ProjectProviderRegistry.registerProvider(provider2);
providerResult = ProjectProviderRegistry.getProviderByProjectType('sqlproj');
should.equal(providerResult, provider2, 'provider2 should be returned for sqlproj project type');
should.strictEqual(ProjectProviderRegistry.providers.length, 2, 'there should be 2 project providers at this time');
// unregister provider1
disposable1.dispose();
providerResult = ProjectProviderRegistry.getProviderByProjectType('testproj');
should.equal(providerResult, undefined, 'undefined should be returned for testproj project type');
providerResult = ProjectProviderRegistry.getProviderByProjectType('testproj1');
should.equal(providerResult, undefined, 'undefined should be returned for testproj1 project type');
providerResult = ProjectProviderRegistry.getProviderByProjectType('sqlproj');
should.equal(providerResult, provider2, 'provider2 should be returned for sqlproj project type after provider1 is disposed');
should.strictEqual(ProjectProviderRegistry.providers.length, 1, 'there should be only one project provider after unregistering a provider');
should.strictEqual(ProjectProviderRegistry.providers[0].supportedProjectTypes[0].projectFileExtension, 'sqlproj', 'the remaining project provider should be sqlproj');
// unregister provider2
disposable2.dispose();
providerResult = ProjectProviderRegistry.getProviderByProjectType('sqlproj');
should.equal(providerResult, undefined, 'undefined should be returned for sqlproj project type after provider2 is disposed');
should.strictEqual(ProjectProviderRegistry.providers.length, 0, 'there should be no project provider after unregistering the providers');
});
test('Clear the project provider registry', async () => {
const provider = createProjectProvider([
{
projectFileExtension: 'testproj',
icon: '',
displayName: 'test project'
}
]);
should.strictEqual(ProjectProviderRegistry.providers.length, 0, 'there should be no project provider at the beginning of the test');
ProjectProviderRegistry.registerProvider(provider);
should.strictEqual(ProjectProviderRegistry.providers.length, 1, 'there should be only one project provider at this time');
ProjectProviderRegistry.clear();
should.strictEqual(ProjectProviderRegistry.providers.length, 0, 'there should be no project provider after clearing the registry');
});
});