diff --git a/extensions/npm/package.json b/extensions/npm/package.json
index 7f2648c3d50..1f5c6526cd3 100644
--- a/extensions/npm/package.json
+++ b/extensions/npm/package.json
@@ -7,6 +7,7 @@
"engines": {
"vscode": "0.10.x"
},
+ "enableProposedApi": true,
"icon": "images/npm_icon.png",
"categories": [
"Other"
@@ -28,9 +29,53 @@
"main": "./out/main",
"activationEvents": [
"onCommand:workbench.action.tasks.runTask",
- "onLanguage:json"
+ "onLanguage:json",
+ "onView:npm"
],
"contributes": {
+ "views": {
+ "explorer": [
+ {
+ "id": "npm",
+ "name": "npm scripts"
+ }
+ ]
+ },
+ "commands": [
+ {
+ "command": "npm.runScript",
+ "title": "Run Script"
+ },
+ {
+ "command": "npm.openScript",
+ "title": "Open Script"
+ },
+ {
+ "command": "npm.refresh",
+ "title": "Refresh",
+ "icon": {
+ "light": "resources/light/refresh.svg",
+ "dark": "resources/dark/refresh.svg"
+ }
+ }
+ ],
+ "menus": {
+ "view/title": [
+ {
+ "command": "npm.refresh",
+ "when": "view == npm",
+ "group": "navigation"
+ }
+ ],
+ "view/item/context": [
+ {
+ "command": "npm.openScript",
+ "when": "view == npm && viewItem == packageJSON",
+ "group": "1_navigation"
+ }
+ ]
+ },
+
"configuration": {
"id": "npm",
"type": "object",
diff --git a/extensions/npm/resources/dark/refresh.svg b/extensions/npm/resources/dark/refresh.svg
new file mode 100644
index 00000000000..d79fdaa4e8e
--- /dev/null
+++ b/extensions/npm/resources/dark/refresh.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/extensions/npm/resources/light/refresh.svg b/extensions/npm/resources/light/refresh.svg
new file mode 100644
index 00000000000..e0345748192
--- /dev/null
+++ b/extensions/npm/resources/light/refresh.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/extensions/npm/src/main.ts b/extensions/npm/src/main.ts
index ee072978725..49530ec615f 100644
--- a/extensions/npm/src/main.ts
+++ b/extensions/npm/src/main.ts
@@ -14,11 +14,22 @@ import * as minimatch from 'minimatch';
const localize = nls.loadMessageBundle();
import { addJSONProviders } from './features/jsonContributions';
+import { NpmScriptsTreeDataProvider } from './npmView';
+import { NpmTaskDefinition, ScriptValidator } from './tasks';
type AutoDetect = 'on' | 'off';
let taskProvider: vscode.Disposable | undefined;
+class Validator implements ScriptValidator {
+ async scriptIsValid(_task: vscode.Task): Promise {
+ // let tasks = await provideNpmScriptsForFolder(packageUri);
+ return true;
+ }
+}
+
export function activate(context: vscode.ExtensionContext): void {
+ vscode.window.registerTreeDataProvider('npm', new NpmScriptsTreeDataProvider(context, new Validator()));
+
if (!vscode.workspace.workspaceFolders) {
return;
}
@@ -67,11 +78,6 @@ async function readFile(file: string): Promise {
});
}
-interface NpmTaskDefinition extends vscode.TaskDefinition {
- script: string;
- path?: string;
-}
-
const buildNames: string[] = ['build', 'compile', 'watch'];
function isBuildTask(name: string): boolean {
for (let buildName of buildNames) {
diff --git a/extensions/npm/src/npmView.ts b/extensions/npm/src/npmView.ts
new file mode 100644
index 00000000000..375bb881b4b
--- /dev/null
+++ b/extensions/npm/src/npmView.ts
@@ -0,0 +1,177 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import {
+ ExtensionContext, Task, TreeDataProvider, TreeItem, TreeItemCollapsibleState,
+ WorkspaceFolder, workspace, commands, window, EventEmitter, Event,
+ ThemeIcon, Uri, TextDocument
+} from 'vscode';
+import { NpmTaskDefinition, ScriptValidator } from './tasks';
+import * as path from 'path';
+
+class Folder extends TreeItem {
+ packages: PackageJSON[] = [];
+
+ constructor(folder: WorkspaceFolder) {
+ super(folder.name, TreeItemCollapsibleState.Collapsed);
+ this.contextValue = 'folder';
+ this.resourceUri = folder.uri;
+ this.iconPath = ThemeIcon.Folder;
+ }
+
+ addPackage(packageJson: PackageJSON) {
+ this.packages.push(packageJson);
+ }
+}
+
+const packageName = 'package.json';
+
+class PackageJSON extends TreeItem {
+ path: string;
+ folder: Folder;
+ scripts: NpmScript[] = [];
+
+ static getLabel(folderName: string, relativePath: string): string {
+ if (relativePath.length > 0) {
+ return path.join(relativePath, packageName);
+ }
+ return path.join(folderName, packageName);
+ }
+
+ constructor(folder: Folder, relativePath: string) {
+ super(PackageJSON.getLabel(folder.label!, relativePath), TreeItemCollapsibleState.Collapsed);
+ this.folder = folder;
+ this.path = relativePath;
+ this.contextValue = 'packageJSON';
+ this.resourceUri = Uri.file(path.join(folder!.resourceUri!.fsPath, relativePath, packageName));
+ this.iconPath = ThemeIcon.File;
+ }
+
+ addScript(script: NpmScript) {
+ this.scripts.push(script);
+ }
+}
+
+class NpmScript extends TreeItem {
+ task: Task;
+ package: PackageJSON;
+
+ constructor(packageJson: PackageJSON, task: Task) {
+ super(task.name, TreeItemCollapsibleState.None);
+ this.contextValue = 'script';
+ this.package = packageJson;
+ this.task = task;
+ this.command = {
+ title: 'Run Script',
+ command: 'npm.runScript',
+ arguments: [task]
+ };
+ }
+}
+
+export class NpmScriptsTreeDataProvider implements TreeDataProvider {
+ private taskTree: Folder[] | PackageJSON[] | null = null;
+ private validator: ScriptValidator;
+ private _onDidChangeTreeData: EventEmitter = new EventEmitter();
+ readonly onDidChangeTreeData: Event = this._onDidChangeTreeData.event;
+
+
+ constructor(context: ExtensionContext, validator: ScriptValidator) {
+ const subscriptions = context.subscriptions;
+ this.validator = validator;
+ subscriptions.push(commands.registerCommand('npm.runScript', this.runScript, this));
+ subscriptions.push(commands.registerCommand('npm.openScript', this.openScript, this));
+ subscriptions.push(commands.registerCommand('npm.refresh', this.refresh, this));
+ }
+
+ private runScript(task: Task) {
+ if (!this.validator.scriptIsValid(task)) {
+ window.showErrorMessage(`Could not find script ${task.name}`);
+ return;
+ }
+ workspace.executeTask(task);
+ }
+
+ private async openScript(packageJSON: PackageJSON) {
+ let document: TextDocument = await workspace.openTextDocument(packageJSON.resourceUri!);
+ window.showTextDocument(document);
+ }
+
+ private refresh() {
+ this.taskTree = null;
+ this._onDidChangeTreeData.fire();
+ }
+
+ getTreeItem(element: TreeItem): TreeItem {
+ return element;
+ }
+
+ getParent(element: TreeItem): TreeItem | null {
+ if (element instanceof Folder) {
+ return null;
+ }
+ if (element instanceof PackageJSON) {
+ return element.folder;
+ }
+ if (element instanceof NpmScript) {
+ return element.package;
+ }
+ return null;
+ }
+
+ async getChildren(element?: TreeItem): Promise {
+ if (!this.taskTree) {
+ let tasks = await workspace.fetchTasks();
+ let npmTasks = tasks.filter(each => each.definition.type === 'npm');
+ this.taskTree = this.buildTaskTree(npmTasks);
+ }
+ if (element instanceof Folder) {
+ return element.packages;
+ }
+ if (element instanceof PackageJSON) {
+ return element.scripts;
+ }
+ if (element instanceof NpmScript) {
+ return [];
+ }
+ return this.taskTree;
+ }
+
+ private isWorkspaceFolder(value: any): value is WorkspaceFolder {
+ return value && typeof value !== 'number';
+ }
+
+ private buildTaskTree(tasks: Task[]): Folder[] | PackageJSON[] {
+ let folders: Map = new Map();
+ let packages: Map = new Map();
+
+ let folder = null;
+ let packageJson = null;
+
+ tasks.forEach(each => {
+ if (this.isWorkspaceFolder(each.scope)) {
+ folder = folders.get(each.scope.name);
+ if (!folder) {
+ folder = new Folder(each.scope);
+ folders.set(each.scope.name, folder);
+ }
+ let definition: NpmTaskDefinition = each.definition;
+ let path = definition.path ? definition.path : '';
+ packageJson = packages.get(path);
+ if (!packageJson) {
+ packageJson = new PackageJSON(folder, path);
+ folder.addPackage(packageJson);
+ packages.set(path, packageJson);
+ }
+ let script = new NpmScript(packageJson, each);
+ packageJson.addScript(script);
+ }
+ });
+ if (folders.size === 1) {
+ return [...packages.values()];
+ }
+ return [...folders.values()];
+ }
+}
\ No newline at end of file
diff --git a/extensions/npm/src/tasks.ts b/extensions/npm/src/tasks.ts
new file mode 100644
index 00000000000..24c4ba2e497
--- /dev/null
+++ b/extensions/npm/src/tasks.ts
@@ -0,0 +1,15 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { TaskDefinition, Task } from 'vscode';
+
+export interface NpmTaskDefinition extends TaskDefinition {
+ script: string;
+ path?: string;
+}
+
+export interface ScriptValidator {
+ scriptIsValid(task: Task): Promise;
+}
\ No newline at end of file