.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

@@ -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;
}
}