mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 18:46:40 -05:00
Initial SQL Project tree viewlet in Explorer sidebar (#8639)
* Adding mock contents for tree * added open sqlproj dialog * reading files from directory * Added directory traversal * Adding tree sorting by folder vs file and label * Improved auto-unfolding of tree based on node type * replacing fs with fs.promise alias * added activation event for when workspace contains sqlproj files * Returning after displaying error
This commit is contained in:
@@ -13,9 +13,11 @@
|
|||||||
"icon": "images/sqlserver.png",
|
"icon": "images/sqlserver.png",
|
||||||
"aiKey": "AIF-c5594e2d-38b5-4d3b-ab1b-ed5d4fe8ee40",
|
"aiKey": "AIF-c5594e2d-38b5-4d3b-ab1b-ed5d4fe8ee40",
|
||||||
"activationEvents": [
|
"activationEvents": [
|
||||||
"*"
|
"onCommand:sqlDatabaseProjects.new",
|
||||||
|
"onCommand:sqlDatabaseProjects.open",
|
||||||
|
"workspaceContains:**/*.sqlproj"
|
||||||
],
|
],
|
||||||
"main": "./out/main",
|
"main": "./out/extension",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/Microsoft/azuredatastudio.git"
|
"url": "https://github.com/Microsoft/azuredatastudio.git"
|
||||||
@@ -45,6 +47,15 @@
|
|||||||
"command": "sqlDatabaseProjects.open"
|
"command": "sqlDatabaseProjects.open"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"views": {
|
||||||
|
"explorer": [
|
||||||
|
{
|
||||||
|
"id": "sqlDatabaseProjectsView",
|
||||||
|
"name": "%title.projectsView%",
|
||||||
|
"when": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -2,5 +2,6 @@
|
|||||||
"sqlDatabaseProjects.displayName": "Database Projects",
|
"sqlDatabaseProjects.displayName": "Database Projects",
|
||||||
"sqlDatabaseProjects.description": "Design and deploy SQL database schemas",
|
"sqlDatabaseProjects.description": "Design and deploy SQL database schemas",
|
||||||
"sqlDatabaseProjects.new": "New Database Project",
|
"sqlDatabaseProjects.new": "New Database Project",
|
||||||
"sqlDatabaseProjects.open": "Open Database Project"
|
"sqlDatabaseProjects.open": "Open Database Project",
|
||||||
|
"title.projectsView": "SQL Database Projects"
|
||||||
}
|
}
|
||||||
|
|||||||
24
extensions/sql-database-projects/src/common/constants.ts
Normal file
24
extensions/sql-database-projects/src/common/constants.ts
Normal file
@@ -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);
|
||||||
26
extensions/sql-database-projects/src/common/promise.ts
Normal file
26
extensions/sql-database-projects/src/common/promise.ts
Normal file
@@ -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<T> {
|
||||||
|
promise: Promise<T>;
|
||||||
|
resolve!: ((value?: T | PromiseLike<T>) => void);
|
||||||
|
reject!: ((reason?: any) => void);
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.promise = new Promise<T>((resolve, reject) => {
|
||||||
|
this.resolve = resolve;
|
||||||
|
this.reject = reject;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
then<TResult>(onfulfilled?: (value: T) => TResult | Thenable<TResult>, onrejected?: (reason: any) => TResult | Thenable<TResult>): Thenable<TResult>;
|
||||||
|
then<TResult>(onfulfilled?: (value: T) => TResult | Thenable<TResult>, onrejected?: (reason: any) => void): Thenable<TResult>;
|
||||||
|
then<TResult>(onfulfilled?: (value: T) => TResult | Thenable<TResult>, onrejected?: (reason: any) => TResult | Thenable<TResult>): Thenable<TResult> {
|
||||||
|
return this.promise.then(onfulfilled, onrejected);
|
||||||
|
}
|
||||||
|
}
|
||||||
9
extensions/sql-database-projects/src/common/utils.ts
Normal file
9
extensions/sql-database-projects/src/common/utils.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<SqlDatabaseProjectItem> {
|
||||||
|
private _onDidChangeTreeData: vscode.EventEmitter<SqlDatabaseProjectItem | undefined> = new vscode.EventEmitter<SqlDatabaseProjectItem | undefined>();
|
||||||
|
readonly onDidChangeTreeData: vscode.Event<SqlDatabaseProjectItem | undefined> = 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<SqlDatabaseProjectItem> {
|
||||||
|
let projectsNode = await this.constructFileTreeNode(directoryPath, undefined);
|
||||||
|
|
||||||
|
projectsNode.label = constants.projectNodeName;
|
||||||
|
|
||||||
|
return projectsNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async constructFileTreeNode(entryPath: string, parentNode: SqlDatabaseProjectItem | undefined): Promise<SqlDatabaseProjectItem> {
|
||||||
|
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<SqlDatabaseProjectItem> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,12 +4,21 @@
|
|||||||
*--------------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
import * as vscode from 'vscode';
|
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
|
* The main controller class that initializes the extension
|
||||||
*/
|
*/
|
||||||
export default class MainController implements vscode.Disposable {
|
export default class MainController implements vscode.Disposable {
|
||||||
protected _context: vscode.ExtensionContext;
|
protected _context: vscode.ExtensionContext;
|
||||||
|
protected dbProjectTreeViewProvider: SqlDatabaseProjectTreeViewProvider = new SqlDatabaseProjectTreeViewProvider();
|
||||||
|
|
||||||
public constructor(context: vscode.ExtensionContext) {
|
public constructor(context: vscode.ExtensionContext) {
|
||||||
this._context = context;
|
this._context = context;
|
||||||
@@ -22,14 +31,36 @@ export default class MainController implements vscode.Disposable {
|
|||||||
public deactivate(): void {
|
public deactivate(): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
public activate(): Promise<boolean> {
|
public async activate(): Promise<void> {
|
||||||
this.initializeDatabaseProjects();
|
await this.initializeDatabaseProjects();
|
||||||
return Promise.resolve(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private initializeDatabaseProjects(): void {
|
private async initializeDatabaseProjects(): Promise<void> {
|
||||||
vscode.commands.registerCommand('sqlDatabaseProjects.new', () => { console.log('new database project called'); });
|
// init commands
|
||||||
vscode.commands.registerCommand('sqlDatabaseProjects.open', () => { console.log('open database project called'); });
|
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<void> {
|
||||||
|
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 {
|
public dispose(): void {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ let controllers: MainController[] = [];
|
|||||||
|
|
||||||
export async function activate(context: vscode.ExtensionContext): Promise<void> {
|
export async function activate(context: vscode.ExtensionContext): Promise<void> {
|
||||||
// Start the main controller
|
// Start the main controller
|
||||||
let mainController = new MainController(context);
|
const mainController = new MainController(context);
|
||||||
controllers.push(mainController);
|
controllers.push(mainController);
|
||||||
context.subscriptions.push(mainController);
|
context.subscriptions.push(mainController);
|
||||||
|
|
||||||
Reference in New Issue
Block a user