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