.sqlproj and datasources.json file parsing (#8921)

* Checkpoint

* 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

* PR feedback

* added activation event for when workspace contains sqlproj files

* Returning after displaying error

* Fixing linter errors

* Reworked tree

* Fixing missing grandchildren

* Correcting tree URI construction

* Refactoring to isolate tree item responsibilities from data model responsibilities

* project file parsing

* constructing tree from project files rather than filesystem

* Fixing double-initialization

* Changing projectEntry to take enum for file type

* Correct node type for project item

* Parsing datasources.json

* Child nodes for sql data source

* Localizing strings

* Checkpoint

* 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

* PR feedback

* added activation event for when workspace contains sqlproj files

* Returning after displaying error

* Fixing linter errors

* Reworked tree

* Fixing missing grandchildren

* Correcting tree URI construction

* Refactoring to isolate tree item responsibilities from data model responsibilities

* project file parsing

* constructing tree from project files rather than filesystem

* Fixing double-initialization

* Changing projectEntry to take enum for file type

* Correct node type for project item

* Parsing datasources.json

* Child nodes for sql data source

* Localizing strings

* missed file in merge

* changed extension method to helper

* cleanup

* Adding docstrings
This commit is contained in:
Benjin Dubishar
2020-02-24 12:11:41 -08:00
committed by GitHub
parent 933cfb21ef
commit 1a639f83c4
18 changed files with 718 additions and 152 deletions

View File

@@ -15,10 +15,14 @@ export const dataSourcesFileName = 'datasources.json';
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.
export const sqlConnectionStringFriendly = localize('sqlConnectionStringFriendly', "SQL connection string");
// 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);
export const missingVersion = localize('missingVersion', "Missing 'version' entry in {0}", dataSourcesFileName);
export const unrecognizedDataSourcesVersion = localize('unrecognizedDataSourcesVersion', "Unrecognized version: ");
export const unknownDataSourceType = localize('unknownDataSourceType', "Unknown data source type: ");
export const invalidSqlConnectionString = localize('invalidSqlConnectionString', "Invalid SQL connection string");

View File

@@ -1,26 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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);
}
}

View File

@@ -3,7 +3,45 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
/**
* Consolidates on the error message string
*/
export function getErrorMessage(error: Error | string): string {
return (error instanceof Error) ? error.message : error;
}
/**
* removes any leading portion shared between the two URIs from outerUri.
* e.g. [@param innerUri: 'this\is'; @param outerUri: '\this\is\my\path'] => 'my\path'
* @param innerUri the URI that will be cut away from the outer URI
* @param outerUri the URI that will have any shared beginning portion removed
*/
export function trimUri(innerUri: vscode.Uri, outerUri: vscode.Uri): string {
let innerParts = innerUri.path.split('/');
let outerParts = outerUri.path.split('/');
while (innerParts.length > 0 && outerParts.length > 0 && innerParts[0].toLocaleLowerCase() === outerParts[0].toLocaleLowerCase()) {
innerParts = innerParts.slice(1);
outerParts = outerParts.slice(1);
}
return outerParts.join('/');
}
/**
* Trims any character contained in @param chars from both the beginning and end of @param input
*/
export function trimChars(input: string, chars: string): string {
let output = input;
let i = 0;
while (chars.includes(output[i])) { i++; }
output = output.substr(i);
i = 0;
while (chars.includes(output[output.length - i - 1])) { i++; }
output = output.substring(0, output.length - i);
return output;
}

View File

@@ -1,24 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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;
}
}

View File

