diff --git a/extensions/sql-database-projects/package.json b/extensions/sql-database-projects/package.json index 05f269b6c6..6b7d5631d1 100644 --- a/extensions/sql-database-projects/package.json +++ b/extensions/sql-database-projects/package.json @@ -13,9 +13,11 @@ "icon": "images/sqlserver.png", "aiKey": "AIF-c5594e2d-38b5-4d3b-ab1b-ed5d4fe8ee40", "activationEvents": [ - "*" + "onCommand:sqlDatabaseProjects.new", + "onCommand:sqlDatabaseProjects.open", + "workspaceContains:**/*.sqlproj" ], - "main": "./out/main", + "main": "./out/extension", "repository": { "type": "git", "url": "https://github.com/Microsoft/azuredatastudio.git" @@ -45,6 +47,15 @@ "command": "sqlDatabaseProjects.open" } ] + }, + "views": { + "explorer": [ + { + "id": "sqlDatabaseProjectsView", + "name": "%title.projectsView%", + "when": "" + } + ] } }, "dependencies": { diff --git a/extensions/sql-database-projects/package.nls.json b/extensions/sql-database-projects/package.nls.json index cb5a11b152..888c5b8a03 100644 --- a/extensions/sql-database-projects/package.nls.json +++ b/extensions/sql-database-projects/package.nls.json @@ -2,5 +2,6 @@ "sqlDatabaseProjects.displayName": "Database Projects", "sqlDatabaseProjects.description": "Design and deploy SQL database schemas", "sqlDatabaseProjects.new": "New Database Project", - "sqlDatabaseProjects.open": "Open Database Project" + "sqlDatabaseProjects.open": "Open Database Project", + "title.projectsView": "SQL Database Projects" } diff --git a/extensions/sql-database-projects/src/common/constants.ts b/extensions/sql-database-projects/src/common/constants.ts new file mode 100644 index 0000000000..24eeb3291c --- /dev/null +++ b/extensions/sql-database-projects/src/common/constants.ts @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vscode-nls'; + +const localize = nls.loadMessageBundle(); + +// Placeholder values +export const dataSourcesFileName = 'datasources.json'; + +// UI Strings + +export const noOpenProjectMessage = localize('noProjectOpenMessage', "No open database project"); +export const projectNodeName = localize('projectNodeName', "Database Project"); +export const dataSourcesNodeName = localize('dataSourcesNodeName', "Data Sources"); +export const foundDataSourcesFile = localize('foundDataSourcesFile', "Found {0}: ", dataSourcesFileName); // TODO: remove once datasources.json is actually getting removed. + +// Error messages + +export const multipleSqlProjFiles = localize('multipleSqlProjFilesSelected', "Multiple .sqlproj files selected; please select only one."); +export const noSqlProjFiles = localize('noSqlProjFilesSelected', "No .sqlproj file selected; please select one."); +export const noDataSourcesFile = localize('noDataSourcesFile', "No {0} found", dataSourcesFileName); diff --git a/extensions/sql-database-projects/src/common/promise.ts b/extensions/sql-database-projects/src/common/promise.ts new file mode 100644 index 0000000000..21d0bee37e --- /dev/null +++ b/extensions/sql-database-projects/src/common/promise.ts @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Deferred promise + */ +export class Deferred { + promise: Promise; + resolve!: ((value?: T | PromiseLike) => void); + reject!: ((reason?: any) => void); + + constructor() { + this.promise = new Promise((resolve, reject) => { + this.resolve = resolve; + this.reject = reject; + }); + } + + then(onfulfilled?: (value: T) => TResult | Thenable, onrejected?: (reason: any) => TResult | Thenable): Thenable; + then(onfulfilled?: (value: T) => TResult | Thenable, onrejected?: (reason: any) => void): Thenable; + then(onfulfilled?: (value: T) => TResult | Thenable, onrejected?: (reason: any) => TResult | Thenable): Thenable { + return this.promise.then(onfulfilled, onrejected); + } +} diff --git a/extensions/sql-database-projects/src/common/utils.ts b/extensions/sql-database-projects/src/common/utils.ts new file mode 100644 index 0000000000..1ca5b993dd --- /dev/null +++ b/extensions/sql-database-projects/src/common/utils.ts @@ -0,0 +1,9 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + +export function getErrorMessage(error: Error | string): string { + return (error instanceof Error) ? error.message : error; +} diff --git a/extensions/sql-database-projects/src/controllers/databaseProjectTreeItem.ts b/extensions/sql-database-projects/src/controllers/databaseProjectTreeItem.ts new file mode 100644 index 0000000000..ec0370ef2e --- /dev/null +++ b/extensions/sql-database-projects/src/controllers/databaseProjectTreeItem.ts @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export class SqlDatabaseProjectItem { + label: string; + readonly isFolder: boolean; + readonly parent?: SqlDatabaseProjectItem; + children: SqlDatabaseProjectItem[] = []; + + constructor(label: string, isFolder: boolean, parent?: SqlDatabaseProjectItem) { + this.label = label; + this.isFolder = isFolder; + this.parent = parent; + } + + public createChild(label: string, isFolder: boolean): SqlDatabaseProjectItem { + let child = new SqlDatabaseProjectItem(label, isFolder, this); + this.children.push(child); + + return child; + } +} diff --git a/extensions/sql-database-projects/src/controllers/databaseProjectTreeViewProvider.ts b/extensions/sql-database-projects/src/controllers/databaseProjectTreeViewProvider.ts new file mode 100644 index 0000000000..839c329cd4 --- /dev/null +++ b/extensions/sql-database-projects/src/controllers/databaseProjectTreeViewProvider.ts @@ -0,0 +1,120 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { promises as fs } from 'fs'; +import * as path from 'path'; +import * as constants from '../common/constants'; + +import { SqlDatabaseProjectItem } from './databaseProjectTreeItem'; + +export class SqlDatabaseProjectTreeViewProvider implements vscode.TreeDataProvider { + private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); + readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; + + private roots: SqlDatabaseProjectItem[] = []; + + constructor() { + this.initialize(); + } + + private initialize() { + this.roots = [new SqlDatabaseProjectItem(constants.noOpenProjectMessage, false)]; + } + + public getTreeItem(element: SqlDatabaseProjectItem): vscode.TreeItem { + return { + label: element.label, + collapsibleState: element.parent === undefined + ? vscode.TreeItemCollapsibleState.Expanded + : element.isFolder + ? vscode.TreeItemCollapsibleState.Collapsed + : vscode.TreeItemCollapsibleState.None + }; + } + + public getChildren(element?: SqlDatabaseProjectItem): SqlDatabaseProjectItem[] { + if (element === undefined) { + return this.roots; + } + + return element.children; + } + + public async openProject(projectFiles: vscode.Uri[]) { + if (projectFiles.length > 1) { // TODO: how to handle opening a folder with multiple .sqlproj files? + vscode.window.showErrorMessage(constants.multipleSqlProjFiles); + return; + } + + if (projectFiles.length === 0) { + vscode.window.showErrorMessage(constants.noSqlProjFiles); + return; + } + + let directoryPath = path.dirname(projectFiles[0].fsPath); + console.log('Opening project directory: ' + directoryPath); + + let newRoots: SqlDatabaseProjectItem[] = []; + + newRoots.push(await this.constructDataSourcesTree(directoryPath)); + newRoots.push(await this.constructProjectTree(directoryPath)); + + this.roots = newRoots; + this._onDidChangeTreeData.fire(); + } + + private async constructProjectTree(directoryPath: string): Promise { + let projectsNode = await this.constructFileTreeNode(directoryPath, undefined); + + projectsNode.label = constants.projectNodeName; + + return projectsNode; + } + + private async constructFileTreeNode(entryPath: string, parentNode: SqlDatabaseProjectItem | undefined): Promise { + let stat = await fs.stat(entryPath); + + let output = parentNode === undefined + ? new SqlDatabaseProjectItem(path.basename(entryPath), stat.isDirectory()) + : parentNode.createChild(path.basename(entryPath), stat.isDirectory()); + + if (stat.isDirectory()) { + let contents = await fs.readdir(entryPath); + + for (const entry of contents) { + await this.constructFileTreeNode(path.join(entryPath, entry), output); + } + + // sort children so that folders come first, then alphabetical + output.children.sort((a: SqlDatabaseProjectItem, b: SqlDatabaseProjectItem) => { + if (a.isFolder && !b.isFolder) { return -1; } + else if (!a.isFolder && b.isFolder) { return 1; } + else { return a.label.localeCompare(b.label); } + }); + } + + return output; + } + + private async constructDataSourcesTree(directoryPath: string): Promise { + let dataSourceNode = new SqlDatabaseProjectItem(constants.dataSourcesNodeName, true); + + let dataSourcesFilePath = path.join(directoryPath, constants.dataSourcesFileName); + + try { + let connections = await fs.readFile(dataSourcesFilePath, 'r'); + + // TODO: parse connections.json + + dataSourceNode.createChild(constants.foundDataSourcesFile + connections.length, false); + } + catch { + dataSourceNode.createChild(constants.noDataSourcesFile, false); + } + + return dataSourceNode; + } +} diff --git a/extensions/sql-database-projects/src/controllers/mainController.ts b/extensions/sql-database-projects/src/controllers/mainController.ts index 32c316ea85..d4bbf1f825 100644 --- a/extensions/sql-database-projects/src/controllers/mainController.ts +++ b/extensions/sql-database-projects/src/controllers/mainController.ts @@ -4,12 +4,21 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; +import * as nls from 'vscode-nls'; + +import { SqlDatabaseProjectTreeViewProvider } from './databaseProjectTreeViewProvider'; +import { getErrorMessage } from '../common/utils'; + +const SQL_DATABASE_PROJECTS_VIEW_ID = 'sqlDatabaseProjectsView'; + +const localize = nls.loadMessageBundle(); /** * The main controller class that initializes the extension */ export default class MainController implements vscode.Disposable { protected _context: vscode.ExtensionContext; + protected dbProjectTreeViewProvider: SqlDatabaseProjectTreeViewProvider = new SqlDatabaseProjectTreeViewProvider(); public constructor(context: vscode.ExtensionContext) { this._context = context; @@ -22,14 +31,36 @@ export default class MainController implements vscode.Disposable { public deactivate(): void { } - public activate(): Promise { - this.initializeDatabaseProjects(); - return Promise.resolve(true); + public async activate(): Promise { + await this.initializeDatabaseProjects(); } - private initializeDatabaseProjects(): void { - vscode.commands.registerCommand('sqlDatabaseProjects.new', () => { console.log('new database project called'); }); - vscode.commands.registerCommand('sqlDatabaseProjects.open', () => { console.log('open database project called'); }); + private async initializeDatabaseProjects(): Promise { + // init commands + vscode.commands.registerCommand('sqlDatabaseProjects.new', () => { console.log('"New Database Project" called.'); }); + vscode.commands.registerCommand('sqlDatabaseProjects.open', async () => { this.openProjectFolder(); }); + + // init view + this.dbProjectTreeViewProvider = new SqlDatabaseProjectTreeViewProvider(); + + this.extensionContext.subscriptions.push(vscode.window.registerTreeDataProvider(SQL_DATABASE_PROJECTS_VIEW_ID, this.dbProjectTreeViewProvider)); + } + + public async openProjectFolder(): Promise { + try { + let filter: { [key: string]: string[] } = {}; + + filter[localize('sqlDatabaseProject', "SQL database project")] = ['sqlproj']; + + let file = await vscode.window.showOpenDialog({ filters: filter }); + + if (file) { + await this.dbProjectTreeViewProvider.openProject(file); + } + } + catch (err) { + vscode.window.showErrorMessage(getErrorMessage(err)); + } } public dispose(): void { diff --git a/extensions/sql-database-projects/src/main.ts b/extensions/sql-database-projects/src/extension.ts similarity index 93% rename from extensions/sql-database-projects/src/main.ts rename to extensions/sql-database-projects/src/extension.ts index 002986d324..dfd92eb254 100644 --- a/extensions/sql-database-projects/src/main.ts +++ b/extensions/sql-database-projects/src/extension.ts @@ -10,7 +10,7 @@ let controllers: MainController[] = []; export async function activate(context: vscode.ExtensionContext): Promise { // Start the main controller - let mainController = new MainController(context); + const mainController = new MainController(context); controllers.push(mainController); context.subscriptions.push(mainController);