diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 69a0b61f1f5..27ac0cd9c73 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -1255,6 +1255,30 @@ declare module 'vscode' { */ clear?: boolean; } + + + export enum RerunBehavior { + reevaluate = 1, + useEvaluated = 2, + } + + + export interface RunOptions { + /** + * Controls the behavior of a task when it is rerun. + */ + rerunBehavior?: RerunBehavior; + } + + /** + * A task to execute + */ + export class Task2 extends Task { + /** + * Run options for the task + */ + runOptions: RunOptions; + } //#endregion } diff --git a/src/vs/workbench/api/electron-browser/mainThreadTask.ts b/src/vs/workbench/api/electron-browser/mainThreadTask.ts index 8d827f0bacb..61d5a633621 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadTask.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadTask.ts @@ -17,7 +17,7 @@ import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspac import { ContributedTask, ExtensionTaskSourceTransfer, KeyedTaskIdentifier, TaskExecution, Task, TaskEvent, TaskEventKind, - PresentationOptions, CommandOptions, CommandConfiguration, RuntimeType, CustomTask, TaskScope, TaskSource, TaskSourceKind, ExtensionTaskSource, RevealKind, PanelKind + PresentationOptions, CommandOptions, CommandConfiguration, RuntimeType, CustomTask, TaskScope, TaskSource, TaskSourceKind, ExtensionTaskSource, RevealKind, PanelKind, RunOptions } from 'vs/workbench/parts/tasks/common/tasks'; @@ -30,7 +30,8 @@ import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostC import { ExtHostContext, MainThreadTaskShape, ExtHostTaskShape, MainContext, IExtHostContext } from 'vs/workbench/api/node/extHost.protocol'; import { TaskDefinitionDTO, TaskExecutionDTO, ProcessExecutionOptionsDTO, TaskPresentationOptionsDTO, - ProcessExecutionDTO, ShellExecutionDTO, ShellExecutionOptionsDTO, TaskDTO, TaskSourceDTO, TaskHandleDTO, TaskFilterDTO, TaskProcessStartedDTO, TaskProcessEndedDTO, TaskSystemInfoDTO + ProcessExecutionDTO, ShellExecutionDTO, ShellExecutionOptionsDTO, TaskDTO, TaskSourceDTO, TaskHandleDTO, TaskFilterDTO, TaskProcessStartedDTO, TaskProcessEndedDTO, TaskSystemInfoDTO, + RunOptionsDTO } from 'vs/workbench/api/shared/tasks'; namespace TaskExecutionDTO { @@ -99,6 +100,15 @@ namespace TaskPresentationOptionsDTO { } } +namespace RunOptionsDTO { + export function from(value: RunOptions): RunOptionsDTO { + if (value === void 0 || value === null) { + return undefined; + } + return Objects.assign(Object.create(null), value); + } +} + namespace ProcessExecutionOptionsDTO { export function from(value: CommandOptions): ProcessExecutionOptionsDTO { if (value === void 0 || value === null) { @@ -289,7 +299,8 @@ namespace TaskDTO { presentationOptions: task.command ? TaskPresentationOptionsDTO.from(task.command.presentation) : undefined, isBackground: task.isBackground, problemMatchers: [], - hasDefinedMatchers: ContributedTask.is(task) ? task.hasDefinedMatchers : false + hasDefinedMatchers: ContributedTask.is(task) ? task.hasDefinedMatchers : false, + runOptions: RunOptionsDTO.from(task.runOptions), }; if (task.group) { result.group = task.group; @@ -344,7 +355,8 @@ namespace TaskDTO { command: command, isBackground: !!task.isBackground, problemMatchers: task.problemMatchers.slice(), - hasDefinedMatchers: task.hasDefinedMatchers + hasDefinedMatchers: task.hasDefinedMatchers, + runOptions: task.runOptions, }; return result; } diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index 6a076d0ac89..d173cde3ed4 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -784,6 +784,7 @@ export function createApiFactory( QuickInputButtons: extHostTypes.QuickInputButtons, Range: extHostTypes.Range, RelativePattern: extHostTypes.RelativePattern, + RerunBehavior: extHostTypes.RerunBehavior, Selection: extHostTypes.Selection, ShellExecution: extHostTypes.ShellExecution, ShellQuoting: extHostTypes.ShellQuoting, @@ -797,6 +798,7 @@ export function createApiFactory( SymbolInformation: extHostTypes.SymbolInformation, SymbolKind: extHostTypes.SymbolKind, Task: extHostTypes.Task, + Task2: extHostTypes.Task, TaskGroup: extHostTypes.TaskGroup, TaskPanelKind: extHostTypes.TaskPanelKind, TaskRevealKind: extHostTypes.TaskRevealKind, diff --git a/src/vs/workbench/api/node/extHostTask.ts b/src/vs/workbench/api/node/extHostTask.ts index bbb2ca0138c..bbb65956b8d 100644 --- a/src/vs/workbench/api/node/extHostTask.ts +++ b/src/vs/workbench/api/node/extHostTask.ts @@ -403,7 +403,8 @@ namespace Tasks { command: command, isBackground: !!task.isBackground, problemMatchers: task.problemMatchers.slice(), - hasDefinedMatchers: (task as types.Task).hasDefinedMatchers + hasDefinedMatchers: (task as types.Task).hasDefinedMatchers, + runOptions: (task).runOptions ? (task).runOptions : { rerunBehavior: tasks.RerunBehavior.reevaluate }, }; return result; } @@ -629,7 +630,8 @@ namespace TaskDTO { group: group, presentationOptions: TaskPresentationOptionsDTO.from(value.presentationOptions), problemMatchers: value.problemMatchers, - hasDefinedMatchers: (value as types.Task).hasDefinedMatchers + hasDefinedMatchers: (value as types.Task).hasDefinedMatchers, + runOptions: (value).runOptions ? (value).runOptions : { rerunBehavior: tasks.RerunBehavior.reevaluate }, }; return result; } diff --git a/src/vs/workbench/api/node/extHostTypes.ts b/src/vs/workbench/api/node/extHostTypes.ts index 87e5be63ef8..e5a97b4d9bc 100644 --- a/src/vs/workbench/api/node/extHostTypes.ts +++ b/src/vs/workbench/api/node/extHostTypes.ts @@ -1591,7 +1591,12 @@ export enum TaskScope { Workspace = 2 } -export class Task implements vscode.Task { +export enum RerunBehavior { + reevaluate = 1, + useEvaluated = 2, +} + +export class Task implements vscode.Task2 { private __id: string; @@ -1605,6 +1610,7 @@ export class Task implements vscode.Task { private _source: string; private _group: TaskGroup; private _presentationOptions: vscode.TaskPresentationOptions; + private _runOptions: vscode.RunOptions; constructor(definition: vscode.TaskDefinition, name: string, source: string, execution?: ProcessExecution | ShellExecution, problemMatchers?: string | string[]); constructor(definition: vscode.TaskDefinition, scope: vscode.TaskScope.Global | vscode.TaskScope.Workspace | vscode.WorkspaceFolder, name: string, source: string, execution?: ProcessExecution | ShellExecution, problemMatchers?: string | string[]); @@ -1782,6 +1788,18 @@ export class Task implements vscode.Task { this.clear(); this._presentationOptions = value; } + + get runOptions(): vscode.RunOptions { + return this._runOptions; + } + + set runOptions(value: vscode.RunOptions) { + if (value === null) { + value = undefined; + } + this.clear(); + this._runOptions = value; + } } diff --git a/src/vs/workbench/api/shared/tasks.ts b/src/vs/workbench/api/shared/tasks.ts index 4710c201e2a..79ae249f68b 100644 --- a/src/vs/workbench/api/shared/tasks.ts +++ b/src/vs/workbench/api/shared/tasks.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { UriComponents } from 'vs/base/common/uri'; +import { RerunBehavior } from 'vs/workbench/parts/tasks/common/tasks'; export interface TaskDefinitionDTO { type: string; @@ -19,6 +20,10 @@ export interface TaskPresentationOptionsDTO { clear?: boolean; } +export interface RunOptionsDTO { + rerunBehavior?: RerunBehavior; +} + export interface ExecutionOptionsDTO { cwd?: string; env?: { [key: string]: string }; @@ -82,6 +87,7 @@ export interface TaskDTO { presentationOptions: TaskPresentationOptionsDTO; problemMatchers: string[]; hasDefinedMatchers: boolean; + runOptions: RunOptionsDTO; } export interface TaskExecutionDTO { diff --git a/src/vs/workbench/parts/tasks/common/taskSystem.ts b/src/vs/workbench/parts/tasks/common/taskSystem.ts index a23db094065..9eb4864cb67 100644 --- a/src/vs/workbench/parts/tasks/common/taskSystem.ts +++ b/src/vs/workbench/parts/tasks/common/taskSystem.ts @@ -9,9 +9,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { TerminateResponse } from 'vs/base/common/processes'; import { Event } from 'vs/base/common/event'; import { Platform } from 'vs/base/common/platform'; - import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; - import { Task, TaskEvent, KeyedTaskIdentifier } from './tasks'; export const enum TaskErrors { @@ -85,6 +83,7 @@ export const enum TaskExecuteKind { export interface ITaskExecuteResult { kind: TaskExecuteKind; promise: TPromise; + task: Task; started?: { restartOnFileChanges?: string; }; @@ -130,6 +129,7 @@ export interface TaskSystemInfoResovler { export interface ITaskSystem { onDidStateChange: Event; run(task: Task, resolver: ITaskResolver): ITaskExecuteResult; + rerun(): ITaskExecuteResult | undefined; isActive(): TPromise; isActiveSync(): boolean; getActiveTasks(): Task[]; diff --git a/src/vs/workbench/parts/tasks/common/tasks.ts b/src/vs/workbench/parts/tasks/common/tasks.ts index 5d8f1f88557..000ba0e110f 100644 --- a/src/vs/workbench/parts/tasks/common/tasks.ts +++ b/src/vs/workbench/parts/tasks/common/tasks.ts @@ -176,6 +176,21 @@ export namespace PanelKind { } } +export namespace RerunBehavior { + export function fromString(value: string | undefined): RerunBehavior { + if (!value) { + return RerunBehavior.reevaluate; + } + switch (value.toLowerCase()) { + case 'useevaluated': + return RerunBehavior.useEvaluated; + case 'reevaulate': + default: + return RerunBehavior.reevaluate; + } + } +} + export interface PresentationOptions { /** * Controls whether the task output is reveal in the user interface. @@ -421,6 +436,14 @@ export interface ConfigurationProperties { problemMatchers?: (string | ProblemMatcher)[]; } +export enum RerunBehavior { + reevaluate = 1, + useEvaluated = 2, +} + +export interface RunOptions { + rerunBehavior?: RerunBehavior; +} export interface CommonTask { /** @@ -434,6 +457,8 @@ export interface CommonTask { _label: string; type: string; + + runOptions: RunOptions; } export interface CustomTask extends CommonTask, ConfigurationProperties { 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 8759d4367ae..d891dc33b22 100644 --- a/src/vs/workbench/parts/tasks/electron-browser/jsonSchema_v2.ts +++ b/src/vs/workbench/parts/tasks/electron-browser/jsonSchema_v2.ts @@ -281,6 +281,20 @@ const identifier: IJSONSchema = { deprecationMessage: nls.localize('JsonSchema.tasks.identifier.deprecated', 'User defined identifiers are deprecated. For custom task use the name as a reference and for tasks provided by extensions use their defined task identifier.') }; +const rerunBehavior: IJSONSchema = { + type: 'string', + enum: ['reevauate', 'useEvaluated'], + description: nls.localize('JsonSchema.tasks.rerunBehavior', 'The task\'s behavior on rerun') +}; + +const runOptions: IJSONSchema = { + type: 'object', + properties: { + rerunBehavior: Objects.deepClone(rerunBehavior), + }, + description: nls.localize('JsonSchema.tasks.runOptions', 'The task\'s run related options') +}; + const options: IJSONSchema = Objects.deepClone(commonSchema.definitions.options); options.properties.shell = Objects.deepClone(commonSchema.definitions.shellConfiguration); @@ -314,7 +328,8 @@ let taskConfiguration: IJSONSchema = { problemMatcher: { $ref: '#/definitions/problemMatcherType', description: nls.localize('JsonSchema.tasks.matchers', 'The problem matcher(s) to use. Can either be a string or a problem matcher definition or an array of strings and problem matchers.') - } + }, + runOptions: Objects.deepClone(runOptions), } }; @@ -362,6 +377,7 @@ taskDescription.properties.type = Objects.deepClone(taskType); taskDescription.properties.presentation = Objects.deepClone(presentation); taskDescription.properties.terminal = terminal; taskDescription.properties.group = Objects.deepClone(group); +taskDescription.properties.runOptions = Objects.deepClone(runOptions); taskDescription.properties.taskName.deprecationMessage = nls.localize( 'JsonSchema.tasks.taskName.deprecated', 'The task\'s name property is deprecated. Use the label property instead.' 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 7fe4376b8a0..156acce0ee0 100644 --- a/src/vs/workbench/parts/tasks/electron-browser/task.contribution.ts +++ b/src/vs/workbench/parts/tasks/electron-browser/task.contribution.ts @@ -70,11 +70,11 @@ import { Scope, IActionBarRegistry, Extensions as ActionBarExtensions } from 'vs import { ITerminalService } from 'vs/workbench/parts/terminal/common/terminal'; -import { ITaskSystem, ITaskResolver, ITaskSummary, TaskExecuteKind, TaskError, TaskErrors, TaskTerminateResponse, TaskSystemInfo } from 'vs/workbench/parts/tasks/common/taskSystem'; +import { ITaskSystem, ITaskResolver, ITaskSummary, TaskExecuteKind, TaskError, TaskErrors, TaskTerminateResponse, TaskSystemInfo, ITaskExecuteResult } from 'vs/workbench/parts/tasks/common/taskSystem'; import { Task, CustomTask, ConfiguringTask, ContributedTask, InMemoryTask, TaskEvent, TaskEventKind, TaskSet, TaskGroup, GroupType, ExecutionEngine, JsonSchemaVersion, TaskSourceKind, - TaskSorter, TaskIdentifier, KeyedTaskIdentifier, TASK_RUNNING_STATE + TaskSorter, TaskIdentifier, KeyedTaskIdentifier, TASK_RUNNING_STATE, RerunBehavior } from 'vs/workbench/parts/tasks/common/tasks'; import { ITaskService, ITaskProvider, RunOptions, CustomizationProperties, TaskFilter } from 'vs/workbench/parts/tasks/common/taskService'; import { getTemplates as getTaskTemplates } from 'vs/workbench/parts/tasks/common/taskTemplates'; @@ -555,6 +555,10 @@ class TaskService extends Disposable implements ITaskService { this.runTaskCommand(arg); }); + CommandsRegistry.registerCommand('workbench.action.tasks.reRunTask', (accessor, arg) => { + this.reRunTaskCommand(arg); + }); + CommandsRegistry.registerCommand('workbench.action.tasks.restartTask', (accessor, arg) => { this.runRestartTaskCommand(arg); }); @@ -1174,7 +1178,8 @@ class TaskService extends Disposable implements ITaskService { type: 'inMemory', name: id, identifier: id, - dependsOn: extensionTasks.map((task) => { return { workspaceFolder: Task.getWorkspaceFolder(task), task: task._id }; }) + dependsOn: extensionTasks.map((task) => { return { workspaceFolder: Task.getWorkspaceFolder(task), task: task._id }; }), + runOptions: { rerunBehavior: RerunBehavior.reevaluate }, }; return { task, resolver }; } @@ -1223,39 +1228,43 @@ class TaskService extends Disposable implements ITaskService { return ProblemMatcherRegistry.onReady().then(() => { return this.textFileService.saveAll().then((value) => { // make sure all dirty files are saved let executeResult = this.getTaskSystem().run(task, resolver); - let key = Task.getRecentlyUsedKey(task); - if (key) { - this.getRecentlyUsedTasks().set(key, key, Touch.AsOld); - } - if (executeResult.kind === TaskExecuteKind.Active) { - let active = executeResult.active; - if (active.same) { - let message; - if (active.background) { - message = nls.localize('TaskSystem.activeSame.background', 'The task \'{0}\' is already active and in background mode.', Task.getQualifiedLabel(task)); - } else { - message = nls.localize('TaskSystem.activeSame.noBackground', 'The task \'{0}\' is already active.', Task.getQualifiedLabel(task)); - } - this.notificationService.prompt(Severity.Info, message, - [{ - label: nls.localize('terminateTask', "Terminate Task"), - run: () => this.terminate(task) - }, - { - label: nls.localize('restartTask', "Restart Task"), - run: () => this.restart(task) - }], - { sticky: true } - ); - } else { - throw new TaskError(Severity.Warning, nls.localize('TaskSystem.active', 'There is already a task running. Terminate it first before executing another task.'), TaskErrors.RunningTask); - } - } - return executeResult.promise; + return this.handleExecuteResult(executeResult); }); }); } + private handleExecuteResult(executeResult: ITaskExecuteResult): TPromise { + let key = Task.getRecentlyUsedKey(executeResult.task); + if (key) { + this.getRecentlyUsedTasks().set(key, key, Touch.AsOld); + } + if (executeResult.kind === TaskExecuteKind.Active) { + let active = executeResult.active; + if (active.same) { + let message; + if (active.background) { + message = nls.localize('TaskSystem.activeSame.background', 'The task \'{0}\' is already active and in background mode.', Task.getQualifiedLabel(executeResult.task)); + } else { + message = nls.localize('TaskSystem.activeSame.noBackground', 'The task \'{0}\' is already active.', Task.getQualifiedLabel(executeResult.task)); + } + this.notificationService.prompt(Severity.Info, message, + [{ + label: nls.localize('terminateTask', "Terminate Task"), + run: () => this.terminate(executeResult.task) + }, + { + label: nls.localize('restartTask', "Restart Task"), + run: () => this.restart(executeResult.task) + }], + { sticky: true } + ); + } else { + throw new TaskError(Severity.Warning, nls.localize('TaskSystem.active', 'There is already a task running. Terminate it first before executing another task.'), TaskErrors.RunningTask); + } + } + return executeResult.promise; + } + public restart(task: Task): void { if (!this._taskSystem) { return; @@ -1981,6 +1990,24 @@ class TaskService extends Disposable implements ITaskService { }); } + private reRunTaskCommand(arg?: any): void { + if (!this.canRunCommand()) { + return; + } + + ProblemMatcherRegistry.onReady().then(() => { + return this.textFileService.saveAll().then((value) => { // make sure all dirty files are saved + let executeResult = this.getTaskSystem().rerun(); + if (executeResult) { + return this.handleExecuteResult(executeResult); + } else { + this.doRunTaskCommand(); + return undefined; + } + }); + }); + } + private splitPerGroupType(tasks: Task[]): { none: Task[], defaults: Task[], users: Task[] } { let none: Task[] = []; let defaults: Task[] = []; @@ -2529,6 +2556,7 @@ MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, { MenuRegistry.addCommand({ id: ConfigureTaskAction.ID, title: { value: ConfigureTaskAction.TEXT, original: 'Configure Task' }, category: { value: tasksCategory, original: 'Tasks' } }); MenuRegistry.addCommand({ id: 'workbench.action.tasks.showLog', title: { value: nls.localize('ShowLogAction.label', "Show Task Log"), original: 'Show Task Log' }, category: { value: tasksCategory, original: 'Tasks' } }); MenuRegistry.addCommand({ id: 'workbench.action.tasks.runTask', title: { value: nls.localize('RunTaskAction.label', "Run Task"), original: 'Run Task' }, category: { value: tasksCategory, original: 'Tasks' } }); +MenuRegistry.addCommand({ id: 'workbench.action.tasks.reRunTask', title: { value: nls.localize('ReRunTaskAction.label', "Rerun Last Task"), original: 'Rerun Last Task' }, category: { value: tasksCategory, original: 'Tasks' } }); MenuRegistry.addCommand({ id: 'workbench.action.tasks.restartTask', title: { value: nls.localize('RestartTaskAction.label', "Restart Running Task"), original: 'Restart Running Task' }, category: { value: tasksCategory, original: 'Tasks' } }); MenuRegistry.addCommand({ id: 'workbench.action.tasks.showTasks', title: { value: nls.localize('ShowTasksAction.label', "Show Running Tasks"), original: 'Show Running Tasks' }, category: { value: tasksCategory, original: 'Tasks' } }); MenuRegistry.addCommand({ id: 'workbench.action.tasks.terminate', title: { value: nls.localize('TerminateAction.label', "Terminate Task"), original: 'Terminate Task' }, category: { value: tasksCategory, original: 'Tasks' } }); diff --git a/src/vs/workbench/parts/tasks/electron-browser/terminalTaskSystem.ts b/src/vs/workbench/parts/tasks/electron-browser/terminalTaskSystem.ts index ba217e82139..fff47058f52 100644 --- a/src/vs/workbench/parts/tasks/electron-browser/terminalTaskSystem.ts +++ b/src/vs/workbench/parts/tasks/electron-browser/terminalTaskSystem.ts @@ -33,11 +33,11 @@ import { IOutputService, IOutputChannel } from 'vs/workbench/parts/output/common import { StartStopProblemCollector, WatchingProblemCollector, ProblemCollectorEventKind } from 'vs/workbench/parts/tasks/common/problemCollectors'; import { Task, CustomTask, ContributedTask, RevealKind, CommandOptions, ShellConfiguration, RuntimeType, PanelKind, - TaskEvent, TaskEventKind, ShellQuotingOptions, ShellQuoting, CommandString, CommandConfiguration + TaskEvent, TaskEventKind, ShellQuotingOptions, ShellQuoting, CommandString, CommandConfiguration, RerunBehavior } from 'vs/workbench/parts/tasks/common/tasks'; import { ITaskSystem, ITaskSummary, ITaskExecuteResult, TaskExecuteKind, TaskError, TaskErrors, ITaskResolver, - TelemetryEvent, Triggers, TaskTerminateResponse, TaskSystemInfoResovler, TaskSystemInfo, ResolveSet + TelemetryEvent, Triggers, TaskTerminateResponse, TaskSystemInfoResovler, TaskSystemInfo, ResolveSet, ResolvedVariables } from 'vs/workbench/parts/tasks/common/taskSystem'; interface TerminalData { @@ -69,6 +69,35 @@ class VariableResolver { } } + +export class VerifiedTask { + readonly task: Task; + readonly resolver: ITaskResolver; + readonly trigger: string; + resolvedVariables?: ResolvedVariables; + systemInfo?: TaskSystemInfo; + workspaceFolder?: IWorkspaceFolder; + shellLaunchConfig?: IShellLaunchConfig; + + constructor(task: Task, resolver: ITaskResolver, trigger: string) { + this.task = task; + this.resolver = resolver; + this.trigger = trigger; + } + + public verify(): boolean { + return this.trigger && this.resolvedVariables && this.workspaceFolder && (this.shellLaunchConfig !== undefined); + } + + public getVerifiedTask(): { task: Task, resolver: ITaskResolver, trigger: string, resolvedVariables: ResolvedVariables, systemInfo: TaskSystemInfo, workspaceFolder: IWorkspaceFolder, shellLaunchConfig: IShellLaunchConfig } { + if (this.verify()) { + return { task: this.task, resolver: this.resolver, trigger: this.trigger, resolvedVariables: this.resolvedVariables, systemInfo: this.systemInfo, workspaceFolder: this.workspaceFolder, shellLaunchConfig: this.shellLaunchConfig }; + } else { + throw new Error('VerifiedTask was not checked. verify must be checked before getVerifiedTask.'); + } + } +} + export class TerminalTaskSystem implements ITaskSystem { public static TelemetryEventName: string = 'taskService'; @@ -117,6 +146,9 @@ export class TerminalTaskSystem implements ITaskSystem { private idleTaskTerminals: LinkedMap; private sameTaskTerminals: IStringDictionary; private taskSystemInfoResolver: TaskSystemInfoResovler; + private lastTask: VerifiedTask; + private currentTask: VerifiedTask; + private isRerun: boolean; private readonly _onDidStateChange: Emitter; @@ -151,6 +183,7 @@ export class TerminalTaskSystem implements ITaskSystem { } public run(task: Task, resolver: ITaskResolver, trigger: string = Triggers.command): ITaskExecuteResult { + this.currentTask = new VerifiedTask(task, resolver, trigger); let terminalData = this.activeTasks[Task.getMapKey(task)]; if (terminalData && terminalData.promise) { let reveal = RevealKind.Always; @@ -163,11 +196,14 @@ export class TerminalTaskSystem implements ITaskSystem { this.terminalService.setActiveInstance(terminalData.terminal); this.terminalService.showPanel(focus); } - return { kind: TaskExecuteKind.Active, active: { same: true, background: task.isBackground }, promise: terminalData.promise }; + this.lastTask = this.currentTask; + return { kind: TaskExecuteKind.Active, task, active: { same: true, background: task.isBackground }, promise: terminalData.promise }; } try { - return { kind: TaskExecuteKind.Started, started: {}, promise: this.executeTask(task, resolver, trigger) }; + const executeResult = { kind: TaskExecuteKind.Started, task, started: {}, promise: this.executeTask(task, resolver, trigger) }; + this.lastTask = this.currentTask; + return executeResult; } catch (error) { if (error instanceof TaskError) { throw error; @@ -181,6 +217,18 @@ export class TerminalTaskSystem implements ITaskSystem { } } + public rerun(): ITaskExecuteResult | undefined { + if (this.lastTask && this.lastTask.verify()) { + if (this.lastTask.task.runOptions.rerunBehavior === RerunBehavior.useEvaluated) { + this.isRerun = true; + } + const result = this.run(this.lastTask.task, this.lastTask.resolver); + this.isRerun = false; + return result; + } else { + return undefined; + } + } public revealTask(task: Task): boolean { let terminalData = this.activeTasks[Task.getMapKey(task)]; @@ -284,7 +332,11 @@ export class TerminalTaskSystem implements ITaskSystem { return { exitCode: summary.exitCode }; } } - return this.executeCommand(task, trigger); + if (this.isRerun) { + return this.reexecuteCommand(task, trigger); + } else { + return this.executeCommand(task, trigger); + } }); } else { return TPromise.join(promises).then((summaries): ITaskSummary => { @@ -298,15 +350,7 @@ export class TerminalTaskSystem implements ITaskSystem { } } - private executeCommand(task: CustomTask | ContributedTask, trigger: string): TPromise { - let variables = new Set(); - this.collectTaskVariables(variables, task); - let workspaceFolder = Task.getWorkspaceFolder(task); - let taskSystemInfo: TaskSystemInfo; - if (workspaceFolder) { - taskSystemInfo = this.taskSystemInfoResolver(workspaceFolder); - } - let variableResolver: TPromise; + private resolveVariablesFromSet(taskSystemInfo: TaskSystemInfo, workspaceFolder: IWorkspaceFolder, task: CustomTask | ContributedTask, variables: Set): TPromise { let isProcess = task.command && task.command.runtime === RuntimeType.Process; let options = task.command && task.command.options ? task.command.options : undefined; let cwd = options ? options.cwd : undefined; @@ -321,10 +365,13 @@ export class TerminalTaskSystem implements ITaskSystem { } } } + + let resolvedVariables: TPromise; if (taskSystemInfo) { let resolveSet: ResolveSet = { variables }; + if (taskSystemInfo.platform === Platform.Platform.Windows && isProcess) { resolveSet.process = { name: CommandString.value(task.command.name) }; if (cwd) { @@ -334,16 +381,7 @@ export class TerminalTaskSystem implements ITaskSystem { resolveSet.process.path = envPath; } } - variableResolver = taskSystemInfo.resolveVariables(workspaceFolder, resolveSet).then((resolved) => { - let result = new Map(); - resolved.variables.forEach(variable => { - result.set(variable, this.configurationResolverService.resolve(workspaceFolder, variable)); - }); - if (resolved.process !== void 0) { - result.set(TerminalTaskSystem.ProcessVarName, resolved.process); - } - return new VariableResolver(workspaceFolder, taskSystemInfo, result, undefined); - }); + resolvedVariables = taskSystemInfo.resolveVariables(workspaceFolder, resolveSet); } else { let result = new Map(); variables.forEach(variable => { @@ -362,13 +400,54 @@ export class TerminalTaskSystem implements ITaskSystem { } result.set(TerminalTaskSystem.ProcessVarName, processVarValue); } - variableResolver = TPromise.as(new VariableResolver(workspaceFolder, taskSystemInfo, result, this.configurationResolverService)); + let resolvedVariablesResult: ResolvedVariables = { + variables: result, + }; + resolvedVariables = TPromise.as(resolvedVariablesResult); } - return variableResolver.then((resolver) => { - return this.executeInTerminal(task, trigger, resolver); + return resolvedVariables; + } + + private executeCommand(task: CustomTask | ContributedTask, trigger: string): TPromise { + this.currentTask.workspaceFolder = Task.getWorkspaceFolder(task); + if (this.currentTask.workspaceFolder) { + this.currentTask.systemInfo = this.taskSystemInfoResolver(this.currentTask.workspaceFolder); + } + + let variables = new Set(); + this.collectTaskVariables(variables, task); + const resolvedVariables = this.resolveVariablesFromSet(this.currentTask.systemInfo, this.currentTask.workspaceFolder, task, variables); + + return resolvedVariables.then((resolvedVariables) => { + this.currentTask.resolvedVariables = resolvedVariables; + return this.executeInTerminal(task, trigger, new VariableResolver(this.currentTask.workspaceFolder, this.currentTask.systemInfo, resolvedVariables.variables, this.configurationResolverService)); }); } + private reexecuteCommand(task: CustomTask | ContributedTask, trigger: string): TPromise { + this.currentTask.workspaceFolder = this.lastTask.workspaceFolder; + let variables = new Set(); + this.collectTaskVariables(variables, task); + + // Check that the task hasn't changed to include new variables + let hasAllVariables = true; + variables.forEach(value => { + if (value in this.lastTask.getVerifiedTask().resolvedVariables) { + hasAllVariables = false; + } + }); + + if (!hasAllVariables) { + return this.resolveVariablesFromSet(this.lastTask.getVerifiedTask().systemInfo, this.lastTask.getVerifiedTask().workspaceFolder, task, variables).then((resolvedVariables) => { + this.currentTask.resolvedVariables = resolvedVariables; + return this.executeInTerminal(task, trigger, new VariableResolver(this.lastTask.getVerifiedTask().workspaceFolder, this.lastTask.getVerifiedTask().systemInfo, resolvedVariables.variables, this.configurationResolverService)); + }); + } else { + this.currentTask.resolvedVariables = this.lastTask.getVerifiedTask().resolvedVariables; + return this.executeInTerminal(task, trigger, new VariableResolver(this.lastTask.getVerifiedTask().workspaceFolder, this.lastTask.getVerifiedTask().systemInfo, this.lastTask.getVerifiedTask().resolvedVariables.variables, this.configurationResolverService)); + } + } + private executeInTerminal(task: CustomTask | ContributedTask, trigger: string, resolver: VariableResolver): TPromise { let terminal: ITerminalInstance | undefined = undefined; let executedCommand: string | undefined = undefined; @@ -574,27 +653,12 @@ export class TerminalTaskSystem implements ITaskSystem { }); } - private createTerminal(task: CustomTask | ContributedTask, resolver: VariableResolver): [ITerminalInstance, string, TaskError | undefined] { - let platform = resolver.taskSystemInfo ? resolver.taskSystemInfo.platform : Platform.platform; - let options = this.resolveOptions(resolver, task.command.options); - let originalCommand = task.command.name; - let { command, args } = this.resolveCommandAndArgs(resolver, task.command); - let commandExecutable = CommandString.value(command); - let workspaceFolder = Task.getWorkspaceFolder(task); - let needsFolderQualification = workspaceFolder && this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE; - let terminalName = nls.localize('TerminalTaskSystem.terminalName', 'Task - {0}', needsFolderQualification ? Task.getQualifiedLabel(task) : task.name); - let waitOnExit: boolean | string = false; - if (task.command.presentation.reveal !== RevealKind.Never || !task.isBackground) { - if (task.command.presentation.panel === PanelKind.New) { - waitOnExit = nls.localize('closeTerminal', 'Press any key to close the terminal.'); - } else if (task.command.presentation.showReuseMessage) { - waitOnExit = nls.localize('reuseTerminal', 'Terminal will be reused by tasks, press any key to close it.'); - } else { - waitOnExit = true; - } - } + private createShellLaunchConfig(task: CustomTask | ContributedTask, variableResolver: VariableResolver, platform: Platform.Platform, options: CommandOptions, command: CommandString, args: CommandString[], waitOnExit: boolean | string): IShellLaunchConfig | undefined { let shellLaunchConfig: IShellLaunchConfig | undefined = undefined; let isShellCommand = task.command.runtime === RuntimeType.Shell; + let needsFolderQualification = this.currentTask.workspaceFolder && this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE; + let terminalName = nls.localize('TerminalTaskSystem.terminalName', 'Task - {0}', needsFolderQualification ? Task.getQualifiedLabel(task) : task.name); + let originalCommand = task.command.name; if (isShellCommand) { shellLaunchConfig = { name: terminalName, executable: null, args: null, waitOnExit }; this.terminalService.configHelper.mergeDefaultShellPathAndArgs(shellLaunchConfig, platform); @@ -602,11 +666,11 @@ export class TerminalTaskSystem implements ITaskSystem { let shellOptions: ShellConfiguration = task.command.options && task.command.options.shell; if (shellOptions) { if (shellOptions.executable) { - shellLaunchConfig.executable = this.resolveVariable(resolver, shellOptions.executable); + shellLaunchConfig.executable = this.resolveVariable(variableResolver, shellOptions.executable); shellSpecified = true; } if (shellOptions.args) { - shellLaunchConfig.args = this.resolveVariables(resolver, shellOptions.args.slice()); + shellLaunchConfig.args = this.resolveVariables(variableResolver, shellOptions.args.slice()); } else { shellLaunchConfig.args = []; } @@ -628,7 +692,7 @@ export class TerminalTaskSystem implements ITaskSystem { windowsShellArgs = true; let basename = path.basename(shellLaunchConfig.executable).toLowerCase(); if (basename === 'cmd.exe' && ((options.cwd && TPath.isUNC(options.cwd)) || (!options.cwd && TPath.isUNC(process.cwd())))) { - return [undefined, undefined, new TaskError(Severity.Error, nls.localize('TerminalTaskSystem', 'Can\'t execute a shell command on an UNC drive using cmd.exe.'), TaskErrors.UnknownError)]; + return undefined; } if (basename === 'powershell.exe' || basename === 'pwsh.exe') { if (!shellSpecified) { @@ -665,14 +729,15 @@ export class TerminalTaskSystem implements ITaskSystem { shellLaunchConfig.args = windowsShellArgs ? shellArgs.join(' ') : shellArgs; if (task.command.presentation.echo) { if (needsFolderQualification) { - shellLaunchConfig.initialText = `\x1b[1m> Executing task in folder ${workspaceFolder.name}: ${commandLine} <\x1b[0m\n`; + shellLaunchConfig.initialText = `\x1b[1m> Executing task in folder ${this.currentTask.workspaceFolder.name}: ${commandLine} <\x1b[0m\n`; } else { shellLaunchConfig.initialText = `\x1b[1m> Executing task: ${commandLine} <\x1b[0m\n`; } } } else { + let commandExecutable = CommandString.value(command); let executable = !isShellCommand - ? this.resolveVariable(resolver, TerminalTaskSystem.ProcessVarName) + ? this.resolveVariable(variableResolver, TerminalTaskSystem.ProcessVarName) : commandExecutable; // When we have a process task there is no need to quote arguments. So we go ahead and take the string value. @@ -693,12 +758,13 @@ export class TerminalTaskSystem implements ITaskSystem { return args.join(' '); }; if (needsFolderQualification) { - shellLaunchConfig.initialText = `\x1b[1m> Executing task in folder ${workspaceFolder.name}: ${shellLaunchConfig.executable} ${getArgsToEcho(shellLaunchConfig.args)} <\x1b[0m\n`; + shellLaunchConfig.initialText = `\x1b[1m> Executing task in folder ${this.currentTask.workspaceFolder.name}: ${shellLaunchConfig.executable} ${getArgsToEcho(shellLaunchConfig.args)} <\x1b[0m\n`; } else { shellLaunchConfig.initialText = `\x1b[1m> Executing task: ${shellLaunchConfig.executable} ${getArgsToEcho(shellLaunchConfig.args)} <\x1b[0m\n`; } } } + if (options.cwd) { let cwd = options.cwd; let p: typeof path; @@ -722,6 +788,29 @@ export class TerminalTaskSystem implements ITaskSystem { if (options.env) { shellLaunchConfig.env = options.env; } + return shellLaunchConfig; + } + + private createTerminal(task: CustomTask | ContributedTask, resolver: VariableResolver): [ITerminalInstance, string, TaskError | undefined] { + let platform = resolver.taskSystemInfo ? resolver.taskSystemInfo.platform : Platform.platform; + let options = this.resolveOptions(resolver, task.command.options); + let waitOnExit: boolean | string = false; + let { command, args } = this.resolveCommandAndArgs(resolver, task.command); + let commandExecutable = CommandString.value(command); + + if (task.command.presentation.reveal !== RevealKind.Never || !task.isBackground) { + if (task.command.presentation.panel === PanelKind.New) { + waitOnExit = nls.localize('closeTerminal', 'Press any key to close the terminal.'); + } else if (task.command.presentation.showReuseMessage) { + waitOnExit = nls.localize('reuseTerminal', 'Terminal will be reused by tasks, press any key to close it.'); + } else { + waitOnExit = true; + } + } + this.currentTask.shellLaunchConfig = this.isRerun ? this.lastTask.getVerifiedTask().shellLaunchConfig : this.createShellLaunchConfig(task, resolver, platform, options, command, args, waitOnExit); + if (this.currentTask.shellLaunchConfig === undefined) { + return [undefined, undefined, new TaskError(Severity.Error, nls.localize('TerminalTaskSystem', 'Can\'t execute a shell command on an UNC drive using cmd.exe.'), TaskErrors.UnknownError)]; + } let prefersSameTerminal = task.command.presentation.panel === PanelKind.Dedicated; let allowsSharedTerminal = task.command.presentation.panel === PanelKind.Shared; @@ -740,14 +829,14 @@ export class TerminalTaskSystem implements ITaskSystem { } } if (terminalToReuse) { - terminalToReuse.terminal.reuseTerminal(shellLaunchConfig); + terminalToReuse.terminal.reuseTerminal(this.currentTask.shellLaunchConfig); if (task.command.presentation.clear) { terminalToReuse.terminal.clear(); } return [terminalToReuse.terminal, commandExecutable, undefined]; } - const result = this.terminalService.createTerminal(shellLaunchConfig); + const result = this.terminalService.createTerminal(this.currentTask.shellLaunchConfig); const terminalKey = result.id.toString(); result.onDisposed((terminal) => { let terminalData = this.terminals[terminalKey]; diff --git a/src/vs/workbench/parts/tasks/node/processTaskSystem.ts b/src/vs/workbench/parts/tasks/node/processTaskSystem.ts index 717f148e5e4..bc7e662afde 100644 --- a/src/vs/workbench/parts/tasks/node/processTaskSystem.ts +++ b/src/vs/workbench/parts/tasks/node/processTaskSystem.ts @@ -92,7 +92,7 @@ export class ProcessTaskSystem implements ITaskSystem { public run(task: Task): ITaskExecuteResult { if (this.activeTask) { - return { kind: TaskExecuteKind.Active, active: { same: this.activeTask._id === task._id, background: this.activeTask.isBackground }, promise: this.activeTaskPromise }; + return { kind: TaskExecuteKind.Active, task, active: { same: this.activeTask._id === task._id, background: this.activeTask.isBackground }, promise: this.activeTaskPromise }; } return this.executeTask(task); } @@ -194,6 +194,10 @@ export class ProcessTaskSystem implements ITaskSystem { } } + public rerun(): ITaskExecuteResult | undefined { + return undefined; + } + private doExecuteTask(task: CustomTask, telemetryEvent: TelemetryEvent): ITaskExecuteResult { let taskSummary: ITaskSummary = {}; let commandConfig: CommandConfiguration = task.command; @@ -301,8 +305,8 @@ export class ProcessTaskSystem implements ITaskSystem { return this.handleError(task, error); }); let result: ITaskExecuteResult = (task).tscWatch - ? { kind: TaskExecuteKind.Started, started: { restartOnFileChanges: '**/*.ts' }, promise: this.activeTaskPromise } - : { kind: TaskExecuteKind.Started, started: {}, promise: this.activeTaskPromise }; + ? { kind: TaskExecuteKind.Started, task, started: { restartOnFileChanges: '**/*.ts' }, promise: this.activeTaskPromise } + : { kind: TaskExecuteKind.Started, task, started: {}, promise: this.activeTaskPromise }; return result; } else { this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.Start, task)); @@ -345,7 +349,7 @@ export class ProcessTaskSystem implements ITaskSystem { this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.End, task)); return this.handleError(task, error); }); - return { kind: TaskExecuteKind.Started, started: {}, promise: this.activeTaskPromise }; + return { kind: TaskExecuteKind.Started, task, started: {}, promise: this.activeTaskPromise }; } } diff --git a/src/vs/workbench/parts/tasks/node/taskConfiguration.ts b/src/vs/workbench/parts/tasks/node/taskConfiguration.ts index dc2578d5fd3..87ff768450f 100644 --- a/src/vs/workbench/parts/tasks/node/taskConfiguration.ts +++ b/src/vs/workbench/parts/tasks/node/taskConfiguration.ts @@ -119,6 +119,10 @@ export interface PresentationOptions { clear?: boolean; } +export interface RunOptions { + rerunBehavior?: string; +} + export interface TaskIdentifier { type?: string; [name: string]: any; @@ -313,6 +317,11 @@ export interface ConfigurationProperties { * output. */ problemMatcher?: ProblemMatcherConfig.ProblemMatcherType; + + /** + * Task run options. Control run related properties. + */ + runOptions?: RunOptions; } export interface CustomTask extends CommandProperties, ConfigurationProperties { @@ -1285,7 +1294,8 @@ namespace ConfiguringTask { configures: taskIdentifier, _id: `${typeDeclaration.extensionId}.${taskIdentifier._key}`, _source: Objects.assign({}, source, { config: configElement }), - _label: undefined + _label: undefined, + runOptions: { rerunBehavior: external.runOptions ? Tasks.RerunBehavior.fromString(external.runOptions.rerunBehavior) : Tasks.RerunBehavior.reevaluate }, }; let configuration = ConfigurationProperties.from(external, context, true); if (configuration) { @@ -1344,7 +1354,8 @@ namespace CustomTask { name: taskName, identifier: taskName, hasDefinedMatchers: false, - command: undefined + command: undefined, + runOptions: { rerunBehavior: external.runOptions ? Tasks.RerunBehavior.fromString(external.runOptions.rerunBehavior) : Tasks.RerunBehavior.reevaluate } }; let configuration = ConfigurationProperties.from(external, context, false); if (configuration) { @@ -1417,7 +1428,8 @@ namespace CustomTask { command: contributedTask.command, name: configuredProps.name || contributedTask.name, identifier: configuredProps.identifier || contributedTask.identifier, - hasDefinedMatchers: false + hasDefinedMatchers: false, + runOptions: contributedTask.runOptions, }; let resultConfigProps: Tasks.ConfigurationProperties = result; @@ -1852,6 +1864,7 @@ class ConfigurationParser { isBackground: isBackground, problemMatchers: matchers, hasDefinedMatchers: false, + runOptions: { rerunBehavior: Tasks.RerunBehavior.reevaluate }, }; let value = GroupKind.from(fileConfig.group); if (value) { diff --git a/src/vs/workbench/parts/tasks/test/electron-browser/configuration.test.ts b/src/vs/workbench/parts/tasks/test/electron-browser/configuration.test.ts index 6adf850b5cc..dbfb58f1134 100644 --- a/src/vs/workbench/parts/tasks/test/electron-browser/configuration.test.ts +++ b/src/vs/workbench/parts/tasks/test/electron-browser/configuration.test.ts @@ -192,7 +192,8 @@ class CustomTaskBuilder { isBackground: false, promptOnClose: true, problemMatchers: [], - hasDefinedMatchers: false + hasDefinedMatchers: false, + runOptions: { rerunBehavior: Tasks.RerunBehavior.reevaluate }, }; } diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminal.contribution.ts b/src/vs/workbench/parts/terminal/electron-browser/terminal.contribution.ts index 2cf59b28164..db8cc15c3da 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminal.contribution.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminal.contribution.ts @@ -300,6 +300,7 @@ configurationRegistry.registerConfiguration({ 'workbench.action.tasks.build', 'workbench.action.tasks.restartTask', 'workbench.action.tasks.runTask', + 'workbench.action.tasks.reRunTask', 'workbench.action.tasks.showLog', 'workbench.action.tasks.showTasks', 'workbench.action.tasks.terminate',