提交 ef2b0a4b 编写于 作者: D Dirk Baeumer

Fixes #24073: Improve task API and adopt gulp extension to it.

上级 0bcc6fc5
......@@ -26,5 +26,23 @@
"onCommand:workbench.action.tasks.runTask",
"onCommand:workbench.action.tasks.build",
"onCommand:workbench.action.tasks.test"
]
],
"contributes": {
"configuration": {
"id": "gulp",
"type": "object",
"title": "Gulp",
"properties": {
"gulp.autoDetect": {
"type": "string",
"enum": [
"off",
"on"
],
"default": "on",
"description": "%config.gulp.autoDetect%"
}
}
}
}
}
\ No newline at end of file
{
"config.gulp.autoDetect": "Controls whether auto detection of gulp tasks in on or off. Default is on."
}
\ No newline at end of file
......@@ -8,93 +8,138 @@ import * as path from 'path';
import * as fs from 'fs';
import * as cp from 'child_process';
import * as vscode from 'vscode';
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 gulpfile = path.join(workspaceRoot, 'gulpfile.js');
let gulpPromise: Thenable<vscode.TaskSet> | undefined = undefined;
let fileWatcher = vscode.workspace.createFileSystemWatcher(gulpfile);
let pattern = path.join(workspaceRoot, 'gulpfile{.babel.js,.js}');
let gulpPromise: Thenable<vscode.Task[]> | undefined = undefined;
let fileWatcher = vscode.workspace.createFileSystemWatcher(pattern);
fileWatcher.onDidChange(() => gulpPromise = undefined);
fileWatcher.onDidCreate(() => gulpPromise = undefined);
fileWatcher.onDidDelete(() => gulpPromise = undefined);
vscode.workspace.registerTaskProvider({
provideTasks: () => {
if (!gulpPromise) {
gulpPromise = getGulpTasks();
}
return gulpPromise;
function onConfigurationChanged() {
let autoDetect = vscode.workspace.getConfiguration('gulp').get<AutoDetect>('autoDetect');
if (taskProvider && autoDetect === 'off') {
gulpPromise = undefined;
taskProvider.dispose();
taskProvider = undefined;
} else if (!taskProvider && autoDetect === 'on') {
taskProvider = vscode.workspace.registerTaskProvider({
provideTasks: () => {
if (!gulpPromise) {
gulpPromise = getGulpTasks();
}
return gulpPromise;
}
});
}
}
vscode.workspace.onDidChangeConfiguration(onConfigurationChanged);
onConfigurationChanged();
}
export function deactivate(): void {
if (taskProvider) {
taskProvider.dispose();
}
}
function exists(file: string): Promise<boolean> {
return new Promise<boolean>((resolve, _reject) => {
fs.exists(file, (value) => {
resolve(value);
});
});
}
function getGulpTasks(): Thenable<vscode.TaskSet> {
return new Promise<vscode.TaskSet>((resolve, _reject) => {
let workspaceRoot = vscode.workspace.rootPath;
let emptyTaskSet = { tasks: [] };
if (!workspaceRoot) {
return resolve(emptyTaskSet);
}
let gulpfile = path.join(workspaceRoot, 'gulpfile.js');
fs.exists(gulpfile, (value) => {
if (!value) {
resolve(emptyTaskSet);
return;
function exec(command: string, options: cp.ExecOptions): Promise<{ stdout: string; stderr: string }> {
return new Promise<{ stdout: string; stderr: string }>((resolve, reject) => {
cp.exec(command, options, (error, stdout, stderr) => {
if (error) {
reject({ error, stdout, stderr });
}
resolve({ stdout, stderr });
});
});
}
let commandLine: string;
let platform = process.platform;
if (platform === 'win32' && fs.existsSync(path.join(workspaceRoot!, 'node_modules', '.bin', 'gulp.cmd'))) {
commandLine = `${path.join('.', 'node_modules', '.bin', 'gulp.cmd')} --tasks-simple --no-color`;
} else if ((platform === 'linux' || platform === 'darwin') && fs.existsSync(path.join(workspaceRoot!, 'node_modules', '.bin', 'gulp.cmd'))) {
commandLine = `${path.join('.', 'node_modules', '.bin', 'gulp')} --tasks-simple --no-color`;
} else {
commandLine = 'gulp --tasks-simple --no-color';
}
cp.exec(commandLine, { cwd: workspaceRoot }, (error, stdout, stderr) => {
let channel = vscode.window.createOutputChannel('tasks');
if (stderr) {
channel.appendLine(stderr);
}
if (error) {
channel.appendLine(`Auto detecting gulp failed with error: ${error ? error.toString() : 'unknown'}`);
resolve(emptyTaskSet);
return;
async function getGulpTasks(): Promise<vscode.Task[]> {
let workspaceRoot = vscode.workspace.rootPath;
let emptyTasks: vscode.Task[] = [];
if (!workspaceRoot) {
return emptyTasks;
}
let gulpfile = path.join(workspaceRoot, 'gulpfile.js');
if (!await exists(gulpfile)) {
gulpfile = path.join(workspaceRoot, 'gulpfile.babel.js');
if (! await exists(gulpfile)) {
return emptyTasks;
}
}
let gulpCommand: string;
let platform = process.platform;
if (platform === 'win32' && await exists(path.join(workspaceRoot!, 'node_modules', '.bin', 'gulp.cmd'))) {
gulpCommand = path.join('.', 'node_modules', '.bin', 'gulp.cmd');
} else if ((platform === 'linux' || platform === 'darwin') && await exists(path.join(workspaceRoot!, 'node_modules', '.bin', 'gulp'))) {
gulpCommand = path.join('.', 'node_modules', '.bin', 'gulp');
} else {
gulpCommand = 'gulp';
}
let commandLine = `${gulpCommand} --tasks-simple --no-color`;
let channel = vscode.window.createOutputChannel('tasks');
try {
let { stdout, stderr } = await exec(commandLine, { cwd: workspaceRoot });
if (stderr) {
channel.appendLine(stderr);
}
let result: vscode.Task[] = [];
if (stdout) {
let buildTask: { task: vscode.Task | undefined, rank: number } = { task: undefined, rank: 0 };
let testTask: { task: vscode.Task | undefined, rank: number } = { task: undefined, rank: 0 };
let lines = stdout.split(/\r{0,1}\n/);
for (let line of lines) {
if (line.length === 0) {
continue;
}
let result: vscode.TaskSet = { tasks: [], buildTasks: [], testTasks: [] };
if (stdout) {
let buildTask: { id: string | undefined, rank: number } = { id: undefined, rank: 0 };
let testTask: { id: string | undefined, rank: number } = { id: undefined, rank: 0 };
let lines = stdout.split(/\r{0,1}\n/);
for (let line of lines) {
if (line.length === 0) {
continue;
}
let task = new vscode.ShellTask(`gulp ${line}`, `gulp ${line}`);
result.tasks.push(task);
let lowerCaseLine = line.toLowerCase();
if (lowerCaseLine === 'build') {
buildTask = { id: line, rank: 2 };
} else if (lowerCaseLine.indexOf('build') !== -1 && buildTask.rank < 1) {
buildTask = { id: line, rank: 1 };
} else if (lowerCaseLine === 'test') {
testTask = { id: line, rank: 2 };
} else if (lowerCaseLine.indexOf('test') !== -1 && testTask.rank < 1) {
testTask = { id: line, rank: 1 };
}
}
if (buildTask.id) {
result.buildTasks!.push(buildTask.id);
}
if (testTask.id) {
result.testTasks!.push(testTask.id);
}
let task = new vscode.ShellTask(`gulp: ${line}`, `${gulpCommand} ${line}`);
task.identifier = `gulp.${line}`;
result.push(task);
let lowerCaseLine = line.toLowerCase();
if (lowerCaseLine === 'build') {
buildTask = { task, rank: 2 };
} else if (lowerCaseLine.indexOf('build') !== -1 && buildTask.rank < 1) {
buildTask = { task, rank: 1 };
} else if (lowerCaseLine === 'test') {
testTask = { task, rank: 2 };
} else if (lowerCaseLine.indexOf('test') !== -1 && testTask.rank < 1) {
testTask = { task, rank: 1 };
}
resolve(result);
});
});
});
}
if (buildTask.task) {
buildTask.task.group = vscode.TaskGroup.Build;
}
if (testTask.task) {
testTask.task.group = vscode.TaskGroup.Test;
}
}
return result;
} catch (err) {
if (err.stderr) {
channel.appendLine(err.stderr);
}
channel.appendLine(localize('execFailed', 'Auto detecting gulp failed with error: {0}', err.error ? err.error.toString() : 'unknown'));
return emptyTasks;
}
}
\ No newline at end of file
......@@ -3,8 +3,7 @@
"target": "es6",
"module": "commonjs",
"lib": [
"es6",
"es2015.promise"
"es2016"
],
"outDir": "./out",
"strictNullChecks": true,
......
......@@ -267,6 +267,30 @@ declare module 'vscode' {
env?: { [key: string]: string };
}
export namespace TaskGroup {
/**
* The clean task group
*/
export const Clean: 'clean';
/**
* The build task group
*/
export const Build: 'build';
/**
* The rebuild all task group
*/
export const RebuildAll: 'rebuildAll';
/**
* The test task group
*/
export const Test: 'test';
}
/**
* The supported task groups.
*/
export type TaskGroup = 'clean' | 'build' | 'rebuildAll' | 'test';
/**
* A task that starts an external process.
*/
......@@ -328,6 +352,12 @@ declare module 'vscode' {
*/
args: string[];
/**
* The task group this tasks belongs to. Defaults to undefined meaning
* that the task doesn't belong to any special group.
*/
group?: TaskGroup;
/**
* The process options used when the process is executed.
* Defaults to an empty object literal.
......@@ -374,7 +404,7 @@ declare module 'vscode' {
* The current working directory of the executed shell.
* If omitted VSCode's current workspace root is used.
*/
cwd?: string;
cwd: string;
/**
* The additional environment of the executed shell. If omitted
......@@ -382,6 +412,19 @@ declare module 'vscode' {
* the parent process' environment.
*/
env?: { [key: string]: string };
} | {
/**
* The current working directory of the executed shell.
* If omitted VSCode's current workspace root is used.
*/
cwd?: string;
/**
* The additional environment of the executed shell. If omitted
* the parent process' environment is used. If provided it is merged with
* the parent process' environment.
*/
env: { [key: string]: string };
};
/**
......@@ -429,6 +472,12 @@ declare module 'vscode' {
*/
readonly commandLine: string;
/**
* The task group this tasks belongs to. Defaults to undefined meaning
* that the task doesn't belong to any special group.
*/
group?: TaskGroup;
/**
* The shell options used when the shell is executed. Defaults to an
* empty object literal.
......@@ -449,29 +498,6 @@ declare module 'vscode' {
export type Task = ProcessTask | ShellTask;
/**
* Result return from a task provider.
*/
export interface TaskSet {
/**
* The actual tasks returned.
*/
tasks: Task[];
/**
* The build tasks provided. Tasks must be identified using
* `Task.identifier`
*/
buildTasks?: string[];
/**
* The test tasks provided. Tasks must be identified using
* `Task.identifier`
*/
testTasks?: string[];
}
/**
* A task provider allows to add tasks to the task service.
* A task provider is registerd via #workspace.registerTaskProvider.
......@@ -482,7 +508,7 @@ declare module 'vscode' {
* @param token A cancellation token.
* @return a #TaskSet
*/
provideTasks(token: CancellationToken): ProviderResult<TaskSet>;
provideTasks(token: CancellationToken): ProviderResult<Task[]>;
}
export namespace workspace {
......
......@@ -529,6 +529,7 @@ export function createApiFactory(
FileLocationKind: extHostTypes.FileLocationKind,
ApplyToKind: extHostTypes.ApplyToKind,
RevealKind: extHostTypes.RevealKind,
TaskGroup: extHostTypes.TaskGroup,
ShellTask: extHostTypes.ShellTask,
ProcessTask: extHostTypes.ProcessTask
};
......
......@@ -309,6 +309,7 @@ namespace Tasks {
_id: uuidMap.getUUID(task.identifier),
name: task.name,
identifier: task.identifier,
group: types.TaskGroup.is(task.group) ? task.group : undefined,
command: command,
showOutput: behaviour.showOutput,
isBackground: !!task.isBackground,
......@@ -381,57 +382,6 @@ class UUIDMap {
}
}
namespace TaskSet {
const idMaps: Map<string, UUIDMap> = new Map<string, UUIDMap>();
function getUUIDMap(extensionId: string): UUIDMap {
let result = idMaps.get(extensionId);
if (result) {
return result;
}
result = new UUIDMap();
idMaps.set(extensionId, result);
return result;
}
export function from(extension: IExtensionDescription, value: vscode.TaskSet): TaskSystem.TaskSet {
if (value === void 0 || value === null || !Array.isArray(value.tasks)) {
return { tasks: Object.create(null) };
}
let tasks = Tasks.from(value.tasks, getUUIDMap(extension.id));
let buildTasks: string[];
let testTasks: string[];
if (value.buildTasks || value.testTasks) {
let map: Map<string, TaskSystem.Task> = new Map<string, TaskSystem.Task>();
tasks.forEach(task => map.set(task.identifier, task));
if (Array.isArray(value.buildTasks)) {
buildTasks = [];
for (let elem of value.buildTasks) {
if (typeof elem === 'string' && map.has(elem)) {
buildTasks.push(map.get(elem)._id);
}
}
}
if (Array.isArray(value.testTasks)) {
testTasks = [];
for (let elem of value.testTasks) {
if (typeof elem === 'string' && map.has(elem)) {
testTasks.push(map.get(elem)._id);
}
}
}
}
return {
tasks,
buildTasks,
testTasks
};
}
}
interface HandlerData {
provider: vscode.TaskProvider;
extension: IExtensionDescription;
......@@ -442,12 +392,14 @@ export class ExtHostTask extends ExtHostTaskShape {
private _proxy: MainThreadTaskShape;
private _handleCounter: number;
private _handlers: Map<number, HandlerData>;
private _idMaps: Map<string, UUIDMap>;
constructor(threadService: IThreadService) {
super();
this._proxy = threadService.get(MainContext.MainThreadTask);
this._handleCounter = 0;
this._handlers = new Map<number, HandlerData>();
this._idMaps = new Map<string, UUIDMap>();
};
public registerTaskProvider(extension: IExtensionDescription, provider: vscode.TaskProvider): vscode.Disposable {
......@@ -469,11 +421,24 @@ export class ExtHostTask extends ExtHostTaskShape {
return TPromise.wrapError<TaskSystem.TaskSet>(new Error('no handler found'));
}
return asWinJsPromise(token => handler.provider.provideTasks(token)).then(value => {
return TaskSet.from(handler.extension, value);
return {
tasks: Tasks.from(value, this.getUUIDMap(handler.extension.id)),
extension: handler.extension
};
});
}
private nextHandle(): number {
return this._handleCounter++;
}
private getUUIDMap(extensionId: string): UUIDMap {
let result = this._idMaps.get(extensionId);
if (result) {
return result;
}
result = new UUIDMap();
this._idMaps.set(extensionId, result);
return result;
}
}
\ No newline at end of file
......@@ -1033,9 +1033,12 @@ export class BaseTask {
}
set identifier(value: string) {
if (typeof name !== 'string') {
if (typeof value !== 'string') {
throw illegalArgument('identifier');
}
if (value.indexOf(':') !== -1) {
throw illegalArgument('identifier must not contain \':\'');
}
this._identifier = value;
}
......@@ -1084,11 +1087,37 @@ namespace ProblemMatcher {
}
}
export namespace TaskGroup {
/**
* The clean task group
*/
export const Clean: 'clean' = 'clean';
/**
* The build task group
*/
export const Build: 'build' = 'build';
/**
* The rebuild all task group
*/
export const RebuildAll: 'rebuildAll' = 'rebuildAll';
/**
* The test task group
*/
export const Test: 'test' = 'test';
export function is(value: string): value is vscode.TaskGroup {
return value === Clean || value === Build || value === RebuildAll || value === Test;
}
}
export class ProcessTask extends BaseTask {
private _process: string;
private _args: string[];
private _group: vscode.TaskGroup;
private _options: vscode.ProcessOptions;
private static parseArguments(restArgs: any[]): { args: string[]; options: vscode.ProcessOptions; problemMatchers: vscode.ProblemMatcher[] } {
......@@ -1145,6 +1174,17 @@ export class ProcessTask extends BaseTask {
this._args = value;
}
get group(): vscode.TaskGroup {
return this._group;
}
set group(value: vscode.TaskGroup) {
if (!TaskGroup.is(value)) {
throw illegalArgument('group');
}
this._group = value;
}
get options(): vscode.ProcessOptions {
return this._options;
}
......@@ -1160,6 +1200,7 @@ export class ProcessTask extends BaseTask {
export class ShellTask extends BaseTask {
private _commandLine: string;
private _group: vscode.TaskGroup;
private _options: vscode.ShellOptions;
private static parseArguments(restArgs: any[]): { options: vscode.ShellOptions; problemMatchers: vscode.ProblemMatcher[] } {
......@@ -1197,6 +1238,17 @@ export class ShellTask extends BaseTask {
return this._commandLine;
}
get group(): vscode.TaskGroup {
return this._group;
}
set group(value: vscode.TaskGroup) {
if (!TaskGroup.is(value)) {
throw illegalArgument('group');
}
this._group = value;
}
get options(): vscode.ShellOptions {
return this._options;
}
......
......@@ -286,12 +286,6 @@ function mergeProperty<T, K extends keyof T>(target: T, source: T, key: K) {
}
}
function fillProperty<T, K extends keyof T>(target: T, source: T, key: K) {
if (target[key] === void 0 && source[key] !== void 0) {
target[key] = source[key];
}
}
interface ParseContext {
problemReporter: IProblemReporter;
namedProblemMatchers: IStringDictionary<NamedProblemMatcher>;
......@@ -639,23 +633,13 @@ namespace ProblemMatcherConverter {
namespace TaskDescription {
export interface TaskConfiguration {
tasks: Tasks.Task[];
buildTask?: string;
testTask?: string;
}
export function isEmpty(value: TaskConfiguration): boolean {
return !value || !value.tasks || Object.keys(value.tasks).length === 0;
}
export function from(this: void, tasks: TaskDescription[], globals: Globals, context: ParseContext): TaskConfiguration {
export function from(this: void, tasks: TaskDescription[], globals: Globals, context: ParseContext): Tasks.Task[] {
if (!tasks) {
return undefined;
}
let parsedTasks: Tasks.Task[] = [];
let defaultBuildTask: { id: string; exact: number; } = { id: null, exact: -1 };
let defaultTestTask: { id: string; exact: number; } = { id: null, exact: -1 };
let defaultBuildTask: { task: Tasks.Task; rank: number; } = { task: null, rank: -1 };
let defaultTestTask: { task: Tasks.Task; rank: number; } = { task: null, rank: -1 };
tasks.forEach((externalTask) => {
let taskName = externalTask.taskName;
if (!taskName) {
......@@ -733,68 +717,57 @@ namespace TaskDescription {
}
if (addTask) {
parsedTasks.push(task);
if (!Types.isUndefined(externalTask.isBuildCommand) && externalTask.isBuildCommand && defaultBuildTask.exact < 2) {
defaultBuildTask.id = task._id;
defaultBuildTask.exact = 2;
} else if (taskName === 'build' && defaultBuildTask.exact < 2) {
defaultBuildTask.id = task._id;
defaultBuildTask.exact = 1;
if (!Types.isUndefined(externalTask.isBuildCommand) && externalTask.isBuildCommand && defaultBuildTask.rank < 2) {
defaultBuildTask.task = task;
defaultBuildTask.rank = 2;
} else if (taskName === 'build' && defaultBuildTask.rank < 2) {
defaultBuildTask.task = task;
defaultBuildTask.rank = 1;
}
if (!Types.isUndefined(externalTask.isTestCommand) && externalTask.isTestCommand && defaultTestTask.exact < 2) {
defaultTestTask.id = task._id;
defaultTestTask.exact = 2;
} else if (taskName === 'test' && defaultTestTask.exact < 2) {
defaultTestTask.id = task._id;
defaultTestTask.exact = 1;
if (!Types.isUndefined(externalTask.isTestCommand) && externalTask.isTestCommand && defaultTestTask.rank < 2) {
defaultTestTask.task = task;
defaultTestTask.rank = 2;
} else if (taskName === 'test' && defaultTestTask.rank < 2) {
defaultTestTask.task = task;
defaultTestTask.rank = 1;
}
}
});
let buildTask: string;
if (defaultBuildTask.exact > 0) {
buildTask = defaultBuildTask.id;
if (defaultBuildTask.task) {
defaultBuildTask.task.group = Tasks.TaskGroup.Build;
}
let testTask: string;
if (defaultTestTask.exact > 0) {
testTask = defaultTestTask.id;
if (defaultTestTask.task) {
defaultTestTask.task.group = Tasks.TaskGroup.Test;
}
let result = { tasks: parsedTasks, buildTask, testTask };
return isEmpty(result) ? undefined : result;
return parsedTasks.length === 0 ? undefined : parsedTasks;
}
export function merge(target: TaskConfiguration, source: TaskConfiguration): TaskConfiguration {
if (isEmpty(source)) {
export function merge(target: Tasks.Task[], source: Tasks.Task[]): Tasks.Task[] {
if (source === void 0 || source.length === 0) {
return target;
}
if (isEmpty(target)) {
if (target === void 0 || target.length === 0) {
return source;
}
if (source.tasks) {
if (source) {
// Tasks are keyed by ID but we need to merge by name
let targetNames: IStringDictionary<string> = Object.create(null);
Object.keys(target.tasks).forEach(key => {
let task = target.tasks[key];
targetNames[task.name] = task.id;
let map: IStringDictionary<Tasks.Task> = Object.create(null);
target.forEach((task) => {
map[task.name] = task;
});
let sourceNames: IStringDictionary<string> = Object.create(null);
Object.keys(source.tasks).forEach(key => {
let task = source.tasks[key];
sourceNames[task.name] = task.id;
source.forEach((task) => {
map[task.name] = task;
});
Object.keys(sourceNames).forEach(taskName => {
let targetId = targetNames[taskName];
let sourceId = sourceNames[taskName];
// Same name exists globally
if (targetId) {
delete target.tasks[targetId];
}
target.tasks[sourceId] = source.tasks[sourceId];
let newTarget: Tasks.Task[] = [];
target.forEach(task => {
newTarget.push(map[task.name]);
delete map[task.name];
});
Object.keys(map).forEach(key => newTarget.push(map[key]));
target = newTarget;
}
fillProperty(target, source, 'buildTask');
fillProperty(target, source, 'testTask');
return target;
}
......@@ -976,7 +949,7 @@ export namespace ExecutionEngine {
export interface ParseResult {
validationStatus: ValidationStatus;
taskSet: Tasks.TaskSet;
tasks: Tasks.Task[];
engine: Tasks.ExecutionEngine;
}
......@@ -1000,18 +973,18 @@ class ConfigurationParser {
let context: ParseContext = { problemReporter: this.problemReporter, namedProblemMatchers: undefined, isTermnial: engine === Tasks.ExecutionEngine.Terminal };
return {
validationStatus: this.problemReporter.status,
taskSet: this.createTaskRunnerConfiguration(fileConfig, context),
tasks: this.createTaskRunnerConfiguration(fileConfig, context),
engine
};
}
private createTaskRunnerConfiguration(fileConfig: ExternalTaskRunnerConfiguration, context: ParseContext): Tasks.TaskSet {
private createTaskRunnerConfiguration(fileConfig: ExternalTaskRunnerConfiguration, context: ParseContext): Tasks.Task[] {
let globals = Globals.from(fileConfig, context);
if (this.problemReporter.status.isFatal()) {
return undefined;
}
context.namedProblemMatchers = ProblemMatcherConverter.namedFrom(fileConfig.declares, context);
let globalTasks: TaskDescription.TaskConfiguration;
let globalTasks: Tasks.Task[];
if (fileConfig.windows && Platform.platform === Platform.Platform.Windows) {
globalTasks = TaskDescription.from(fileConfig.windows.tasks, globals, context);
} else if (fileConfig.osx && Platform.platform === Platform.Platform.Mac) {
......@@ -1020,15 +993,13 @@ class ConfigurationParser {
globalTasks = TaskDescription.from(fileConfig.linux.tasks, globals, context);
}
let taskConfig: TaskDescription.TaskConfiguration;
let tasks: Tasks.Task[];
if (fileConfig.tasks) {
taskConfig = TaskDescription.from(fileConfig.tasks, globals, context);
tasks = TaskDescription.from(fileConfig.tasks, globals, context);
}
taskConfig = TaskDescription.merge(taskConfig, globalTasks);
tasks = TaskDescription.merge(tasks, globalTasks);
if (TaskDescription.isEmpty(taskConfig)) {
let tasks: Tasks.Task[] = [];
let buildTask: string;
if (!tasks || tasks.length === 0) {
if (globals.command && globals.command.name) {
let matchers: ProblemMatcher[] = ProblemMatcherConverter.from(fileConfig.problemMatcher, context);;
let isBackground = fileConfig.isBackground ? !!fileConfig.isBackground : fileConfig.isWatching ? !!fileConfig.isWatching : undefined;
......@@ -1036,6 +1007,7 @@ class ConfigurationParser {
_id: UUID.generateUuid(),
name: globals.command.name,
identifier: UUID.generateUuid(),
group: Tasks.TaskGroup.Build,
command: undefined,
isBackground: isBackground,
showOutput: undefined,
......@@ -1044,21 +1016,10 @@ class ConfigurationParser {
};
TaskDescription.mergeGlobals(task, globals);
TaskDescription.fillDefaults(task);
tasks.push(task);
buildTask = task._id;
tasks = [task];
}
taskConfig = {
tasks: tasks,
buildTask
};
}
return {
tasks: taskConfig.tasks,
buildTasks: taskConfig.buildTask ? [taskConfig.buildTask] : [],
testTasks: taskConfig.testTask ? [taskConfig.testTask] : []
};
return tasks || [];
}
}
......
......@@ -6,6 +6,7 @@
import * as Types from 'vs/base/common/types';
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { ProblemMatcher } from 'vs/platform/markers/common/problemMatcher';
export interface CommandOptions {
......@@ -93,6 +94,22 @@ export namespace ShowOutput {
}
}
export namespace TaskGroup {
export const Clean: 'clean' = 'clean';
export const Build: 'build' = 'build';
export const RebuildAll: 'rebuildAll' = 'rebuildAll';
export const Test: 'test' = 'test';
export function is(value: string): value is TaskGroup {
return value === Clean || value === Build || value === RebuildAll || value === Test;
}
}
export type TaskGroup = 'clean' | 'build' | 'rebuildAll' | 'test';
/**
* A task description
*/
......@@ -113,6 +130,11 @@ export interface Task {
*/
identifier: string;
/**
* the task's group;
*/
group?: TaskGroup;
/**
* The command configuration
*/
......@@ -156,28 +178,13 @@ export interface Task {
problemMatchers?: ProblemMatcher[];
}
/**
* Describes a task set.
*/
export interface TaskSet {
/**
* The inferred build tasks
*/
buildTasks?: string[];
/**
* The inferred test tasks;
*/
testTasks?: string[];
/**
* The configured tasks
*/
tasks: Task[];
}
export enum ExecutionEngine {
Unknown = 0,
Terminal = 1,
Process = 2
}
export interface TaskSet {
tasks: Task[];
extension?: IExtensionDescription;
}
\ No newline at end of file
......@@ -69,7 +69,7 @@ import { IOutputService, IOutputChannelRegistry, Extensions as OutputExt, IOutpu
import { ITerminalService } from 'vs/workbench/parts/terminal/common/terminal';
import { ITaskSystem, ITaskResolver, ITaskSummary, ITaskExecuteResult, TaskExecuteKind, TaskError, TaskErrors, TaskSystemEvents } from 'vs/workbench/parts/tasks/common/taskSystem';
import { Task, TaskSet, ExecutionEngine, ShowOutput } from 'vs/workbench/parts/tasks/common/tasks';
import { Task, TaskSet, TaskGroup, ExecutionEngine, ShowOutput } from 'vs/workbench/parts/tasks/common/tasks';
import { ITaskService, TaskServiceEvents, ITaskProvider } from 'vs/workbench/parts/tasks/common/taskService';
import { templates as taskTemplates } from 'vs/workbench/parts/tasks/common/taskTemplates';
......@@ -467,7 +467,7 @@ class ProblemReporter implements TaskConfig.IProblemReporter {
}
interface WorkspaceTaskResult {
taskSet: TaskSet;
set: TaskSet;
hasErrors: boolean;
}
......@@ -646,7 +646,7 @@ class TaskService extends EventEmitter implements ITaskService {
public build(): TPromise<ITaskSummary> {
return this.getTaskSets().then((values) => {
let runnable = this.createRunnableTask(values, (set) => set.buildTasks);
let runnable = this.createRunnableTask(values, TaskGroup.Build);
if (!runnable || !runnable.task) {
throw new TaskError(Severity.Info, nls.localize('TaskService.noBuildTask', 'No build task defined. Mark a task with \'isBuildCommand\' in the tasks.json file.'), TaskErrors.NoBuildTask);
}
......@@ -667,7 +667,7 @@ class TaskService extends EventEmitter implements ITaskService {
public runTest(): TPromise<ITaskSummary> {
return this.getTaskSets().then((values) => {
let runnable = this.createRunnableTask(values, (set) => set.testTasks);
let runnable = this.createRunnableTask(values, TaskGroup.Test);
if (!runnable || !runnable.task) {
throw new TaskError(Severity.Info, nls.localize('TaskService.noTestTask', 'No test task defined. Mark a task with \'isTestCommand\' in the tasks.json file.'), TaskErrors.NoTestTask);
}
......@@ -701,22 +701,21 @@ class TaskService extends EventEmitter implements ITaskService {
});
}
private createRunnableTask(sets: TaskSet[], idFetcher: (set: TaskSet) => string[]): { task: Task; resolver: ITaskResolver } {
private createRunnableTask(sets: TaskSet[], group: TaskGroup): { task: Task; resolver: ITaskResolver } {
let uuidMap: IStringDictionary<Task> = Object.create(null);
let identifierMap: IStringDictionary<Task> = Object.create(null);
let taskIds: string[] = [];
let primaryTasks: Task[] = [];
sets.forEach((set) => {
set.tasks.forEach((task) => {
uuidMap[task._id] = task;
identifierMap[task.identifier] = task;
if (group && task.group === group) {
primaryTasks.push(task);
}
});
let ids: string[] = idFetcher(set);
if (ids) {
taskIds.push(...ids);
}
});
if (taskIds.length === 0) {
if (primaryTasks.length === 0) {
return undefined;
}
let resolver: ITaskResolver = {
......@@ -728,15 +727,15 @@ class TaskService extends EventEmitter implements ITaskService {
return identifierMap[id];
}
};
if (taskIds.length === 1) {
return { task: resolver.resolve(taskIds[0]), resolver };
if (primaryTasks.length === 1) {
return { task: primaryTasks[0], resolver };
} else {
let id: string = UUID.generateUuid();
let task: Task = {
_id: id,
name: id,
identifier: id,
dependsOn: taskIds,
dependsOn: primaryTasks.map(task => task._id),
command: undefined,
showOutput: ShowOutput.Never
};
......@@ -880,7 +879,7 @@ class TaskService extends EventEmitter implements ITaskService {
if (this._taskSystem instanceof ProcessTaskSystem) {
this._taskSystem.hasErrors(this._configHasErrors);
}
return value.taskSet;
return value.set;
});
return this._workspaceTasksPromise;
}
......@@ -888,7 +887,7 @@ class TaskService extends EventEmitter implements ITaskService {
private updateWorkspaceTasks(): void {
this._workspaceTasksPromise = this.computeWorkspaceTasks().then(value => {
this._configHasErrors = value.hasErrors;
return value.taskSet;
return value.set;
});
}
......@@ -897,7 +896,7 @@ class TaskService extends EventEmitter implements ITaskService {
{
let { config, hasParseErrors } = this.getConfiguration();
if (hasParseErrors) {
return TPromise.as({ taskSet: undefined, hasErrors: true });
return TPromise.as({ set: undefined, hasErrors: true });
}
if (config) {
let engine = TaskConfig.ExecutionEngine.from(config);
......@@ -950,7 +949,7 @@ class TaskService extends EventEmitter implements ITaskService {
problemReporter.fatal(nls.localize('TaskSystem.configurationErrors', 'Error: the provided task configuration has validation errors and can\'t not be used. Please correct the errors first.'));
return { taskSet: undefined, hasErrors };
}
return { taskSet: parseResult.taskSet, hasErrors };
return { set: { tasks: parseResult.tasks }, hasErrors };
});
});
}
......
......@@ -55,31 +55,17 @@ class ProblemReporter implements IProblemReporter {
class ConfiguationBuilder {
public result: Tasks.TaskSet;
public result: Tasks.Task[];
constructor() {
this.result = {
tasks: [],
buildTasks: [],
testTasks: []
};
this.result = [];
}
public task(name: string, command: string): TaskBuilder {
let builder = new TaskBuilder(this, name, command);
this.result.tasks.push(builder.result);
this.result.push(builder.result);
return builder;
}
public buildTask(id: string): ConfiguationBuilder {
this.result.buildTasks.push(id);
return this;
}
public testTask(id: string): ConfiguationBuilder {
this.result.testTasks.push(id);
return this;
}
}
class CommandConfigurationBuilder {
......@@ -153,6 +139,11 @@ class TaskBuilder {
return this;
}
public group(value: Tasks.TaskGroup): TaskBuilder {
this.result.group = value;
return this;
}
public args(value: string[]): TaskBuilder {
this.result.args = value;
return this;
......@@ -308,12 +299,10 @@ function testDefaultProblemMatcher(external: ExternalTaskRunnerConfiguration, re
let reporter = new ProblemReporter();
let result = parse(external, reporter);
assert.ok(!reporter.receivedMessage);
let taskSet = result.taskSet;
assert.strictEqual(taskSet.tasks.length, 1);
let task = taskSet.tasks[0];
assert.strictEqual(result.tasks.length, 1);
let task = result.tasks[0];
assert.ok(task);
assert.strictEqual(task.problemMatchers.length, resolved);
}
function testConfiguration(external: ExternalTaskRunnerConfiguration, builder: ConfiguationBuilder): void {
......@@ -325,52 +314,86 @@ function testConfiguration(external: ExternalTaskRunnerConfiguration, builder: C
assertConfiguration(result, builder.result);
}
function assertConfiguration(result: ParseResult, expected: Tasks.TaskSet) {
assert.ok(result.validationStatus.isOK());
let actual = result.taskSet;
assert.strictEqual(typeof actual.tasks, typeof expected.tasks);
let actualBuildTasks: string[] = [];
let actualTestTasks: string[] = [];
if (actual.tasks && expected.tasks) {
// We can't compare Ids since the parser uses UUID which are random
// So create a new map using the name.
let actualTasks: { [key: string]: Tasks.Task; } = Object.create(null);
let actualId2Name: { [key: string]: string; } = Object.create(null);
actual.tasks.forEach(task => {
assert.ok(!actualTasks[task.name]);
actualTasks[task.name] = task;
actualId2Name[task._id] = task.name;
});
let expectedTasks: { [key: string]: Tasks.Task; } = Object.create(null);
expected.tasks.forEach(task => {
assert.ok(!expectedTasks[task.name]);
expectedTasks[task.name] = task;
});
let actualKeys = Object.keys(actualTasks);
assert.strictEqual(actualKeys.length, Object.keys(expected.tasks).length);
class TaskGroupMap {
private _store: { [key: string]: Tasks.Task[] };
constructor() {
this._store = Object.create(null);
}
public add(group: string, task: Tasks.Task): void {
let tasks = this._store[group];
if (!tasks) {
tasks = [];
this._store[group] = tasks;
}
tasks.push(task);
}
public static assert(actual: TaskGroupMap, expected: TaskGroupMap): void {
let actualKeys = Object.keys(actual._store);
let expectedKeys = Object.keys(expected._store);
if (actualKeys.length === 0 && expectedKeys.length === 0) {
return;
}
assert.strictEqual(actualKeys.length, expectedKeys.length);
actualKeys.forEach(key => assert.ok(expected._store[key]));
expectedKeys.forEach(key => actual._store[key]);
actualKeys.forEach((key) => {
let actualTask = actualTasks[key];
let expectedTask = expectedTasks[key];
assert.ok(expectedTask);
assertTask(actualTask, expectedTask);
});
actual.buildTasks.forEach((id) => {
actualBuildTasks.push(actualId2Name[id]);
});
actual.testTasks.forEach((id) => {
actualTestTasks.push(actualId2Name[id]);
let actualTasks = actual._store[key];
let expectedTasks = expected._store[key];
assert.strictEqual(actualTasks.length, expectedTasks.length);
if (actualTasks.length === 1) {
assert.strictEqual(actualTasks[0].name, expectedTasks[0].name);
return;
}
let expectedTaskMap: { [key: string]: boolean } = Object.create(null);
expectedTasks.forEach(task => expectedTaskMap[task.name] = true);
actualTasks.forEach(task => delete expectedTaskMap[task.name]);
assert.strictEqual(Object.keys(expectedTaskMap).length, 0);
});
}
assertTaskConfig(actualBuildTasks, expected.buildTasks);
assertTaskConfig(actualTestTasks, expected.testTasks);
}
function assertTaskConfig(actual: string[], expected: string[]): void {
function assertConfiguration(result: ParseResult, expected: Tasks.Task[]): void {
assert.ok(result.validationStatus.isOK());
let actual = result.tasks;
assert.strictEqual(typeof actual, typeof expected);
if (actual && expected) {
assert.strictEqual(actual.length, expected.length);
assert.deepEqual(actual, expected);
if (!actual) {
return;
}
// We can't compare Ids since the parser uses UUID which are random
// So create a new map using the name.
let actualTasks: { [key: string]: Tasks.Task; } = Object.create(null);
let actualId2Name: { [key: string]: string; } = Object.create(null);
let actualTaskGroups = new TaskGroupMap();
actual.forEach(task => {
assert.ok(!actualTasks[task.name]);
actualTasks[task.name] = task;
actualId2Name[task._id] = task.name;
if (task.group) {
actualTaskGroups.add(task.group, task);
}
});
let expectedTasks: { [key: string]: Tasks.Task; } = Object.create(null);
let expectedTaskGroup = new TaskGroupMap();
expected.forEach(task => {
assert.ok(!expectedTasks[task.name]);
expectedTasks[task.name] = task;
if (task.group) {
expectedTaskGroup.add(task.group, task);
}
});
let actualKeys = Object.keys(actualTasks);
assert.strictEqual(actualKeys.length, expected.length);
actualKeys.forEach((key) => {
let actualTask = actualTasks[key];
let expectedTask = expectedTasks[key];
assert.ok(expectedTask);
assertTask(actualTask, expectedTask);
});
TaskGroupMap.assert(actualTaskGroups, expectedTaskGroup);
}
function assertTask(actual: Tasks.Task, expected: Tasks.Task) {
......@@ -461,8 +484,9 @@ function assertProblemPattern(actual: ProblemPattern, expected: ProblemPattern)
suite('Tasks Configuration parsing tests', () => {
test('tasks: all default', () => {
let builder = new ConfiguationBuilder().buildTask('tsc');
let builder = new ConfiguationBuilder();
builder.task('tsc', 'tsc').
group(Tasks.TaskGroup.Build).
suppressTaskName(true);
testConfiguration(
{
......@@ -472,8 +496,9 @@ suite('Tasks Configuration parsing tests', () => {
});
test('tasks: global isShellCommand', () => {
let builder = new ConfiguationBuilder().buildTask('tsc');
let builder = new ConfiguationBuilder();
builder.task('tsc', 'tsc').
group(Tasks.TaskGroup.Build).
suppressTaskName(true).
command().
shell(true);
......@@ -487,9 +512,10 @@ suite('Tasks Configuration parsing tests', () => {
});
test('tasks: global show output silent', () => {
let builder = new ConfiguationBuilder().buildTask('tsc');
let builder = new ConfiguationBuilder();
builder.
task('tsc', 'tsc').
group(Tasks.TaskGroup.Build).
suppressTaskName(true).
showOutput(Tasks.ShowOutput.Silent);
testConfiguration(
......@@ -503,8 +529,9 @@ suite('Tasks Configuration parsing tests', () => {
});
test('tasks: global promptOnClose default', () => {
let builder = new ConfiguationBuilder().buildTask('tsc');
let builder = new ConfiguationBuilder();
builder.task('tsc', 'tsc').
group(Tasks.TaskGroup.Build).
suppressTaskName(true);
testConfiguration(
{
......@@ -517,9 +544,10 @@ suite('Tasks Configuration parsing tests', () => {
});
test('tasks: global promptOnClose', () => {
let builder = new ConfiguationBuilder().buildTask('tsc');
let builder = new ConfiguationBuilder();
builder.task('tsc', 'tsc').
suppressTaskName(true).
group(Tasks.TaskGroup.Build).
promptOnClose(false);
testConfiguration(
{
......@@ -532,9 +560,10 @@ suite('Tasks Configuration parsing tests', () => {
});
test('tasks: global promptOnClose default watching', () => {
let builder = new ConfiguationBuilder().buildTask('tsc');
let builder = new ConfiguationBuilder();
builder.task('tsc', 'tsc').
suppressTaskName(true).
group(Tasks.TaskGroup.Build).
isBackground(true).
promptOnClose(false);
testConfiguration(
......@@ -548,9 +577,10 @@ suite('Tasks Configuration parsing tests', () => {
});
test('tasks: global show output never', () => {
let builder = new ConfiguationBuilder().buildTask('tsc');
let builder = new ConfiguationBuilder();
builder.
task('tsc', 'tsc').
group(Tasks.TaskGroup.Build).
suppressTaskName(true).
showOutput(Tasks.ShowOutput.Never);
testConfiguration(
......@@ -564,9 +594,10 @@ suite('Tasks Configuration parsing tests', () => {
});
test('tasks: global echo Command', () => {
let builder = new ConfiguationBuilder().buildTask('tsc');
let builder = new ConfiguationBuilder();
builder.
task('tsc', 'tsc').
group(Tasks.TaskGroup.Build).
suppressTaskName(true).
command().
echo(true);
......@@ -581,9 +612,10 @@ suite('Tasks Configuration parsing tests', () => {
});
test('tasks: global args', () => {
let builder = new ConfiguationBuilder().buildTask('tsc');
let builder = new ConfiguationBuilder();
builder.
task('tsc', 'tsc').
group(Tasks.TaskGroup.Build).
suppressTaskName(true).
command().
args(['--p']);
......@@ -600,9 +632,10 @@ suite('Tasks Configuration parsing tests', () => {
});
test('tasks: options - cwd', () => {
let builder = new ConfiguationBuilder().buildTask('tsc');
let builder = new ConfiguationBuilder();
builder.
task('tsc', 'tsc').
group(Tasks.TaskGroup.Build).
suppressTaskName(true).
command().
options({
......@@ -621,9 +654,10 @@ suite('Tasks Configuration parsing tests', () => {
});
test('tasks: options - env', () => {
let builder = new ConfiguationBuilder().buildTask('tsc');
let builder = new ConfiguationBuilder();
builder.
task('tsc', 'tsc').
group(Tasks.TaskGroup.Build).
suppressTaskName(true).
command().
options({ cwd: '${workspaceRoot}', env: { key: 'value' } });
......@@ -643,9 +677,10 @@ suite('Tasks Configuration parsing tests', () => {
test('tasks: os windows', () => {
let name: string = Platform.isWindows ? 'tsc.win' : 'tsc';
let builder = new ConfiguationBuilder().buildTask(name);
let builder = new ConfiguationBuilder();
builder.
task(name, name).
group(Tasks.TaskGroup.Build).
suppressTaskName(true);
let external: ExternalTaskRunnerConfiguration = {
version: '0.1.0',
......@@ -659,9 +694,10 @@ suite('Tasks Configuration parsing tests', () => {
test('tasks: os windows & global isShellCommand', () => {
let name: string = Platform.isWindows ? 'tsc.win' : 'tsc';
let builder = new ConfiguationBuilder().buildTask(name);
let builder = new ConfiguationBuilder();
builder.
task(name, name).
group(Tasks.TaskGroup.Build).
suppressTaskName(true).
command().
shell(true);
......@@ -678,9 +714,10 @@ suite('Tasks Configuration parsing tests', () => {
test('tasks: os mac', () => {
let name: string = Platform.isMacintosh ? 'tsc.osx' : 'tsc';
let builder = new ConfiguationBuilder().buildTask(name);
let builder = new ConfiguationBuilder();
builder.
task(name, name).
group(Tasks.TaskGroup.Build).
suppressTaskName(true);
let external: ExternalTaskRunnerConfiguration = {
version: '0.1.0',
......@@ -694,9 +731,10 @@ suite('Tasks Configuration parsing tests', () => {
test('tasks: os linux', () => {
let name: string = Platform.isLinux ? 'tsc.linux' : 'tsc';
let builder = new ConfiguationBuilder().buildTask(name);
let builder = new ConfiguationBuilder();
builder.
task(name, name).
group(Tasks.TaskGroup.Build).
suppressTaskName(true);
let external: ExternalTaskRunnerConfiguration = {
version: '0.1.0',
......@@ -709,9 +747,10 @@ suite('Tasks Configuration parsing tests', () => {
});
test('tasks: overwrite showOutput', () => {
let builder = new ConfiguationBuilder().buildTask('tsc');
let builder = new ConfiguationBuilder();
builder.
task('tsc', 'tsc').
group(Tasks.TaskGroup.Build).
showOutput(Platform.isWindows ? Tasks.ShowOutput.Always : Tasks.ShowOutput.Never).
suppressTaskName(true);
let external: ExternalTaskRunnerConfiguration = {
......@@ -726,9 +765,10 @@ suite('Tasks Configuration parsing tests', () => {
});
test('tasks: overwrite echo Command', () => {
let builder = new ConfiguationBuilder().buildTask('tsc');
let builder = new ConfiguationBuilder();
builder.
task('tsc', 'tsc').
group(Tasks.TaskGroup.Build).
suppressTaskName(true).
command().
echo(Platform.isWindows ? false : true);
......@@ -787,8 +827,8 @@ suite('Tasks Configuration parsing tests', () => {
}
]
};
let builder = new ConfiguationBuilder().buildTask('taskName');
builder.task('taskName', 'tsc');
let builder = new ConfiguationBuilder();
builder.task('taskName', 'tsc').group(Tasks.TaskGroup.Build);
testConfiguration(external, builder);
});
......@@ -802,8 +842,8 @@ suite('Tasks Configuration parsing tests', () => {
}
]
};
let builder = new ConfiguationBuilder().buildTask('build');
builder.task('build', 'tsc');
let builder = new ConfiguationBuilder();
builder.task('build', 'tsc').group(Tasks.TaskGroup.Build);
testConfiguration(external, builder);
});
......@@ -818,8 +858,8 @@ suite('Tasks Configuration parsing tests', () => {
}
]
};
let builder = new ConfiguationBuilder().testTask('taskName');
builder.task('taskName', 'tsc');
let builder = new ConfiguationBuilder();
builder.task('taskName', 'tsc').group(Tasks.TaskGroup.Test);
testConfiguration(external, builder);
});
......@@ -833,8 +873,8 @@ suite('Tasks Configuration parsing tests', () => {
}
]
};
let builder = new ConfiguationBuilder().testTask('test');
builder.task('test', 'tsc');
let builder = new ConfiguationBuilder();
builder.task('test', 'tsc').group(Tasks.TaskGroup.Test);
testConfiguration(external, builder);
});
......@@ -852,8 +892,9 @@ suite('Tasks Configuration parsing tests', () => {
}
]
};
let builder = new ConfiguationBuilder().testTask('test');
let builder = new ConfiguationBuilder();
builder.task('test', 'tsc').
group(Tasks.TaskGroup.Test).
showOutput(Tasks.ShowOutput.Never).
args(['--p']).
isBackground(true).
......@@ -876,8 +917,9 @@ suite('Tasks Configuration parsing tests', () => {
}
]
};
let builder = new ConfiguationBuilder().testTask('test');
let builder = new ConfiguationBuilder();
builder.task('test', 'tsc').
group(Tasks.TaskGroup.Test).
showOutput(Tasks.ShowOutput.Never).
command().
echo(true);
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册