add existing project to workspace feature (#12249)

* add existing project to workspace feature

* update file name

* new test and use URI

* handle workspace with no folder

* add more validation

* and more tests

* use forward slash
This commit is contained in:
Alan Ren
2020-09-14 15:43:29 -07:00
committed by GitHub
parent 7a524d7a35
commit 23c16ebfb3
13 changed files with 530 additions and 30 deletions

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 { EOL } from 'os';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
export const ExtensionActivationErrorMessage = (extensionId: string, err: any): string => { return localize('activateExtensionFailed', "Failed to load the project provider extension '{0}'. Error message: {1}", extensionId, err.message ?? err); };
export const UnknownProjectsErrorMessage = (projectFiles: string[]): string => { return localize('UnknownProjectsError', "No provider was found for the following projects: {0}", projectFiles.join(EOL)); };
export const SelectProjectFileActionName = localize('SelectProjectFileActionName', "Select");

View File

@@ -45,13 +45,19 @@ export interface IWorkspaceService {
/**
* Gets the project files in current workspace
*/
getProjectsInWorkspace(): Promise<string[]>;
getProjectsInWorkspace(): Promise<vscode.Uri[]>;
/**
* Gets the project provider by project file
* @param projectFilePath The full path of the project file
* @param projectFileUri The Uri of the project file
*/
getProjectProvider(projectFilePath: string): Promise<IProjectProvider | undefined>;
getProjectProvider(projectFileUri: vscode.Uri): Promise<IProjectProvider | undefined>;
/**
* Adds the projects to workspace, if a project is not in the workspace folder, its containing folder will be added to the workspace
* @param projectFiles the list of project files to be added, the project file should be absolute path.
*/
addProjectsToWorkspace(projectFiles: vscode.Uri[]): Promise<void>;
}
/**

View File

@@ -5,9 +5,7 @@
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();
import { UnknownProjectsErrorMessage } from './constants';
/**
* Tree data provider for the workspace main view
@@ -36,11 +34,10 @@ export class WorkspaceTreeDataProvider implements vscode.TreeDataProvider<Worksp
const projects = await this._workspaceService.getProjectsInWorkspace();
const unknownProjects: string[] = [];
const treeItems: WorkspaceTreeItem[] = [];
let project: string;
for (project of projects) {
for (const project of projects) {
const projectProvider = await this._workspaceService.getProjectProvider(project);
if (projectProvider === undefined) {
unknownProjects.push(project);
unknownProjects.push(project.path);
continue;
}
const treeDataProvider = await projectProvider.getProjectTreeDataProvider(project);
@@ -58,7 +55,7 @@ export class WorkspaceTreeDataProvider implements vscode.TreeDataProvider<Worksp
});
}
if (unknownProjects.length > 0) {
vscode.window.showErrorMessage(localize('UnknownProjectsError', "No provider was found for the following projects: {0}", unknownProjects.join(EOL)));
vscode.window.showErrorMessage(UnknownProjectsErrorMessage(unknownProjects));
}
return treeItems;
}

View File

@@ -27,9 +27,9 @@ declare module 'dataworkspace' {
export interface IProjectProvider {
/**
* Gets the tree data provider for the given project file
* @param projectFilePath The full path of the project file
* @param projectFile The Uri of the project file
*/
getProjectTreeDataProvider(projectFilePath: string): Promise<vscode.TreeDataProvider<any>>;
getProjectTreeDataProvider(projectFile: vscode.Uri): Promise<vscode.TreeDataProvider<any>>;
/**
* Gets the supported project types

View File

@@ -4,16 +4,40 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as path from 'path';
import * as dataworkspace from 'dataworkspace';
import { WorkspaceTreeDataProvider } from './common/workspaceTreeDataProvider';
import { WorkspaceService } from './services/workspaceService';
import { DataWorkspaceExtension } from './dataWorkspaceExtension';
import { SelectProjectFileActionName } from './common/constants';
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('projects.addProject', async () => {
// To Sakshi - You can replace the implementation with your complete dialog implementation
// but all the code here should be reusable by you
if (vscode.workspace.workspaceFile) {
const filter: { [name: string]: string[] } = {};
const projectTypes = await workspaceService.getAllProjectTypes();
projectTypes.forEach(type => {
filter[type.displayName] = projectTypes.map(projectType => projectType.projectFileExtension);
});
let fileUris = await vscode.window.showOpenDialog({
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: false,
defaultUri: vscode.Uri.file(path.dirname(vscode.workspace.workspaceFile.path)),
openLabel: SelectProjectFileActionName,
filters: filter
});
if (!fileUris || fileUris.length === 0) {
return;
}
await workspaceService.addProjectsToWorkspace(fileUris);
workspaceTreeDataProvider.refresh();
}
}));
context.subscriptions.push(vscode.commands.registerCommand('dataworkspace.refresh', () => {

View File

@@ -8,14 +8,44 @@ 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';
import { ExtensionActivationErrorMessage } from '../common/constants';
const localize = nls.loadMessageBundle();
const WorkspaceConfigurationName = 'dataworkspace';
const ProjectsConfigurationName = 'projects';
export class WorkspaceService implements IWorkspaceService {
async addProjectsToWorkspace(projectFiles: vscode.Uri[]): Promise<void> {
if (vscode.workspace.workspaceFile) {
const currentProjects: vscode.Uri[] = await this.getProjectsInWorkspace();
const newWorkspaceFolders: string[] = [];
let newProjectFileAdded = false;
for (const projectFile of projectFiles) {
if (currentProjects.findIndex((p: vscode.Uri) => p.fsPath === projectFile.fsPath) === -1) {
currentProjects.push(projectFile);
newProjectFileAdded = true;
// if the relativePath and the original path is the same, that means the project file is not under
// any workspace folders, we should add the parent folder of the project file to the workspace
const relativePath = vscode.workspace.asRelativePath(projectFile, false);
if (vscode.Uri.file(relativePath).fsPath === projectFile.fsPath) {
newWorkspaceFolders.push(path.dirname(projectFile.path));
}
}
}
if (newProjectFileAdded) {
// Save the new set of projects to the workspace configuration.
await this.setWorkspaceConfigurationValue(ProjectsConfigurationName, currentProjects.map(project => this.toRelativePath(project)));
}
if (newWorkspaceFolders.length > 0) {
// second parameter is null means don't remove any workspace folders
vscode.workspace.updateWorkspaceFolders(vscode.workspace.workspaceFolders!.length, null, ...(newWorkspaceFolders.map(folder => ({ uri: vscode.Uri.file(folder) }))));
}
}
}
async getAllProjectTypes(): Promise<dataworkspace.IProjectType[]> {
await this.ensureProviderExtensionLoaded();
const projectTypes: dataworkspace.IProjectType[] = [];
@@ -25,16 +55,12 @@ export class WorkspaceService implements IWorkspaceService {
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 getProjectsInWorkspace(): Promise<vscode.Uri[]> {
return vscode.workspace.workspaceFile ? this.getWorkspaceConfigurationValue<string[]>(ProjectsConfigurationName).map(project => this.toUri(project)) : [];
}
async getProjectProvider(projectFilePath: string): Promise<dataworkspace.IProjectProvider | undefined> {
const projectType = path.extname(projectFilePath).replace(/\./g, '');
async getProjectProvider(projectFile: vscode.Uri): Promise<dataworkspace.IProjectProvider | undefined> {
const projectType = path.extname(projectFile.path).replace(/\./g, '');
let provider = ProjectProviderRegistry.getProviderByProjectType(projectType);
if (!provider) {
await this.ensureProviderExtensionLoaded(projectType);
@@ -70,7 +96,32 @@ export class WorkspaceService implements IWorkspaceService {
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));
Logger.error(ExtensionActivationErrorMessage(extension.id, err));
}
}
getWorkspaceConfigurationValue<T>(configurationName: string): T {
return vscode.workspace.getConfiguration(WorkspaceConfigurationName).get(configurationName) as T;
}
async setWorkspaceConfigurationValue(configurationName: string, value: any): Promise<void> {
await vscode.workspace.getConfiguration(WorkspaceConfigurationName).update(configurationName, value, vscode.ConfigurationTarget.Workspace);
}
/**
* Gets the relative path to the workspace file
* @param filePath the absolute path
*/
private toRelativePath(filePath: vscode.Uri): string {
return path.relative(path.dirname(vscode.workspace.workspaceFile!.path!), filePath.path);
}
/**
* Gets the Uri of the given relative path
* @param relativePath the relative path
*/
private toUri(relativePath: string): vscode.Uri {
const fullPath = path.join(path.dirname(vscode.workspace.workspaceFile!.path!), relativePath);
return vscode.Uri.file(fullPath);
}
}

