From 2f5061ff2d68637c1ab9c10d06a188333f29c22d Mon Sep 17 00:00:00 2001 From: Dirk Baeumer Date: Tue, 20 Mar 2018 22:19:00 +0100 Subject: [PATCH] Implements #45664: Add API to query and execute tasks --- src/vs/vscode.proposed.d.ts | 59 ++- .../api/electron-browser/mainThreadTask.ts | 383 +++++++++++++++++- src/vs/workbench/api/node/extHost.api.impl.ts | 17 +- src/vs/workbench/api/node/extHost.protocol.ts | 6 + .../workbench/api/node/extHostApiCommands.ts | 37 +- src/vs/workbench/api/node/extHostTask.ts | 295 +++++++++++++- src/vs/workbench/api/node/extHostTypes.ts | 38 ++ src/vs/workbench/api/shared/tasks.ts | 88 ++++ .../parts/tasks/common/taskService.ts | 4 +- src/vs/workbench/parts/tasks/common/tasks.ts | 58 ++- .../electron-browser/task.contribution.ts | 25 +- .../electron-browser/terminalTaskSystem.ts | 4 + .../api/extHostApiCommands.test.ts | 5 +- 13 files changed, 941 insertions(+), 78 deletions(-) create mode 100644 src/vs/workbench/api/shared/tasks.ts diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 820c5b7762b..4d855f14639 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -781,32 +781,65 @@ declare module 'vscode' { //#region Tasks /** - * A task item represents a task in the system. It can be used to - * present task information in the user interface or to execute the - * underlying task. + * An object representing an executed Task. It can be used + * to terminate a task. */ - export interface TaskItem { + export interface TaskExecution { + } + + /** + * An event signaling the start of a task execution. + */ + interface TaskStartEvent { + /** + * The task item representing the task that got started. + */ + execution: TaskExecution; + } + + /** + * An event signaling the end of an executed task. + */ + interface TaskEndEvent { + /** + * The task item representing the task that finished. + */ + execution: TaskExecution; + } + + export namespace workspace { /** - * A unique ID representing the underlying task. + * Fetches all task available in the systems. This includes tasks + * from `tasks.json` files as well as tasks from task providers + * contributed through extensions. */ - readonly id: string; + export function fetchTasks(): Thenable; /** - * A human readable label of the task. + * Executes a task that is managed by VS Code. The returned + * task execution can be used to terminate the task. + * + * @param task the task to execute */ - readonly label: string; + export function executeTask(task: Task): Thenable; /** - * The task definition. + * Fires when a task starts. + */ + export const onDidStartTask: Event; + + /** + * Terminates a task that was previously started using `executeTask` + * + * @param task the task to terminate */ - readonly definition: TaskDefinition; + export function terminateTask(task: TaskExecution): void; /** - * The workspace folder the task belongs to. Is undefined - * to tasks that aren't scoped to a workspace folder. + * Fires when a task ends. */ - readonly workspaceFolder: WorkspaceFolder | undefined; + export const onDidEndTask: Event; } //#endregion diff --git a/src/vs/workbench/api/electron-browser/mainThreadTask.ts b/src/vs/workbench/api/electron-browser/mainThreadTask.ts index b6bf7cc83c5..03fa8d2b3b8 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadTask.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadTask.ts @@ -4,16 +4,327 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; +import * as crypto from 'crypto'; + +import * as nls from 'vs/nls'; + +import URI from 'vs/base/common/uri'; +import * as Objects from 'vs/base/common/objects'; import { TPromise } from 'vs/base/common/winjs.base'; +import * as Types from 'vs/base/common/types'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -import { ContributedTask, ExtensionTaskSourceTransfer } from 'vs/workbench/parts/tasks/common/tasks'; +import { + ContributedTask, ExtensionTaskSourceTransfer, TaskIdentifier, TaskExecution, Task, TaskEvent, TaskEventKind, + PresentationOptions, CommandOptions, CommandConfiguration, RuntimeType, CustomTask, TaskScope, TaskSource, TaskSourceKind, ExtensionTaskSource +} from 'vs/workbench/parts/tasks/common/tasks'; import { ITaskService } from 'vs/workbench/parts/tasks/common/taskService'; -import { ExtHostContext, MainThreadTaskShape, ExtHostTaskShape, MainContext, IExtHostContext } from '../node/extHost.protocol'; + import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; -import URI from 'vs/base/common/uri'; +import { ExtHostContext, MainThreadTaskShape, ExtHostTaskShape, MainContext, IExtHostContext } from 'vs/workbench/api/node/extHost.protocol'; +import { + TaskDefinitionDTO, TaskExecutionDTO, ProcessExecutionOptionsDTO, TaskPresentationOptionsDTO, + ProcessExecutionDTO, ShellExecutionDTO, ShellExecutionOptionsDTO, TaskDTO, TaskSourceDTO, TaskHandleDTO +} from 'vs/workbench/api/shared/tasks'; + +export { TaskDTO, TaskHandleDTO, TaskExecutionDTO }; + +namespace TaskExecutionDTO { + export function from(value: TaskExecution): TaskExecutionDTO { + return { + id: value.id, + }; + } + export function to(value: TaskExecutionDTO, workspace: IWorkspaceContextService): TaskExecution { + return { + id: value.id, + }; + } +} + +namespace TaskDefinitionDTO { + export function from(value: TaskIdentifier): TaskDefinitionDTO { + let result = Objects.assign(Object.create(null), value); + delete result._key; + return result; + } + export function to(value: TaskDefinitionDTO): TaskIdentifier { + const hash = crypto.createHash('md5'); + hash.update(JSON.stringify(value)); + let result = Objects.assign(Object.create(null), value); + result._key = hash.digest('hex'); + return result; + } +} + +namespace TaskPresentationOptionsDTO { + export function from(value: PresentationOptions): TaskPresentationOptionsDTO { + if (value === void 0 || value === null) { + return undefined; + } + return Objects.assign(Object.create(null), value); + } + export function to(value: TaskPresentationOptionsDTO): PresentationOptions { + 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) { + return undefined; + } + return { + cwd: value.cwd, + env: value.env + }; + } + export function to(value: ProcessExecutionOptionsDTO): CommandOptions { + if (value === void 0 || value === null) { + return undefined; + } + return { + cwd: value.cwd, + env: value.env + }; + } +} + +namespace ProcessExecutionDTO { + export function is(value: ShellExecutionDTO | ProcessExecutionDTO): value is ProcessExecutionDTO { + let candidate = value as ProcessExecutionDTO; + return candidate && !!candidate.process; + } + export function from(value: CommandConfiguration): ProcessExecutionDTO { + let process: string = Types.isString(value.name) ? value.name : value.name.value; + let args: string[] = value.args ? value.args.map(value => Types.isString(value) ? value : value.value) : []; + let result: ProcessExecutionDTO = { + process: process, + args: args + }; + if (value.options) { + result.options = ProcessExecutionOptionsDTO.from(value.options); + } + return result; + } + export function to(value: ProcessExecutionDTO): CommandConfiguration { + let result: CommandConfiguration = { + runtime: RuntimeType.Process, + name: value.process, + args: value.args, + presentation: undefined + }; + if (value.options) { + result.options = ProcessExecutionOptionsDTO.to(value.options); + } + return result; + } +} + +namespace ShellExecutionOptionsDTO { + export function from(value: CommandOptions): ShellExecutionOptionsDTO { + if (value === void 0 || value === null) { + return undefined; + } + let result: ShellExecutionOptionsDTO = { + cwd: value.cwd, + env: value.env + }; + if (value.shell) { + result.executable = value.shell.executable; + result.shellArgs = value.shell.args; + result.shellQuoting = value.shell.quoting; + } + return result; + } + export function to(value: ShellExecutionOptionsDTO): CommandOptions { + if (value === void 0 || value === null) { + return undefined; + } + let result: CommandOptions = { + cwd: value.cwd, + env: value.env + }; + if (value.executable) { + result.shell = { + executable: value.executable + }; + if (value.shellArgs) { + result.shell.args = value.shellArgs; + } + if (value.shellQuoting) { + result.shell.quoting = value.shellQuoting; + } + } + return result; + } +} + +namespace ShellExecutionDTO { + export function is(value: ShellExecutionDTO | ProcessExecutionDTO): value is ShellExecutionDTO { + let candidate = value as ShellExecutionDTO; + return candidate && (!!candidate.commandLine || !!candidate.command); + } + export function from(value: CommandConfiguration): ShellExecutionDTO { + let result: ShellExecutionDTO = {}; + if (value.name && Types.isString(value.name) && (value.args === void 0 || value.args === null || value.args.length === 0)) { + result.commandLine = value.name; + } else { + result.command = value.name; + result.args = value.args; + } + if (value.options) { + result.options = ShellExecutionOptionsDTO.from(value.options); + } + return result; + } + export function to(value: ShellExecutionDTO): CommandConfiguration { + let result: CommandConfiguration = { + runtime: RuntimeType.Shell, + name: value.commandLine ? value.commandLine : value.command, + args: value.args, + presentation: undefined + }; + if (value.options) { + result.options = ShellExecutionOptionsDTO.to(value.options); + } + return result; + } +} + +namespace TaskSourceDTO { + export function from(value: TaskSource): TaskSourceDTO { + let result: TaskSourceDTO = { + label: value.label + }; + if (value.kind === TaskSourceKind.Extension) { + result.extensionId = value.extension; + if (value.workspaceFolder) { + result.scope = value.workspaceFolder.uri; + } else { + result.scope = value.scope; + } + } else if (value.kind === TaskSourceKind.Workspace) { + result.extensionId = '$core'; + result.scope = value.config.workspaceFolder.uri; + } + return result; + } + export function to(value: TaskSourceDTO, workspace: IWorkspaceContextService): ExtensionTaskSource { + let scope: TaskScope; + let workspaceFolder: IWorkspaceFolder; + if (value.scope === void 0) { + if (workspace.getWorkspace().folders.length === 0) { + scope = TaskScope.Global; + workspaceFolder = undefined; + } else { + scope = TaskScope.Folder; + workspaceFolder = workspace.getWorkspace().folders[0]; + } + } else if (typeof value.scope === 'number') { + scope = value.scope; + } else { + scope = TaskScope.Folder; + workspaceFolder = workspace.getWorkspaceFolder(URI.revive(value.scope)); + } + let result: ExtensionTaskSource = { + kind: TaskSourceKind.Extension, + label: value.label, + extension: value.extensionId, + scope, + workspaceFolder + }; + return result; + } +} + +namespace TaskHandleDTO { + export function is(value: any): value is TaskHandleDTO { + let candidate: TaskHandleDTO = value; + return candidate && Types.isString(candidate.id) && !!candidate.workspaceFolder; + } +} + +namespace TaskDTO { + export function from(task: Task): TaskDTO { + if (task === void 0 || task === null || (!CustomTask.is(task) && !ContributedTask.is(task))) { + return undefined; + } + let result: TaskDTO = { + _id: task._id, + name: task.name, + definition: TaskDefinitionDTO.from(Task.getTaskDefinition(task)), + source: TaskSourceDTO.from(task._source), + execution: undefined, + presentationOptions: task.command ? TaskPresentationOptionsDTO.from(task.command.presentation) : undefined, + isBackground: task.isBackground, + problemMatchers: [], + hasDefinedMatchers: ContributedTask.is(task) ? task.hasDefinedMatchers : false + }; + if (task.group) { + result.group = task.group; + } + if (task.command) { + if (task.command.runtime === RuntimeType.Process) { + result.execution = ProcessExecutionDTO.from(task.command); + } else if (task.command.runtime === RuntimeType.Shell) { + result.execution = ShellExecutionDTO.from(task.command); + } + } + if (task.problemMatchers) { + for (let matcher of task.problemMatchers) { + if (Types.isString(matcher)) { + result.problemMatchers.push(matcher); + } + } + } + if (!result.execution) { + return undefined; + } + return result; + } + + export function to(task: TaskDTO, workspace: IWorkspaceContextService): Task { + if (typeof task.name !== 'string') { + return undefined; + } + let command: CommandConfiguration; + if (ShellExecutionDTO.is(task.execution)) { + command = ShellExecutionDTO.to(task.execution); + } else if (ProcessExecutionDTO.is(task.execution)) { + command = ProcessExecutionDTO.to(task.execution); + } + if (!command) { + return undefined; + } + command.presentation = TaskPresentationOptionsDTO.to(task.presentationOptions); + let source = TaskSourceDTO.to(task.source, workspace); + + let label = nls.localize('task.label', '{0}: {1}', source.label, task.name); + let definition = TaskDefinitionDTO.to(task.definition); + let id = `${task.source.extensionId}.${definition._key}`; + let result: ContributedTask = { + _id: id, // uuidMap.getUUID(identifier), + _source: source, + _label: label, + type: definition.type, + defines: definition, + name: task.name, + identifier: label, + group: task.group, + command: command, + isBackground: !!task.isBackground, + problemMatchers: task.problemMatchers.slice(), + hasDefinedMatchers: task.hasDefinedMatchers + }; + return result; + } +} @extHostNamedCustomer(MainContext.MainThreadTask) export class MainThreadTask implements MainThreadTaskShape { @@ -28,6 +339,14 @@ export class MainThreadTask implements MainThreadTaskShape { ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTask); this._activeHandles = Object.create(null); + this._taskService.onDidStateChange((event: TaskEvent) => { + let task = event.__task; + if (event.kind === TaskEventKind.Start) { + this._proxy.$taskStarted(TaskExecutionDTO.from(Task.getTaskExecution(task))); + } else if (event.kind === TaskEventKind.End) { + this._proxy.$taskEnded(TaskExecutionDTO.from(Task.getTaskExecution(task))); + } + }); } public dispose(): void { @@ -63,4 +382,60 @@ export class MainThreadTask implements MainThreadTaskShape { delete this._activeHandles[handle]; return TPromise.wrap(undefined); } + + public $executeTaskProvider(): TPromise { + return this._taskService.tasks().then((tasks) => { + let result: TaskDTO[] = []; + for (let task of tasks) { + let item = TaskDTO.from(task); + if (item) { + result.push(item); + } + } + return result; + }); + } + + public $executeTask(value: TaskHandleDTO | TaskDTO): TPromise { + return new TPromise((resolve, reject) => { + if (TaskHandleDTO.is(value)) { + let workspaceFolder = this._workspaceContextServer.getWorkspaceFolder(URI.revive(value.workspaceFolder)); + this._taskService.getTask(workspaceFolder, value.id, true).then((task: Task) => { + this._taskService.run(task); + let result: TaskExecutionDTO = { + id: value.id + }; + resolve(result); + }, (error) => { + reject(new Error('Task not found')); + }); + } else { + let task = TaskDTO.to(value, this._workspaceContextServer); + this._taskService.run(task); + let result: TaskExecutionDTO = { + id: task._id + }; + resolve(result); + } + }); + } + + public $terminateTask(value: TaskExecutionDTO): TPromise { + let execution: TaskExecution = TaskExecutionDTO.to(value, this._workspaceContextServer); + return new TPromise((resolve, reject) => { + this._taskService.getActiveTasks().then((tasks) => { + for (let task of tasks) { + if (execution.id === task._id) { + this._taskService.terminate(task).then((value) => { + resolve(undefined); + }, (error) => { + reject(undefined); + }); + return; + } + } + reject(new Error('Task to terminate not found')); + }); + }); + } } diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index aded2356984..bea7d79834a 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -132,7 +132,7 @@ export function createApiFactory( const extHostLanguages = new ExtHostLanguages(rpcProtocol); // Register API-ish commands - ExtHostApiCommands.register(extHostCommands, extHostWorkspace); + ExtHostApiCommands.register(extHostCommands, extHostTask); return function (extension: IExtensionDescription): typeof vscode { @@ -517,6 +517,21 @@ export function createApiFactory( registerTaskProvider: (type: string, provider: vscode.TaskProvider) => { return extHostTask.registerTaskProvider(extension, provider); }, + fetchTasks: proposedApiFunction(extension, (): Thenable => { + return extHostTask.executeTaskProvider(); + }), + executeTask: proposedApiFunction(extension, (task: vscode.Task): Thenable => { + return extHostTask.executeTask(extension, task); + }), + onDidStartTask: (listeners, thisArgs?, disposables?) => { + return extHostTask.onDidStartTask(listeners, thisArgs, disposables); + }, + terminateTask: proposedApiFunction(extension, (task: vscode.TaskExecution): void => { + extHostTask.terminateTask(task); + }), + onDidEndTask: (listeners, thisArgs?, disposables?) => { + return extHostTask.onDidEndTask(listeners, thisArgs, disposables); + }, registerFileSystemProvider: proposedApiFunction(extension, (scheme, provider) => { return extHostFileSystem.registerFileSystemProvider(scheme, provider); }), diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index e2027c4815f..43bd8d279c7 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -48,6 +48,7 @@ import { CommentRule, CharacterPair, EnterAction } from 'vs/editor/common/modes/ import { ISingleEditOperation } from 'vs/editor/common/model'; import { ILineMatch, IPatternInfo } from 'vs/platform/search/common/search'; import { LogLevel } from 'vs/platform/log/common/log'; +import { TaskExecutionDTO, TaskDTO, TaskHandleDTO } from 'vs/workbench/api/shared/tasks'; export interface IEnvironment { isExtensionDevelopmentDebug: boolean; @@ -388,6 +389,9 @@ export interface MainThreadFileSystemShape extends IDisposable { export interface MainThreadTaskShape extends IDisposable { $registerTaskProvider(handle: number): TPromise; + $executeTaskProvider(): TPromise; + $executeTask(task: TaskHandleDTO | TaskDTO): TPromise; + $terminateTask(task: TaskExecutionDTO): TPromise; $unregisterTaskProvider(handle: number): TPromise; } @@ -727,6 +731,8 @@ export interface ExtHostSCMShape { export interface ExtHostTaskShape { $provideTasks(handle: number): TPromise; + $taskStarted(execution: TaskExecutionDTO): void; + $taskEnded(execution: TaskExecutionDTO): void; } export interface IBreakpointDto { diff --git a/src/vs/workbench/api/node/extHostApiCommands.ts b/src/vs/workbench/api/node/extHostApiCommands.ts index 9239a90a5f2..ee9a32cc41c 100644 --- a/src/vs/workbench/api/node/extHostApiCommands.ts +++ b/src/vs/workbench/api/node/extHostApiCommands.ts @@ -6,7 +6,6 @@ import URI from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; -import * as Objects from 'vs/base/common/objects'; import { IDisposable } from 'vs/base/common/lifecycle'; import * as vscode from 'vscode'; import * as typeConverters from 'vs/workbench/api/node/extHostTypeConverters'; @@ -18,22 +17,21 @@ import { ExtHostCommands } from 'vs/workbench/api/node/extHostCommands'; import { IWorkspaceSymbolProvider } from 'vs/workbench/parts/search/common/search'; import { Position as EditorPosition, ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { CustomCodeAction } from 'vs/workbench/api/node/extHostLanguageFeatures'; -import * as TaskSystem from 'vs/workbench/parts/tasks/common/tasks'; -import { ExtHostWorkspace } from './extHostWorkspace'; +import { ExtHostTask } from './extHostTask'; export class ExtHostApiCommands { - static register(commands: ExtHostCommands, workspace: ExtHostWorkspace) { + static register(commands: ExtHostCommands, workspace: ExtHostTask) { return new ExtHostApiCommands(commands, workspace).registerCommands(); } private _commands: ExtHostCommands; - private _workspace: ExtHostWorkspace; + private _tasks: ExtHostTask; private _disposables: IDisposable[] = []; - private constructor(commands: ExtHostCommands, workspace: ExtHostWorkspace) { + private constructor(commands: ExtHostCommands, task: ExtHostTask) { this._commands = commands; - this._workspace = workspace; + this._tasks = task; } registerCommands() { @@ -476,29 +474,8 @@ export class ExtHostApiCommands { .then(tryMapWith(typeConverters.DocumentLink.to)); } - private _executeTaskProvider(): Thenable { - return this._commands.executeCommand('_executeTaskProvider').then((values) => { - let workspace = this._workspace; - return values.map(handle => { - let definition: vscode.TaskDefinition = Objects.assign(Object.create(null), handle.definition); - delete definition._key; - let uri = URI.revive(handle.workspaceFolderUri); - return new class { - get id(): string { - return handle.id; - } - get label(): string { - return handle.label; - } - get definition(): vscode.TaskDefinition { - return definition; - } - get workspaceFolder(): vscode.WorkspaceFolder { - return uri ? workspace.resolveWorkspaceFolder(uri) : undefined; - } - }; - }); - }); + private _executeTaskProvider(): Thenable { + return this._tasks.executeTaskProvider(); } } diff --git a/src/vs/workbench/api/node/extHostTask.ts b/src/vs/workbench/api/node/extHostTask.ts index 937f0985ea0..828b31b8c96 100644 --- a/src/vs/workbench/api/node/extHostTask.ts +++ b/src/vs/workbench/api/node/extHostTask.ts @@ -4,11 +4,12 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import URI from 'vs/base/common/uri'; +import URI, { UriComponents } from 'vs/base/common/uri'; import * as nls from 'vs/nls'; import { TPromise } from 'vs/base/common/winjs.base'; import * as Objects from 'vs/base/common/objects'; import { asWinJsPromise } from 'vs/base/common/async'; +import { Event, Emitter } from 'vs/base/common/event'; import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; import * as TaskSystem from 'vs/workbench/parts/tasks/common/tasks'; @@ -18,7 +19,12 @@ import { MainContext, MainThreadTaskShape, ExtHostTaskShape, IMainContext } from import * as types from 'vs/workbench/api/node/extHostTypes'; import { ExtHostWorkspace } from 'vs/workbench/api/node/extHostWorkspace'; import * as vscode from 'vscode'; +import { + TaskDefinitionDTO, TaskExecutionDTO, TaskPresentationOptionsDTO, ProcessExecutionOptionsDTO, ProcessExecutionDTO, + ShellExecutionOptionsDTO, ShellExecutionDTO, TaskDTO, TaskHandleDTO +} from '../shared/tasks'; +export { TaskExecutionDTO }; /* namespace ProblemPattern { @@ -450,6 +456,240 @@ namespace Tasks { } } +namespace TaskDefinitionDTO { + export function from(value: vscode.TaskDefinition): TaskDefinitionDTO { + if (value === void 0 || value === null) { + return undefined; + } + return value; + } + export function to(value: TaskDefinitionDTO): vscode.TaskDefinition { + if (value === void 0 || value === null) { + return undefined; + } + return value; + } +} + +namespace TaskPresentationOptionsDTO { + export function from(value: vscode.TaskPresentationOptions): TaskPresentationOptionsDTO { + if (value === void 0 || value === null) { + return undefined; + } + return value; + } + export function to(value: TaskPresentationOptionsDTO): vscode.TaskPresentationOptions { + if (value === void 0 || value === null) { + return undefined; + } + return value; + } +} + +namespace ProcessExecutionOptionsDTO { + export function from(value: vscode.ProcessExecutionOptions): ProcessExecutionOptionsDTO { + if (value === void 0 || value === null) { + return undefined; + } + return value; + } + export function to(value: ProcessExecutionOptionsDTO): vscode.ProcessExecutionOptions { + if (value === void 0 || value === null) { + return undefined; + } + return value; + } +} + +namespace ProcessExecutionDTO { + export function is(value: ShellExecutionDTO | ProcessExecutionDTO): value is ProcessExecutionDTO { + let candidate = value as ProcessExecutionDTO; + return candidate && !!candidate.process; + } + export function from(value: vscode.ProcessExecution): ProcessExecutionDTO { + if (value === void 0 || value === null) { + return undefined; + } + let result: ProcessExecutionDTO = { + process: value.process, + args: value.args + }; + if (value.options) { + result.options = ProcessExecutionOptionsDTO.from(value.options); + } + return result; + } + export function to(value: ProcessExecutionDTO): types.ProcessExecution { + if (value === void 0 || value === null) { + return undefined; + } + return new types.ProcessExecution(value.process, value.args, value.options); + } +} + +namespace ShellExecutionOptionsDTO { + export function from(value: vscode.ShellExecutionOptions): ShellExecutionOptionsDTO { + if (value === void 0 || value === null) { + return undefined; + } + return value; + } + export function to(value: ShellExecutionOptionsDTO): vscode.ShellExecutionOptions { + if (value === void 0 || value === null) { + return undefined; + } + return value; + } +} + +namespace ShellExecutionDTO { + export function is(value: ShellExecutionDTO | ProcessExecutionDTO): value is ShellExecutionDTO { + let candidate = value as ShellExecutionDTO; + return candidate && (!!candidate.commandLine || !!candidate.command); + } + export function from(value: vscode.ShellExecution): ShellExecutionDTO { + if (value === void 0 || value === null) { + return undefined; + } + let result: ShellExecutionDTO = { + }; + if (value.commandLine !== void 0) { + result.commandLine = value.commandLine; + } else { + result.command = value.command; + result.args = value.args; + } + if (value.options) { + result.options = ShellExecutionOptionsDTO.from(value.options); + } + return result; + } + export function to(value: ShellExecutionDTO): types.ShellExecution { + if (value === void 0 || value === null) { + return undefined; + } + if (value.commandLine) { + return new types.ShellExecution(value.commandLine, value.options); + } else { + return new types.ShellExecution(value.command, value.args ? value.args : [], value.options); + } + } +} + +namespace TaskHandleDTO { + export function from(value: types.Task): TaskHandleDTO { + let folder: UriComponents; + if (value.scope !== void 0 && typeof value.scope !== 'number') { + folder = value.scope.uri; + } + return { + id: value._id, + workspaceFolder: folder + }; + } +} + +namespace TaskDTO { + + export function from(value: vscode.Task, extension: IExtensionDescription): TaskDTO { + if (value === void 0 || value === null) { + return undefined; + } + let execution: ShellExecutionDTO | ProcessExecutionDTO; + if (value.execution instanceof types.ProcessExecution) { + execution = ProcessExecutionDTO.from(value.execution); + } else if (value.execution instanceof types.ShellExecution) { + execution = ShellExecutionDTO.from(value.execution); + } + let definition: TaskDefinitionDTO = TaskDefinitionDTO.from(value.definition); + let scope: number | UriComponents; + if (value.scope) { + if (typeof value.scope === 'number') { + scope = value.scope; + } else { + scope = value.scope.uri.toJSON(); + } + } + if (!execution || !definition || !scope) { + return undefined; + } + let result: TaskDTO = { + _id: (value as types.Task)._id, + definition, + name: value.name, + source: { + extensionId: extension.id, + label: value.source, + scope: scope + }, + execution, + isBackground: value.isBackground, + group: (value.group as types.TaskGroup).id, + presentationOptions: TaskPresentationOptionsDTO.from(value.presentationOptions), + problemMatchers: value.problemMatchers, + hasDefinedMatchers: (value as types.Task).hasDefinedMatchers + }; + return result; + } + export function to(value: TaskDTO, workspace: ExtHostWorkspace): types.Task { + if (value === void 0 || value === null) { + return undefined; + } + let execution: types.ShellExecution | types.ProcessExecution; + if (ProcessExecutionDTO.is(value.execution)) { + execution = ProcessExecutionDTO.to(value.execution); + } else if (ShellExecutionDTO.is(value.execution)) { + execution = ShellExecutionDTO.to(value.execution); + } + let definition: vscode.TaskDefinition = TaskDefinitionDTO.to(value.definition); + let scope: vscode.TaskScope.Global | vscode.TaskScope.Workspace | vscode.WorkspaceFolder; + if (value.source) { + if (value.source.scope !== void 0) { + if (typeof value.source.scope === 'number') { + scope = value.source.scope; + } else { + scope = workspace.resolveWorkspaceFolder(URI.revive(value.source.scope)); + } + } else { + scope = types.TaskScope.Workspace; + } + } + if (!execution || !definition || !scope) { + return undefined; + } + let result = new types.Task(definition, scope, value.name, value.source.label, execution, value.problemMatchers); + if (value.isBackground !== void 0) { + result.isBackground = value.isBackground; + } + if (value.group !== void 0) { + result.group = types.TaskGroup.from(value.group); + } + if (value.presentationOptions) { + result.presentationOptions = TaskPresentationOptionsDTO.to(value.presentationOptions); + } + if (value._id) { + result._id = value._id; + } + return result; + } +} + +class TaskExecutionImpl implements vscode.TaskExecution { + constructor(readonly _id: string) { + } +} + +namespace TaskExecutionDTO { + export function to(value: TaskExecutionDTO): vscode.TaskExecution { + return new TaskExecutionImpl(value.id); + } + export function from(value: vscode.TaskExecution): TaskExecutionDTO { + return { + id: (value as TaskExecutionImpl)._id + }; + } +} + interface HandlerData { provider: vscode.TaskProvider; extension: IExtensionDescription; @@ -462,6 +702,9 @@ export class ExtHostTask implements ExtHostTaskShape { private _handleCounter: number; private _handlers: Map; + private readonly _onDidExecuteTask: Emitter = new Emitter(); + private readonly _onDidTerminateTask: Emitter = new Emitter(); + constructor(mainContext: IMainContext, extHostWorkspace: ExtHostWorkspace) { this._proxy = mainContext.getProxy(MainContext.MainThreadTask); this._extHostWorkspace = extHostWorkspace; @@ -482,6 +725,56 @@ export class ExtHostTask implements ExtHostTaskShape { }); } + public executeTaskProvider(): Thenable { + return this._proxy.$executeTaskProvider().then((values) => { + let result: vscode.Task[] = []; + for (let value of values) { + let task = TaskDTO.to(value, this._extHostWorkspace); + if (task) { + result.push(task); + } + } + return result; + }); + } + + public executeTask(extension: IExtensionDescription, task: vscode.Task): Thenable { + let tTask = (task as types.Task); + // We have a preserved ID. So the task didn't change. + if (tTask._id !== void 0) { + return this._proxy.$executeTask(TaskHandleDTO.from(tTask)).then(value => TaskExecutionDTO.to(value)); + } else { + return this._proxy.$executeTask(TaskDTO.from(task, extension)).then(value => TaskExecutionDTO.to(value)); + } + } + + public $taskStarted(execution: TaskExecutionDTO): void { + this._onDidExecuteTask.fire({ + execution: TaskExecutionDTO.to(execution) + }); + } + + get onDidStartTask(): Event { + return this._onDidExecuteTask.event; + } + + public terminateTask(execution: vscode.TaskExecution): TPromise { + if (!(execution instanceof TaskExecutionImpl)) { + throw new Error('No valid task execution provided'); + } + return this._proxy.$terminateTask(TaskExecutionDTO.from(execution)); + } + + public $taskEnded(execution: TaskExecutionDTO): void { + this._onDidTerminateTask.fire({ + execution: TaskExecutionDTO.to(execution) + }); + } + + get onDidEndTask(): Event { + return this._onDidTerminateTask.event; + } + public $provideTasks(handle: number): TPromise { let handler = this._handlers.get(handle); if (!handler) { diff --git a/src/vs/workbench/api/node/extHostTypes.ts b/src/vs/workbench/api/node/extHostTypes.ts index cb0319e4855..ca2e6190759 100644 --- a/src/vs/workbench/api/node/extHostTypes.ts +++ b/src/vs/workbench/api/node/extHostTypes.ts @@ -1257,6 +1257,21 @@ export class TaskGroup implements vscode.TaskGroup { public static Test: TaskGroup = new TaskGroup('test', 'Test'); + public static from(value: string) { + switch (value) { + case 'clean': + return TaskGroup.Clean; + case 'build': + return TaskGroup.Build; + case 'rebuild': + return TaskGroup.Rebuild; + case 'test': + return TaskGroup.Test; + default: + return undefined; + } + } + constructor(id: string, _label: string) { if (typeof id !== 'string') { throw illegalArgument('name'); @@ -1411,6 +1426,8 @@ export enum TaskScope { export class Task implements vscode.Task { + private __id: string; + private _definition: vscode.TaskDefinition; private _definitionKey: string; private _scope: vscode.TaskScope.Global | vscode.TaskScope.Workspace | vscode.WorkspaceFolder; @@ -1459,6 +1476,18 @@ export class Task implements vscode.Task { this._isBackground = false; } + get _id(): string { + return this.__id; + } + + set _id(value: string) { + this.__id = value; + } + + private clear(): void { + this.__id = undefined; + } + get definition(): vscode.TaskDefinition { return this._definition; } @@ -1467,6 +1496,7 @@ export class Task implements vscode.Task { if (value === void 0 || value === null) { throw illegalArgument('Kind can\'t be undefined or null'); } + this.clear(); this._definitionKey = undefined; this._definition = value; } @@ -1485,6 +1515,7 @@ export class Task implements vscode.Task { } set target(value: vscode.TaskScope.Global | vscode.TaskScope.Workspace | vscode.WorkspaceFolder) { + this.clear(); this._scope = value; } @@ -1496,6 +1527,7 @@ export class Task implements vscode.Task { if (typeof value !== 'string') { throw illegalArgument('name'); } + this.clear(); this._name = value; } @@ -1507,6 +1539,7 @@ export class Task implements vscode.Task { if (value === null) { value = undefined; } + this.clear(); this._execution = value; } @@ -1520,6 +1553,7 @@ export class Task implements vscode.Task { this._hasDefinedMatchers = false; return; } + this.clear(); this._problemMatchers = value; this._hasDefinedMatchers = true; } @@ -1536,6 +1570,7 @@ export class Task implements vscode.Task { if (value !== true && value !== false) { value = false; } + this.clear(); this._isBackground = value; } @@ -1547,6 +1582,7 @@ export class Task implements vscode.Task { if (typeof value !== 'string' || value.length === 0) { throw illegalArgument('source must be a string of length > 0'); } + this.clear(); this._source = value; } @@ -1559,6 +1595,7 @@ export class Task implements vscode.Task { this._group = undefined; return; } + this.clear(); this._group = value; } @@ -1570,6 +1607,7 @@ export class Task implements vscode.Task { if (value === null) { value = undefined; } + this.clear(); this._presentationOptions = value; } } diff --git a/src/vs/workbench/api/shared/tasks.ts b/src/vs/workbench/api/shared/tasks.ts new file mode 100644 index 00000000000..cba95638cbc --- /dev/null +++ b/src/vs/workbench/api/shared/tasks.ts @@ -0,0 +1,88 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import { UriComponents } from 'vs/base/common/uri'; + +export interface TaskDefinitionDTO { + type: string; + [name: string]: any; +} + +export interface TaskExecutionDTO { + id: string; +} + +export interface TaskPresentationOptionsDTO { + reveal?: number; + echo?: boolean; + focus?: boolean; + panel?: number; +} + +export interface ExecutionOptionsDTO { + cwd?: string; + env?: { [key: string]: string }; +} + +export interface ProcessExecutionOptionsDTO extends ExecutionOptionsDTO { +} + +export interface ProcessExecutionDTO { + process: string; + args: string[]; + options?: ProcessExecutionOptionsDTO; +} + +export interface ShellQuotingOptionsDTO { + escape?: string | { + escapeChar: string; + charsToEscape: string; + }; + strong?: string; + weak?: string; +} + +export interface ShellExecutionOptionsDTO extends ExecutionOptionsDTO { + executable?: string; + shellArgs?: string[]; + shellQuoting?: ShellQuotingOptionsDTO; +} + +export interface ShellQuotedStringDTO { + value: string; + quoting: number; +} + +export interface ShellExecutionDTO { + commandLine?: string; + command?: string | ShellQuotedStringDTO; + args?: (string | ShellQuotedStringDTO)[]; + options?: ShellExecutionOptionsDTO; +} + +export interface TaskSourceDTO { + label: string; + extensionId?: string; + scope?: number | UriComponents; +} + +export interface TaskHandleDTO { + id: string; + workspaceFolder: UriComponents; +} + +export interface TaskDTO { + _id: string; + name: string; + execution: ProcessExecutionDTO | ShellExecutionDTO; + definition: TaskDefinitionDTO; + isBackground: boolean; + source: TaskSourceDTO; + group?: string; + presentationOptions: TaskPresentationOptionsDTO; + problemMatchers: string[]; + hasDefinedMatchers: boolean; +} \ No newline at end of file diff --git a/src/vs/workbench/parts/tasks/common/taskService.ts b/src/vs/workbench/parts/tasks/common/taskService.ts index b0c4e062ce3..53aad958232 100644 --- a/src/vs/workbench/parts/tasks/common/taskService.ts +++ b/src/vs/workbench/parts/tasks/common/taskService.ts @@ -47,9 +47,9 @@ export interface ITaskService { terminateAll(): TPromise; tasks(): TPromise; /** - * @param identifier The task's name, label or defined identifier. + * @param alias The task's name, label or defined identifier. */ - getTask(workspaceFolder: IWorkspaceFolder | string, identifier: string): TPromise; + getTask(workspaceFolder: IWorkspaceFolder | string, alias: string, compareId?: boolean): TPromise; getTasksForGroup(group: string): TPromise; getRecentlyUsedTasks(): LinkedMap; createSorter(): TaskSorter; diff --git a/src/vs/workbench/parts/tasks/common/tasks.ts b/src/vs/workbench/parts/tasks/common/tasks.ts index 08640b0476c..b5a93a7cb63 100644 --- a/src/vs/workbench/parts/tasks/common/tasks.ts +++ b/src/vs/workbench/parts/tasks/common/tasks.ts @@ -4,15 +4,15 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import URI, { UriComponents } from 'vs/base/common/uri'; import * as Types from 'vs/base/common/types'; import { IJSONSchemaMap } from 'vs/base/common/jsonSchema'; import * as Objects from 'vs/base/common/objects'; +import { generateUuid } from 'vs/base/common/uuid'; +import { UriComponents } from 'vs/base/common/uri'; import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; import { ProblemMatcher } from 'vs/workbench/parts/tasks/common/problemMatcher'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -import { generateUuid } from '../../../../base/common/uuid'; export enum ShellQuoting { @@ -598,8 +598,8 @@ export namespace Task { } } - export function matches(task: Task, alias: string): boolean { - return alias === task._label || alias === task.identifier; + export function matches(task: Task, alias: string, compareId: boolean = false): boolean { + return alias === task._label || alias === task.identifier || (compareId && alias === task._id); } export function getQualifiedLabel(task: Task): string { @@ -610,13 +610,53 @@ export namespace Task { return task._label; } } + + export function getTaskItem(task: Task): TaskItem { + let folder: IWorkspaceFolder = Task.getWorkspaceFolder(task); + let definition: TaskIdentifier; + if (ContributedTask.is(task)) { + definition = task.defines; + } else if (CustomTask.is(task) && task.command !== void 0) { + definition = CustomTask.getDefinition(task); + } else { + return undefined; + } + let result: TaskItem = { + id: task._id, + label: task._label, + definition: definition, + workspaceFolder: folder + }; + return result; + } + + export function getTaskDefinition(task: Task): TaskIdentifier { + if (ContributedTask.is(task)) { + return task.defines; + } else if (CustomTask.is(task) && task.command !== void 0) { + return CustomTask.getDefinition(task); + } else { + return undefined; + } + } + + export function getTaskExecution(task: Task): TaskExecution { + let result: TaskExecution = { + id: task._id + }; + return result; + } } -export interface TaskItemTransfer { +export interface TaskItem { id: string; label: string; definition: TaskIdentifier; - workspaceFolderUri: URI; + workspaceFolder: IWorkspaceFolder; +} + +export interface TaskExecution { + id: string; } export enum ExecutionEngine { @@ -679,10 +719,12 @@ export class TaskSorter { } export enum TaskEventKind { + Start = 'start', Active = 'active', Inactive = 'inactive', - Terminated = 'terminated', Changed = 'changed', + Terminated = 'terminated', + End = 'end' } @@ -701,7 +743,7 @@ export interface TaskEvent { } export namespace TaskEvent { - export function create(kind: TaskEventKind.Active | TaskEventKind.Inactive | TaskEventKind.Terminated, task: Task); + export function create(kind: TaskEventKind.Active | TaskEventKind.Inactive | TaskEventKind.Terminated | TaskEventKind.Start | TaskEventKind.End, task: Task); export function create(kind: TaskEventKind.Changed); export function create(kind: TaskEventKind, task?: Task): TaskEvent { if (task) { 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 69b20c7402c..6ce2cb1fe71 100644 --- a/src/vs/workbench/parts/tasks/electron-browser/task.contribution.ts +++ b/src/vs/workbench/parts/tasks/electron-browser/task.contribution.ts @@ -74,7 +74,7 @@ import { ITaskSystem, ITaskResolver, ITaskSummary, TaskExecuteKind, TaskError, T import { Task, CustomTask, ConfiguringTask, ContributedTask, InMemoryTask, TaskEvent, TaskEventKind, TaskSet, TaskGroup, GroupType, ExecutionEngine, JsonSchemaVersion, TaskSourceKind, - TaskIdentifier, TaskSorter, TaskItemTransfer + TaskIdentifier, TaskSorter, TaskItem } from 'vs/workbench/parts/tasks/common/tasks'; import { ITaskService, ITaskProvider, RunOptions, CustomizationProperties } from 'vs/workbench/parts/tasks/common/taskService'; import { getTemplates as getTaskTemplates } from 'vs/workbench/parts/tasks/common/taskTemplates'; @@ -582,23 +582,12 @@ class TaskService implements ITaskService { CommandsRegistry.registerCommand('_executeTaskProvider', (accessor, args) => { return this.tasks().then((tasks) => { - let result: TaskItemTransfer[] = []; + let result: TaskItem[] = []; for (let task of tasks) { - let folder: IWorkspaceFolder = Task.getWorkspaceFolder(task); - let folderUri = folder ? folder.uri : undefined; - let definition: TaskIdentifier; - if (ContributedTask.is(task)) { - definition = task.defines; - } else if (CustomTask.is(task) && task.command !== void 0) { - definition = CustomTask.getDefinition(task); + let item = Task.getTaskItem(task); + if (item) { + result.push(item); } - let handle: TaskItemTransfer = { - id: task._id, - label: task._label, - definition: definition, - workspaceFolderUri: folderUri - }; - result.push(handle); } return result; }); @@ -685,7 +674,7 @@ class TaskService implements ITaskService { return this._providers.delete(handle); } - public getTask(folder: IWorkspaceFolder | string, alias: string): TPromise { + public getTask(folder: IWorkspaceFolder | string, alias: string, compareId: boolean = false): TPromise { let name = Types.isString(folder) ? folder : folder.name; if (this.ignoredWorkspaceFolders.some(ignored => ignored.name === name)) { return TPromise.wrapError(new Error(nls.localize('TaskServer.folderIgnored', 'The folder {0} is ignored since it uses task version 0.1.0', name))); @@ -696,7 +685,7 @@ class TaskService implements ITaskService { return undefined; } for (let task of values) { - if (Task.matches(task, alias)) { + if (Task.matches(task, alias, compareId)) { return task; } } diff --git a/src/vs/workbench/parts/tasks/electron-browser/terminalTaskSystem.ts b/src/vs/workbench/parts/tasks/electron-browser/terminalTaskSystem.ts index 3613adaa878..17de814f26e 100644 --- a/src/vs/workbench/parts/tasks/electron-browser/terminalTaskSystem.ts +++ b/src/vs/workbench/parts/tasks/electron-browser/terminalTaskSystem.ts @@ -288,6 +288,7 @@ export class TerminalTaskSystem implements ITaskSystem { watchingProblemMatcher.aboutToStart(); let delayer: Async.Delayer = undefined; [terminal, executedCommand] = this.createTerminal(task); + this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.Start, task)); const registeredLinkMatchers = this.registerLinkMatchers(terminal, problemMatchers); const onData = terminal.onLineData((line) => { watchingProblemMatcher.processLine(line); @@ -328,12 +329,14 @@ export class TerminalTaskSystem implements ITaskSystem { this.terminalService.setActiveInstance(terminal); this.terminalService.showPanel(false); } + this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.End, task)); resolve({ exitCode }); }); }); } else { promise = new TPromise((resolve, reject) => { [terminal, executedCommand] = this.createTerminal(task); + this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.Start, task)); this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.Active, task)); let problemMatchers = this.resolveMatchers(task, task.problemMatchers); let startStopProblemMatcher = new StartStopProblemCollector(problemMatchers, this.markerService, this.modelService); @@ -359,6 +362,7 @@ export class TerminalTaskSystem implements ITaskSystem { startStopProblemMatcher.dispose(); registeredLinkMatchers.forEach(handle => terminal.deregisterLinkMatcher(handle)); this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.Inactive, task)); + this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.End, task)); // See https://github.com/Microsoft/vscode/issues/31965 if (exitCode === 0 && startStopProblemMatcher.numberOfMatches > 0) { exitCode = 1; diff --git a/src/vs/workbench/test/electron-browser/api/extHostApiCommands.test.ts b/src/vs/workbench/test/electron-browser/api/extHostApiCommands.test.ts index 133e9d9667c..5e6b7013ec1 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostApiCommands.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostApiCommands.test.ts @@ -35,6 +35,7 @@ import { NullLogService } from 'vs/platform/log/common/log'; import { ITextModel } from 'vs/editor/common/model'; import { ExtHostWorkspace } from 'vs/workbench/api/node/extHostWorkspace'; import { generateUuid } from 'vs/base/common/uuid'; +import { ExtHostTask } from 'vs/workbench/api/node/extHostTask'; const defaultSelector = { scheme: 'far' }; const model: ITextModel = EditorModel.createFromString( @@ -51,6 +52,7 @@ let rpcProtocol: TestRPCProtocol; let extHost: ExtHostLanguageFeatures; let mainThread: MainThreadLanguageFeatures; let commands: ExtHostCommands; +let task: ExtHostTask; let workspace: ExtHostWorkspace; let disposables: vscode.Disposable[] = []; let originalErrorHandler: (e: any) => any; @@ -119,9 +121,10 @@ suite('ExtHostLanguageFeatureCommands', function () { commands = new ExtHostCommands(rpcProtocol, heapService, new NullLogService()); workspace = new ExtHostWorkspace(rpcProtocol, { id: generateUuid(), name: 'Test', folders: [] }, new NullLogService()); + task = new ExtHostTask(rpcProtocol, workspace); rpcProtocol.set(ExtHostContext.ExtHostCommands, commands); rpcProtocol.set(MainContext.MainThreadCommands, inst.createInstance(MainThreadCommands, rpcProtocol)); - ExtHostApiCommands.register(commands, workspace); + ExtHostApiCommands.register(commands, task); const diagnostics = new ExtHostDiagnostics(rpcProtocol); rpcProtocol.set(ExtHostContext.ExtHostDiagnostics, diagnostics); -- GitLab