diff --git a/src/vs/workbench/api/node/extHostTask.ts b/src/vs/workbench/api/node/extHostTask.ts index aa6497bc410..656f57739d7 100644 --- a/src/vs/workbench/api/node/extHostTask.ts +++ b/src/vs/workbench/api/node/extHostTask.ts @@ -230,7 +230,10 @@ namespace Strings { } namespace CommandOptions { - export function from(value: { cwd?: string; env?: { [key: string]: string; } }): TaskSystem.CommandOptions { + function isShellOptions(value: any): value is vscode.ShellOptions { + return value && typeof value.executable === 'string'; + } + export function from(value: vscode.ShellOptions | vscode.ProcessOptions): TaskSystem.CommandOptions { if (value === void 0 || value === null) { return undefined; } @@ -248,14 +251,17 @@ namespace CommandOptions { } }); } + if (isShellOptions(value)) { + result.shell = ShellConfiguration.from(value); + } return result; } } namespace ShellConfiguration { - export function from(value: { executable?: string, args?: string[] }): boolean | TaskSystem.ShellConfiguration { - if (value === void 0 || value === null || typeof value.executable !== 'string') { - return true; + export function from(value: { executable?: string, args?: string[] }): TaskSystem.ShellConfiguration { + if (value === void 0 || value === null || !value.executable) { + return undefined; } let result: TaskSystem.ShellConfiguration = { @@ -323,7 +329,7 @@ namespace Tasks { let result: TaskSystem.CommandConfiguration = { name: value.process, args: Strings.from(value.args), - isShellCommand: false, + type: TaskSystem.CommandType.Process, terminal: TerminalBehaviour.from(value.terminal) }; if (value.options) { @@ -338,7 +344,7 @@ namespace Tasks { } let result: TaskSystem.CommandConfiguration = { name: value.commandLine, - isShellCommand: ShellConfiguration.from(value.options), + type: TaskSystem.CommandType.Shell, terminal: TerminalBehaviour.from(value.terminal) }; if (value.options) { diff --git a/src/vs/workbench/parts/tasks/browser/quickOpen.ts b/src/vs/workbench/parts/tasks/browser/quickOpen.ts index 6c9afcd0b74..e282ee68321 100644 --- a/src/vs/workbench/parts/tasks/browser/quickOpen.ts +++ b/src/vs/workbench/parts/tasks/browser/quickOpen.ts @@ -97,10 +97,10 @@ export abstract class QuickOpenHandler extends Quickopen.QuickOpenHandler { if (task._source.kind === TaskSourceKind.Workspace && groupWorkspace) { groupWorkspace = false; hadWorkspace = true; - entries.push(new TaskGroupEntry(this.createEntry(this.taskService, task, highlights), nls.localize('workspace', 'From Workspace'), false)); + entries.push(new TaskGroupEntry(this.createEntry(this.taskService, task, highlights), nls.localize('configured', 'Configured Tasks'), false)); } else if (task._source.kind === TaskSourceKind.Extension && groupExtension) { groupExtension = false; - entries.push(new TaskGroupEntry(this.createEntry(this.taskService, task, highlights), nls.localize('extension', 'From Extensions'), hadWorkspace)); + entries.push(new TaskGroupEntry(this.createEntry(this.taskService, task, highlights), nls.localize('detected', 'Detected Tasks'), hadWorkspace)); } else { entries.push(this.createEntry(this.taskService, task, highlights)); } @@ -125,7 +125,7 @@ class CustomizeTaskAction extends Action { private static ID = 'workbench.action.tasks.customizeTask'; private static LABEL = nls.localize('customizeTask', "Customize Task"); - constructor() { + constructor(private taskService: ITaskService, private task: Task) { super(CustomizeTaskAction.ID, CustomizeTaskAction.LABEL); this.updateClass(); } @@ -134,14 +134,14 @@ class CustomizeTaskAction extends Action { this.class = 'quick-open-task-configure'; } - public run(context: any): TPromise { - return TPromise.as(false); + public run(context: any): TPromise { + return this.taskService.customize(this.task, true).then(_ => false, _ => false); } } export class QuickOpenActionContributor extends ActionBarContributor { - constructor() { + constructor( @ITaskService private taskService: ITaskService) { super(); } @@ -156,7 +156,7 @@ export class QuickOpenActionContributor extends ActionBarContributor { const entry = this.getEntry(context); if (entry && entry.task._source.kind === TaskSourceKind.Extension) { - actions.push(new CustomizeTaskAction()); + actions.push(new CustomizeTaskAction(this.taskService, entry.task)); } return actions; } diff --git a/src/vs/workbench/parts/tasks/common/taskConfiguration.ts b/src/vs/workbench/parts/tasks/common/taskConfiguration.ts index b06f6c15bf3..9af9833e240 100644 --- a/src/vs/workbench/parts/tasks/common/taskConfiguration.ts +++ b/src/vs/workbench/parts/tasks/common/taskConfiguration.ts @@ -11,7 +11,6 @@ import { IStringDictionary } from 'vs/base/common/collections'; import * as Platform from 'vs/base/common/platform'; import * as Types from 'vs/base/common/types'; import * as UUID from 'vs/base/common/uuid'; -import { Config as ProcessConfig } from 'vs/base/common/processes'; import { ValidationStatus, IProblemReporter as IProblemReporterBase } from 'vs/base/common/parsers'; import { @@ -32,7 +31,37 @@ export class ProblemHandling { public static clean: string = 'cleanMatcherMatchers'; } +export interface ShellConfiguration { + executable: string; + args?: string[]; +} + +export interface CommandOptions { + /** + * The current working directory of the executed program or shell. + * If omitted VSCode's current workspace root is used. + */ + cwd?: string; + + /** + * The additional environment of the executed program or shell. If omitted + * the parent process' environment is used. + */ + env?: IStringDictionary; + + /** + * The shell configuration; + */ + shell?: ShellConfiguration; +} + export interface PlatformTaskDescription { + + /** + * Whether the task is a shell task or a process task. + */ + type?: string; + /** * The command to be executed. Can be an external program or a shell * command. @@ -40,17 +69,18 @@ export interface PlatformTaskDescription { command?: string; /** + * @deprecated use the task type instead. * Specifies whether the command is a shell command and therefore must * be executed in a shell interpreter (e.g. cmd.exe, bash, ...). * * Defaults to false if omitted. */ - isShellCommand?: boolean; + isShellCommand?: boolean | ShellConfiguration; /** * The command options used when the command is executed. Can be omitted. */ - options?: ProcessConfig.CommandOptions; + options?: CommandOptions; /** * The arguments passed to the command or additional arguments passed to the @@ -166,7 +196,7 @@ export interface BaseTaskRunnerConfiguration { /** * The command options used when the command is executed. Can be omitted. */ - options?: ProcessConfig.CommandOptions; + options?: CommandOptions; /** * The arguments passed to the command. Can be omitted. @@ -308,7 +338,7 @@ interface ParseContext { } namespace CommandOptions { - export function from(this: void, options: ProcessConfig.CommandOptions, context: ParseContext): Tasks.CommandOptions { + export function from(this: void, options: CommandOptions, context: ParseContext): Tasks.CommandOptions { let result: Tasks.CommandOptions = {}; if (options.cwd !== void 0) { if (Types.isString(options.cwd)) { @@ -320,11 +350,12 @@ namespace CommandOptions { if (options.env !== void 0) { result.env = Objects.clone(options.env); } + result.shell = ShellConfiguration.from(options.shell, context); return isEmpty(result) ? undefined : result; } export function isEmpty(value: Tasks.CommandOptions): boolean { - return !value || value.cwd === void 0 && value.env === void 0; + return !value || value.cwd === void 0 && value.env === void 0 && value.shell === void 0; } export function merge(target: Tasks.CommandOptions, source: Tasks.CommandOptions): Tasks.CommandOptions { @@ -343,6 +374,7 @@ namespace CommandOptions { Object.keys(source.env).forEach(key => env[key = source.env[key]]); target.env = env; } + target.shell = ShellConfiguration.merge(target.shell, source.shell); return target; } @@ -356,6 +388,7 @@ namespace CommandOptions { if (value.cwd === void 0) { value.cwd = '${workspaceRoot}'; } + ShellConfiguration.fillDefaults(value.shell); return value; } @@ -364,14 +397,10 @@ namespace CommandOptions { if (value.env) { Object.freeze(value.env); } + ShellConfiguration.freeze(value.shell); } } -interface ShellConfiguration { - executable: string; - args?: string[]; -} - namespace ShellConfiguration { export function is(value: any): value is ShellConfiguration { let candidate: ShellConfiguration = value; @@ -424,9 +453,10 @@ namespace CommandConfiguration { interface BaseCommandConfiguationShape { command?: string; + type?: string; isShellCommand?: boolean | ShellConfiguration; args?: string[]; - options?: ProcessConfig.CommandOptions; + options?: CommandOptions; echoCommand?: boolean; showOutput?: string; terminal?: TerminalBehavior; @@ -524,21 +554,20 @@ namespace CommandConfiguration { function fromBase(this: void, config: BaseCommandConfiguationShape, context: ParseContext): Tasks.CommandConfiguration { let result: Tasks.CommandConfiguration = { name: undefined, - isShellCommand: undefined, + type: undefined, terminal: undefined }; if (Types.isString(config.command)) { result.name = config.command; } - if (Types.isBoolean(config.isShellCommand)) { - result.isShellCommand = config.isShellCommand; - } else if (ShellConfiguration.is(config.isShellCommand)) { - result.isShellCommand = ShellConfiguration.from(config.isShellCommand, context); - if (!context.isTermnial) { - context.problemReporter.warn(nls.localize('ConfigurationParser.noShell', 'Warning: shell configuration is only supported when executing tasks in the terminal.')); - } + if (Types.isString(config.type)) { + result.type = Tasks.CommandType.fromString(config.type); + } + let isShellConfiguration = ShellConfiguration.is(config.isShellCommand); + if (Types.isBoolean(config.isShellCommand) || isShellConfiguration) { + result.type = Tasks.CommandType.Shell; } else if (config.isShellCommand !== void 0) { - result.isShellCommand = !!config.isShellCommand; + result.type = !!config.isShellCommand ? Tasks.CommandType.Shell : Tasks.CommandType.Process; } if (config.args !== void 0) { if (Types.isStringArray(config.args)) { @@ -549,6 +578,12 @@ namespace CommandConfiguration { } if (config.options !== void 0) { result.options = CommandOptions.from(config.options, context); + if (result.options && result.options.shell === void 0 && isShellConfiguration) { + result.options.shell = ShellConfiguration.from(config.isShellCommand as ShellConfiguration, context); + if (!context.isTermnial) { + context.problemReporter.warn(nls.localize('ConfigurationParser.noShell', 'Warning: shell configuration is only supported when executing tasks in the terminal.')); + } + } } let terminal = TerminalBehavior.from(config, context); if (terminal) { @@ -561,13 +596,13 @@ namespace CommandConfiguration { } export function isEmpty(value: Tasks.CommandConfiguration): boolean { - return !value || value.name === void 0 && value.isShellCommand === void 0 && value.args === void 0 && CommandOptions.isEmpty(value.options) && value.terminal === void 0; + return !value || value.name === void 0 && value.type === void 0 && value.args === void 0 && CommandOptions.isEmpty(value.options) && value.terminal === void 0; } export function onlyTerminalBehaviour(value: Tasks.CommandConfiguration): boolean { return value && - value.terminal && (value.terminal.echo !== void 0 || value.terminal.reveal === void 0) && - value.name === void 0 && value.isShellCommand === void 0 && value.args === void 0 && CommandOptions.isEmpty(value.options); + value.terminal && (value.terminal.echo !== void 0 || value.terminal.reveal !== void 0) && + value.name === void 0 && value.type === void 0 && value.args === void 0 && CommandOptions.isEmpty(value.options); } export function merge(target: Tasks.CommandConfiguration, source: Tasks.CommandConfiguration): Tasks.CommandConfiguration { @@ -578,15 +613,10 @@ namespace CommandConfiguration { return source; } mergeProperty(target, source, 'name'); + mergeProperty(target, source, 'type'); // Merge isShellCommand - if (target.isShellCommand === void 0) { - target.isShellCommand = source.isShellCommand; - } if (Types.isBoolean(target.isShellCommand) && Types.isBoolean(source.isShellCommand)) { - mergeProperty(target, source, 'isShellCommand'); - } else if (ShellConfiguration.is(target.isShellCommand) && ShellConfiguration.is(source.isShellCommand)) { - ShellConfiguration.merge(target.isShellCommand, source.isShellCommand); - } else if (Types.isBoolean(target.isShellCommand) && ShellConfiguration.is(source.isShellCommand)) { - target.isShellCommand = source.isShellCommand; + if (target.type === void 0) { + target.type = source.type; } target.terminal = TerminalBehavior.merge(target.terminal, source.terminal); @@ -606,8 +636,8 @@ namespace CommandConfiguration { if (!value || Object.isFrozen(value)) { return; } - if (value.name !== void 0 && value.isShellCommand === void 0) { - value.isShellCommand = false; + if (value.name !== void 0 && value.type === void 0) { + value.type = Tasks.CommandType.Process; } value.terminal = TerminalBehavior.fillDefault(value.terminal); if (value.args === void 0) { @@ -629,9 +659,6 @@ namespace CommandConfiguration { if (value.terminal) { TerminalBehavior.freeze(value.terminal); } - if (ShellConfiguration.is(value.isShellCommand)) { - ShellConfiguration.freeze(value.isShellCommand); - } } } @@ -751,7 +778,7 @@ namespace TaskDescription { let command: Tasks.CommandConfiguration = externalTask.command !== void 0 ? CommandConfiguration.from(externalTask, context) : externalTask.echoCommand !== void 0 - ? { name: undefined, isShellCommand: undefined, terminal: CommandConfiguration.TerminalBehavior.from(externalTask, context) } + ? { name: undefined, type: undefined, terminal: CommandConfiguration.TerminalBehavior.from(externalTask, context) } : undefined; let identifer = Types.isString(externalTask.identifier) ? externalTask.identifier : taskName; let task: Tasks.Task = { @@ -797,7 +824,7 @@ namespace TaskDescription { } fillDefaults(task); let addTask: boolean = true; - if (context.isTermnial && task.command && task.command.name && task.command.isShellCommand && task.command.args && task.command.args.length > 0) { + if (context.isTermnial && task.command && task.command.name && task.command.type === Tasks.CommandType.Shell && task.command.args && task.command.args.length > 0) { if (hasUnescapedSpaces(task.command.name) || task.command.args.some(hasUnescapedSpaces)) { context.problemReporter.warn(nls.localize('taskConfiguration.shellArgs', 'Warning: the task \'{0}\' is a shell command and either the command name or one of its arguments has unescaped spaces. To ensure correct command line quoting please merge args into the command.', task.name)); } diff --git a/src/vs/workbench/parts/tasks/common/taskService.ts b/src/vs/workbench/parts/tasks/common/taskService.ts index 53f92ee18ee..63b2da04137 100644 --- a/src/vs/workbench/parts/tasks/common/taskService.ts +++ b/src/vs/workbench/parts/tasks/common/taskService.ts @@ -43,6 +43,8 @@ export interface ITaskService extends IEventEmitter { terminateAll(): TPromise; tasks(): TPromise; + customize(task: Task, openConfig?: boolean): TPromise; + registerTaskProvider(handle: number, taskProvider: ITaskProvider): void; unregisterTaskProvider(handle: number): boolean; } \ No newline at end of file diff --git a/src/vs/workbench/parts/tasks/common/taskTemplates.ts b/src/vs/workbench/parts/tasks/common/taskTemplates.ts index 0d7253582b8..ca345475266 100644 --- a/src/vs/workbench/parts/tasks/common/taskTemplates.ts +++ b/src/vs/workbench/parts/tasks/common/taskTemplates.ts @@ -14,104 +14,6 @@ export interface TaskEntry extends IPickOpenEntry { content: string; } -const gulp: TaskEntry = { - id: 'gulp', - label: 'Gulp', - autoDetect: true, - content: [ - '{', - '\t// See https://go.microsoft.com/fwlink/?LinkId=733558', - '\t// for the documentation about the tasks.json format', - '\t"version": "2.0.0",', - '\t"command": "gulp --no-color",', - '\t"isShellCommand": true,', - '}' - ].join('\n') -}; - -const grunt: TaskEntry = { - id: 'grunt', - label: 'Grunt', - autoDetect: true, - content: [ - '{', - '\t// See https://go.microsoft.com/fwlink/?LinkId=733558', - '\t// for the documentation about the tasks.json format', - '\t"version": "2.0.0",', - '\t"command": "grunt --no-color",', - '\t"isShellCommand": true,', - '}' - ].join('\n') -}; - -const npm: TaskEntry = { - id: 'npm', - label: 'npm', - sort: 'NPM', - autoDetect: false, - content: [ - '{', - '\t// See https://go.microsoft.com/fwlink/?LinkId=733558', - '\t// for the documentation about the tasks.json format', - '\t"version": "2.0.0",', - '\t"tasks": [', - '\t\t{', - '\t\t\t"taskName": "install",', - '\t\t\t"command": "npm install",', - '\t\t\t"isShellCommand": true,', - '\t\t},', - '\t\t{', - '\t\t\t"taskName": "update",', - '\t\t\t"command": "npm update",', - '\t\t\t"isShellCommand": true,', - '\t\t},', - '\t\t{', - '\t\t\t"taskName": "test",', - '\t\t\t"command": "npm run test",', - '\t\t\t"isShellCommand": true,', - '\t\t}', - '\t]', - '}' - ].join('\n') -}; - -const tscConfig: TaskEntry = { - id: 'tsc.config', - label: 'TypeScript - tsconfig.json', - autoDetect: false, - description: nls.localize('tsc.config', 'Compiles a TypeScript project'), - content: [ - '{', - '\t// See https://go.microsoft.com/fwlink/?LinkId=733558', - '\t// for the documentation about the tasks.json format', - '\t"version": "2.0.0",', - '\t"command": "tsc -p .",', - '\t"isShellCommand": true,', - '\t"showOutput": "silent",', - '\t"problemMatcher": "$tsc"', - '}' - ].join('\n') -}; - -const tscWatch: TaskEntry = { - id: 'tsc.watch', - label: 'TypeScript - Watch Mode', - autoDetect: false, - description: nls.localize('tsc.watch', 'Compiles a TypeScript project in watch mode'), - content: [ - '{', - '\t// See https://go.microsoft.com/fwlink/?LinkId=733558', - '\t// for the documentation about the tasks.json format', - '\t"version": "2.0.0",', - '\t"command": "tsc -w -p .",', - '\t"isShellCommand": true,', - '\t"showOutput": "silent",', - '\t"isBackground": true,', - '\t"problemMatcher": "$tsc-watch"', - '}' - ].join('\n') -}; - const dotnetBuild: TaskEntry = { id: 'dotnetCore', label: '.NET Core', @@ -122,16 +24,16 @@ const dotnetBuild: TaskEntry = { '{', '\t// See https://go.microsoft.com/fwlink/?LinkId=733558', '\t// for the documentation about the tasks.json format', - '\t"version": "0.1.0",', - '\t"command": "dotnet",', - '\t"isShellCommand": true,', - '\t"args": [],', + '\t"version": "2.0.0",', '\t"tasks": [', '\t\t{', '\t\t\t"taskName": "build",', - '\t\t\t"args": [ ],', - '\t\t\t"isBuildCommand": true,', - '\t\t\t"showOutput": "silent",', + '\t\t\t"command": "dotnet",', + '\t\t\t"isShellCommand": true,', + '\t\t\t"group": "build",', + '\t\t\t"terminal": {', + '\t\t\t\t"reveal": "silent"', + '\t\t\t},', '\t\t\t"problemMatcher": "$msCompile"', '\t\t}', '\t]', @@ -149,18 +51,20 @@ const msbuild: TaskEntry = { '\t// See https://go.microsoft.com/fwlink/?LinkId=733558', '\t// for the documentation about the tasks.json format', '\t"version": "2.0.0",', - '\t"command": "msbuild",', - '\t"args": [', - '\t\t// Ask msbuild to generate full paths for file names.', - '\t\t"/property:GenerateFullPaths=true"', - '\t],', - '\t"taskSelector": "/t:",', - '\t"showOutput": "silent",', '\t"tasks": [', '\t\t{', '\t\t\t"taskName": "build",', - '\t\t\t// Show the output window only if unrecognized errors occur.', - '\t\t\t"showOutput": "silent",', + '\t\t\t"command": "msbuild",', + '\t\t\t"args": [', + '\t\t\t\t// Ask msbuild to generate full paths for file names.', + '\t\t\t\t"/property:GenerateFullPaths=true",', + '\t\t\t\t"/t:build"', + '\t\t\t],', + '\t\t\t"group": "build",', + '\t\t\t"terminal": {', + '\t\t\t\t// Reveal the terminal only if unrecognized errors occur.', + '\t\t\t\t"reveal": "silent"', + '\t\t\t},', '\t\t\t// Use the standard MS compiler pattern to detect errors, warnings and infos', '\t\t\t"problemMatcher": "$msCompile"', '\t\t}', @@ -201,26 +105,25 @@ const maven: TaskEntry = { '\t// See https://go.microsoft.com/fwlink/?LinkId=733558', '\t// for the documentation about the tasks.json format', '\t"version": "2.0.0",', - '\t"showOutput": "always",', '\t"tasks": [', '\t\t{', '\t\t\t"taskName": "verify",', '\t\t\t"command": "mvn -B verify",', '\t\t\t"isShellCommand": true,', - '\t\t\t"isBuildCommand": true', + '\t\t\t"group": "build"', '\t\t},', '\t\t{', '\t\t\t"taskName": "test",', '\t\t\t"command": "mvn -B test",', '\t\t\t"isShellCommand": true,', - '\t\t\t"isTestCommand": true', + '\t\t\t"group": "test"', '\t\t}', '\t]', '}' ].join('\n') }; -export let templates: TaskEntry[] = [gulp, grunt, tscConfig, tscWatch, dotnetBuild, msbuild, npm, maven].sort((a, b) => { +export let templates: TaskEntry[] = [dotnetBuild, msbuild, maven].sort((a, b) => { return (a.sort || a.label).localeCompare(b.sort || b.label); }); templates.push(command); diff --git a/src/vs/workbench/parts/tasks/common/tasks.ts b/src/vs/workbench/parts/tasks/common/tasks.ts index 4b3031da91f..c3e90f83fb6 100644 --- a/src/vs/workbench/parts/tasks/common/tasks.ts +++ b/src/vs/workbench/parts/tasks/common/tasks.ts @@ -9,20 +9,6 @@ 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 { - /** - * The current working directory of the executed program or shell. - * If omitted VSCode's current workspace root is used. - */ - cwd?: string; - - /** - * The environment of the executed program or shell. If omitted - * the parent process' environment is used. - */ - env?: { [key: string]: string; }; -} - export interface ShellConfiguration { /** * The shell executable. @@ -41,6 +27,26 @@ export namespace ShellConfiguration { } } +export interface CommandOptions { + + /** + * The shell to use if the task is a shell command. + */ + shell?: ShellConfiguration; + + /** + * The current working directory of the executed program or shell. + * If omitted VSCode's current workspace root is used. + */ + cwd?: string; + + /** + * The environment of the executed program or shell. If omitted + * the parent process' environment is used. + */ + env?: { [key: string]: string; }; +} + export enum RevealKind { /** * Always brings the terminal to front if the task is executed. @@ -87,17 +93,36 @@ export interface TerminalBehavior { echo: boolean; } +export enum CommandType { + Shell = 1, + Process = 2 +} + +export namespace CommandType { + export function fromString(value: string): CommandType { + switch (value.toLowerCase()) { + case 'shell': + return CommandType.Shell; + case 'process': + return CommandType.Process; + default: + return CommandType.Process; + } + } +} + export interface CommandConfiguration { + + /** + * The task type + */ + type: CommandType; + /** * The command to execute */ name: string; - /** - * Whether the command is a shell command or not - */ - isShellCommand: boolean | ShellConfiguration; - /** * Additional command options. */ diff --git a/src/vs/workbench/parts/tasks/electron-browser/jsonSchema_v2.ts b/src/vs/workbench/parts/tasks/electron-browser/jsonSchema_v2.ts index 4ece04baf2e..2efb28f4d6a 100644 --- a/src/vs/workbench/parts/tasks/electron-browser/jsonSchema_v2.ts +++ b/src/vs/workbench/parts/tasks/electron-browser/jsonSchema_v2.ts @@ -104,6 +104,13 @@ const group: IJSONSchema = { description: nls.localize('JsonSchema.tasks.group', 'Defines to which execution group this task belongs to. If omitted the task belongs to no group') }; +const taskType: IJSONSchema = { + type: 'string', + enum: ['shell', 'process'], + default: 'process', + description: nls.localize('JsonSchema.tasks.type', 'Defines whether the task is run as a process or as a command inside a shell. Default is process') +}; + schema.definitions = Objects.deepClone(commonSchema.definitions); let definitions = schema.definitions; definitions.commandConfiguration.properties.isShellCommand = Objects.deepClone(shellCommand); @@ -113,6 +120,8 @@ definitions.showOutputType.deprecationMessage = nls.localize('JsonSchema.tasks.s definitions.taskDescription.properties.echoCommand.deprecationMessage = nls.localize('JsonSchema.tasks.echoCommand.deprecated', 'The property echoCommand is deprecated. Use the terminal property instead.'); definitions.taskDescription.properties.isBuildCommand.deprecationMessage = nls.localize('JsonSchema.tasks.isBuildCommand.deprecated', 'The property isBuildCommand is deprecated. Use the group property instead.'); definitions.taskDescription.properties.isTestCommand.deprecationMessage = nls.localize('JsonSchema.tasks.isTestCommand.deprecated', 'The property isTestCommand is deprecated. Use the group property instead.'); +definitions.taskDescription.properties.type = taskType; +definitions.taskDescription.properties.isShellCommand.deprecationMessage = nls.localize('JsonSchema.tasks.isShellCommand.deprecated', 'The property isShellCommand is deprecated. Use the type property instead.'); definitions.taskDescription.properties.terminal = terminal; definitions.taskDescription.properties.group = group; definitions.taskRunnerConfiguration.properties.isShellCommand = Objects.deepClone(shellCommand); diff --git a/src/vs/workbench/parts/tasks/electron-browser/task.contribution.ts b/src/vs/workbench/parts/tasks/electron-browser/task.contribution.ts index 98b390f2f21..49c14ca2110 100644 --- a/src/vs/workbench/parts/tasks/electron-browser/task.contribution.ts +++ b/src/vs/workbench/parts/tasks/electron-browser/task.contribution.ts @@ -61,6 +61,7 @@ import Constants from 'vs/workbench/parts/markers/common/constants'; import { IPartService } from 'vs/workbench/services/part/common/partService'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; +import { IConfigurationEditingService, ConfigurationTarget } from 'vs/workbench/services/configuration/common/configurationEditing'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; @@ -118,40 +119,13 @@ abstract class OpenTaskConfigurationAction extends Action { if (!selection) { return undefined; } - let contentPromise: TPromise; - if (selection.autoDetect) { - contentPromise = this.extensionService.activateByEvent('onCommand:workbench.action.tasks.runTask').then(() => { - return this.taskService.tasks().then((tasks) => { - let tasksToInsert: Task[] = tasks.filter((task) => { - return task.identifier && task.identifier.indexOf(selection.id) === 0 && task.identifier[selection.id.length] === '.' && task.group !== void 0; - }); - if (tasksToInsert.length === 0) { - return selection.content; - } - let config: TaskConfig.ExternalTaskRunnerConfiguration = { - version: '2.0.0', - tasks: tasksToInsert.map((task) => { return { taskName: task.name }; }) - }; - let content = JSON.stringify(config, null, '\t'); - content = [ - '{', - '\t// See https://go.microsoft.com/fwlink/?LinkId=733558', - '\t// for the documentation about the tasks.json format', - ].join('\n') + content.substr(1); - return content; - }); - }); - } else { - contentPromise = TPromise.as(selection.content); + let content = selection.content; + let editorConfig = this.configurationService.getConfiguration(); + if (editorConfig.editor.insertSpaces) { + content = content.replace(/(\n)(\t+)/g, (_, s1, s2) => s1 + strings.repeat(' ', s2.length * editorConfig.editor.tabSize)); } - return contentPromise.then(content => { - let editorConfig = this.configurationService.getConfiguration(); - if (editorConfig.editor.insertSpaces) { - content = content.replace(/(\n)(\t+)/g, (_, s1, s2) => s1 + strings.repeat(' ', s2.length * editorConfig.editor.tabSize)); - } - configFileCreated = true; - return this.fileService.createFile(this.contextService.toResource('.vscode/tasks.json'), content); - }); + configFileCreated = true; + return this.fileService.createFile(this.contextService.toResource('.vscode/tasks.json'), content); }); }).then((stat) => { if (!stat) { @@ -187,7 +161,6 @@ class ConfigureTaskRunnerAction extends OpenTaskConfigurationAction { outputService, messageService, quickOpenService, environmentService, configurationResolverService, extensionService); } - } class ConfigureBuildTaskAction extends OpenTaskConfigurationAction { @@ -503,6 +476,7 @@ class TaskService extends EventEmitter implements ITaskService { private modeService: IModeService; private configurationService: IConfigurationService; + private configurationEditingService: IConfigurationEditingService; private markerService: IMarkerService; private outputService: IOutputService; private messageService: IMessageService; @@ -526,6 +500,7 @@ class TaskService extends EventEmitter implements ITaskService { private _outputChannel: IOutputChannel; constructor( @IModeService modeService: IModeService, @IConfigurationService configurationService: IConfigurationService, + @IConfigurationEditingService configurationEditingService: IConfigurationEditingService, @IMarkerService markerService: IMarkerService, @IOutputService outputService: IOutputService, @IMessageService messageService: IMessageService, @IWorkbenchEditorService editorService: IWorkbenchEditorService, @IFileService fileService: IFileService, @IWorkspaceContextService contextService: IWorkspaceContextService, @@ -542,6 +517,7 @@ class TaskService extends EventEmitter implements ITaskService { super(); this.modeService = modeService; this.configurationService = configurationService; + this.configurationEditingService = configurationEditingService; this.markerService = markerService; this.outputService = outputService; this.messageService = messageService; @@ -723,6 +699,43 @@ class TaskService extends EventEmitter implements ITaskService { }); } + public customize(task: Task, openConfig: boolean = false): TPromise { + if (task._source.kind !== TaskSourceKind.Extension) { + return TPromise.as(undefined); + } + let configuration = this.getConfiguration(); + if (configuration.hasParseErrors) { + this.messageService.show(Severity.Warning, nls.localize('customizeParseErrors', 'The current task configuration has errors. Please fix the errors first before customizing a task.')); + return TPromise.as(undefined); + } + let fileConfig = configuration.config; + let customize = { taskName: task.name }; + if (!fileConfig) { + fileConfig = { + version: '2.0.0', + tasks: [customize] + }; + } else { + if (Array.isArray(fileConfig.tasks)) { + fileConfig.tasks.push(customize); + } else { + fileConfig.tasks = [customize]; + } + }; + return this.configurationEditingService.writeConfiguration(ConfigurationTarget.WORKSPACE, { key: 'tasks', value: fileConfig }).then(() => { + if (openConfig) { + let resource = this.contextService.toResource('.vscode/tasks.json'); + this.editorService.openEditor({ + resource: resource, + options: { + forceOpen: true, + pinned: false + } + }, false); + } + }); + } + private createRunnableTask(sets: TaskSet[], group: TaskGroup): { task: Task; resolver: ITaskResolver } { let uuidMap: IStringDictionary = Object.create(null); let identifierMap: IStringDictionary = Object.create(null); @@ -740,6 +753,8 @@ class TaskService extends EventEmitter implements ITaskService { if (primaryTasks.length === 0) { return undefined; } + // check for a WORKSPACE build task and use that onemptied.apply + let resolver: ITaskResolver = { resolve: (id: string) => { let result = uuidMap[id]; diff --git a/src/vs/workbench/parts/tasks/electron-browser/terminalTaskSystem.ts b/src/vs/workbench/parts/tasks/electron-browser/terminalTaskSystem.ts index 504d0114385..bf6065aa66a 100644 --- a/src/vs/workbench/parts/tasks/electron-browser/terminalTaskSystem.ts +++ b/src/vs/workbench/parts/tasks/electron-browser/terminalTaskSystem.ts @@ -33,7 +33,7 @@ import { IConfigurationResolverService } from 'vs/workbench/services/configurati import { ITerminalService, ITerminalInstance, IShellLaunchConfig } from 'vs/workbench/parts/terminal/common/terminal'; import { IOutputService, IOutputChannel } from 'vs/workbench/parts/output/common/output'; import { StartStopProblemCollector, WatchingProblemCollector, ProblemCollectorEvents } from 'vs/workbench/parts/tasks/common/problemCollectors'; -import { Task, RevealKind, CommandOptions, ShellConfiguration } from 'vs/workbench/parts/tasks/common/tasks'; +import { Task, RevealKind, CommandOptions, ShellConfiguration, CommandType } from 'vs/workbench/parts/tasks/common/tasks'; import { ITaskSystem, ITaskSummary, ITaskExecuteResult, TaskExecuteKind, TaskError, TaskErrors, ITaskResolver, TelemetryEvent, Triggers, TaskSystemEvents, TaskEvent, TaskType @@ -368,17 +368,19 @@ export class TerminalTaskSystem extends EventEmitter implements ITaskSystem { let terminalName = nls.localize('TerminalTaskSystem.terminalName', 'Task - {0}', task.name); let waitOnExit = task.command.terminal.reveal !== RevealKind.Never || !task.isBackground; let shellLaunchConfig: IShellLaunchConfig = undefined; - if (task.command.isShellCommand) { + let isShellCommand = task.command.type === CommandType.Shell; + if (isShellCommand) { if (Platform.isWindows && ((options.cwd && TPath.isUNC(options.cwd)) || (!options.cwd && TPath.isUNC(process.cwd())))) { throw new TaskError(Severity.Error, nls.localize('TerminalTaskSystem', 'Can\'t execute a shell command on an UNC drive.'), TaskErrors.UnknownError); } shellLaunchConfig = { name: terminalName, executable: null, args: null, waitOnExit }; let shellSpecified: boolean = false; - if (ShellConfiguration.is(task.command.isShellCommand)) { - shellLaunchConfig.executable = task.command.isShellCommand.executable; + let shellOptions: ShellConfiguration = task.command.options && task.command.options.shell; + if (shellOptions && shellOptions.executable) { + shellLaunchConfig.executable = shellOptions.executable; shellSpecified = true; - if (task.command.isShellCommand.args) { - shellLaunchConfig.args = task.command.isShellCommand.args.slice(); + if (shellOptions.args) { + shellLaunchConfig.args = shellOptions.args.slice(); } else { shellLaunchConfig.args = []; } @@ -422,7 +424,7 @@ export class TerminalTaskSystem extends EventEmitter implements ITaskSystem { let cwd = options && options.cwd ? options.cwd : process.cwd(); // On Windows executed process must be described absolute. Since we allowed command without an // absolute path (e.g. "command": "node") we need to find the executable in the CWD or PATH. - let executable = Platform.isWindows && !task.command.isShellCommand ? this.findExecutable(command, cwd) : command; + let executable = Platform.isWindows && !isShellCommand ? this.findExecutable(command, cwd) : command; shellLaunchConfig = { name: terminalName, executable: executable, diff --git a/src/vs/workbench/parts/tasks/node/processTaskSystem.ts b/src/vs/workbench/parts/tasks/node/processTaskSystem.ts index 848368e6dc4..2a1fcb76e73 100644 --- a/src/vs/workbench/parts/tasks/node/processTaskSystem.ts +++ b/src/vs/workbench/parts/tasks/node/processTaskSystem.ts @@ -29,7 +29,7 @@ import { StartStopProblemCollector, WatchingProblemCollector, ProblemCollectorEv import { ITaskSystem, ITaskSummary, ITaskExecuteResult, TaskExecuteKind, TaskError, TaskErrors, TelemetryEvent, Triggers, TaskSystemEvents, TaskEvent, TaskType } from 'vs/workbench/parts/tasks/common/taskSystem'; -import { Task, CommandOptions, RevealKind, CommandConfiguration } from 'vs/workbench/parts/tasks/common/tasks'; +import { Task, CommandOptions, RevealKind, CommandConfiguration, CommandType } from 'vs/workbench/parts/tasks/common/tasks'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; @@ -175,7 +175,7 @@ export class ProcessTaskSystem extends EventEmitter implements ITaskSystem { } args = this.resolveVariables(args); let command: string = this.resolveVariable(commandConfig.name); - this.childProcess = new LineProcess(command, args, !!commandConfig.isShellCommand, this.resolveOptions(commandConfig.options)); + this.childProcess = new LineProcess(command, args, commandConfig.type === CommandType.Shell, this.resolveOptions(commandConfig.options)); telemetryEvent.command = this.childProcess.getSanitizedCommand(); // we have no problem matchers defined. So show the output log let reveal = task.command.terminal.reveal; diff --git a/src/vs/workbench/parts/tasks/test/node/configuration.test.ts b/src/vs/workbench/parts/tasks/test/node/configuration.test.ts index 091899e941a..03610ad7ecc 100644 --- a/src/vs/workbench/parts/tasks/test/node/configuration.test.ts +++ b/src/vs/workbench/parts/tasks/test/node/configuration.test.ts @@ -96,7 +96,7 @@ class CommandConfigurationBuilder { this.terminalBuilder = new TerminalBehaviorBuilder(this); this.result = { name: command, - isShellCommand: false, + type: Tasks.CommandType.Process, args: [], options: { cwd: '${workspaceRoot}' @@ -110,8 +110,8 @@ class CommandConfigurationBuilder { return this; } - public shell(value: boolean): CommandConfigurationBuilder { - this.result.isShellCommand = value; + public type(value: Tasks.CommandType): CommandConfigurationBuilder { + this.result.type = value; return this; } @@ -433,7 +433,7 @@ function assertCommandConfiguration(actual: Tasks.CommandConfiguration, expected if (actual && expected) { assertTerminalBehavior(actual.terminal, expected.terminal); assert.strictEqual(actual.name, expected.name, 'name'); - assert.strictEqual(actual.isShellCommand, expected.isShellCommand, 'isShellCommand'); + assert.strictEqual(actual.type, expected.type, 'task type'); assert.deepEqual(actual.args, expected.args, 'args'); assert.strictEqual(typeof actual.options, typeof expected.options); if (actual.options && expected.options) { @@ -531,7 +531,7 @@ suite('Tasks Configuration parsing tests', () => { group(Tasks.TaskGroup.Build). suppressTaskName(true). command(). - shell(true); + type(Tasks.CommandType.Shell); testConfiguration( { version: '0.1.0', @@ -730,7 +730,7 @@ suite('Tasks Configuration parsing tests', () => { group(Tasks.TaskGroup.Build). suppressTaskName(true). command(). - shell(true); + type(Tasks.CommandType.Shell); let external: ExternalTaskRunnerConfiguration = { version: '0.1.0', command: 'tsc', @@ -1280,7 +1280,7 @@ suite('Tasks Configuration parsing tests', () => { }; let builder = new ConfiguationBuilder(); builder.task('taskNameOne', 'tsc').suppressTaskName(true).command(). - shell(true).args(['arg']).options({ cwd: 'cwd', env: { env: 'env' } }); + type(Tasks.CommandType.Shell).args(['arg']).options({ cwd: 'cwd', env: { env: 'env' } }); testConfiguration(external, builder); }); @@ -1355,7 +1355,7 @@ suite('Tasks Configuration parsing tests', () => { ] }; let builder = new ConfiguationBuilder(); - builder.task('taskNameOne', 'tsc').command().shell(false); + builder.task('taskNameOne', 'tsc').command().type(Tasks.CommandType.Process); testConfiguration(external, builder); }); });