View File

@@ -23,7 +23,7 @@ export function createProjectProvider(projectTypes: IProjectType[]): IProjectPro
const treeDataProvider = new MockTreeDataProvider();
const projectProvider: IProjectProvider = {
supportedProjectTypes: projectTypes,
getProjectTreeDataProvider: (projectFile: string): Promise<vscode.TreeDataProvider<any>> => {
getProjectTreeDataProvider: (projectFile: vscode.Uri): Promise<vscode.TreeDataProvider<any>> => {
return Promise.resolve(treeDataProvider);
}
};

View File

@@ -0,0 +1,211 @@
/*---------------------------------------------------------------------------------------------
* 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 sinon from 'sinon';
import * as should from 'should';
import * as path from 'path';
import { WorkspaceService } from '../services/workspaceService';
import { ProjectProviderRegistry } from '../common/projectProviderRegistry';
import { createProjectProvider } from './projectProviderRegistry.test';
const DefaultWorkspaceFilePath = '/test/folder/ws.code-workspace';
/**
* Create a stub for vscode.workspace.workspaceFile
* @param workspaceFilePath The workspace file to return
*/
function stubWorkspaceFile(workspaceFilePath: string | undefined): sinon.SinonStub {
return sinon.stub(vscode.workspace, 'workspaceFile').value(workspaceFilePath ? vscode.Uri.file(workspaceFilePath) : undefined);
}
/**
* Create a stub for vscode.workspace.getConfiguration
* @param returnValue the configuration value to return
*/
function stubGetConfigurationValue(getStub?: sinon.SinonStub, updateStub?: sinon.SinonStub): sinon.SinonStub {
return sinon.stub(vscode.workspace, 'getConfiguration').returns({
get: (configurationName: string) => {
return getStub!(configurationName);
},
update: (section: string, value: any, configurationTarget?: vscode.ConfigurationTarget | boolean, overrideInLanguage?: boolean) => {
updateStub!(section, value, configurationTarget);
}
} as vscode.WorkspaceConfiguration);
}
/**
* Create a stub for vscode.extensions.all
* @param extensions extensions to return
*/
function stubAllExtensions(extensions: vscode.Extension<any>[]): sinon.SinonStub {
return sinon.stub(vscode.extensions, 'all').value(extensions);
}
function createMockExtension(id: string, isActive: boolean, projectTypes: string[] | undefined): { extension: vscode.Extension<any>, activationStub: sinon.SinonStub } {
const activationStub = sinon.stub();
const extension: vscode.Extension<any> = {
id: id,
isActive: isActive,
packageJSON: {},
activate: () => { return activationStub(); }
} as vscode.Extension<any>;
extension.packageJSON.contributes = projectTypes === undefined ? undefined : { projects: projectTypes };
return {
extension: extension,
activationStub: activationStub
};
}
suite('WorkspaceService Tests', function (): void {
const service = new WorkspaceService();
this.afterEach(() => {
sinon.restore();
});
test('test getProjectsInWorkspace', async () => {
// No workspace is loaded
stubWorkspaceFile(undefined);
let projects = await service.getProjectsInWorkspace();
should.strictEqual(projects.length, 0, 'no projects should be returned when no workspace is loaded');
// from this point on, workspace is loaded
stubWorkspaceFile(DefaultWorkspaceFilePath);
// No projects are present in the workspace file
const getConfigurationStub = stubGetConfigurationValue(sinon.stub().returns([]));
projects = await service.getProjectsInWorkspace();
should.strictEqual(projects.length, 0, 'no projects should be returned when projects are present in the workspace file');
getConfigurationStub.restore();
// Projects are present
stubGetConfigurationValue(sinon.stub().returns(['abc.sqlproj', 'folder1/abc1.sqlproj', 'folder2/abc2.sqlproj']));
projects = await service.getProjectsInWorkspace();
should.strictEqual(projects.length, 3, 'there should be 2 projects');
const project1 = vscode.Uri.file('/test/folder/abc.sqlproj');
const project2 = vscode.Uri.file('/test/folder/folder1/abc1.sqlproj');
const project3 = vscode.Uri.file('/test/folder/folder2/abc2.sqlproj');
should.strictEqual(projects[0].path, project1.path);
should.strictEqual(projects[1].path, project2.path);
should.strictEqual(projects[2].path, project3.path);
});
test('test getAllProjectTypes', async () => {
// extensions that are already activated
const extension1 = createMockExtension('ext1', true, ['csproj']); // with projects contribution
const extension2 = createMockExtension('ext2', true, []); // with empty projects contribution
const extension3 = createMockExtension('ext3', true, undefined); // with no contributes in packageJSON
// extensions that are still not activated
const extension4 = createMockExtension('ext4', false, ['sqlproj']); // with projects contribution
const extension5 = createMockExtension('ext5', false, ['dbproj']); // with projects contribution but activate() will throw error
extension5.activationStub.throws(); // extension activation failure shouldn't cause the getAllProjectTypes() call to fail
const extension6 = createMockExtension('ext6', false, undefined); // with no contributes in packageJSON
const extension7 = createMockExtension('ext7', false, []); // with empty projects contribution
stubAllExtensions([extension1, extension2, extension3, extension4, extension5, extension6, extension7].map(ext => ext.extension));
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'
}
]);
sinon.stub(ProjectProviderRegistry, 'providers').value([provider1, provider2]);
const consoleErrorStub = sinon.stub(console, 'error');
const projectTypes = await service.getAllProjectTypes();
should.strictEqual(projectTypes.length, 3);
should.strictEqual(projectTypes[0].projectFileExtension, 'testproj');
should.strictEqual(projectTypes[1].projectFileExtension, 'testproj1');
should.strictEqual(projectTypes[2].projectFileExtension, 'sqlproj');
should.strictEqual(extension1.activationStub.notCalled, true, 'extension1.activate() should not have been called');
should.strictEqual(extension2.activationStub.notCalled, true, 'extension2.activate() should not have been called');
should.strictEqual(extension3.activationStub.notCalled, true, 'extension3.activate() should not have been called');
should.strictEqual(extension4.activationStub.calledOnce, true, 'extension4.activate() should have been called');
should.strictEqual(extension5.activationStub.called, true, 'extension5.activate() should have been called');
should.strictEqual(extension6.activationStub.notCalled, true, 'extension6.activate() should not have been called');
should.strictEqual(extension7.activationStub.notCalled, true, 'extension7.activate() should not have been called');
should.strictEqual(consoleErrorStub.calledOnce, true, 'Logger.error should be called once');
});
test('test getProjectProvider', async () => {
const extension1 = createMockExtension('ext1', true, ['csproj']);
const extension2 = createMockExtension('ext2', false, ['sqlproj']);
const extension3 = createMockExtension('ext3', false, ['dbproj']);
stubAllExtensions([extension1, extension2, extension3].map(ext => ext.extension));
const getProviderByProjectTypeStub = sinon.stub(ProjectProviderRegistry, 'getProviderByProjectType');
getProviderByProjectTypeStub.onFirstCall().returns(undefined);
getProviderByProjectTypeStub.onSecondCall().returns(createProjectProvider([
{
projectFileExtension: 'sqlproj',
icon: '',
displayName: 'test project'
}
]));
let provider = await service.getProjectProvider(vscode.Uri.file('abc.sqlproj'));
should.notStrictEqual(provider, undefined, 'Provider should be returned for sqlproj');
should.strictEqual(provider!.supportedProjectTypes[0].projectFileExtension, 'sqlproj');
should.strictEqual(extension1.activationStub.notCalled, true, 'the ext1.activate() should not have been called for sqlproj');
should.strictEqual(extension2.activationStub.calledOnce, true, 'the ext2.activate() should have been called once after requesting sqlproj provider');
should.strictEqual(extension3.activationStub.notCalled, true, 'the ext3.activate() should not have been called for sqlproj');
getProviderByProjectTypeStub.reset();
getProviderByProjectTypeStub.returns(createProjectProvider([{
projectFileExtension: 'csproj',
icon: '',
displayName: 'test cs project'
}]));
provider = await service.getProjectProvider(vscode.Uri.file('abc.csproj'));
should.notStrictEqual(provider, undefined, 'Provider should be returned for csproj');
should.strictEqual(provider!.supportedProjectTypes[0].projectFileExtension, 'csproj');
should.strictEqual(extension1.activationStub.notCalled, true, 'the ext1.activate() should not have been called for csproj');
should.strictEqual(extension2.activationStub.calledOnce, true, 'the ext2.activate() should still have been called once');
should.strictEqual(extension3.activationStub.notCalled, true, 'the ext3.activate() should not have been called for csproj');
});
test('test addProjectsToWorkspace', async () => {
const processPath = (original: string): string => {
return original.replace(/\//g, path.sep);
};
stubWorkspaceFile(DefaultWorkspaceFilePath);
const updateConfigurationStub = sinon.stub();
const getConfigurationStub = sinon.stub().returns([processPath('folder1/proj2.sqlproj')]);
stubGetConfigurationValue(getConfigurationStub, updateConfigurationStub);
const asRelativeStub = sinon.stub(vscode.workspace, 'asRelativePath');
sinon.stub(vscode.workspace, 'workspaceFolders').value(['.']);
asRelativeStub.onFirstCall().returns(`proj1.sqlproj`);
asRelativeStub.onSecondCall().returns(processPath('/test/other/proj3.sqlproj'));
const updateWorkspaceFoldersStub = sinon.stub(vscode.workspace, 'updateWorkspaceFolders');
await service.addProjectsToWorkspace([
vscode.Uri.file('/test/folder/proj1.sqlproj'), // within the workspace folder
vscode.Uri.file('/test/folder/folder1/proj2.sqlproj'), //already exists
vscode.Uri.file('/test/other/proj3.sqlproj') // outside of workspace folder
]);
should.strictEqual(updateConfigurationStub.calledOnce, true, 'update configuration should have been called once');
should.strictEqual(updateWorkspaceFoldersStub.calledOnce, true, 'updateWorkspaceFolders should have been called once');
should.strictEqual(updateConfigurationStub.calledWith('projects', sinon.match.array.deepEquals([
processPath('folder1/proj2.sqlproj'),
processPath('proj1.sqlproj'),
processPath('../other/proj3.sqlproj')
]), vscode.ConfigurationTarget.Workspace), true, 'updateConfiguration parameters does not match expectation');
should.strictEqual(updateWorkspaceFoldersStub.calledWith(1, null, sinon.match((arg) => {
return arg.uri.path === '/test/other';
})), true, 'updateWorkspaceFolder parameters does not match expectation');
});
});

