Data workspace projects changes (#13466)

* Fix project context menu actions (#12541)

* delete works again

* make fewer changes

* update all sql db project commands

* cleanup

* Remove old projects view (#12563)

* remove old projects view from file explorer view

* fix tests failing

* remove projects in open folder opening up in old view

* Update db reference dialog to show projects in the workspace (#12580)

* update database reference dialog to show projects in the workspace in the project dropdown

* remove workspace stuff from sql projects extension

* undo change

* add class that implements IExtension

* undo a change

* update DataWorkspaceExtension to take workspaceService as a parameter

* add type

* Update sql database project commands (#12595)

* remove sql proj's open and create new project from comman palette

* hook up create project from database to data workspace

* rename the remaining import databases to create project from database

* remove open, new, and close commands

* expose addProjectsToWorkspace() in IExtension instead of calling command

* Addressing comments

* fix failing sql project tests (#12651)

* update SSDT projects opened in projects viewlet (#12669)

* fix action not refreshing the tree issue (#12692)

* fix adding project references in new projects viewlet (#12688)

* Remove old projects tree provider (#12702)

* Remove old projects tree provider and fix tests

* formatting

* update refreshProjectsTree() to accept workspaceTreeItem()

* Cleanup ProjectsController (#12718)

* remove openProject from ProjectController and some cleanup

* rename

* add project and open project dialogs (#12729)

* empty dialogs

* wip

* new project dialog implementation

* revert gitattributes

* open project dialog

* implement add project

* remove icon helper

* refactor

* revert script change

* adjust views

* more updates

* make data-workspace a builtin extension

* show the view only when project provider is detected (#12819)

* only show the view when proj provider is available

* update

* fix sql project tests after merge (#12793)

* Update dialogs to be closer to mockups (#12879)

* small UI changes to dialogs

* center radio card group text

* Create workspace if needed when opening/new project (#12930)

* empty dialogs

* wip

* new project dialog implementation

* revert gitattributes

* open project dialog

* implement add project

* remove icon helper

* refactor

* revert script change

* create workspace

* initial changes

* create new workspace working

* fix tests

* cleanup

* remove showWorkspaceRequiredNotification()

* Add test for no workspace open

* update blue buttons

* move loading temp project to activate() instead of workspaceService constructor

* move workspace creation warning message to before project is created

* pass uri to createWorkspace

* add tests

Co-authored-by: Alan Ren <alanren@microsoft.com>

* Additional create workspace changes (#13004)

* Dialogs workspace updates (#13010)

* adding workspace text boxes

* match new project dialog to mockups

* Add validation error message for workspace file

* add enterWorkspace api

* add warning message for opening workspace

* cleanup

* update commands to remove project so they're more generic

* remove 'empty' from string

* Move default project location setting to data workspace extension (#13022)

* remove project location setting and notification from sql database projects extension

* add default project location setting to data workspace extension

* fix typo

* Add back project name incrementing

* other merge fixes

* fix strings from other PR

* default to last opened directory instead of home directory if no specified default location

* A few small updates (#13092)

* fix build error

* update title for inputboxes

* add missing file

* Add tests for data workspace dialogs (#13324)

* add tests for dialogs

* create helper functions

* New project dialog workspace inputbox fixes (#13407)

* workspace inputbox fixes

* fix folder icons

* Update package.jsons and readme (#13451)

* update package.jsons

* update readme

* add workspace information to open existing dialog (#13455)

Co-authored-by: Alan Ren <alanren@microsoft.com>
This commit is contained in:
Kim Santiago
2020-11-18 16:13:43 -08:00
committed by GitHub
parent 34170e7741
commit ddc8c00090
63 changed files with 1835 additions and 931 deletions

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#231f20;}.cls-2{fill:#212121;}</style></defs><title>file_16x16</title><polygon class="cls-1" points="13.59 2.21 13.58 2.22 13.58 2.2 13.59 2.21"/><path class="cls-2" d="M8.71,0,14,5.29V16H2V0ZM3,15H13V6H8V1H3ZM9,1.71V5h3.29Z"/></svg>

After

Width:  |  Height:  |  Size: 351 B

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#fff;}</style></defs><title>file_inverse_16x16</title><polygon class="cls-1" points="13.59 2.21 13.58 2.22 13.58 2.2 13.59 2.21"/><path class="cls-1" d="M8.71,0,14,5.29V16H2V0ZM3,15H13V6H8V1H3ZM9,1.71V5h3.29Z"/></svg>

After

Width:  |  Height:  |  Size: 335 B

View File

@@ -0,0 +1,3 @@
<svg width="17" height="12" viewBox="0 0 17 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.8457 3V11C16.8457 11.1406 16.8197 11.2708 16.7676 11.3906C16.7155 11.5104 16.6426 11.6172 16.5488 11.7109C16.4603 11.7995 16.3561 11.8698 16.2363 11.9219C16.1165 11.974 15.9863 12 15.8457 12H1.8457C1.70508 12 1.57487 11.974 1.45508 11.9219C1.33529 11.8698 1.22852 11.7995 1.13477 11.7109C1.04622 11.6172 0.975911 11.5104 0.923828 11.3906C0.871745 11.2708 0.845703 11.1406 0.845703 11V1C0.845703 0.859375 0.871745 0.729167 0.923828 0.609375C0.975911 0.489583 1.04622 0.385417 1.13477 0.296875C1.22852 0.203125 1.33529 0.130208 1.45508 0.078125C1.57487 0.0260417 1.70508 0 1.8457 0H7.5957C7.78841 0 7.9681 0.0364583 8.13477 0.109375C8.30143 0.177083 8.45247 0.270833 8.58789 0.390625C8.72852 0.505208 8.85352 0.638021 8.96289 0.789062C9.07747 0.934896 9.18164 1.08594 9.27539 1.24219C9.3431 1.36198 9.4082 1.46875 9.4707 1.5625C9.53841 1.65625 9.61133 1.73698 9.68945 1.80469C9.77279 1.86719 9.86393 1.91667 9.96289 1.95312C10.0671 1.98438 10.1947 2 10.3457 2H15.8457C15.9863 2 16.1165 2.02604 16.2363 2.07812C16.3561 2.13021 16.4603 2.20312 16.5488 2.29688C16.6426 2.38542 16.7155 2.48958 16.7676 2.60938C16.8197 2.72917 16.8457 2.85938 16.8457 3ZM7.5957 1H1.8457V3H7.5957C7.73633 3 7.85352 2.97656 7.94727 2.92969C8.04622 2.88281 8.13737 2.82552 8.2207 2.75781C8.30924 2.6901 8.39779 2.61719 8.48633 2.53906C8.57487 2.45573 8.67643 2.38281 8.79102 2.32031C8.71289 2.23177 8.62956 2.11458 8.54102 1.96875C8.45768 1.81771 8.36654 1.67188 8.26758 1.53125C8.16862 1.38542 8.06185 1.26042 7.94727 1.15625C7.83789 1.05208 7.7207 1 7.5957 1ZM15.8457 11V3H10.3457C10.054 3 9.81706 3.02604 9.63477 3.07812C9.45768 3.125 9.30664 3.1849 9.18164 3.25781C9.06185 3.33073 8.95768 3.41146 8.86914 3.5C8.7806 3.58854 8.68164 3.66927 8.57227 3.74219C8.4681 3.8151 8.34049 3.8776 8.18945 3.92969C8.03841 3.97656 7.84049 4 7.5957 4H1.8457V11H15.8457Z" fill="#0078D4"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -10,10 +10,10 @@
"aiKey": "AIF-37eefaf0-8022-4671-a3fb-64752724682e",
"engines": {
"vscode": "*",
"azdata": ">=1.22.0"
"azdata": ">=1.25.0"
},
"activationEvents": [
"onView:dataworkspace.views.main"
"*"
],
"main": "./out/main",
"repository": {
@@ -30,17 +30,27 @@
"type": "array",
"default": [],
"description": ""
},
"projects.defaultProjectSaveLocation": {
"type": "string",
"description": "%projects.defaultProjectSaveLocation%"
}
}
}
],
"commands": [
{
"command": "projects.addProject",
"title": "%add-project-command%",
"category": "",
"command": "projects.new",
"title": "%new-command%",
"category": "%data-workspace-view-container-name%",
"icon": "$(add)"
},
{
"command": "projects.openExisting",
"title": "%open-existing-command%",
"category": "%data-workspace-view-container-name%",
"icon": "$(folder-opened)"
},
{
"command": "dataworkspace.refresh",
"title": "%refresh-workspace-command%",
@@ -57,18 +67,22 @@
{
"command": "dataworkspace.refresh",
"when": "view == dataworkspace.views.main",
"group": "secondary"
},
{
"command": "projects.new",
"when": "view == dataworkspace.views.main",
"group": "navigation"
},
{
"command": "projects.addProject",
"command": "projects.openExisting",
"when": "view == dataworkspace.views.main",
"group": "navigation"
}
],
"commandPalette": [
{
"command": "projects.addProject",
"when": "false"
"command": "projects.new"
},
{
"command": "dataworkspace.refresh",
@@ -77,6 +91,9 @@
{
"command": "projects.removeProject",
"when": "false"
},
{
"command": "projects.openExisting"
}
],
"view/item/context": [
@@ -102,7 +119,8 @@
"id": "dataworkspace.views.main",
"name": "%main-view-name%",
"contextualTitle": "%data-workspace-view-container-name%",
"icon": "images/data-workspace.svg"
"icon": "images/data-workspace.svg",
"when": "isProjectProviderAvailable"
}
]
},
@@ -111,6 +129,11 @@
"view": "dataworkspace.views.main",
"contents": "%projects-view-no-workspace-content%",
"when": "workbenchState != workspace"
},
{
"view": "dataworkspace.views.main",
"contents": "%projects-view-no-project-content%",
"when": "workbenchState == workspace && isProjectsViewEmpty"
}
]
},

View File

@@ -3,8 +3,11 @@
"extension-description": "Data workspace",
"data-workspace-view-container-name": "Projects",
"main-view-name": "Projects",
"add-project-command": "Add Project",
"new-command": "New",
"refresh-workspace-command": "Refresh",
"remove-project-command": "Remove Project",
"projects-view-no-workspace-content": "To use projects, open a workspace and add projects to it, or use the 'Add Project' feature and we will create a workspace for you.\n[Open Workspace](command:workbench.action.openWorkspace)\n[Add Project](command:projects.addProject)"
"projects-view-no-workspace-content": "[Create new](command:projects.new)\n[Open existing](command:projects.openExisting)\n",
"projects-view-no-project-content": "No projects found in current workspace.\n[Create new](command:projects.new)\n[Open existing](command:projects.openExisting)\n",
"open-existing-command": "Open existing",
"projects.defaultProjectSaveLocation": "Full path to folder where new projects are saved by default."
}

View File

@@ -7,9 +7,48 @@ 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 ExtensionActivationError = (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 UnknownProjectsError = (projectFiles: string[]): string => { return localize('UnknownProjectsError', "No provider was found for the following projects: {0}", projectFiles.join(EOL)); };
export const SelectProjectFileActionName = localize('SelectProjectFileActionName', "Select");
export const AllProjectTypes = localize('AllProjectTypes', "All Project Types");
export const ProviderNotFoundForProjectTypeError = (projectType: string): string => { return localize('UnknownProjectTypeError', "No provider was found for project type with id: '{0}'", projectType); };
export const WorkspaceRequiredMessage = localize('dataworkspace.workspaceRequiredMessage', "A workspace is required in order to use the project feature.");
export const OpenWorkspace = localize('dataworkspace.openWorkspace', "Open Workspace…");
export const CreateWorkspaceConfirmation = localize('dataworkspace.createWorkspaceConfirmation', "A new workspace will be created and opened in order to open project. The Extension Host will restart and if there is a folder currently open, it will be closed.");
export const EnterWorkspaceConfirmation = localize('dataworkspace.enterWorkspaceConfirmation', "To open this workspace, the Extension Host will restart and if there is a workspace or folder currently open, it will be closed.");
// UI
export const OkButtonText = localize('dataworkspace.ok', "OK");
export const CancelButtonText = localize('dataworkspace.cancel', "Cancel");
export const BrowseButtonText = localize('dataworkspace.browse', "Browse");
export const DefaultInputWidth = '400px';
export const DefaultButtonWidth = '80px';
// New Project Dialog
export const NewProjectDialogTitle = localize('dataworkspace.NewProjectDialogTitle', "Create new project");
export const TypeTitle = localize('dataworkspace.Type', "Type");
export const ProjectNameTitle = localize('dataworkspace.projectNameTitle', "Name");
export const ProjectNamePlaceholder = localize('dataworkspace.projectNamePlaceholder', "Enter project name");
export const ProjectLocationTitle = localize('dataworkspace.projectLocationTitle', "Location");
export const ProjectLocationPlaceholder = localize('dataworkspace.projectLocationPlaceholder', "Enter project location");
export const AddProjectToCurrentWorkspace = localize('dataworkspace.AddProjectToCurrentWorkspace', "This project will be added to the current workspace.");
export const NewWorkspaceWillBeCreated = localize('dataworkspace.NewWorkspaceWillBeCreated', "A new workspace will be created for this project.");
export const WorkspaceLocationTitle = localize('dataworkspace.workspaceLocationTitle', "Workspace location");
export const ProjectParentDirectoryNotExistError = (location: string): string => { return localize('dataworkspace.projectParentDirectoryNotExistError', "The selected location: '{0}' does not exist or is not a directory.", location); };
export const ProjectDirectoryAlreadyExistError = (projectName: string, location: string): string => { return localize('dataworkspace.projectDirectoryAlreadyExistError', "There is already a directory named '{0}' in the selected location: '{1}'.", projectName, location); };
//Open Existing Dialog
export const OpenExistingDialogTitle = localize('dataworkspace.openExistingDialogTitle', "Open existing");
export const ProjectFileNotExistError = (projectFilePath: string): string => { return localize('dataworkspace.projectFileNotExistError', "The selected project file '{0}' does not exist or is not a file.", projectFilePath); };
export const WorkspaceFileNotExistError = (workspaceFilePath: string): string => { return localize('dataworkspace.workspaceFileNotExistError', "The selected workspace file '{0}' does not exist or is not a file.", workspaceFilePath); };
export const Project = localize('dataworkspace.project', "Project");
export const Workspace = localize('dataworkspace.workspace', "Workspace");
export const LocationSelectorTitle = localize('dataworkspace.locationSelectorTitle', "Location");
export const ProjectFilePlaceholder = localize('dataworkspace.projectFilePlaceholder', "Enter project location");
export const WorkspacePlaceholder = localize('dataworkspace.workspacePlaceholder', "Enter workspace location");
export const WorkspaceFileExtension = 'code-workspace';
// Workspace settings for saving new projects
export const ProjectConfigurationKey = 'projects';
export const ProjectSaveLocationKey = 'defaultProjectSaveLocation';

View File

@@ -0,0 +1,30 @@
/*---------------------------------------------------------------------------------------------
* 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 { IExtension } from 'dataworkspace';
import { WorkspaceService } from '../services/workspaceService';
import { defaultProjectSaveLocation } from './projectLocationHelper';
export class DataWorkspaceExtension implements IExtension {
constructor(private workspaceService: WorkspaceService) {
}
getProjectsInWorkspace(): vscode.Uri[] {
return this.workspaceService.getProjectsInWorkspace();
}
addProjectsToWorkspace(projectFiles: vscode.Uri[]): Promise<void> {
return this.workspaceService.addProjectsToWorkspace(projectFiles);
}
showProjectsView(): void {
vscode.commands.executeCommand('dataworkspace.views.main.focus');
}
get defaultProjectSaveLocation(): vscode.Uri | undefined {
return defaultProjectSaveLocation();
}
}

View File

@@ -0,0 +1,38 @@
/*---------------------------------------------------------------------------------------------
* 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';
export interface IconPath {
dark: string;
light: string;
}
export class IconPathHelper {
private static extensionContext: vscode.ExtensionContext;
public static folder: IconPath;
public static setExtensionContext(extensionContext: vscode.ExtensionContext) {
IconPathHelper.extensionContext = extensionContext;
IconPathHelper.folder = IconPathHelper.makeIcon('folder', true);
}
private static makeIcon(name: string, sameIcon: boolean = false) {
const folder = 'images';
if (sameIcon) {
return {
dark: IconPathHelper.extensionContext.asAbsolutePath(`${folder}/${name}.svg`),
light: IconPathHelper.extensionContext.asAbsolutePath(`${folder}/${name}.svg`)
};
} else {
return {
dark: IconPathHelper.extensionContext.asAbsolutePath(`${folder}/dark/${name}.svg`),
light: IconPathHelper.extensionContext.asAbsolutePath(`${folder}/light/${name}.svg`)
};
}
}
}

View File

@@ -26,9 +26,15 @@ export interface IProjectProviderRegistry {
*/
readonly providers: IProjectProvider[];
/**
* Gets the project provider for the specified project extension
* @param extension The file extension of the project
*/
getProviderByProjectExtension(extension: string): IProjectProvider | undefined;
/**
* Gets the project provider for the specified project type
* @param projectType The project type, file extension of the project
* @param projectType The id of the project type
*/
getProviderByProjectType(projectType: string): IProjectProvider | undefined;
}
@@ -45,7 +51,7 @@ export interface IWorkspaceService {
/**
* Gets the project files in current workspace
*/
getProjectsInWorkspace(): Promise<vscode.Uri[]>;
getProjectsInWorkspace(): vscode.Uri[];
/**
* Gets the project provider by project file
@@ -65,8 +71,28 @@ export interface IWorkspaceService {
*/
removeProject(projectFile: vscode.Uri): Promise<void>;
/**
* Creates a new project from workspace
* @param name The name of the project
* @param location The location of the project
* @param projectTypeId The project type id
*/
createProject(name: string, location: vscode.Uri, projectTypeId: string): Promise<vscode.Uri>;
readonly isProjectProviderAvailable: boolean;
/**
* Event fires when projects in workspace changes
*/
readonly onDidWorkspaceProjectsChange: vscode.Event<void>;
/**
* Verify that a workspace is open or if one isn't, ask user to pick whether a workspace should be automatically created
*/
validateWorkspace(): Promise<boolean>;
/**
* Shows confirmation message that the extension host will be restarted and current workspace/file will be closed. If confirmed, the specified workspace will be entered.
*/
enterWorkspace(workspaceFile: vscode.Uri): Promise<void>;
}

View File

@@ -0,0 +1,46 @@
/*---------------------------------------------------------------------------------------------
* 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 fs from 'fs';
import * as constants from '../common/constants';
/**
* Returns the default location to save a new database project
*/
export function defaultProjectSaveLocation(): vscode.Uri | undefined {
return projectSaveLocationSettingIsValid() ? vscode.Uri.file(projectSaveLocationSetting()) : undefined;
}
/**
* Get workspace configurations for this extension
*/
function config(): vscode.WorkspaceConfiguration {
return vscode.workspace.getConfiguration(constants.ProjectConfigurationKey);
}
/**
* Returns the workspace setting on the default location to save new database projects
*/
function projectSaveLocationSetting(): string {
return config()[constants.ProjectSaveLocationKey];
}
/**
* Returns if the default save location for new database projects workspace setting exists and is
* a valid path
*/
function projectSaveLocationSettingIsValid(): boolean {
return projectSaveLocationSettingExists() && fs.existsSync(projectSaveLocationSetting());
}
/**
* Returns if a value for the default save location for new database projects exists
*/
function projectSaveLocationSettingExists(): boolean {
return projectSaveLocationSetting() !== undefined && projectSaveLocationSetting() !== null
&& projectSaveLocationSetting().trim() !== '';
}

View File

@@ -9,20 +9,24 @@ import { IProjectProviderRegistry } from './interfaces';
export const ProjectProviderRegistry: IProjectProviderRegistry = new class implements IProjectProviderRegistry {
private _providers = new Array<IProjectProvider>();
private _providerMapping: { [key: string]: IProjectProvider } = {};
private _providerFileExtensionMapping: { [key: string]: IProjectProvider } = {};
private _providerProjectTypeMapping: { [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;
this._providerFileExtensionMapping[projectType.projectFileExtension.toUpperCase()] = provider;
this._providerProjectTypeMapping[projectType.id.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()];
delete this._providerFileExtensionMapping[projectType.projectFileExtension.toUpperCase()];
delete this._providerProjectTypeMapping[projectType.id.toUpperCase()];
});
}
});
@@ -39,7 +43,11 @@ export const ProjectProviderRegistry: IProjectProviderRegistry = new class imple
validateProvider(provider: IProjectProvider): void {
}
getProviderByProjectExtension(extension: string): IProjectProvider | undefined {
return extension ? this._providerFileExtensionMapping[extension.toUpperCase()] : undefined;
}
getProviderByProjectType(projectType: string): IProjectProvider | undefined {
return projectType ? this._providerMapping[projectType.toUpperCase()] : undefined;
return projectType ? this._providerProjectTypeMapping[projectType.toUpperCase()] : undefined;
}
};

View File

@@ -0,0 +1,31 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as fs from 'fs';
export async function directoryExist(directoryPath: string): Promise<boolean> {
const stats = await getFileStatus(directoryPath);
return stats ? stats.isDirectory() : false;
}
export async function fileExist(filePath: string): Promise<boolean> {
const stats = await getFileStatus(filePath);
return stats ? stats.isFile() : false;
}
async function getFileStatus(path: string): Promise<fs.Stats | undefined> {
try {
const stats = await fs.promises.stat(path);
return stats;
}
catch (e) {
if (e.code === 'ENOENT') {
return undefined;
}
else {
throw e;
}
}
}

View File

@@ -5,7 +5,7 @@
import * as vscode from 'vscode';
import { IWorkspaceService } from './interfaces';
import { UnknownProjectsErrorMessage } from './constants';
import { UnknownProjectsError } from './constants';
import { WorkspaceTreeItem } from 'dataworkspace';
/**
@@ -37,6 +37,7 @@ export class WorkspaceTreeDataProvider implements vscode.TreeDataProvider<Worksp
else {
// if the element is undefined return the project tree items
const projects = await this._workspaceService.getProjectsInWorkspace();
await vscode.commands.executeCommand('setContext', 'isProjectsViewEmpty', projects.length === 0);
const unknownProjects: string[] = [];
const treeItems: WorkspaceTreeItem[] = [];
for (const project of projects) {
@@ -60,7 +61,7 @@ export class WorkspaceTreeDataProvider implements vscode.TreeDataProvider<Worksp
});
}
if (unknownProjects.length > 0) {
vscode.window.showErrorMessage(UnknownProjectsErrorMessage(unknownProjects));
vscode.window.showErrorMessage(UnknownProjectsError(unknownProjects));
}
return treeItems;
}

View File

@@ -14,11 +14,25 @@ declare module 'dataworkspace' {
*/
export interface IExtension {
/**
* register a project provider
* @param provider new project provider
* @requires a disposable object, upon disposal, the provider will be unregistered.
* Returns all the projects in the workspace
*/
registerProjectProvider(provider: IProjectProvider): vscode.Disposable;
getProjectsInWorkspace(): vscode.Uri[];
/**
* Add projects to the workspace
* @param projectFiles Uris of project files to add
*/
addProjectsToWorkspace(projectFiles: vscode.Uri[]): Promise<void>
/**
* Change focus to Projects view
*/
showProjectsView(): void;
/**
* Returns the default location to save projects
*/
defaultProjectSaveLocation: vscode.Uri | undefined;
}
/**
@@ -37,6 +51,13 @@ declare module 'dataworkspace' {
*/
RemoveProject(projectFile: vscode.Uri): Promise<void>;
/**
*
* @param name Create a project
* @param location the parent directory of the project
*/
createProject(name: string, location: vscode.Uri): Promise<vscode.Uri>;
/**
* Gets the supported project types
*/
@@ -47,11 +68,21 @@ declare module 'dataworkspace' {
* Defines the project type
*/
export interface IProjectType {
/**
* id of the project type
*/
readonly id: string;
/**
* display name of the project type
*/
readonly displayName: string;
/**
* description of the project type
*/
readonly description: string;
/**
* project file extension, e.g. sqlproj
*/

View File

@@ -0,0 +1,125 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import * as path from 'path';
import * as constants from '../common/constants';
interface Deferred<T> {
resolve: (result: T | Promise<T>) => void;
reject: (reason: any) => void;
}
export abstract class DialogBase {
protected _toDispose: vscode.Disposable[] = [];
protected _dialogObject: azdata.window.Dialog;
protected initDialogComplete: Deferred<void> | undefined;
protected initDialogPromise: Promise<void> = new Promise<void>((resolve, reject) => this.initDialogComplete = { resolve, reject });
protected workspaceFormComponent: azdata.FormComponent | undefined;
protected workspaceInputBox: azdata.InputBoxComponent | undefined;
constructor(dialogTitle: string, dialogName: string, dialogWidth: azdata.window.DialogWidth = 600) {
this._dialogObject = azdata.window.createModelViewDialog(dialogTitle, dialogName, dialogWidth);
this._dialogObject.okButton.label = constants.OkButtonText;
this.register(this._dialogObject.cancelButton.onClick(() => this.onCancelButtonClicked()));
this.register(this._dialogObject.okButton.onClick(() => this.onOkButtonClicked()));
this._dialogObject.registerCloseValidator(async () => {
return this.validate();
});
}
protected abstract initialize(view: azdata.ModelView): Promise<void>;
protected async validate(): Promise<boolean> {
return Promise.resolve(true);
}
public async open(): Promise<void> {
const tab = azdata.window.createTab('');
tab.registerContent(async (view: azdata.ModelView) => {
return this.initialize(view);
});
this._dialogObject.content = [tab];
azdata.window.openDialog(this._dialogObject);
await this.initDialogPromise;
}
private onCancelButtonClicked(): void {
this.dispose();
}
private async onOkButtonClicked(): Promise<void> {
await this.onComplete();
this.dispose();
}
protected async onComplete(): Promise<void> {
}
protected dispose(): void {
this._toDispose.forEach(disposable => disposable.dispose());
}
protected register(disposable: vscode.Disposable): void {
this._toDispose.push(disposable);
}
protected showErrorMessage(message: string): void {
this._dialogObject.message = {
text: message,
level: azdata.window.MessageLevel.Error
};
}
protected createHorizontalContainer(view: azdata.ModelView, items: azdata.Component[]): azdata.FlexContainer {
return view.modelBuilder.flexContainer().withItems(items, { CSSStyles: { 'margin-right': '5px', 'margin-bottom': '10px' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component();
}
/**
* Creates container with information on which workspace the project will be added to and where the workspace will be
* created if no workspace is currently open
* @param view
*/
protected createWorkspaceContainer(view: azdata.ModelView): azdata.FormComponent {
const workspaceDescription = view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
value: vscode.workspace.workspaceFile ? constants.AddProjectToCurrentWorkspace : constants.NewWorkspaceWillBeCreated,
CSSStyles: { 'margin-top': '3px', 'margin-bottom': '10px' }
}).component();
this.workspaceInputBox = view.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
ariaLabel: constants.WorkspaceLocationTitle,
width: constants.DefaultInputWidth,
enabled: false,
value: vscode.workspace.workspaceFile?.fsPath ?? '',
title: vscode.workspace.workspaceFile?.fsPath ?? '' // hovertext for if file path is too long to be seen in textbox
}).component();
const container = view.modelBuilder.flexContainer()
.withItems([workspaceDescription, this.workspaceInputBox])
.withLayout({ flexFlow: 'column' })
.component();
this.workspaceFormComponent = {
title: constants.Workspace,
component: container
};
return this.workspaceFormComponent;
}
/**
* Update the workspace inputbox based on the passed in location and name if there isn't a workspace currently open
* @param location
* @param name
*/
protected updateWorkspaceInputbox(location: string, name: string): void {
if (!vscode.workspace.workspaceFile) {
const fileLocation = location && name ? path.join(location, `${name}.code-workspace`) : '';
this.workspaceInputBox!.value = fileLocation;
this.workspaceInputBox!.title = fileLocation;
}
}
}

View File

@@ -0,0 +1,171 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import * as path from 'path';
import { DialogBase } from './dialogBase';
import { IWorkspaceService } from '../common/interfaces';
import * as constants from '../common/constants';
import { IProjectType } from 'dataworkspace';
import { directoryExist } from '../common/utils';
import { IconPathHelper } from '../common/iconHelper';
import { defaultProjectSaveLocation } from '../common/projectLocationHelper';
class NewProjectDialogModel {
projectTypeId: string = '';
projectFileExtension: string = '';
name: string = '';
location: string = '';
}
export class NewProjectDialog extends DialogBase {
public model: NewProjectDialogModel = new NewProjectDialogModel();
constructor(private workspaceService: IWorkspaceService) {
super(constants.NewProjectDialogTitle, 'NewProject');
}
async validate(): Promise<boolean> {
try {
// the selected location should be an existing directory
const parentDirectoryExists = await directoryExist(this.model.location);
if (!parentDirectoryExists) {
this.showErrorMessage(constants.ProjectParentDirectoryNotExistError(this.model.location));
return false;
}
// there shouldn't be an existing sub directory with the same name as the project in the selected location
const projectDirectoryExists = await directoryExist(path.join(this.model.location, this.model.name));
if (projectDirectoryExists) {
this.showErrorMessage(constants.ProjectDirectoryAlreadyExistError(this.model.name, this.model.location));
return false;
}
return true;
}
catch (err) {
this.showErrorMessage(err?.message ? err.message : err);
return false;
}
}
async onComplete(): Promise<void> {
try {
const validateWorkspace = await this.workspaceService.validateWorkspace();
if (validateWorkspace) {
await this.workspaceService.createProject(this.model.name, vscode.Uri.file(this.model.location), this.model.projectTypeId);
}
}
catch (err) {
vscode.window.showErrorMessage(err?.message ? err.message : err);
}
}
protected async initialize(view: azdata.ModelView): Promise<void> {
const allProjectTypes = await this.workspaceService.getAllProjectTypes();
const projectTypeRadioCardGroup = view.modelBuilder.radioCardGroup().withProperties<azdata.RadioCardGroupComponentProperties>({
cards: allProjectTypes.map((projectType: IProjectType) => {
return <azdata.RadioCard>{
id: projectType.id,
label: projectType.displayName,
icon: projectType.icon,
descriptions: [
{
textValue: projectType.displayName,
textStyles: {
'font-size': '13px',
'font-weight': 'bold'
}
}, {
textValue: projectType.description
}
]
};
}),
iconHeight: '50px',
iconWidth: '50px',
cardWidth: '170px',
cardHeight: '170px',
ariaLabel: constants.TypeTitle,
width: '500px',
iconPosition: 'top',
selectedCardId: allProjectTypes.length > 0 ? allProjectTypes[0].id : undefined
}).component();
this.register(projectTypeRadioCardGroup.onSelectionChanged((e) => {
this.model.projectTypeId = e.cardId;
}));
const projectNameTextBox = view.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
ariaLabel: constants.ProjectNameTitle,
placeHolder: constants.ProjectNamePlaceholder,
required: true,
width: constants.DefaultInputWidth
}).component();
this.register(projectNameTextBox.onTextChanged(() => {
this.model.name = projectNameTextBox.value!;
projectNameTextBox.updateProperty('title', projectNameTextBox.value);
this.updateWorkspaceInputbox(this.model.location, this.model.name);
}));
const locationTextBox = view.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
ariaLabel: constants.ProjectLocationTitle,
placeHolder: constants.ProjectLocationPlaceholder,
required: true,
width: constants.DefaultInputWidth
}).component();
this.register(locationTextBox.onTextChanged(() => {
this.model.location = locationTextBox.value!;
locationTextBox.updateProperty('title', locationTextBox.value);
this.updateWorkspaceInputbox(this.model.location, this.model.name);
}));
const browseFolderButton = view.modelBuilder.button().withProperties<azdata.ButtonProperties>({
ariaLabel: constants.BrowseButtonText,
iconPath: IconPathHelper.folder,
height: '16px',
width: '18px'
}).component();
this.register(browseFolderButton.onDidClick(async () => {
let folderUris = await vscode.window.showOpenDialog({
canSelectFiles: false,
canSelectFolders: true,
canSelectMany: false,
defaultUri: defaultProjectSaveLocation()
});
if (!folderUris || folderUris.length === 0) {
return;
}
const selectedFolder = folderUris[0].fsPath;
locationTextBox.value = selectedFolder;
this.model.location = selectedFolder;
this.updateWorkspaceInputbox(this.model.location, this.model.name);
}));
const form = view.modelBuilder.formContainer().withFormItems([
{
title: constants.TypeTitle,
required: true,
component: projectTypeRadioCardGroup
},
{
title: constants.ProjectNameTitle,
required: true,
component: this.createHorizontalContainer(view, [projectNameTextBox])
}, {
title: constants.ProjectLocationTitle,
required: true,
component: this.createHorizontalContainer(view, [locationTextBox, browseFolderButton])
},
this.createWorkspaceContainer(view)
]).component();
await view.initializeModel(form);
this.initDialogComplete?.resolve();
}
}

View File

@@ -0,0 +1,208 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import * as path from 'path';
import { DialogBase } from './dialogBase';
import * as constants from '../common/constants';
import { IWorkspaceService } from '../common/interfaces';
import { fileExist } from '../common/utils';
import { IconPathHelper } from '../common/iconHelper';
export class OpenExistingDialog extends DialogBase {
public _projectFile: string = '';
public _workspaceFile: string = '';
public _targetTypeRadioCardGroup: azdata.RadioCardGroupComponent | undefined;
public _filePathTextBox: azdata.InputBoxComponent | undefined;
public formBuilder: azdata.FormBuilder | undefined;
private _targetTypes = [
{
name: constants.Project,
icon: {
dark: this.extensionContext.asAbsolutePath('images/file_inverse.svg'),
light: this.extensionContext.asAbsolutePath('images/file.svg')
}
}, {
name: constants.Workspace,
icon: {
dark: this.extensionContext.asAbsolutePath('images/file_inverse.svg'), // temporary - still waiting for real icon from UX
light: this.extensionContext.asAbsolutePath('images/file.svg')
}
}
];
constructor(private workspaceService: IWorkspaceService, private extensionContext: vscode.ExtensionContext) {
super(constants.OpenExistingDialogTitle, 'OpenProject');
}
async validate(): Promise<boolean> {
try {
// the selected location should be an existing directory
if (this._targetTypeRadioCardGroup?.selectedCardId === constants.Project) {
const fileExists = await fileExist(this._projectFile);
if (!fileExists) {
this.showErrorMessage(constants.ProjectFileNotExistError(this._projectFile));
return false;
}
} else if (this._targetTypeRadioCardGroup?.selectedCardId === constants.Workspace) {
const fileExists = await fileExist(this._workspaceFile);
if (!fileExists) {
this.showErrorMessage(constants.WorkspaceFileNotExistError(this._workspaceFile));
return false;
}
}
return true;
}
catch (err) {
this.showErrorMessage(err?.message ? err.message : err);
return false;
}
}
async onComplete(): Promise<void> {
try {
if (this._targetTypeRadioCardGroup?.selectedCardId === constants.Workspace) {
await this.workspaceService.enterWorkspace(vscode.Uri.file(this._workspaceFile));
} else {
const validateWorkspace = await this.workspaceService.validateWorkspace();
if (validateWorkspace) {
await this.workspaceService.addProjectsToWorkspace([vscode.Uri.file(this._projectFile)]);
}
}
}
catch (err) {
vscode.window.showErrorMessage(err?.message ? err.message : err);
}
}
protected async initialize(view: azdata.ModelView): Promise<void> {
this._targetTypeRadioCardGroup = view.modelBuilder.radioCardGroup().withProperties<azdata.RadioCardGroupComponentProperties>({
cards: this._targetTypes.map((targetType) => {
return <azdata.RadioCard>{
id: targetType.name,
label: targetType.name,
icon: targetType.icon,
descriptions: [
{
textValue: targetType.name,
textStyles: {
'font-size': '13px'
}
}
]
};
}),
iconHeight: '50px',
iconWidth: '50px',
cardWidth: '170px',
cardHeight: '170px',
ariaLabel: constants.TypeTitle,
width: '500px',
iconPosition: 'top',
selectedCardId: constants.Project
}).component();
this._filePathTextBox = view.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
ariaLabel: constants.LocationSelectorTitle,
placeHolder: constants.ProjectFilePlaceholder,
required: true,
width: constants.DefaultInputWidth
}).component();
this.register(this._filePathTextBox.onTextChanged(() => {
this._projectFile = this._filePathTextBox!.value!;
this._filePathTextBox!.updateProperty('title', this._projectFile);
this.updateWorkspaceInputbox(path.dirname(this._projectFile), path.basename(this._projectFile, path.extname(this._projectFile)));
}));
const browseFolderButton = view.modelBuilder.button().withProperties<azdata.ButtonProperties>({
ariaLabel: constants.BrowseButtonText,
iconPath: IconPathHelper.folder,
width: '18px',
height: '16px',
}).component();
this.register(browseFolderButton.onDidClick(async () => {
if (this._targetTypeRadioCardGroup?.selectedCardId === constants.Project) {
await this.projectBrowse();
} else if (this._targetTypeRadioCardGroup?.selectedCardId === constants.Workspace) {
await this.workspaceBrowse();
}
}));
this.register(this._targetTypeRadioCardGroup.onSelectionChanged(({ cardId }) => {
if (cardId === constants.Project) {
this._filePathTextBox!.placeHolder = constants.ProjectFilePlaceholder;
this.formBuilder?.addFormItem(this.workspaceFormComponent!);
} else if (cardId === constants.Workspace) {
this._filePathTextBox!.placeHolder = constants.WorkspacePlaceholder;
this.formBuilder?.removeFormItem(this.workspaceFormComponent!);
}
// clear selected file textbox
this._filePathTextBox!.value = '';
}));
this.formBuilder = view.modelBuilder.formContainer().withFormItems([
{
title: constants.TypeTitle,
required: true,
component: this._targetTypeRadioCardGroup,
}, {
title: constants.LocationSelectorTitle,
required: true,
component: this.createHorizontalContainer(view, [this._filePathTextBox, browseFolderButton])
},
this.createWorkspaceContainer(view)
]);
await view.initializeModel(this.formBuilder?.component());
this.initDialogComplete?.resolve();
}
public async workspaceBrowse(): Promise<void> {
const filters: { [name: string]: string[] } = { [constants.Workspace]: [constants.WorkspaceFileExtension] };
const fileUris = await vscode.window.showOpenDialog({
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: false,
openLabel: constants.SelectProjectFileActionName,
filters: filters
});
if (!fileUris || fileUris.length === 0) {
return;
}
const workspaceFilePath = fileUris[0].fsPath;
this._filePathTextBox!.value = workspaceFilePath;
this._workspaceFile = workspaceFilePath;
}
public async projectBrowse(): Promise<void> {
const filters: { [name: string]: string[] } = {};
const projectTypes = await this.workspaceService.getAllProjectTypes();
filters[constants.AllProjectTypes] = projectTypes.map(type => type.projectFileExtension);
projectTypes.forEach(type => {
filters[type.displayName] = [type.projectFileExtension];
});
const fileUris = await vscode.window.showOpenDialog({
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: false,
openLabel: constants.SelectProjectFileActionName,
filters: filters
});
if (!fileUris || fileUris.length === 0) {
return;
}
const projectFilePath = fileUris[0].fsPath;
this._filePathTextBox!.value = projectFilePath;
this._projectFile = projectFilePath;
}
}

View File

@@ -4,48 +4,48 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as path from 'path';
import { WorkspaceTreeDataProvider } from './common/workspaceTreeDataProvider';
import { WorkspaceService } from './services/workspaceService';
import { AllProjectTypes, SelectProjectFileActionName } from './common/constants';
import { WorkspaceTreeItem } from 'dataworkspace';
import { WorkspaceTreeItem, IExtension } from 'dataworkspace';
import { DataWorkspaceExtension } from './common/dataWorkspaceExtension';
import { NewProjectDialog } from './dialogs/newProjectDialog';
import { OpenExistingDialog } from './dialogs/openExistingDialog';
import { IWorkspaceService } from './common/interfaces';
import { IconPathHelper } from './common/iconHelper';
export function activate(context: vscode.ExtensionContext): void {
const workspaceService = new WorkspaceService();
export function activate(context: vscode.ExtensionContext): Promise<IExtension> {
const workspaceService = new WorkspaceService(context);
workspaceService.loadTempProjects();
const workspaceTreeDataProvider = new WorkspaceTreeDataProvider(workspaceService);
const dataWorkspaceExtension = new DataWorkspaceExtension(workspaceService);
context.subscriptions.push(vscode.window.registerTreeDataProvider('dataworkspace.views.main', workspaceTreeDataProvider));
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 filters: { [name: string]: string[] } = {};
const projectTypes = await workspaceService.getAllProjectTypes();
filters[AllProjectTypes] = projectTypes.map(type => type.projectFileExtension);
projectTypes.forEach(type => {
filters[type.displayName] = [type.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: filters
});
if (!fileUris || fileUris.length === 0) {
return;
}
await workspaceService.addProjectsToWorkspace(fileUris);
}
context.subscriptions.push(vscode.extensions.onDidChange(() => {
setProjectProviderContextValue(workspaceService);
}));
setProjectProviderContextValue(workspaceService);
context.subscriptions.push(vscode.commands.registerCommand('projects.new', async () => {
const dialog = new NewProjectDialog(workspaceService);
await dialog.open();
}));
context.subscriptions.push(vscode.commands.registerCommand('projects.openExisting', async () => {
const dialog = new OpenExistingDialog(workspaceService, context);
await dialog.open();
}));
context.subscriptions.push(vscode.commands.registerCommand('dataworkspace.refresh', () => {
workspaceTreeDataProvider.refresh();
}));
context.subscriptions.push(vscode.commands.registerCommand('projects.removeProject', async (treeItem: WorkspaceTreeItem) => {
await workspaceService.removeProject(vscode.Uri.file(treeItem.element.project.projectFilePath));
}));
IconPathHelper.setExtensionContext(context);
return Promise.resolve(dataWorkspaceExtension);
}
function setProjectProviderContextValue(workspaceService: IWorkspaceService): void {
vscode.commands.executeCommand('setContext', 'isProjectProviderAvailable', workspaceService.isProjectProviderAvailable);
}
export function deactivate(): void {

View File

@@ -3,50 +3,137 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import * as dataworkspace from 'dataworkspace';
import * as path from 'path';
import * as constants from '../common/constants';
import { IWorkspaceService } from '../common/interfaces';
import { ProjectProviderRegistry } from '../common/projectProviderRegistry';
import Logger from '../common/logger';
import { ExtensionActivationErrorMessage } from '../common/constants';
const WorkspaceConfigurationName = 'dataworkspace';
const ProjectsConfigurationName = 'projects';
const TempProject = 'tempProject';
export class WorkspaceService implements IWorkspaceService {
private _onDidWorkspaceProjectsChange: vscode.EventEmitter<void> = new vscode.EventEmitter<void>();
readonly onDidWorkspaceProjectsChange: vscode.Event<void> = this._onDidWorkspaceProjectsChange?.event;
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;
constructor(private _context: vscode.ExtensionContext) {
}
// 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));
}
/**
* Load any temp project that needed to be loaded before the extension host was restarted
* which would happen if a workspace was created in order open or create a project
*/
async loadTempProjects(): Promise<void> {
const tempProjects: string[] | undefined = this._context.globalState.get(TempProject) ?? undefined;
if (tempProjects && vscode.workspace.workspaceFile) {
// add project to workspace now that the workspace has been created and saved
for (let project of tempProjects) {
await this.addProjectsToWorkspace([vscode.Uri.file(<string>project)]);
}
await this._context.globalState.update(TempProject, undefined);
}
}
/**
* Creates a new workspace in the same folder as the project. Because the extension host gets restared when
* a new workspace is created and opened, the project needs to be saved as the temp project that will be loaded
* when the extension gets restarted
* @param projectFileFsPath project to add to the workspace
*/
async CreateNewWorkspaceForProject(projectFileFsPath: string): Promise<void> {
// save temp project
await this._context.globalState.update(TempProject, [projectFileFsPath]);
// create a new workspace - the workspace file will be created in the same folder as the project
const workspaceFile = vscode.Uri.file(path.join(path.dirname(projectFileFsPath), `${path.parse(projectFileFsPath).name}.code-workspace`));
const projectFolder = vscode.Uri.file(path.dirname(projectFileFsPath));
await azdata.workspace.createWorkspace(projectFolder, workspaceFile);
}
get isProjectProviderAvailable(): boolean {
for (const extension of vscode.extensions.all) {
const projectTypes = extension.packageJSON.contributes && extension.packageJSON.contributes.projects as string[];
if (projectTypes && projectTypes.length > 0) {
return true;
}
}
return false;
}
/**
* Verify that a workspace is open or that if one isn't, it's ok to create a workspace
*/
async validateWorkspace(): Promise<boolean> {
if (!vscode.workspace.workspaceFile) {
const result = await vscode.window.showWarningMessage(constants.CreateWorkspaceConfirmation, constants.OkButtonText, constants.CancelButtonText);
if (result === constants.OkButtonText) {
return true;
} else {
return false;
}
} else {
// workspace is open
return true;
}
}
/**
* Shows confirmation message that the extension host will be restarted and current workspace/file will be closed. If confirmed, the specified workspace will be entered.
* @param workspaceFile
*/
async enterWorkspace(workspaceFile: vscode.Uri): Promise<void> {
const result = await vscode.window.showWarningMessage(constants.EnterWorkspaceConfirmation, constants.OkButtonText, constants.CancelButtonText);
if (result === constants.OkButtonText) {
await azdata.workspace.enterWorkspace(workspaceFile);
} else {
return;
}
}
async addProjectsToWorkspace(projectFiles: vscode.Uri[]): Promise<void> {
if (!projectFiles || projectFiles.length === 0) {
return;
}
// a workspace needs to be open to add projects
if (!vscode.workspace.workspaceFile) {
await this.CreateNewWorkspaceForProject(projectFiles[0].fsPath);
// this won't get hit since the extension host will get restarted, but helps with testing
return;
}
const currentProjects: vscode.Uri[] = 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)));
this._onDidWorkspaceProjectsChange.fire();
}
if (newProjectFileAdded) {
// Save the new set of projects to the workspace configuration.
await this.setWorkspaceConfigurationValue(ProjectsConfigurationName, currentProjects.map(project => this.toRelativePath(project)));
this._onDidWorkspaceProjectsChange.fire();
}
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) }))));
}
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) }))));
}
}
@@ -59,22 +146,22 @@ export class WorkspaceService implements IWorkspaceService {
return projectTypes;
}
async getProjectsInWorkspace(): Promise<vscode.Uri[]> {
getProjectsInWorkspace(): vscode.Uri[] {
return vscode.workspace.workspaceFile ? this.getWorkspaceConfigurationValue<string[]>(ProjectsConfigurationName).map(project => this.toUri(project)) : [];
}
async getProjectProvider(projectFile: vscode.Uri): Promise<dataworkspace.IProjectProvider | undefined> {
const projectType = path.extname(projectFile.path).replace(/\./g, '');
let provider = ProjectProviderRegistry.getProviderByProjectType(projectType);
let provider = ProjectProviderRegistry.getProviderByProjectExtension(projectType);
if (!provider) {
await this.ensureProviderExtensionLoaded(projectType);
}
return ProjectProviderRegistry.getProviderByProjectType(projectType);
return ProjectProviderRegistry.getProviderByProjectExtension(projectType);
}
async removeProject(projectFile: vscode.Uri): Promise<void> {
if (vscode.workspace.workspaceFile) {
const currentProjects: vscode.Uri[] = await this.getProjectsInWorkspace();
const currentProjects: vscode.Uri[] = this.getProjectsInWorkspace();
const projectIdx = currentProjects.findIndex((p: vscode.Uri) => p.fsPath === projectFile.fsPath);
if (projectIdx !== -1) {
currentProjects.splice(projectIdx, 1);
@@ -84,6 +171,18 @@ export class WorkspaceService implements IWorkspaceService {
}
}
async createProject(name: string, location: vscode.Uri, projectTypeId: string): Promise<vscode.Uri> {
const provider = ProjectProviderRegistry.getProviderByProjectType(projectTypeId);
if (provider) {
const projectFile = await provider.createProject(name, location);
this.addProjectsToWorkspace([projectFile]);
this._onDidWorkspaceProjectsChange.fire();
return projectFile;
} else {
throw new Error(constants.ProviderNotFoundForProjectTypeError(projectTypeId));
}
}
/**
* 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.
@@ -113,7 +212,7 @@ export class WorkspaceService implements IWorkspaceService {
await extension.activate();
}
} catch (err) {
Logger.error(ExtensionActivationErrorMessage(extension.id, err));
Logger.error(constants.ExtensionActivationError(extension.id, err));
}
if (extension.isActive && extension.exports && !ProjectProviderRegistry.providers.includes(extension.exports)) {

View File

@@ -0,0 +1,58 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as should from 'should';
import * as TypeMoq from 'typemoq';
import * as os from 'os';
import * as path from 'path';
import * as vscode from 'vscode';
import * as sinon from 'sinon';
import { promises as fs } from 'fs';
import { NewProjectDialog } from '../../dialogs/newProjectDialog';
import { WorkspaceService } from '../../services/workspaceService';
import { testProjectType } from '../testUtils';
suite('New Project Dialog', function (): void {
test('Should validate project location', async function (): Promise<void> {
const workspaceServiceMock = TypeMoq.Mock.ofType<WorkspaceService>();
workspaceServiceMock.setup(x => x.getAllProjectTypes()).returns(() => Promise.resolve([testProjectType]));
const dialog = new NewProjectDialog(workspaceServiceMock.object);
await dialog.open();
dialog.model.name = 'TestProject';
dialog.model.location = '';
should.equal(await dialog.validate(), false, 'Validation should fail becausee the parent directory does not exist');
// create a folder with the same name
const folderPath = path.join(os.tmpdir(), dialog.model.name);
await fs.mkdir(folderPath, { recursive: true });
dialog.model.location = os.tmpdir();
should.equal(await dialog.validate(), false, 'Validation should fail because a folder with the same name exists');
// change project name to be unique
dialog.model.name = `TestProject_${new Date().getTime()}`;
should.equal(await dialog.validate(), true, 'Validation should pass because name is unique and parent directory exists');
});
test('Should validate workspace in onComplete', async function (): Promise<void> {
const workspaceServiceMock = TypeMoq.Mock.ofType<WorkspaceService>();
workspaceServiceMock.setup(x => x.validateWorkspace()).returns(() => Promise.resolve(true));
workspaceServiceMock.setup(x => x.getAllProjectTypes()).returns(() => Promise.resolve([testProjectType]));
const dialog = new NewProjectDialog(workspaceServiceMock.object);
await dialog.open();
dialog.model.name = 'TestProject';
dialog.model.location = '';
should.doesNotThrow(async () => await dialog.onComplete());
workspaceServiceMock.setup(x => x.validateWorkspace()).throws(new Error('test error'));
const spy = sinon.spy(vscode.window, 'showErrorMessage');
should.doesNotThrow(async () => await dialog.onComplete(), 'Error should be caught');
should(spy.calledOnce).be.true();
});
});

View File

@@ -0,0 +1,106 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as should from 'should';
import * as TypeMoq from 'typemoq';
import * as sinon from 'sinon';
import * as vscode from 'vscode';
import * as constants from '../../common/constants';
import { promises as fs } from 'fs';
import { WorkspaceService } from '../../services/workspaceService';
import { OpenExistingDialog } from '../../dialogs/openExistingDialog';
import { createProjectFile, generateUniqueProjectFilePath, generateUniqueWorkspaceFilePath, testProjectType } from '../testUtils';
suite('Open Existing Dialog', function (): void {
const mockExtensionContext = TypeMoq.Mock.ofType<vscode.ExtensionContext>();
this.afterEach(() => {
sinon.restore();
});
test('Should validate project file exists', async function (): Promise<void> {
const workspaceServiceMock = TypeMoq.Mock.ofType<WorkspaceService>();
const dialog = new OpenExistingDialog(workspaceServiceMock.object, mockExtensionContext.object);
await dialog.open();
dialog._targetTypeRadioCardGroup?.updateProperty( 'selectedCardId', constants.Project);
dialog._projectFile = '';
should.equal(await dialog.validate(), false, 'Validation fail because project file does not exist');
// create a project file
dialog._projectFile = await createProjectFile('testproj');
should.equal(await dialog.validate(), true, 'Validation pass because project file exists');
});
test('Should validate workspace file exists', async function (): Promise<void> {
const workspaceServiceMock = TypeMoq.Mock.ofType<WorkspaceService>();
const dialog = new OpenExistingDialog(workspaceServiceMock.object, mockExtensionContext.object);
await dialog.open();
dialog._targetTypeRadioCardGroup?.updateProperty( 'selectedCardId', constants.Workspace);
dialog._workspaceFile = '';
should.equal(await dialog.validate(), false, 'Validation fail because workspace file does not exist');
// create a workspace file
dialog._workspaceFile = generateUniqueWorkspaceFilePath();
await fs.writeFile(dialog._workspaceFile, '');
should.equal(await dialog.validate(), true, 'Validation pass because workspace file exists');
});
test('Should validate workspace in onComplete when opening project', async function (): Promise<void> {
const workspaceServiceMock = TypeMoq.Mock.ofType<WorkspaceService>();
workspaceServiceMock.setup(x => x.validateWorkspace()).returns(() => Promise.resolve(true));
workspaceServiceMock.setup(x => x.addProjectsToWorkspace(TypeMoq.It.isAny())).returns(() => Promise.resolve());
const dialog = new OpenExistingDialog(workspaceServiceMock.object, mockExtensionContext.object);
await dialog.open();
dialog._projectFile = generateUniqueProjectFilePath('testproj');
should.doesNotThrow(async () => await dialog.onComplete());
workspaceServiceMock.setup(x => x.validateWorkspace()).throws(new Error('test error'));
const spy = sinon.spy(vscode.window, 'showErrorMessage');
should.doesNotThrow(async () => await dialog.onComplete(), 'Error should be caught');
should(spy.calledOnce).be.true();
});
test('workspace browse', async function (): Promise<void> {
const workspaceServiceMock = TypeMoq.Mock.ofType<WorkspaceService>();
sinon.stub(vscode.window, 'showOpenDialog').returns(Promise.resolve([]));
const dialog = new OpenExistingDialog(workspaceServiceMock.object, mockExtensionContext.object);
await dialog.open();
should.equal(dialog._workspaceFile, '');
await dialog.workspaceBrowse();
should.equal(dialog._workspaceFile, '', 'Workspace file should not be set when no file is selected');
sinon.restore();
const workspaceFile = vscode.Uri.file(generateUniqueWorkspaceFilePath());
sinon.stub(vscode.window, 'showOpenDialog').returns(Promise.resolve([workspaceFile]));
await dialog.workspaceBrowse();
should.equal(dialog._workspaceFile, workspaceFile.fsPath, 'Workspace file should get set');
should.equal(dialog._filePathTextBox?.value, workspaceFile.fsPath);
});
test('project browse', async function (): Promise<void> {
const workspaceServiceMock = TypeMoq.Mock.ofType<WorkspaceService>();
workspaceServiceMock.setup(x => x.getAllProjectTypes()).returns(() => Promise.resolve([testProjectType]));
sinon.stub(vscode.window, 'showOpenDialog').returns(Promise.resolve([]));
const dialog = new OpenExistingDialog(workspaceServiceMock.object, mockExtensionContext.object);
await dialog.open();
should.equal(dialog._projectFile, '');
await dialog.projectBrowse();
should.equal(dialog._projectFile, '', 'Project file should not be set when no file is selected');
sinon.restore();
const projectFile = vscode.Uri.file(generateUniqueProjectFilePath('testproj'));
sinon.stub(vscode.window, 'showOpenDialog').returns(Promise.resolve([projectFile]));
await dialog.projectBrowse();
should.equal(dialog._projectFile, projectFile.fsPath, 'Project file should be set');
should.equal(dialog._filePathTextBox?.value, projectFile.fsPath);
});
});

View File

@@ -28,6 +28,9 @@ export function createProjectProvider(projectTypes: IProjectType[]): IProjectPro
},
getProjectTreeDataProvider: (projectFile: vscode.Uri): Promise<vscode.TreeDataProvider<any>> => {
return Promise.resolve(treeDataProvider);
},
createProject: (name: string, location: vscode.Uri): Promise<vscode.Uri> => {
return Promise.resolve(location);
}
};
return projectProvider;
@@ -37,51 +40,57 @@ suite('ProjectProviderRegistry Tests', function (): void {
test('register and unregister project providers', async () => {
const provider1 = createProjectProvider([
{
id: 'tp1',
projectFileExtension: 'testproj',
icon: '',
displayName: 'test project'
displayName: 'test project',
description: ''
}, {
id: 'tp2',
projectFileExtension: 'testproj1',
icon: '',
displayName: 'test project 1'
displayName: 'test project 1',
description: ''
}
]);
const provider2 = createProjectProvider([
{
id: 'sp1',
projectFileExtension: 'sqlproj',
icon: '',
displayName: 'sql project'
displayName: 'sql project',
description: ''
}
]);
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');
let providerResult = ProjectProviderRegistry.getProviderByProjectExtension('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');
providerResult = ProjectProviderRegistry.getProviderByProjectExtension('TeStProJ');
should.equal(providerResult, provider1, 'provider1 should be returned for testproj project type');
providerResult = ProjectProviderRegistry.getProviderByProjectType('testproj1');
providerResult = ProjectProviderRegistry.getProviderByProjectExtension('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');
providerResult = ProjectProviderRegistry.getProviderByProjectExtension('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');
providerResult = ProjectProviderRegistry.getProviderByProjectExtension('testproj');
should.equal(providerResult, undefined, 'undefined should be returned for testproj project type');
providerResult = ProjectProviderRegistry.getProviderByProjectType('testproj1');
providerResult = ProjectProviderRegistry.getProviderByProjectExtension('testproj1');
should.equal(providerResult, undefined, 'undefined should be returned for testproj1 project type');
providerResult = ProjectProviderRegistry.getProviderByProjectType('sqlproj');
providerResult = ProjectProviderRegistry.getProviderByProjectExtension('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');
providerResult = ProjectProviderRegistry.getProviderByProjectExtension('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');
});
@@ -89,9 +98,11 @@ suite('ProjectProviderRegistry Tests', function (): void {
test('Clear the project provider registry', async () => {
const provider = createProjectProvider([
{
id: 'tp1',
projectFileExtension: 'testproj',
icon: '',
displayName: 'test project'
displayName: 'test project',
description: ''
}
]);
should.strictEqual(ProjectProviderRegistry.providers.length, 0, 'there should be no project provider at the beginning of the test');

View File

@@ -0,0 +1,37 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as os from 'os';
import * as path from 'path';
import { IProjectType } from 'dataworkspace';
import { promises as fs } from 'fs';
export const testProjectType: IProjectType = {
id: 'tp1',
description: '',
projectFileExtension: 'testproj',
icon: '',
displayName: 'test project'
};
/**
* Creates a unique test project file
* @param fileExt
* @param contents
*/
export async function createProjectFile(fileExt: string, contents?: string): Promise<string> {
const filepath = generateUniqueProjectFilePath(fileExt);
await fs.writeFile(filepath, contents ?? '');
return filepath;
}
export function generateUniqueProjectFilePath(fileExt: string): string {
return path.join(os.tmpdir(), `TestProject_${new Date().getTime()}.${fileExt}`);
}
export function generateUniqueWorkspaceFilePath(): string {
return path.join(os.tmpdir(), `TestWorkspace_${new Date().getTime()}.code-workspace`);
}

View File

@@ -5,9 +5,11 @@
import 'mocha';
import * as vscode from 'vscode';
import * as azdata from 'azdata';
import * as sinon from 'sinon';
import * as should from 'should';
import * as path from 'path';
import * as TypeMoq from 'typemoq';
import { WorkspaceService } from '../services/workspaceService';
import { ProjectProviderRegistry } from '../common/projectProviderRegistry';
import { createProjectProvider } from './projectProviderRegistry.test';
@@ -61,7 +63,12 @@ function createMockExtension(id: string, isActive: boolean, projectTypes: string
}
suite('WorkspaceService Tests', function (): void {
const service = new WorkspaceService();
const mockExtensionContext = TypeMoq.Mock.ofType<vscode.ExtensionContext>();
const mockGlobalState = TypeMoq.Mock.ofType<vscode.Memento>();
mockGlobalState.setup(x => x.update(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve());
mockExtensionContext.setup(x => x.globalState).returns(() => mockGlobalState.object);
const service = new WorkspaceService(mockExtensionContext.object);
this.afterEach(() => {
sinon.restore();
@@ -111,10 +118,14 @@ suite('WorkspaceService Tests', function (): void {
const provider1 = createProjectProvider([
{
id: 'tp1',
description: '',
projectFileExtension: 'testproj',
icon: '',
displayName: 'test project'
}, {
id: 'tp2',
description: '',
projectFileExtension: 'testproj1',
icon: '',
displayName: 'test project 1'
@@ -122,6 +133,8 @@ suite('WorkspaceService Tests', function (): void {
]);
const provider2 = createProjectProvider([
{
id: 'sp1',
description: '',
projectFileExtension: 'sqlproj',
icon: '',
displayName: 'sql project'
@@ -149,10 +162,12 @@ suite('WorkspaceService Tests', function (): void {
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');
const getProviderByProjectTypeStub = sinon.stub(ProjectProviderRegistry, 'getProviderByProjectExtension');
getProviderByProjectTypeStub.onFirstCall().returns(undefined);
getProviderByProjectTypeStub.onSecondCall().returns(createProjectProvider([
{
id: 'sp1',
description: '',
projectFileExtension: 'sqlproj',
icon: '',
displayName: 'test project'
@@ -167,6 +182,8 @@ suite('WorkspaceService Tests', function (): void {
getProviderByProjectTypeStub.reset();
getProviderByProjectTypeStub.returns(createProjectProvider([{
id: 'tp2',
description: '',
projectFileExtension: 'csproj',
icon: '',
displayName: 'test cs project'
@@ -215,6 +232,45 @@ suite('WorkspaceService Tests', function (): void {
onWorkspaceProjectsChangedDisposable.dispose();
});
test('test addProjectsToWorkspace when no workspace open', async () => {
stubWorkspaceFile(undefined);
const onWorkspaceProjectsChangedStub = sinon.stub();
const onWorkspaceProjectsChangedDisposable = service.onDidWorkspaceProjectsChange(() => {
onWorkspaceProjectsChangedStub();
});
const createWorkspaceStub = sinon.stub(azdata.workspace, 'createWorkspace').resolves(undefined);
await service.addProjectsToWorkspace([
vscode.Uri.file('/test/folder/proj1.sqlproj')
]);
should.strictEqual(createWorkspaceStub.calledOnce, true, 'createWorkspace should have been called once');
should.strictEqual(onWorkspaceProjectsChangedStub.notCalled, true, 'the onDidWorkspaceProjectsChange event should not have been fired');
onWorkspaceProjectsChangedDisposable.dispose();
});
test('test loadTempProjects', async () => {
const processPath = (original: string): string => {
return original.replace(/\//g, path.sep);
};
stubWorkspaceFile('/test/folder/proj1.code-workspace');
const updateConfigurationStub = sinon.stub();
const getConfigurationStub = sinon.stub().returns([processPath('folder1/proj2.sqlproj')]);
const onWorkspaceProjectsChangedStub = sinon.stub();
const onWorkspaceProjectsChangedDisposable = service.onDidWorkspaceProjectsChange(() => {
onWorkspaceProjectsChangedStub();
});
stubGetConfigurationValue(getConfigurationStub, updateConfigurationStub);
sinon.stub(azdata.workspace, 'createWorkspace').resolves(undefined);
sinon.stub(vscode.workspace, 'workspaceFolders').value(['folder1']);
mockGlobalState.setup(x => x.get(TypeMoq.It.isAny())).returns(() => [processPath('folder1/proj2.sqlproj')]);
await service.loadTempProjects();
should.strictEqual(onWorkspaceProjectsChangedStub.calledOnce, true, 'the onDidWorkspaceProjectsChange event should have been fired');
onWorkspaceProjectsChangedDisposable.dispose();
});
test('test removeProject', async () => {
const processPath = (original: string): string => {
return original.replace(/\//g, path.sep);

View File

@@ -7,13 +7,19 @@ import 'mocha';
import * as sinon from 'sinon';
import * as vscode from 'vscode';
import * as should from 'should';
import * as TypeMoq from 'typemoq';
import { WorkspaceTreeDataProvider } from '../common/workspaceTreeDataProvider';
import { WorkspaceService } from '../services/workspaceService';
import { IProjectProvider, WorkspaceTreeItem } from 'dataworkspace';
import { MockTreeDataProvider } from './projectProviderRegistry.test';
suite('workspaceTreeDataProvider Tests', function (): void {
const workspaceService = new WorkspaceService();
const mockExtensionContext = TypeMoq.Mock.ofType<vscode.ExtensionContext>();
const mockGlobalState = TypeMoq.Mock.ofType<vscode.Memento>();
mockGlobalState.setup(x => x.update(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve());
mockExtensionContext.setup(x => x.globalState).returns(() => mockGlobalState.object);
const workspaceService = new WorkspaceService(mockExtensionContext.object);
const treeProvider = new WorkspaceTreeDataProvider(workspaceService);
this.afterEach(() => {
@@ -66,15 +72,20 @@ suite('workspaceTreeDataProvider Tests', function (): void {
const treeDataProvider = new MockTreeDataProvider();
const projectProvider: IProjectProvider = {
supportedProjectTypes: [{
id: 'sp1',
projectFileExtension: 'sqlproj',
icon: '',
displayName: 'sql project'
displayName: 'sql project',
description: ''
}],
RemoveProject: (projectFile: vscode.Uri): Promise<void> => {
return Promise.resolve();
},
getProjectTreeDataProvider: (projectFile: vscode.Uri): Promise<vscode.TreeDataProvider<any>> => {
return Promise.resolve(treeDataProvider);
},
createProject: (name: string, location: vscode.Uri): Promise<vscode.Uri> => {
return Promise.resolve(location);
}
};
const getProjectProviderStub = sinon.stub(workspaceService, 'getProjectProvider');