diff --git a/build/azure-pipelines/sql-product-build.yml b/build/azure-pipelines/sql-product-build.yml index 783b4efe31..418aab1408 100644 --- a/build/azure-pipelines/sql-product-build.yml +++ b/build/azure-pipelines/sql-product-build.yml @@ -46,7 +46,7 @@ jobs: steps: - template: linux/sql-product-build-linux.yml parameters: - extensionsToUnitTest: ["admin-tool-ext-win", "agent", "azdata", "azurecore", "cms", "dacpac", "import", "schema-compare", "notebook", "resource-deployment", "machine-learning", "sql-database-projects"] + extensionsToUnitTest: ["admin-tool-ext-win", "agent", "azdata", "azurecore", "cms", "dacpac", "import", "schema-compare", "notebook", "resource-deployment", "machine-learning", "sql-database-projects", "data-workspace"] timeoutInMinutes: 70 - job: LinuxWeb diff --git a/extensions/data-workspace/package.json b/extensions/data-workspace/package.json index 409f7b9e91..f0a9685ad5 100644 --- a/extensions/data-workspace/package.json +++ b/extensions/data-workspace/package.json @@ -20,20 +20,41 @@ "type": "git", "url": "https://github.com/Microsoft/azuredatastudio.git" }, - "extensionDependencies": [ - "microsoft.mssql" - ], + "extensionDependencies": [], "contributes": { + "configuration": [ + { + "title": "Projects", + "properties": { + "dataworkspace.projects": { + "type": "array", + "default": [], + "description": "" + } + } + } + ], "commands": [ { "command": "projects.addProject", "title": "%add-project-command%", "category": "", "icon": "$(add)" + }, + { + "command": "dataworkspace.refresh", + "title": "%refresh-workspace-command%", + "category": "", + "icon": "$(refresh)" } ], "menus": { "view/title": [ + { + "command": "dataworkspace.refresh", + "when": "view == dataworkspace.views.main", + "group": "navigation" + }, { "command": "projects.addProject", "when": "view == dataworkspace.views.main", @@ -44,6 +65,10 @@ { "command": "projects.addProject", "when": "false" + }, + { + "command": "dataworkspace.refresh", + "when": "false" } ] }, diff --git a/extensions/data-workspace/package.nls.json b/extensions/data-workspace/package.nls.json index c5671c49b6..c9e8199e2a 100644 --- a/extensions/data-workspace/package.nls.json +++ b/extensions/data-workspace/package.nls.json @@ -3,5 +3,6 @@ "extension-description": "Data workspace", "data-workspace-view-container-name": "Projects", "main-view-name": "Projects", - "add-project-command": "Add Project" + "add-project-command": "Add Project", + "refresh-workspace-command": "Refresh" } diff --git a/extensions/data-workspace/src/common/interfaces.ts b/extensions/data-workspace/src/common/interfaces.ts new file mode 100644 index 0000000000..2091e3933b --- /dev/null +++ b/extensions/data-workspace/src/common/interfaces.ts @@ -0,0 +1,70 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IProjectProvider, IProjectType } from 'dataworkspace'; +import * as vscode from 'vscode'; + +/** + * Defines the project provider registry + */ +export interface IProjectProviderRegistry { + /** + * Registers a new project provider + * @param provider The project provider + */ + registerProvider(provider: IProjectProvider): vscode.Disposable; + + /** + * Clear the providers + */ + clear(): void; + + /** + * Gets all the registered providers + */ + readonly providers: IProjectProvider[]; + + /** + * Gets the project provider for the specified project type + * @param projectType The project type, file extension of the project + */ + getProviderByProjectType(projectType: string): IProjectProvider | undefined; +} + +/** + * Defines the project service + */ +export interface IWorkspaceService { + /** + * Gets all supported project types + */ + getAllProjectTypes(): Promise; + + /** + * Gets the project files in current workspace + */ + getProjectsInWorkspace(): Promise; + + /** + * Gets the project provider by project file + * @param projectFilePath The full path of the project file + */ + getProjectProvider(projectFilePath: string): Promise; +} + +/** + * Represents the item for the workspace tree + */ +export interface WorkspaceTreeItem { + /** + * Gets the tree data provider + */ + treeDataProvider: vscode.TreeDataProvider; + + /** + * Gets the raw element returned by the tree data provider + */ + element: any; +} diff --git a/extensions/data-workspace/src/common/logger.ts b/extensions/data-workspace/src/common/logger.ts new file mode 100644 index 0000000000..ba38c4a0d3 --- /dev/null +++ b/extensions/data-workspace/src/common/logger.ts @@ -0,0 +1,12 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export class Log { + error(msg: string): void { + console.error(msg); + } +} +const Logger = new Log(); +export default Logger; diff --git a/extensions/data-workspace/src/common/projectProviderRegistry.ts b/extensions/data-workspace/src/common/projectProviderRegistry.ts new file mode 100644 index 0000000000..c091a25112 --- /dev/null +++ b/extensions/data-workspace/src/common/projectProviderRegistry.ts @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IProjectProvider } from 'dataworkspace'; +import * as vscode from 'vscode'; +import { IProjectProviderRegistry } from './interfaces'; + +export const ProjectProviderRegistry: IProjectProviderRegistry = new class implements IProjectProviderRegistry { + private _providers = new Array(); + private _providerMapping: { [key: string]: IProjectProvider } = {}; + + registerProvider(provider: IProjectProvider): vscode.Disposable { + this.validateProvider(provider); + this._providers.push(provider); + provider.supportedProjectTypes.forEach(projectType => { + this._providerMapping[projectType.projectFileExtension.toUpperCase()] = provider; + }); + return new vscode.Disposable(() => { + const idx = this._providers.indexOf(provider); + if (idx >= 0) { + this._providers.splice(idx, 1); + provider.supportedProjectTypes.forEach(projectType => { + delete this._providerMapping[projectType.projectFileExtension.toUpperCase()]; + }); + } + }); + } + + get providers(): IProjectProvider[] { + return this._providers.slice(0); + } + + clear(): void { + this._providers.length = 0; + } + + validateProvider(provider: IProjectProvider): void { + } + + getProviderByProjectType(projectType: string): IProjectProvider | undefined { + return projectType ? this._providerMapping[projectType.toUpperCase()] : undefined; + } +}; diff --git a/extensions/data-workspace/src/common/workspaceTreeDataProvider.ts b/extensions/data-workspace/src/common/workspaceTreeDataProvider.ts new file mode 100644 index 0000000000..b4d1d1cd46 --- /dev/null +++ b/extensions/data-workspace/src/common/workspaceTreeDataProvider.ts @@ -0,0 +1,66 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { IWorkspaceService, WorkspaceTreeItem as WorkspaceTreeItem } from './interfaces'; +import * as nls from 'vscode-nls'; +import { EOL } from 'os'; +const localize = nls.loadMessageBundle(); + +/** + * Tree data provider for the workspace main view + */ +export class WorkspaceTreeDataProvider implements vscode.TreeDataProvider{ + constructor(private _workspaceService: IWorkspaceService) { } + + private _onDidChangeTreeData: vscode.EventEmitter | undefined = new vscode.EventEmitter(); + readonly onDidChangeTreeData?: vscode.Event | undefined = this._onDidChangeTreeData?.event; + + refresh(): void { + this._onDidChangeTreeData?.fire(); + } + + getTreeItem(element: WorkspaceTreeItem): vscode.TreeItem | Thenable { + return element.treeDataProvider.getTreeItem(element.element); + } + + async getChildren(element?: WorkspaceTreeItem | undefined): Promise { + if (element) { + const items = await element.treeDataProvider.getChildren(element.element); + return items ? items.map(item => { treeDataProvider: element.treeDataProvider, element: item }) : []; + } + else { + // if the element is undefined return the project tree items + const projects = await this._workspaceService.getProjectsInWorkspace(); + const unknownProjects: string[] = []; + const treeItems: WorkspaceTreeItem[] = []; + let project: string; + for (project of projects) { + const projectProvider = await this._workspaceService.getProjectProvider(project); + if (projectProvider === undefined) { + unknownProjects.push(project); + continue; + } + const treeDataProvider = await projectProvider.getProjectTreeDataProvider(project); + if (treeDataProvider.onDidChangeTreeData) { + treeDataProvider.onDidChangeTreeData((e: any) => { + this._onDidChangeTreeData?.fire(e); + }); + } + const children = await treeDataProvider.getChildren(element); + children?.forEach(child => { + treeItems.push({ + treeDataProvider: treeDataProvider, + element: child + }); + }); + } + if (unknownProjects.length > 0) { + vscode.window.showErrorMessage(localize('UnknownProjectsError', "No provider was found for the following projects: {0}", unknownProjects.join(EOL))); + } + return treeItems; + } + } +} diff --git a/extensions/data-workspace/src/dataWorkspaceExtension.ts b/extensions/data-workspace/src/dataWorkspaceExtension.ts new file mode 100644 index 0000000000..72ee5abf23 --- /dev/null +++ b/extensions/data-workspace/src/dataWorkspaceExtension.ts @@ -0,0 +1,14 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import * as dataworkspace from 'dataworkspace'; +import { ProjectProviderRegistry } from './common/projectProviderRegistry'; + +export class DataWorkspaceExtension implements dataworkspace.IExtension { + registerProjectProvider(provider: dataworkspace.IProjectProvider): vscode.Disposable { + return ProjectProviderRegistry.registerProvider(provider); + } +} diff --git a/extensions/data-workspace/src/dataworkspace.d.ts b/extensions/data-workspace/src/dataworkspace.d.ts new file mode 100644 index 0000000000..cebb5ed7a0 --- /dev/null +++ b/extensions/data-workspace/src/dataworkspace.d.ts @@ -0,0 +1,59 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'dataworkspace' { + import * as vscode from 'vscode'; + export const enum extension { + name = 'Microsoft.data-workspace' + } + + /** + * dataworkspace extension + */ + export interface IExtension { + /** + * register a project provider + * @param provider new project provider + * @requires a disposable object, upon disposal, the provider will be unregistered. + */ + registerProjectProvider(provider: IProjectProvider): vscode.Disposable; + } + + /** + * Defines the capabilities of project provider + */ + export interface IProjectProvider { + /** + * Gets the tree data provider for the given project file + * @param projectFilePath The full path of the project file + */ + getProjectTreeDataProvider(projectFilePath: string): Promise>; + + /** + * Gets the supported project types + */ + readonly supportedProjectTypes: IProjectType[]; + } + + /** + * Defines the project type + */ + export interface IProjectType { + /** + * display name of the project type + */ + readonly displayName: string; + + /** + * project file extension, e.g. sqlproj + */ + readonly projectFileExtension: string; + + /** + * Gets the icon path of the project type + */ + readonly icon: string | vscode.Uri | { light: string | vscode.Uri, dark: string | vscode.Uri } + } +} diff --git a/extensions/data-workspace/src/main.ts b/extensions/data-workspace/src/main.ts index eb9bfa5273..e728f7aeea 100644 --- a/extensions/data-workspace/src/main.ts +++ b/extensions/data-workspace/src/main.ts @@ -4,10 +4,23 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; +import * as dataworkspace from 'dataworkspace'; +import { WorkspaceTreeDataProvider } from './common/workspaceTreeDataProvider'; +import { WorkspaceService } from './services/workspaceService'; +import { DataWorkspaceExtension } from './dataWorkspaceExtension'; -export async function activate(context: vscode.ExtensionContext): Promise { - vscode.commands.registerCommand('projects.addProject', () => { - }); +export async function activate(context: vscode.ExtensionContext): Promise { + const workspaceService = new WorkspaceService(); + const workspaceTreeDataProvider = new WorkspaceTreeDataProvider(workspaceService); + context.subscriptions.push(vscode.window.registerTreeDataProvider('dataworkspace.views.main', workspaceTreeDataProvider)); + context.subscriptions.push(vscode.commands.registerCommand('projects.addProject', () => { + })); + + context.subscriptions.push(vscode.commands.registerCommand('dataworkspace.refresh', () => { + workspaceTreeDataProvider.refresh(); + })); + + return new DataWorkspaceExtension(); } export function deactivate(): void { diff --git a/extensions/data-workspace/src/services/workspaceService.ts b/extensions/data-workspace/src/services/workspaceService.ts new file mode 100644 index 0000000000..c41a7fa3cd --- /dev/null +++ b/extensions/data-workspace/src/services/workspaceService.ts @@ -0,0 +1,76 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import * as dataworkspace from 'dataworkspace'; +import * as path from 'path'; +import { IWorkspaceService } from '../common/interfaces'; +import { ProjectProviderRegistry } from '../common/projectProviderRegistry'; +import * as nls from 'vscode-nls'; +import Logger from '../common/logger'; + +const localize = nls.loadMessageBundle(); +const WorkspaceConfigurationName = 'dataworkspace'; +const ProjectsConfigurationName = 'projects'; + +export class WorkspaceService implements IWorkspaceService { + async getAllProjectTypes(): Promise { + await this.ensureProviderExtensionLoaded(); + const projectTypes: dataworkspace.IProjectType[] = []; + ProjectProviderRegistry.providers.forEach(provider => { + projectTypes.push(...provider.supportedProjectTypes); + }); + return projectTypes; + } + + async getProjectsInWorkspace(): Promise { + if (vscode.workspace.workspaceFile) { + const projects = vscode.workspace.getConfiguration(WorkspaceConfigurationName).get(ProjectsConfigurationName); + return projects.map(project => path.isAbsolute(project) ? project : path.join(vscode.workspace.rootPath!, project)); + } + return []; + } + + async getProjectProvider(projectFilePath: string): Promise { + const projectType = path.extname(projectFilePath).replace(/\./g, ''); + let provider = ProjectProviderRegistry.getProviderByProjectType(projectType); + if (!provider) { + await this.ensureProviderExtensionLoaded(projectType); + } + return ProjectProviderRegistry.getProviderByProjectType(projectType); + } + + /** + * Ensure the project provider extension for the specified project is loaded + * @param projectType The file extension of the project, if not specified, all project provider extensions will be loaded. + */ + private async ensureProviderExtensionLoaded(projectType: string | undefined = undefined): Promise { + const inactiveExtensions = vscode.extensions.all.filter(ext => !ext.isActive); + const projType = projectType ? projectType.toUpperCase() : undefined; + let extension: vscode.Extension; + for (extension of inactiveExtensions) { + const projectTypes = extension.packageJSON.contributes && extension.packageJSON.contributes.projects as string[]; + // Process only when this extension is contributing project providers + if (projectTypes && projectTypes.length > 0) { + if (projType) { + if (projectTypes.findIndex((proj: string) => proj.toUpperCase() === projType) !== -1) { + await this.activateExtension(extension); + break; + } + } else { + await this.activateExtension(extension); + } + } + } + } + + private async activateExtension(extension: vscode.Extension): Promise { + 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)); + } + } +} diff --git a/extensions/data-workspace/src/test/dataWorkspaceExtension.test.ts b/extensions/data-workspace/src/test/dataWorkspaceExtension.test.ts new file mode 100644 index 0000000000..b3a7a903fd --- /dev/null +++ b/extensions/data-workspace/src/test/dataWorkspaceExtension.test.ts @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'mocha'; +import * as should from 'should'; +import { DataWorkspaceExtension } from '../dataWorkspaceExtension'; +import { createProjectProvider } from './projectProviderRegistry.test'; +import { ProjectProviderRegistry } from '../common/projectProviderRegistry'; + +suite('DataWorkspaceExtension Tests', function (): void { + test('register and unregister project provider through the extension api', async () => { + const extension = new DataWorkspaceExtension(); + const provider = createProjectProvider([ + { + projectFileExtension: 'testproj', + icon: '', + displayName: 'test project' + } + ]); + const disposable = extension.registerProjectProvider(provider); + should.strictEqual(ProjectProviderRegistry.providers.length, 1, 'project provider should have been registered'); + disposable.dispose(); + should.strictEqual(ProjectProviderRegistry.providers.length, 0, 'there should be nothing in the ProjectProviderRegistry'); + }); +}); diff --git a/extensions/data-workspace/src/test/index.ts b/extensions/data-workspace/src/test/index.ts new file mode 100644 index 0000000000..84b15d77f7 --- /dev/null +++ b/extensions/data-workspace/src/test/index.ts @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as path from 'path'; +const testRunner = require('vscodetestcover'); + +const suite = 'Data Workspace Extension Tests'; + +const mochaOptions: any = { + ui: 'tdd', + useColors: true, + timeout: 10000 +}; + +// set relevant mocha options from the environment +if (process.env.ADS_TEST_GREP) { + mochaOptions.grep = process.env.ADS_TEST_GREP; + console.log(`setting options.grep to: ${mochaOptions.grep}`); +} +if (process.env.ADS_TEST_INVERT_GREP) { + mochaOptions.invert = parseInt(process.env.ADS_TEST_INVERT_GREP); + console.log(`setting options.invert to: ${mochaOptions.invert}`); +} +if (process.env.ADS_TEST_TIMEOUT) { + mochaOptions.timeout = parseInt(process.env.ADS_TEST_TIMEOUT); + console.log(`setting options.timeout to: ${mochaOptions.timeout}`); +} +if (process.env.ADS_TEST_RETRIES) { + mochaOptions.retries = parseInt(process.env.ADS_TEST_RETRIES); + console.log(`setting options.retries to: ${mochaOptions.retries}`); +} + +if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { + mochaOptions.reporter = 'mocha-multi-reporters'; + mochaOptions.reporterOptions = { + reporterEnabled: 'spec, mocha-junit-reporter', + mochaJunitReporterReporterOptions: { + testsuitesTitle: `${suite} ${process.platform}`, + mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) + } + }; +} + +testRunner.configure(mochaOptions, { coverConfig: '../../coverConfig.json' }); + +export = testRunner; diff --git a/extensions/data-workspace/src/test/projectProviderRegistry.test.ts b/extensions/data-workspace/src/test/projectProviderRegistry.test.ts new file mode 100644 index 0000000000..f983eaca08 --- /dev/null +++ b/extensions/data-workspace/src/test/projectProviderRegistry.test.ts @@ -0,0 +1,100 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'mocha'; +import * as vscode from 'vscode'; +import * as should from 'should'; +import { ProjectProviderRegistry } from '../common/projectProviderRegistry'; +import { IProjectProvider, IProjectType } from 'dataworkspace'; + +export class MockTreeDataProvider implements vscode.TreeDataProvider{ + onDidChangeTreeData?: vscode.Event | undefined; + getTreeItem(element: any): vscode.TreeItem | Thenable { + throw new Error('Method not implemented.'); + } + getChildren(element?: any): vscode.ProviderResult { + throw new Error('Method not implemented.'); + } +} + +export function createProjectProvider(projectTypes: IProjectType[]): IProjectProvider { + const treeDataProvider = new MockTreeDataProvider(); + const projectProvider: IProjectProvider = { + supportedProjectTypes: projectTypes, + getProjectTreeDataProvider: (projectFile: string): Promise> => { + return Promise.resolve(treeDataProvider); + } + }; + return projectProvider; +} + +suite('ProjectProviderRegistry Tests', function (): void { + test('register and unregister project providers', async () => { + const provider1 = createProjectProvider([ + { + projectFileExtension: 'testproj', + icon: '', + displayName: 'test project' + }, { + projectFileExtension: 'testproj1', + icon: '', + displayName: 'test project 1' + } + ]); + const provider2 = createProjectProvider([ + { + projectFileExtension: 'sqlproj', + icon: '', + displayName: 'sql project' + } + ]); + should.strictEqual(ProjectProviderRegistry.providers.length, 0, 'there should be no project provider at the beginning of the test'); + const disposable1 = ProjectProviderRegistry.registerProvider(provider1); + let providerResult = ProjectProviderRegistry.getProviderByProjectType('testproj'); + should.equal(providerResult, provider1, 'provider1 should be returned for testproj project type'); + // make sure the project type is case-insensitive for getProviderByProjectType method + providerResult = ProjectProviderRegistry.getProviderByProjectType('TeStProJ'); + should.equal(providerResult, provider1, 'provider1 should be returned for testproj project type'); + providerResult = ProjectProviderRegistry.getProviderByProjectType('testproj1'); + should.equal(providerResult, provider1, 'provider1 should be returned for testproj1 project type'); + should.strictEqual(ProjectProviderRegistry.providers.length, 1, 'there should be only one project provider at this time'); + const disposable2 = ProjectProviderRegistry.registerProvider(provider2); + providerResult = ProjectProviderRegistry.getProviderByProjectType('sqlproj'); + should.equal(providerResult, provider2, 'provider2 should be returned for sqlproj project type'); + should.strictEqual(ProjectProviderRegistry.providers.length, 2, 'there should be 2 project providers at this time'); + + // unregister provider1 + disposable1.dispose(); + providerResult = ProjectProviderRegistry.getProviderByProjectType('testproj'); + should.equal(providerResult, undefined, 'undefined should be returned for testproj project type'); + providerResult = ProjectProviderRegistry.getProviderByProjectType('testproj1'); + should.equal(providerResult, undefined, 'undefined should be returned for testproj1 project type'); + providerResult = ProjectProviderRegistry.getProviderByProjectType('sqlproj'); + should.equal(providerResult, provider2, 'provider2 should be returned for sqlproj project type after provider1 is disposed'); + should.strictEqual(ProjectProviderRegistry.providers.length, 1, 'there should be only one project provider after unregistering a provider'); + should.strictEqual(ProjectProviderRegistry.providers[0].supportedProjectTypes[0].projectFileExtension, 'sqlproj', 'the remaining project provider should be sqlproj'); + + // unregister provider2 + disposable2.dispose(); + providerResult = ProjectProviderRegistry.getProviderByProjectType('sqlproj'); + should.equal(providerResult, undefined, 'undefined should be returned for sqlproj project type after provider2 is disposed'); + should.strictEqual(ProjectProviderRegistry.providers.length, 0, 'there should be no project provider after unregistering the providers'); + }); + + test('Clear the project provider registry', async () => { + const provider = createProjectProvider([ + { + projectFileExtension: 'testproj', + icon: '', + displayName: 'test project' + } + ]); + should.strictEqual(ProjectProviderRegistry.providers.length, 0, 'there should be no project provider at the beginning of the test'); + ProjectProviderRegistry.registerProvider(provider); + should.strictEqual(ProjectProviderRegistry.providers.length, 1, 'there should be only one project provider at this time'); + ProjectProviderRegistry.clear(); + should.strictEqual(ProjectProviderRegistry.providers.length, 0, 'there should be no project provider after clearing the registry'); + }); +}); diff --git a/extensions/sql-database-projects/package.json b/extensions/sql-database-projects/package.json index 0809a6d146..9befc312eb 100644 --- a/extensions/sql-database-projects/package.json +++ b/extensions/sql-database-projects/package.json @@ -16,7 +16,8 @@ "onCommand:sqlDatabaseProjects.new", "onCommand:sqlDatabaseProjects.open", "onCommand:sqlDatabaseProjects.importDatabase", - "workspaceContains:**/*.sqlproj" + "workspaceContains:**/*.sqlproj", + "onView:dataworkspace.views.main" ], "main": "./out/extension", "repository": { @@ -25,9 +26,13 @@ }, "extensionDependencies": [ "Microsoft.mssql", - "Microsoft.schema-compare" + "Microsoft.schema-compare", + "Microsoft.data-workspace" ], "contributes": { + "projects": [ + "sqlproj" + ], "configuration": [ { "title": "%sqlDatabaseProjects.Settings%", @@ -138,7 +143,7 @@ "command": "sqlDatabaseProjects.importDatabase", "title": "%sqlDatabaseProjects.importDatabase%", "category": "%sqlDatabaseProjects.displayName%", - "icon": "images/databaseProjectToolbar.svg" + "icon": "images/databaseProjectToolbar.svg" }, { "command": "sqlDatabaseProjects.addDatabaseReference", @@ -243,87 +248,87 @@ "view/item/context": [ { "command": "sqlDatabaseProjects.build", - "when": "view == sqlDatabaseProjectsView && viewItem == databaseProject.itemType.project", + "when": "view =~ /^(sqlDatabaseProjectsView|dataworkspace.views.main)$/ && viewItem == databaseProject.itemType.project", "group": "1_dbProjectsFirst@1" }, { "command": "sqlDatabaseProjects.publish", - "when": "view == sqlDatabaseProjectsView && viewItem == databaseProject.itemType.project", + "when": "view =~ /^(sqlDatabaseProjectsView|dataworkspace.views.main)$/ && viewItem == databaseProject.itemType.project", "group": "1_dbProjectsFirst@2" }, { "command": "sqlDatabaseProjects.schemaCompare", - "when": "view == sqlDatabaseProjectsView && viewItem == databaseProject.itemType.project", + "when": "view =~ /^(sqlDatabaseProjectsView|dataworkspace.views.main)$/ && viewItem == databaseProject.itemType.project", "group": "1_dbProjectsFirst@3" }, { "command": "sqlDatabaseProjects.newItem", - "when": "view == sqlDatabaseProjectsView && viewItem == databaseProject.itemType.project || viewItem == databaseProject.itemType.folder", + "when": "view =~ /^(sqlDatabaseProjectsView|dataworkspace.views.main)$/ && viewItem == databaseProject.itemType.project || viewItem == databaseProject.itemType.folder", "group": "2_dbProjects_newMain@1" }, { "command": "sqlDatabaseProjects.newFolder", - "when": "view == sqlDatabaseProjectsView && viewItem == databaseProject.itemType.project || viewItem == databaseProject.itemType.folder", + "when": "view =~ /^(sqlDatabaseProjectsView|dataworkspace.views.main)$/ && viewItem == databaseProject.itemType.project || viewItem == databaseProject.itemType.folder", "group": "2_dbProjects_newMain@2" }, { "command": "sqlDatabaseProjects.newTable", - "when": "view == sqlDatabaseProjectsView && viewItem == databaseProject.itemType.project || viewItem == databaseProject.itemType.folder", + "when": "view =~ /^(sqlDatabaseProjectsView|dataworkspace.views.main)$/ && viewItem == databaseProject.itemType.project || viewItem == databaseProject.itemType.folder", "group": "3_dbProjects_newItem@1" }, { "command": "sqlDatabaseProjects.newView", - "when": "view == sqlDatabaseProjectsView && viewItem == databaseProject.itemType.project || viewItem == databaseProject.itemType.folder", + "when": "view =~ /^(sqlDatabaseProjectsView|dataworkspace.views.main)$/ && viewItem == databaseProject.itemType.project || viewItem == databaseProject.itemType.folder", "group": "3_dbProjects_newItem@2" }, { "command": "sqlDatabaseProjects.newStoredProcedure", - "when": "view == sqlDatabaseProjectsView && viewItem == databaseProject.itemType.project || viewItem == databaseProject.itemType.folder", + "when": "view =~ /^(sqlDatabaseProjectsView|dataworkspace.views.main)$/ && viewItem == databaseProject.itemType.project || viewItem == databaseProject.itemType.folder", "group": "3_dbProjects_newItem@3" }, { "command": "sqlDatabaseProjects.newScript", - "when": "view == sqlDatabaseProjectsView && viewItem == databaseProject.itemType.project || viewItem == databaseProject.itemType.folder", + "when": "view =~ /^(sqlDatabaseProjectsView|dataworkspace.views.main)$/ && viewItem == databaseProject.itemType.project || viewItem == databaseProject.itemType.folder", "group": "3_dbProjects_newItem@7" }, { "command": "sqlDatabaseProjects.newPreDeploymentScript", - "when": "view == sqlDatabaseProjectsView && viewItem == databaseProject.itemType.project || viewItem == databaseProject.itemType.folder", + "when": "view =~ /^(sqlDatabaseProjectsView|dataworkspace.views.main)$/ && viewItem == databaseProject.itemType.project || viewItem == databaseProject.itemType.folder", "group": "3_dbProjects_newItem@8" }, { "command": "sqlDatabaseProjects.newPostDeploymentScript", - "when": "view == sqlDatabaseProjectsView && viewItem == databaseProject.itemType.project || viewItem == databaseProject.itemType.folder", + "when": "view =~ /^(sqlDatabaseProjectsView|dataworkspace.views.main)$/ && viewItem == databaseProject.itemType.project || viewItem == databaseProject.itemType.folder", "group": "3_dbProjects_newItem@9" }, { "command": "sqlDatabaseProjects.addDatabaseReference", - "when": "view == sqlDatabaseProjectsView && viewItem == databaseProject.itemType.referencesRoot", + "when": "view =~ /^(sqlDatabaseProjectsView|dataworkspace.views.main)$/ && viewItem == databaseProject.itemType.referencesRoot", "group": "4_dbProjects_addDatabaseReference" }, { "command": "sqlDatabaseProjects.exclude", - "when": "view == sqlDatabaseProjectsView && viewItem == databaseProject.itemType.folder || viewItem == databaseProject.itemType.file", + "when": "view =~ /^(sqlDatabaseProjectsView|dataworkspace.views.main)$/ && viewItem == databaseProject.itemType.folder || viewItem == databaseProject.itemType.file", "group": "9_dbProjectsLast@1" }, { "command": "sqlDatabaseProjects.delete", - "when": "view == sqlDatabaseProjectsView && viewItem == databaseProject.itemType.folder || viewItem == databaseProject.itemType.file", + "when": "view =~ /^(sqlDatabaseProjectsView|dataworkspace.views.main)$/ && viewItem == databaseProject.itemType.folder || viewItem == databaseProject.itemType.file", "group": "9_dbProjectsLast@2" }, { "command": "sqlDatabaseProjects.editProjectFile", - "when": "view == sqlDatabaseProjectsView && viewItem == databaseProject.itemType.project", + "when": "view =~ /^(sqlDatabaseProjectsView|dataworkspace.views.main)$/ && viewItem == databaseProject.itemType.project", "group": "9_dbProjectsLast@7" }, { "command": "sqlDatabaseProjects.openContainingFolder", - "when": "view == sqlDatabaseProjectsView && viewItem == databaseProject.itemType.project", + "when": "view =~ /^(sqlDatabaseProjectsView|dataworkspace.views.main)$/ && viewItem == databaseProject.itemType.project", "group": "9_dbProjectsLast@8" }, { "command": "sqlDatabaseProjects.close", - "when": "view == sqlDatabaseProjectsView && viewItem == databaseProject.itemType.project", + "when": "view =~ /^(sqlDatabaseProjectsView|dataworkspace.views.main)$/ && viewItem == databaseProject.itemType.project", "group": "9_dbProjectsLast@9" } ], @@ -339,7 +344,6 @@ "group": "export" } ], - "dataExplorer/context": [ { "command": "sqlDatabaseProjects.importDatabase", @@ -354,8 +358,8 @@ ], "dashboard/toolbar": [ { - "command": "sqlDatabaseProjects.importDatabase", - "when": "connectionProvider == 'MSSQL' && mssql:engineedition != 11" + "command": "sqlDatabaseProjects.importDatabase", + "when": "connectionProvider == 'MSSQL' && mssql:engineedition != 11" } ] }, diff --git a/extensions/sql-database-projects/src/common/constants.ts b/extensions/sql-database-projects/src/common/constants.ts index 7b30596d4b..d60f6d221e 100644 --- a/extensions/sql-database-projects/src/common/constants.ts +++ b/extensions/sql-database-projects/src/common/constants.ts @@ -22,6 +22,9 @@ export const msdbDacpac = 'msdb.dacpac'; export const MicrosoftDatatoolsSchemaSqlSql = 'Microsoft.Data.Tools.Schema.Sql.Sql'; export const databaseSchemaProvider = 'DatabaseSchemaProvider'; +// Project Provider +export const projectTypeDisplayName = localize('projectTypeDisplayName', 'Database Project'); + // commands export const revealFileInOsCommand = 'revealFileInOS'; export const schemaCompareStartCommand = 'schemaCompare.start'; diff --git a/extensions/sql-database-projects/src/controllers/mainController.ts b/extensions/sql-database-projects/src/controllers/mainController.ts index 8a6391c7da..7dc1fc470d 100644 --- a/extensions/sql-database-projects/src/controllers/mainController.ts +++ b/extensions/sql-database-projects/src/controllers/mainController.ts @@ -5,6 +5,7 @@ import * as azdata from 'azdata'; import * as vscode from 'vscode'; +import * as dataworkspace from 'dataworkspace'; import * as templates from '../templates/templates'; import * as constants from '../common/constants'; import * as path from 'path'; @@ -19,6 +20,7 @@ import { NetCoreTool } from '../tools/netcoreTool'; import { Project } from '../models/project'; import { FileNode, FolderNode } from '../models/tree/fileFolderTreeItem'; import { IconPathHelper } from '../common/iconHelper'; +import { SqlDatabaseProjectProvider } from '../projectProvider/projectProvider'; const SQL_DATABASE_PROJECTS_VIEW_ID = 'sqlDatabaseProjectsView'; @@ -78,6 +80,7 @@ export default class MainController implements vscode.Disposable { vscode.commands.registerCommand('sqlDatabaseProjects.exclude', async (node: FileNode | FolderNode) => { await this.projectsController.exclude(node); }); IconPathHelper.setExtensionContext(this.extensionContext); + this.registerProjectProvider(); // init view const treeView = vscode.window.createTreeView(SQL_DATABASE_PROJECTS_VIEW_ID, { @@ -190,6 +193,13 @@ export default class MainController implements vscode.Disposable { } } + private registerProjectProvider(): void { + const dataWorkspaceApi: dataworkspace.IExtension = vscode.extensions.getExtension(dataworkspace.extension.name)?.exports; + if (dataWorkspaceApi) { + dataWorkspaceApi.registerProjectProvider(new SqlDatabaseProjectProvider()); + } + } + public dispose(): void { this.deactivate(); } diff --git a/extensions/sql-database-projects/src/projectProvider/projectProvider.ts b/extensions/sql-database-projects/src/projectProvider/projectProvider.ts new file mode 100644 index 0000000000..40f3c6d6c1 --- /dev/null +++ b/extensions/sql-database-projects/src/projectProvider/projectProvider.ts @@ -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 dataworkspace from 'dataworkspace'; +import * as vscode from 'vscode'; +import { sqlprojExtension, projectTypeDisplayName } from '../common/constants'; +import { IconPathHelper } from '../common/iconHelper'; +import { SqlDatabaseProjectTreeViewProvider } from '../controllers/databaseProjectTreeViewProvider'; +import { Project } from '../models/project'; +import { BaseProjectTreeItem } from '../models/tree/baseTreeItem'; + +export class SqlDatabaseProjectProvider implements dataworkspace.IProjectProvider { + + /** + * Gets the project tree data provider + * @param projectFilePath The project file path + */ + async getProjectTreeDataProvider(projectFilePath: string): Promise> { + const provider = new SqlDatabaseProjectTreeViewProvider(); + const project = await Project.openProject(projectFilePath); + provider.load([project]); + return provider; + } + + /** + * Gets the supported project types + */ + get supportedProjectTypes(): dataworkspace.IProjectType[] { + return [{ + projectFileExtension: sqlprojExtension.replace(/\./g, ''), + displayName: projectTypeDisplayName, + icon: IconPathHelper.databaseProject + }]; + } +} diff --git a/extensions/sql-database-projects/src/typings/ref.d.ts b/extensions/sql-database-projects/src/typings/ref.d.ts index cfdf5dd135..6137c50b4c 100644 --- a/extensions/sql-database-projects/src/typings/ref.d.ts +++ b/extensions/sql-database-projects/src/typings/ref.d.ts @@ -6,4 +6,5 @@ /// /// /// +/// /// diff --git a/scripts/test-extensions-unit.js b/scripts/test-extensions-unit.js index fc024dd1f8..c2e918c89c 100644 --- a/scripts/test-extensions-unit.js +++ b/scripts/test-extensions-unit.js @@ -22,7 +22,9 @@ const extensionList = [ 'notebook', 'resource-deployment', 'machine-learning', - 'sql-database-projects']; + 'sql-database-projects', + 'data-workspace' +]; let argv = require('yargs') .command('$0 [extensions...]')