View File

@@ -0,0 +1,90 @@
/*---------------------------------------------------------------------------------------------
* 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 sinon from 'sinon';
import * as vscode from 'vscode';
import * as should from 'should';
import { WorkspaceTreeDataProvider } from '../common/workspaceTreeDataProvider';
import { WorkspaceService } from '../services/workspaceService';
import { WorkspaceTreeItem } from '../common/interfaces';
import { IProjectProvider } from 'dataworkspace';
import { MockTreeDataProvider } from './projectProviderRegistry.test';
suite('workspaceTreeDataProvider Tests', function (): void {
const workspaceService = new WorkspaceService();
const treeProvider = new WorkspaceTreeDataProvider(workspaceService);
this.afterEach(() => {
sinon.restore();
});
test('test refresh()', () => {
const treeDataChangeHandler = sinon.stub();
treeProvider.onDidChangeTreeData!((e) => {
treeDataChangeHandler(e);
});
treeProvider.refresh();
should.strictEqual(treeDataChangeHandler.calledOnce, true);
});
test('test getTreeItem()', () => {
const getTreeItemStub = sinon.stub();
treeProvider.getTreeItem(({
treeDataProvider: ({
getTreeItem: (arg: WorkspaceTreeItem) => {
return getTreeItemStub(arg);
}
}) as vscode.TreeDataProvider<any>
}) as WorkspaceTreeItem);
should.strictEqual(getTreeItemStub.calledOnce, true);
});
test('test getChildren() for non-root element', async () => {
const getChildrenStub = sinon.stub().resolves([]);
const element = {
treeDataProvider: ({
getChildren: (arg: any) => {
return getChildrenStub(arg);
}
}) as vscode.TreeDataProvider<any>,
element: 'obj1'
};
const children = await treeProvider.getChildren(element);
should.strictEqual(children.length, 0, 'children count should be 0');
should.strictEqual(getChildrenStub.calledWithExactly('obj1'), true, 'getChildren parameter should be obj1')
});
test('test getChildren() for root element', async () => {
const getProjectsInWorkspaceStub = sinon.stub(workspaceService, 'getProjectsInWorkspace').resolves(
[
vscode.Uri.file('test/proj1/proj1.sqlproj'),
vscode.Uri.file('test/proj2/proj2.csproj')
]
);
const treeDataProvider = new MockTreeDataProvider();
const projectProvider: IProjectProvider = {
supportedProjectTypes: [{
projectFileExtension: 'sqlproj',
icon: '',
displayName: 'sql project'
}],
getProjectTreeDataProvider: (projectFile: vscode.Uri): Promise<vscode.TreeDataProvider<any>> => {
return Promise.resolve(treeDataProvider);
}
};
const getProjectProviderStub = sinon.stub(workspaceService, 'getProjectProvider');
getProjectProviderStub.onFirstCall().resolves(undefined);
getProjectProviderStub.onSecondCall().resolves(projectProvider);
sinon.stub(treeDataProvider, 'getChildren').resolves(['treeitem1']);
const showErrorMessageStub = sinon.stub(vscode.window, 'showErrorMessage');
const children = await treeProvider.getChildren(undefined);
should.strictEqual(children.length, 1, 'there should be 1 tree item returned');
should.strictEqual(children[0].element, 'treeitem1');
should.strictEqual(getProjectsInWorkspaceStub.calledOnce, true, 'getProjectsInWorkspaceStub should be called');
should.strictEqual(getProjectProviderStub.calledTwice, true, 'getProjectProvider should be called twice');
should.strictEqual(showErrorMessageStub.calledOnce, true, 'showErrorMessage should be called once');
});
});