From 48b2d7a964b77517a432833085d1b9f2dd437f69 Mon Sep 17 00:00:00 2001 From: Dirk Baeumer Date: Fri, 15 Sep 2017 13:36:45 +0200 Subject: [PATCH] Make grunt auto detect multi folder aware --- extensions/grunt/package.json | 1 + extensions/grunt/src/main.ts | 402 ++++++++++++++++++++++------------ extensions/gulp/src/main.ts | 3 +- 3 files changed, 271 insertions(+), 135 deletions(-) diff --git a/extensions/grunt/package.json b/extensions/grunt/package.json index 0758d440b08..4c028b73312 100644 --- a/extensions/grunt/package.json +++ b/extensions/grunt/package.json @@ -31,6 +31,7 @@ "title": "Grunt", "properties": { "grunt.autoDetect": { + "scope": "resource", "type": "string", "enum": [ "off", diff --git a/extensions/grunt/src/main.ts b/extensions/grunt/src/main.ts index 554bb4963e6..a5e74585e8e 100644 --- a/extensions/grunt/src/main.ts +++ b/extensions/grunt/src/main.ts @@ -13,49 +13,6 @@ import * as nls from 'vscode-nls'; const localize = nls.config(process.env.VSCODE_NLS_CONFIG)(); type AutoDetect = 'on' | 'off'; -let taskProvider: vscode.Disposable | undefined; - -export function activate(_context: vscode.ExtensionContext): void { - let workspaceRoot = vscode.workspace.rootPath; - if (!workspaceRoot) { - return; - } - let pattern = path.join(workspaceRoot, '[Gg]runtfile.js'); - let detectorPromise: Thenable | undefined = undefined; - let fileWatcher = vscode.workspace.createFileSystemWatcher(pattern); - fileWatcher.onDidChange(() => detectorPromise = undefined); - fileWatcher.onDidCreate(() => detectorPromise = undefined); - fileWatcher.onDidDelete(() => detectorPromise = undefined); - - function onConfigurationChanged() { - let autoDetect = vscode.workspace.getConfiguration('grunt').get('autoDetect'); - if (taskProvider && autoDetect === 'off') { - detectorPromise = undefined; - taskProvider.dispose(); - taskProvider = undefined; - } else if (!taskProvider && autoDetect === 'on') { - taskProvider = vscode.workspace.registerTaskProvider('grunt', { - provideTasks: () => { - if (!detectorPromise) { - detectorPromise = getGruntTasks(); - } - return detectorPromise; - }, - resolveTask(_task: vscode.Task): vscode.Task | undefined { - return undefined; - } - }); - } - } - vscode.workspace.onDidChangeConfiguration(onConfigurationChanged); - onConfigurationChanged(); -} - -export function deactivate(): void { - if (taskProvider) { - taskProvider.dispose(); - } -} function exists(file: string): Promise { return new Promise((resolve, _reject) => { @@ -76,19 +33,6 @@ function exec(command: string, options: cp.ExecOptions): Promise<{ stdout: strin }); } -let _channel: vscode.OutputChannel; -function getOutputChannel(): vscode.OutputChannel { - if (!_channel) { - _channel = vscode.window.createOutputChannel('Grunt Auto Detection'); - } - return _channel; -} - -interface GruntTaskDefinition extends vscode.TaskDefinition { - task: string; - file?: string; -} - const buildNames: string[] = ['build', 'compile', 'watch']; function isBuildTask(name: string): boolean { for (let buildName of buildNames) { @@ -109,97 +53,287 @@ function isTestTask(name: string): boolean { return false; } -async function getGruntTasks(): Promise { - let workspaceRoot = vscode.workspace.rootPath; - let emptyTasks: vscode.Task[] = []; - if (!workspaceRoot) { - return emptyTasks; +let _channel: vscode.OutputChannel; +function getOutputChannel(): vscode.OutputChannel { + if (!_channel) { + _channel = vscode.window.createOutputChannel('Gulp Auto Detection'); } - if (!await exists(path.join(workspaceRoot, 'gruntfile.js')) && !await exists(path.join(workspaceRoot, 'Gruntfile.js'))) { - return emptyTasks; + return _channel; +} + +interface GruntTaskDefinition extends vscode.TaskDefinition { + task: string; + file?: string; +} + +class FolderDetector { + + private fileWatcher: vscode.FileSystemWatcher; + private promise: Thenable | undefined; + + constructor(private _workspaceFolder: vscode.WorkspaceFolder) { } - let command: string; - let platform = process.platform; - if (platform === 'win32' && await exists(path.join(workspaceRoot!, 'node_modules', '.bin', 'grunt.cmd'))) { - command = path.join('.', 'node_modules', '.bin', 'grunt.cmd'); - } else if ((platform === 'linux' || platform === 'darwin') && await exists(path.join(workspaceRoot!, 'node_modules', '.bin', 'grunt'))) { - command = path.join('.', 'node_modules', '.bin', 'grunt'); - } else { - command = 'grunt'; + public get workspaceFolder(): vscode.WorkspaceFolder { + return this._workspaceFolder; } - let commandLine = `${command} --help --no-color`; - try { - let { stdout, stderr } = await exec(commandLine, { cwd: workspaceRoot }); - if (stderr) { - getOutputChannel().appendLine(stderr); - getOutputChannel().show(true); + public isEnabled(): boolean { + return vscode.workspace.getConfiguration('grunt', this._workspaceFolder.uri).get('autoDetect') === 'on'; + } + + public start(): void { + let pattern = path.join(this._workspaceFolder.uri.fsPath, '[Gg]runtfile.js'); + this.fileWatcher = vscode.workspace.createFileSystemWatcher(pattern); + this.fileWatcher.onDidChange(() => this.promise = undefined); + this.fileWatcher.onDidCreate(() => this.promise = undefined); + this.fileWatcher.onDidDelete(() => this.promise = undefined); + } + + public async getTasks(): Promise { + if (!this.promise) { + this.promise = this.computeTasks(); } - let result: vscode.Task[] = []; - if (stdout) { - // grunt lists tasks as follows (description is wrapped into a new line if too long): - // ... - // Available tasks - // uglify Minify files with UglifyJS. * - // jshint Validate files with JSHint. * - // test Alias for "jshint", "qunit" tasks. - // default Alias for "jshint", "qunit", "concat", "uglify" tasks. - // long Alias for "eslint", "qunit", "browserify", "sass", - // "autoprefixer", "uglify", tasks. - // - // Tasks run in the order specified + return this.promise; + } - let lines = stdout.split(/\r{0,1}\n/); - let tasksStart = false; - let tasksEnd = false; - for (let line of lines) { - if (line.length === 0) { - continue; - } - if (!tasksStart && !tasksEnd) { - if (line.indexOf('Available tasks') === 0) { - tasksStart = true; + private async computeTasks(): Promise { + let rootPath = this._workspaceFolder.uri.scheme === 'file' ? this._workspaceFolder.uri.fsPath : undefined; + let emptyTasks: vscode.Task[] = []; + if (!rootPath) { + return emptyTasks; + } + if (!await exists(path.join(rootPath, 'gruntfile.js')) && !await exists(path.join(rootPath, 'Gruntfile.js'))) { + return emptyTasks; + } + + let command: string; + let platform = process.platform; + if (platform === 'win32' && await exists(path.join(rootPath!, 'node_modules', '.bin', 'grunt.cmd'))) { + command = path.join('.', 'node_modules', '.bin', 'grunt.cmd'); + } else if ((platform === 'linux' || platform === 'darwin') && await exists(path.join(rootPath!, 'node_modules', '.bin', 'grunt'))) { + command = path.join('.', 'node_modules', '.bin', 'grunt'); + } else { + command = 'grunt'; + } + + let commandLine = `${command} --help --no-color`; + try { + let { stdout, stderr } = await exec(commandLine, { cwd: rootPath }); + if (stderr) { + getOutputChannel().appendLine(stderr); + getOutputChannel().show(true); + } + let result: vscode.Task[] = []; + if (stdout) { + // grunt lists tasks as follows (description is wrapped into a new line if too long): + // ... + // Available tasks + // uglify Minify files with UglifyJS. * + // jshint Validate files with JSHint. * + // test Alias for "jshint", "qunit" tasks. + // default Alias for "jshint", "qunit", "concat", "uglify" tasks. + // long Alias for "eslint", "qunit", "browserify", "sass", + // "autoprefixer", "uglify", tasks. + // + // Tasks run in the order specified + + let lines = stdout.split(/\r{0,1}\n/); + let tasksStart = false; + let tasksEnd = false; + for (let line of lines) { + if (line.length === 0) { + continue; } - } else if (tasksStart && !tasksEnd) { - if (line.indexOf('Tasks run in the order specified') === 0) { - tasksEnd = true; - } else { - let regExp = /^\s*(\S.*\S) \S/g; - let matches = regExp.exec(line); - if (matches && matches.length === 2) { - let name = matches[1]; - let kind: GruntTaskDefinition = { - type: 'grunt', - task: name - }; - let source = 'grunt'; - let task = name.indexOf(' ') === -1 - ? new vscode.Task(kind, name, source, new vscode.ShellExecution(`${command} ${name}`)) - : new vscode.Task(kind, name, source, new vscode.ShellExecution(`${command} "${name}"`)); - result.push(task); - let lowerCaseTaskName = name.toLowerCase(); - if (isBuildTask(lowerCaseTaskName)) { - task.group = vscode.TaskGroup.Build; - } else if (isTestTask(lowerCaseTaskName)) { - task.group = vscode.TaskGroup.Test; + if (!tasksStart && !tasksEnd) { + if (line.indexOf('Available tasks') === 0) { + tasksStart = true; + } + } else if (tasksStart && !tasksEnd) { + if (line.indexOf('Tasks run in the order specified') === 0) { + tasksEnd = true; + } else { + let regExp = /^\s*(\S.*\S) \S/g; + let matches = regExp.exec(line); + if (matches && matches.length === 2) { + let name = matches[1]; + let kind: GruntTaskDefinition = { + type: 'grunt', + task: name + }; + let source = 'grunt'; + let options: vscode.ShellExecutionOptions = { cwd: this.workspaceFolder.uri.fsPath }; + let task = name.indexOf(' ') === -1 + ? new vscode.Task(kind, this.workspaceFolder, name, source, new vscode.ShellExecution(`${command} ${name}`, options)) + : new vscode.Task(kind, this.workspaceFolder, name, source, new vscode.ShellExecution(`${command} "${name}"`, options)); + result.push(task); + let lowerCaseTaskName = name.toLowerCase(); + if (isBuildTask(lowerCaseTaskName)) { + task.group = vscode.TaskGroup.Build; + } else if (isTestTask(lowerCaseTaskName)) { + task.group = vscode.TaskGroup.Test; + } } } } } } + return result; + } catch (err) { + let channel = getOutputChannel(); + if (err.stderr) { + channel.appendLine(err.stderr); + } + if (err.stdout) { + channel.appendLine(err.stdout); + } + channel.appendLine(localize('execFailed', 'Auto detecting Grunt failed with error: {0}', err.error ? err.error.toString() : 'unknown')); + channel.show(true); + return emptyTasks; } - return result; - } catch (err) { - let channel = getOutputChannel(); - if (err.stderr) { - channel.appendLine(err.stderr); - } - if (err.stdout) { - channel.appendLine(err.stdout); - } - channel.appendLine(localize('execFailed', 'Auto detecting Grunt failed with error: {0}', err.error ? err.error.toString() : 'unknown')); - channel.show(true); - return emptyTasks; } + + public dispose() { + this.promise = undefined; + if (this.fileWatcher) { + this.fileWatcher.dispose(); + } + } +} + +class TaskDetector { + + private taskProvider: vscode.Disposable | undefined; + private detectors: Map = new Map(); + private promise: Promise | undefined; + + constructor() { + } + + public start(): void { + let folders = vscode.workspace.workspaceFolders; + if (folders) { + this.updateWorkspaceFolders(folders, []); + } + vscode.workspace.onDidChangeWorkspaceFolders((event) => this.updateWorkspaceFolders(event.added, event.removed)); + vscode.workspace.onDidChangeConfiguration(this.updateConfiguration, this); + } + + public dispose(): void { + if (this.taskProvider) { + this.taskProvider.dispose(); + this.taskProvider = undefined; + } + this.detectors.clear(); + this.promise = undefined; + } + + private updateWorkspaceFolders(added: vscode.WorkspaceFolder[], removed: vscode.WorkspaceFolder[]): void { + let changed = false; + for (let remove of removed) { + let detector = this.detectors.get(remove.uri.toString()); + if (detector) { + changed = true; + detector.dispose(); + this.detectors.delete(remove.uri.toString()); + } + } + for (let add of added) { + let detector = new FolderDetector(add); + if (detector.isEnabled()) { + changed = true; + this.detectors.set(add.uri.toString(), detector); + detector.start(); + } + } + if (changed) { + this.promise = undefined; + } + this.updateProvider(); + } + + private updateConfiguration(): void { + let changed = false; + for (let detector of this.detectors.values()) { + if (!detector.isEnabled()) { + changed = true; + detector.dispose(); + this.detectors.delete(detector.workspaceFolder.uri.toString()); + } + } + let folders = vscode.workspace.workspaceFolders; + if (folders) { + for (let folder of folders) { + if (!this.detectors.has(folder.uri.toString())) { + let detector = new FolderDetector(folder); + if (detector.isEnabled()) { + changed = true; + this.detectors.set(folder.uri.toString(), detector); + detector.start(); + } + } + } + } + if (changed) { + this.promise = undefined; + } + this.updateProvider(); + } + + private updateProvider(): void { + if (!this.taskProvider && this.detectors.size > 0) { + this.taskProvider = vscode.workspace.registerTaskProvider('gulp', { + provideTasks: () => { + return this.getTasks(); + }, + resolveTask(_task: vscode.Task): vscode.Task | undefined { + return undefined; + } + }); + } + else if (this.taskProvider && this.detectors.size === 0) { + this.taskProvider.dispose(); + this.taskProvider = undefined; + this.promise = undefined; + } + } + + public getTasks(): Promise { + if (!this.promise) { + this.promise = this.computeTasks(); + } + return this.promise; + } + + private computeTasks(): Promise { + if (this.detectors.size === 0) { + return Promise.resolve([]); + } else if (this.detectors.size === 1) { + return this.detectors.values().next().value.getTasks(); + } else { + let promises: Promise[] = []; + for (let detector of this.detectors.values()) { + promises.push(detector.getTasks().then((value) => value, () => [])); + } + return Promise.all(promises).then((values) => { + let result: vscode.Task[] = []; + for (let tasks of values) { + if (tasks && tasks.length > 0) { + result.push(...tasks); + } + } + return result; + }); + } + } +} + +let detector: TaskDetector; +export function activate(_context: vscode.ExtensionContext): void { + detector = new TaskDetector(); + detector.start(); +} + +export function deactivate(): void { + detector.dispose(); } \ No newline at end of file diff --git a/extensions/gulp/src/main.ts b/extensions/gulp/src/main.ts index 28240a29f18..127e07afd64 100644 --- a/extensions/gulp/src/main.ts +++ b/extensions/gulp/src/main.ts @@ -139,7 +139,8 @@ class FolderDetector { type: 'gulp', task: line }; - let task = new vscode.Task(kind, this.workspaceFolder, line, 'gulp', new vscode.ShellExecution(`${gulpCommand} ${line}`)); + let options: vscode.ShellExecutionOptions = { cwd: this.workspaceFolder.uri.fsPath }; + let task = new vscode.Task(kind, this.workspaceFolder, line, 'gulp', new vscode.ShellExecution(`${gulpCommand} ${line}`, options)); result.push(task); let lowerCaseLine = line.toLowerCase(); if (isBuildTask(lowerCaseLine)) {