@@ -4,38 +4,34 @@
*--------------------------------------------------------------------------------------------*/
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';
import { BaseProjectTreeItem, MessageTreeItem } from '../models/tree/baseTreeItem';
import { ProjectRootTreeItem } from '../models/tree/projectTreeItem';
import { Project } from '../models/project';
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;
/**
* Tree view for database projects
*/
export class SqlDatabaseProjectTreeViewProvider implements vscode.TreeDataProvider<BaseProjectTreeItem> {
private _onDidChangeTreeData: vscode.EventEmitter<BaseProjectTreeItem | undefined> = new vscode.EventEmitter<BaseProjectTreeItem | undefined>();
readonly onDidChangeTreeData: vscode.Event<BaseProjectTreeItem | undefined> = this._onDidChangeTreeData.event;
private roots: SqlDatabaseProjectItem[] = [];
private roots: BaseProjectTreeItem[] = [];
constructor() {
this.initialize();
}
private initialize() {
this.roots = [new SqlDatabaseProjectItem(constants.noOpenProjectMessage, false)];
this.roots = [new MessageTreeItem(constants.noOpenProjectMessage)];
}
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 getTreeItem(element: BaseProjectTreeItem): vscode.TreeItem {
return element.treeItem;
}
public getChildren(element?: SqlDatabaseProjectItem): SqlDatabaseProjectItem[] {
public getChildren(element?: BaseProjectTreeItem): BaseProjectTreeItem[] {
if (element === undefined) {
return this.roots;
}
@@ -43,78 +39,19 @@ export class SqlDatabaseProjectTreeViewProvider implements vscode.TreeDataProvid
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) {
public load(projects: Project[]) {
if (projects.length === 0) {
vscode.window.showErrorMessage(constants.noSqlProjFiles);
return;
}
let directoryPath = path.dirname(projectFiles[0].fsPath);
console.log('Opening project directory: ' + directoryPath);
let newRoots: BaseProjectTreeItem[] = [];
let newRoots: SqlDatabaseProjectItem[] = [];
newRoots.push(await this.constructDataSourcesTree(directoryPath));
newRoots.push(await this.constructProjectTree(directoryPath));
for (const proj of projects) {
newRoots.push(new ProjectRootTreeItem(proj));
}
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;
}
}

View File

@@ -8,6 +8,7 @@ import * as nls from 'vscode-nls';
import { SqlDatabaseProjectTreeViewProvider } from './databaseProjectTreeViewProvider';
import { getErrorMessage } from '../common/utils';
import { ProjectsController } from './projectController';
const SQL_DATABASE_PROJECTS_VIEW_ID = 'sqlDatabaseProjectsView';
@@ -19,9 +20,11 @@ const localize = nls.loadMessageBundle();
export default class MainController implements vscode.Disposable {
protected _context: vscode.ExtensionContext;
protected dbProjectTreeViewProvider: SqlDatabaseProjectTreeViewProvider = new SqlDatabaseProjectTreeViewProvider();
protected projectsController: ProjectsController;
public constructor(context: vscode.ExtensionContext) {
this._context = context;
this.projectsController = new ProjectsController(this.dbProjectTreeViewProvider);
}
public get extensionContext(): vscode.ExtensionContext {
@@ -38,24 +41,28 @@ export default class MainController implements vscode.Disposable {
private async initializeDatabaseProjects(): Promise<void> {
// init commands
vscode.commands.registerCommand('sqlDatabaseProjects.new', () => { console.log('"New Database Project" called.'); });
vscode.commands.registerCommand('sqlDatabaseProjects.open', async () => { this.openProjectFolder(); });
vscode.commands.registerCommand('sqlDatabaseProjects.open', async () => { this.openProjectFromFile(); });
// 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> {
/**
* Prompts the user to select a .sqlproj file to open
* TODO: define behavior once projects are automatically opened from workspace
*/
public async openProjectFromFile(): Promise<void> {
try {
let filter: { [key: string]: string[] } = {};
filter[localize('sqlDatabaseProject', "SQL database project")] = ['sqlproj'];
let file = await vscode.window.showOpenDialog({ filters: filter });
let files: vscode.Uri[] | undefined = await vscode.window.showOpenDialog({ filters: filter });
if (file) {
await this.dbProjectTreeViewProvider.openProject(file);
if (files) {
for (const file of files) {
await this.projectsController.openProject(file);
}
}
}
catch (err) {

View File

@@ -0,0 +1,43 @@
/*---------------------------------------------------------------------------------------------
* 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 { Project } from '../models/project';
import { SqlDatabaseProjectTreeViewProvider } from './databaseProjectTreeViewProvider';
import * as path from 'path';
import * as constants from '../common/constants';
import * as dataSources from '../models/dataSources/dataSources';
/**
* Controller for managing project lifecycle
*/
export class ProjectsController {
private projectTreeViewProvider: SqlDatabaseProjectTreeViewProvider;
projects: Project[] = [];
constructor(projTreeViewProvider: SqlDatabaseProjectTreeViewProvider) {
this.projectTreeViewProvider = projTreeViewProvider;
}
public async openProject(projectFile: vscode.Uri) {
console.log('Loading project: ' + projectFile.fsPath);
// Read project file
const newProject = new Project(projectFile.fsPath);
await newProject.readProjFile();
this.projects.push(newProject);
// Read datasources.json (if present)
const dataSourcesFilePath = path.join(path.dirname(projectFile.fsPath), constants.dataSourcesFileName);
newProject.dataSources = await dataSources.load(dataSourcesFilePath);
this.refreshProjectsTree();
}
public refreshProjectsTree() {
this.projectTreeViewProvider.load(this.projects);
}
}

View 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.
*--------------------------------------------------------------------------------------------*/
/**
* JSON format for datasources.json
*/
interface DataSourceFileJson {
version: string;
datasources: DataSourceJson[];
}
/**
* JSON format for a datasource entry in datasources.json
*/
interface DataSourceJson {
name: string;
type: string;
version: string;
/**
* contents for concrete datasource implementation
*/
data: string;
}

View File

@@ -0,0 +1,71 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { promises as fs } from 'fs';
import * as constants from '../../common/constants';
import { SqlConnectionDataSource } from './sqlConnectionStringSource';
/**
* Abstract class for a datasource in a project
*/
export abstract class DataSource {
public name: string;
public abstract get type(): string;
public abstract get friendlyName(): string;
constructor(name: string) {
this.name = name;
}
}
/**
* parses the specified file to load DataSource objects
*/
export async function load(dataSourcesFilePath: string): Promise<DataSource[]> {
let fileContents;
try {
fileContents = await fs.readFile(dataSourcesFilePath);
}
catch (err) {
throw new Error(constants.noDataSourcesFile);
}
const rawJsonContents = JSON.parse(fileContents.toString());
if (rawJsonContents.version === undefined) {
throw new Error(constants.missingVersion);
}
const output: DataSource[] = [];
// TODO: do we have a construct for parsing version numbers?
switch (rawJsonContents.version) {
case '0.0.0':
const dataSources: DataSourceFileJson = rawJsonContents as DataSourceFileJson;
for (const source of dataSources.datasources) {
output.push(createDataSource(source));
}
break;
default:
throw new Error(constants.unrecognizedDataSourcesVersion + rawJsonContents.version);
}
return output;
}
/**
* Creates DataSource object from JSON
*/
function createDataSource(json: DataSourceJson): DataSource {
switch (json.type) {
case SqlConnectionDataSource.type:
return SqlConnectionDataSource.fromJson(json);
default:
throw new Error(constants.unknownDataSourceType + json.type);
}
}

View File

@@ -0,0 +1,55 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { DataSource } from './dataSources';
import * as constants from '../../common/constants';
/**
* Contains information about a SQL connection string data source`
*/
export class SqlConnectionDataSource extends DataSource {
readonly connectionString: string;
readonly connectionStringComponents: { [id: string]: string } = {};
public static get type() {
return 'sql_connection_string';
}
public get type(): string {
return SqlConnectionDataSource.type;
}
public get friendlyName(): string {
return constants.sqlConnectionStringFriendly;
}
constructor(name: string, connectionString: string) {
super(name);
// TODO: do we have a common construct for connection strings?
this.connectionString = connectionString;
for (const component of this.connectionString.split(';')) {
const split = component.split('=');
if (split.length !== 2) {
throw new Error(constants.invalidSqlConnectionString);
}
this.connectionStringComponents[split[0]] = split[1];
}
}
public static fromJson(json: DataSourceJson): SqlConnectionDataSource {
return new SqlConnectionDataSource(json.name, (json.data as unknown as SqlConnectionDataSourceJson).connectionString);
}
}
/**
* JSON structure for a SQL connection string data source
*/
interface SqlConnectionDataSourceJson {
connectionString: string;
}

View File

@@ -0,0 +1,91 @@
/*---------------------------------------------------------------------------------------------
* 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 xml2js from 'xml2js';
import * as path from 'path';
import { promises as fs } from 'fs';
import { DataSource } from './dataSources/dataSources';
/**
* Class representing a Project, and providing functions for operating on it
*/
export class Project {
public projectFile: string;
public files: ProjectEntry[] = [];
public dataSources: DataSource[] = [];
constructor(projectFilePath: string) {
this.projectFile = projectFilePath;
}
/**
* Reads the project setting and contents from the file
*/
public async readProjFile() {
let projFileContents = await fs.readFile(this.projectFile);
const parser = new xml2js.Parser({
explicitArray: true,
explicitCharkey: false,
explicitRoot: false
});
let result;
try {
result = await parser.parseStringPromise(projFileContents.toString());
}
catch (err) {
vscode.window.showErrorMessage(err);
return;
}
// find all folders and files to include
for (const itemGroup of result['ItemGroup']) {
if (itemGroup['Build'] !== undefined) {
for (const fileEntry of itemGroup['Build']) {
this.files.push(this.createProjectEntry(fileEntry.$['Include'], EntryType.File));
}
}
if (itemGroup['Folder'] !== undefined) {
for (const folderEntry of itemGroup['Folder']) {
this.files.push(this.createProjectEntry(folderEntry.$['Include'], EntryType.Folder));
}
}
}
}
private createProjectEntry(relativePath: string, entryType: EntryType): ProjectEntry {
return new ProjectEntry(vscode.Uri.file(path.join(this.projectFile, relativePath)), entryType);
}
}
/**
* Represents an entry in a project file
*/
export class ProjectEntry {
/**
* Absolute file system URI
*/
uri: vscode.Uri;
type: EntryType;
constructor(uri: vscode.Uri, type: EntryType) {
this.uri = uri;
this.type = type;
}
public toString(): string {
return this.uri.path;
}
}
export enum EntryType {
File,
Folder
}

View File

@@ -0,0 +1,54 @@
/*---------------------------------------------------------------------------------------------
* 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 path from 'path';
/**
* Base class for an item that appears in the ADS project tree
*/
export abstract class BaseProjectTreeItem {
uri: vscode.Uri;
parent?: BaseProjectTreeItem;
constructor(uri: vscode.Uri, parent?: BaseProjectTreeItem) {
this.uri = uri;
this.parent = parent;
}
abstract get children(): BaseProjectTreeItem[];
abstract get treeItem(): vscode.TreeItem;
public get root() {
let node: BaseProjectTreeItem = this;
while (node.parent !== undefined) {
node = node.parent;
}
return node;
}
}
/**
* Leaf tree item that just displays text for messaging purposes
*/
export class MessageTreeItem extends BaseProjectTreeItem {
private message: string;
constructor(message: string, parent?: BaseProjectTreeItem) {
super(vscode.Uri.file(path.join(parent?.uri.path ?? 'Message', message)), parent);
this.message = message;
}
public get children(): BaseProjectTreeItem[] {
return [];
}
public get treeItem(): vscode.TreeItem {
return new vscode.TreeItem(this.message, vscode.TreeItemCollapsibleState.None);
}
}

View File

@@ -0,0 +1,85 @@
/*---------------------------------------------------------------------------------------------
* 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 path from 'path';
import { BaseProjectTreeItem, MessageTreeItem } from './baseTreeItem';
import * as constants from '../../common/constants';
import { ProjectRootTreeItem } from './projectTreeItem';
import { DataSource } from '../dataSources/dataSources';
import { SqlConnectionDataSource } from '../dataSources/sqlConnectionStringSource';
/**
* Folder for containing DataSource nodes in the tree
*/
export class DataSourcesTreeItem extends BaseProjectTreeItem {
private dataSources: DataSourceTreeItem[] = [];
constructor(project: ProjectRootTreeItem) {
super(vscode.Uri.file(path.join(project.uri.path, constants.dataSourcesNodeName)), project);
this.construct();
}
private construct() {
for (const dataSource of (this.parent as ProjectRootTreeItem).project.dataSources) {
this.dataSources.push(constructDataSourceTreeItem(dataSource, this));
}
}
public get children(): BaseProjectTreeItem[] {
return this.dataSources;
}
public get treeItem(): vscode.TreeItem {
return new vscode.TreeItem(this.uri, vscode.TreeItemCollapsibleState.Collapsed);
}
}
abstract class DataSourceTreeItem extends BaseProjectTreeItem { }
/**
* Tree item representing a SQL connection string data source
*/
export class SqlConnectionDataSourceTreeItem extends DataSourceTreeItem {
private dataSource: SqlConnectionDataSource;
constructor(dataSource: SqlConnectionDataSource, dataSourcesNode: DataSourcesTreeItem) {
super(vscode.Uri.file(path.join(dataSourcesNode.uri.path, dataSource.name)), dataSourcesNode);
this.dataSource = dataSource;
}
public get treeItem(): vscode.TreeItem {
let item = new vscode.TreeItem(this.uri, vscode.TreeItemCollapsibleState.Collapsed);
item.label = `${this.dataSource.name} (${this.dataSource.friendlyName})`;
return item;
}
/**
* SQL connection string components, displayed as key-value pairs
*/
public get children(): BaseProjectTreeItem[] {
const result: MessageTreeItem[] = [];
for (const comp of Object.keys(this.dataSource.connectionStringComponents).sort()) {
result.push(new MessageTreeItem(`${comp}: ${this.dataSource.connectionStringComponents[comp]}`, this));
}
return result;
}
}
/**
* Constructs a new TreeItem for the specific given DataSource type
*/
export function constructDataSourceTreeItem(dataSource: DataSource, dataSourcesNode: DataSourcesTreeItem): DataSourceTreeItem {
switch (dataSource.type) {
case SqlConnectionDataSource.type:
return new SqlConnectionDataSourceTreeItem(dataSource as SqlConnectionDataSource, dataSourcesNode);
default:
throw new Error(constants.unknownDataSourceType + dataSource.type); // TODO: elegant handling of unknown dataSource type instead of failure
}
}

View File

@@ -0,0 +1,73 @@
/*---------------------------------------------------------------------------------------------
* 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 path from 'path';
import { BaseProjectTreeItem } from './baseTreeItem';
import { ProjectRootTreeItem } from './projectTreeItem';
import { Project } from '../project';
/**
* Node representing a folder in a project
*/
export class FolderNode extends BaseProjectTreeItem {
public fileChildren: { [childName: string]: (FolderNode | FileNode) } = {};
public fileSystemUri: vscode.Uri;
constructor(folderPath: vscode.Uri, parent: FolderNode | ProjectRootTreeItem) {
super(fsPathToProjectUri(folderPath, parent.root as ProjectRootTreeItem), parent);
this.fileSystemUri = folderPath;
}
public get children(): BaseProjectTreeItem[] {
return Object.values(this.fileChildren).sort();
}
public get treeItem(): vscode.TreeItem {
return new vscode.TreeItem(this.uri, vscode.TreeItemCollapsibleState.Expanded);
}
public get project(): Project {
return (<FolderNode | ProjectRootTreeItem>this.parent).project;
}
}
/**
* Node representing a file in a project
*/
export class FileNode extends BaseProjectTreeItem {
public fileSystemUri: vscode.Uri;
constructor(filePath: vscode.Uri, parent: FolderNode | ProjectRootTreeItem) {
super(fsPathToProjectUri(filePath, parent.root as ProjectRootTreeItem), parent);
this.fileSystemUri = filePath;
}
public get children(): BaseProjectTreeItem[] {
return [];
}
public get treeItem(): vscode.TreeItem {
return new vscode.TreeItem(this.uri, vscode.TreeItemCollapsibleState.None);
}
}
/**
* Converts a full filesystem URI to a project-relative URI that's compatible with the project tree
*/
function fsPathToProjectUri(fileSystemUri: vscode.Uri, projectNode: ProjectRootTreeItem): vscode.Uri {
const projBaseDir = path.dirname(projectNode.project.projectFile);
let localUri = '';
if (fileSystemUri.fsPath.startsWith(projBaseDir)) {
localUri = fileSystemUri.fsPath.substring(projBaseDir.length);
}
else {
vscode.window.showErrorMessage('Project pointing to file outside of directory');
throw new Error('Project pointing to file outside of directory');
}
return vscode.Uri.file(path.join(projectNode.uri.path, localUri));
}

View File

@@ -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 * as vscode from 'vscode';
import * as path from 'path';
import { DataSourcesTreeItem } from './dataSourceTreeItem';
import { BaseProjectTreeItem } from './baseTreeItem';
import * as fileTree from './fileFolderTreeItem';
import { Project, ProjectEntry, EntryType } from '../project';
import * as utils from '../../common/utils';
/**
* TreeNode root that represents an entire project
*/
export class ProjectRootTreeItem extends BaseProjectTreeItem {
dataSourceNode: DataSourcesTreeItem;
fileChildren: { [childName: string]: (fileTree.FolderNode | fileTree.FileNode) } = {};
project: Project;
constructor(project: Project) {
super(vscode.Uri.parse(path.basename(project.projectFile)), undefined);
this.project = project;
this.dataSourceNode = new DataSourcesTreeItem(this);
this.construct();
}
public get children(): BaseProjectTreeItem[] {
const output: BaseProjectTreeItem[] = [];
output.push(this.dataSourceNode);
// sort children so that folders come first, then alphabetical
const sortedChildren = Object.values(this.fileChildren).sort((a: (fileTree.FolderNode | fileTree.FileNode), b: (fileTree.FolderNode | fileTree.FileNode)) => {
if (a instanceof fileTree.FolderNode && !(b instanceof fileTree.FolderNode)) { return -1; }
else if (!(a instanceof fileTree.FolderNode) && b instanceof fileTree.FolderNode) { return 1; }
else { return a.uri.fsPath.localeCompare(b.uri.fsPath); }
});
return output.concat(sortedChildren);
}
public get treeItem(): vscode.TreeItem {
return new vscode.TreeItem(this.uri, vscode.TreeItemCollapsibleState.Expanded);
}
/**
* Processes the list of files in a project file to constructs the tree
*/
private construct() {
for (const entry of this.project.files) {
const parentNode = this.getEntryParentNode(entry);
let newNode: fileTree.FolderNode | fileTree.FileNode;
switch (entry.type) {
case EntryType.File:
newNode = new fileTree.FileNode(entry.uri, parentNode);
break;
case EntryType.Folder:
newNode = new fileTree.FolderNode(entry.uri, parentNode);
break;
default:
throw new Error(`Unknown EntryType: '${entry.type}'`);
}
parentNode.fileChildren[path.basename(entry.uri.path)] = newNode;
}
}
/**
* Gets the immediate parent tree node for an entry in a project file
*/
private getEntryParentNode(entry: ProjectEntry): fileTree.FolderNode | ProjectRootTreeItem {
const relativePathParts = utils.trimChars(utils.trimUri(vscode.Uri.file(this.project.projectFile), entry.uri), '/').split('/').slice(0, -1); // remove the last part because we only care about the parent
if (relativePathParts.length === 0) {
return this; // if nothing left after trimming the entry itself, must been root
}
let current: fileTree.FolderNode | ProjectRootTreeItem = this;
for (const part of relativePathParts) {
if (current.fileChildren[part] === undefined) {
current.fileChildren[part] = new fileTree.FolderNode(vscode.Uri.file(path.join(path.dirname(this.project.projectFile), part)), current);
}
if (current.fileChildren[part] instanceof fileTree.FileNode) {
return current;
}
else {
current = current.fileChildren[part] as fileTree.FolderNode | ProjectRootTreeItem;
}
}
return current;
}
}