From 558d4b5386967d0bdd380d29b0980e4a11df8354 Mon Sep 17 00:00:00 2001 From: Roman Nikitenko Date: Mon, 10 Dec 2018 10:53:38 +0200 Subject: [PATCH] Add 'tasks.registerTaskProvider' for Plugin API Signed-off-by: Roman Nikitenko --- CHANGELOG.md | 1 + packages/plugin-ext/package.json | 1 + packages/plugin-ext/src/api/plugin-api.ts | 29 ++ .../src/main/browser/main-context.ts | 4 + .../plugin-ext/src/main/browser/tasks-main.ts | 71 ++++ .../plugin-ext/src/plugin/plugin-context.ts | 29 +- .../src/plugin/tasks/task-provider.ts | 67 +++ packages/plugin-ext/src/plugin/tasks/tasks.ts | 78 ++++ .../plugin-ext/src/plugin/type-converters.ts | 102 ++++- packages/plugin-ext/src/plugin/types-impl.ts | 385 ++++++++++++++++++ packages/plugin/src/theia.d.ts | 376 +++++++++++++++++ 11 files changed, 1140 insertions(+), 3 deletions(-) create mode 100644 packages/plugin-ext/src/main/browser/tasks-main.ts create mode 100644 packages/plugin-ext/src/plugin/tasks/task-provider.ts create mode 100644 packages/plugin-ext/src/plugin/tasks/tasks.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index e9959a01e3e47..fe246e0f85a71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ - [plug-in] added `window.showTextDocument` Plug-in API - [plug-in] added ability to provide custom namespaces for the Plug-in API - [plug-in] registered a type definition provider +- [plug-in] added `tasks.registerTaskProvider` Plug-in API - [preview-editor] added the ability to open editors in preview mode - [process] added the ability to create new node processes through forking - [search-in-workspace] prompt users when performing `Replace All...` to limit accidental triggering diff --git a/packages/plugin-ext/package.json b/packages/plugin-ext/package.json index 94e53a1bc0133..aea64034bb088 100644 --- a/packages/plugin-ext/package.json +++ b/packages/plugin-ext/package.json @@ -13,6 +13,7 @@ "@theia/navigator": "^0.3.17", "@theia/plugin": "^0.3.17", "@theia/workspace": "^0.3.17", + "@theia/task": "^0.3.17", "decompress": "^4.2.0", "jsonc-parser": "^2.0.2", "lodash.clonedeep": "^4.5.0", diff --git a/packages/plugin-ext/src/api/plugin-api.ts b/packages/plugin-ext/src/api/plugin-api.ts index cf001a862b488..887aa3334cc9f 100644 --- a/packages/plugin-ext/src/api/plugin-api.ts +++ b/packages/plugin-ext/src/api/plugin-api.ts @@ -767,6 +767,23 @@ export interface WorkspaceEditDto { rejectReason?: string; } +export interface CommandProperties { + command: string; + args?: string[]; + options?: object; +} +export interface TaskDto { + type: string; + label: string; + // tslint:disable-next-line:no-any + [key: string]: any; +} + +export interface ProcessTaskDto extends TaskDto, CommandProperties { + windows?: CommandProperties; + cwd?: string; +} + export interface LanguagesExt { $provideCompletionItems(handle: number, resource: UriComponents, position: Position, context: CompletionContext): Promise; $resolveCompletionItem(handle: number, resource: UriComponents, position: Position, completion: Completion): Promise; @@ -882,6 +899,7 @@ export const PLUGIN_RPC_CONTEXT = { LANGUAGES_MAIN: createProxyIdentifier('LanguagesMain'), CONNECTION_MAIN: createProxyIdentifier('ConnectionMain'), WEBVIEWS_MAIN: createProxyIdentifier('WebviewsMain'), + TASKS_MAIN: createProxyIdentifier('TasksMain'), }; export const MAIN_RPC_CONTEXT = { @@ -900,4 +918,15 @@ export const MAIN_RPC_CONTEXT = { LANGUAGES_EXT: createProxyIdentifier('LanguagesExt'), CONNECTION_EXT: createProxyIdentifier('ConnectionExt'), WEBVIEWS_EXT: createProxyIdentifier('WebviewsExt'), + TASKS_EXT: createProxyIdentifier('TasksExt'), }; + +export interface TasksExt { + $provideTasks(handle: number): Promise; + $resolveTask(handle: number, task: TaskDto): Promise; +} + +export interface TasksMain { + $registerTaskProvider(handle: number, type: string): void; + $unregister(handle: number): void; +} diff --git a/packages/plugin-ext/src/main/browser/main-context.ts b/packages/plugin-ext/src/main/browser/main-context.ts index d770a79c00b8e..71be9bd289c97 100644 --- a/packages/plugin-ext/src/main/browser/main-context.ts +++ b/packages/plugin-ext/src/main/browser/main-context.ts @@ -33,6 +33,7 @@ import { TreeViewsMainImpl } from './view/tree-views-main'; import { NotificationMainImpl } from './notification-main'; import { ConnectionMainImpl } from './connection-main'; import { WebviewsMainImpl } from './webviews-main'; +import { TasksMainImpl } from './tasks-main'; export function setUpPluginApi(rpc: RPCProtocol, container: interfaces.Container): void { const commandRegistryMain = new CommandRegistryMainImpl(rpc, container); @@ -84,4 +85,7 @@ export function setUpPluginApi(rpc: RPCProtocol, container: interfaces.Container const pluginConnection = new ConnectionMainImpl(rpc); rpc.set(PLUGIN_RPC_CONTEXT.CONNECTION_MAIN, pluginConnection); + + const tasksMain = new TasksMainImpl(rpc, container); + rpc.set(PLUGIN_RPC_CONTEXT.TASKS_MAIN, tasksMain); } diff --git a/packages/plugin-ext/src/main/browser/tasks-main.ts b/packages/plugin-ext/src/main/browser/tasks-main.ts new file mode 100644 index 0000000000000..8636c8dbf54d2 --- /dev/null +++ b/packages/plugin-ext/src/main/browser/tasks-main.ts @@ -0,0 +1,71 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { + TasksMain, + MAIN_RPC_CONTEXT, + TasksExt +} from '../../api/plugin-api'; +import { RPCProtocol } from '../../api/rpc-protocol'; +import { DisposableCollection } from '@theia/core'; +import { TaskProviderRegistry, TaskResolverRegistry, TaskProvider, TaskResolver } from '@theia/task/lib/browser/task-contribution'; +import { interfaces } from 'inversify'; + +export class TasksMainImpl implements TasksMain { + + private readonly proxy: TasksExt; + private readonly disposables = new Map(); + private readonly taskProviderRegistry: TaskProviderRegistry; + private readonly taskResolverRegistry: TaskResolverRegistry; + + constructor(rpc: RPCProtocol, container: interfaces.Container, ) { + this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.TASKS_EXT); + this.taskProviderRegistry = container.get(TaskProviderRegistry); + this.taskResolverRegistry = container.get(TaskResolverRegistry); + } + + $registerTaskProvider(handle: number, type: string): void { + const taskProvider = this.createTaskProvider(handle); + const taskResolver = this.createTaskResolver(handle); + + const disposable = new DisposableCollection(); + disposable.push(this.taskProviderRegistry.register(type, taskProvider)); + disposable.push(this.taskResolverRegistry.register(type, taskResolver)); + this.disposables.set(handle, disposable); + } + + $unregister(handle: number): void { + const disposable = this.disposables.get(handle); + if (disposable) { + disposable.dispose(); + this.disposables.delete(handle); + } + } + + protected createTaskProvider(handle: number): TaskProvider { + return { + provideTasks: () => + this.proxy.$provideTasks(handle).then(v => v!), + }; + } + + protected createTaskResolver(handle: number): TaskResolver { + return { + resolveTask: taskConfig => + this.proxy.$resolveTask(handle, taskConfig).then(v => v!), + }; + } +} diff --git a/packages/plugin-ext/src/plugin/plugin-context.ts b/packages/plugin-ext/src/plugin/plugin-context.ts index b17d6e948e947..65e3c1d78e173 100644 --- a/packages/plugin-ext/src/plugin/plugin-context.ts +++ b/packages/plugin-ext/src/plugin/plugin-context.ts @@ -75,7 +75,15 @@ import { WorkspaceEdit, SymbolInformation, FileType, - FileChangeType + FileChangeType, + ShellQuoting, + ShellExecution, + ProcessExecution, + TaskScope, + TaskPanelKind, + TaskRevealKind, + TaskGroup, + Task } from './types-impl'; import { EditorsAndDocumentsExtImpl } from './editors-and-documents'; import { TextEditorsExtImpl } from './text-editors'; @@ -95,6 +103,7 @@ import { MarkdownString } from './markdown-string'; import { TreeViewsExtImpl } from './tree/tree-views'; import { ConnectionExtImpl } from './connection-ext'; import { WebviewsExtImpl } from './webviews'; +import { TasksExtImpl } from './tasks/tasks'; export function createAPIFactory( rpc: RPCProtocol, @@ -119,6 +128,7 @@ export function createAPIFactory( const languagesExt = rpc.set(MAIN_RPC_CONTEXT.LANGUAGES_EXT, new LanguagesExtImpl(rpc, documents, commandRegistry)); const treeViewsExt = rpc.set(MAIN_RPC_CONTEXT.TREE_VIEWS_EXT, new TreeViewsExtImpl(rpc, commandRegistry)); const webviewExt = rpc.set(MAIN_RPC_CONTEXT.WEBVIEWS_EXT, new WebviewsExtImpl(rpc)); + const tasksExt = rpc.set(MAIN_RPC_CONTEXT.TASKS_EXT, new TasksExtImpl(rpc)); rpc.set(MAIN_RPC_CONTEXT.CONNECTION_EXT, new ConnectionExtImpl(rpc)); return function (plugin: InternalPlugin): typeof theia { @@ -480,6 +490,12 @@ export function createAPIFactory( } }; + const tasks: typeof theia.tasks = { + registerTaskProvider(type: string, provider: theia.TaskProvider): theia.Disposable { + return tasksExt.registerTaskProvider(type, provider); + } + }; + return { version: require('../../package.json').version, commands, @@ -489,6 +505,7 @@ export function createAPIFactory( languages, plugins, debug, + tasks, // Types StatusBarAlignment: StatusBarAlignment, Disposable: Disposable, @@ -543,7 +560,15 @@ export function createAPIFactory( WorkspaceEdit, SymbolInformation, FileType, - FileChangeType + FileChangeType, + ShellQuoting, + ShellExecution, + ProcessExecution, + TaskScope, + TaskRevealKind, + TaskPanelKind, + TaskGroup, + Task }; }; } diff --git a/packages/plugin-ext/src/plugin/tasks/task-provider.ts b/packages/plugin-ext/src/plugin/tasks/task-provider.ts new file mode 100644 index 0000000000000..5e56fd8705fb0 --- /dev/null +++ b/packages/plugin-ext/src/plugin/tasks/task-provider.ts @@ -0,0 +1,67 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import * as theia from '@theia/plugin'; +import * as Converter from '../type-converters'; +import { ObjectIdentifier } from '../../common/object-identifier'; +import { createToken } from '../token-provider'; +import { TaskDto } from '../../common'; + +export class TaskProviderAdapter { + private cacheId = 0; + private cache = new Map(); + + constructor(private readonly provider: theia.TaskProvider) { } + + provideTasks(): Promise { + return Promise.resolve(this.provider.provideTasks(createToken())).then(tasks => { + if (!Array.isArray(tasks)) { + return undefined; + } + const result: TaskDto[] = []; + for (const task of tasks) { + const data = Converter.fromTask(task); + if (!data) { + continue; + } + + const id = this.cacheId++; + ObjectIdentifier.mixin(data, id); + this.cache.set(id, task); + result.push(data); + } + return result; + }); + } + + resolveTask(task: TaskDto): Promise { + if (typeof this.provider.resolveTask !== 'function') { + return Promise.resolve(undefined); + } + const id = ObjectIdentifier.of(task); + const item = this.cache.get(id); + if (!item) { + return Promise.resolve(undefined); + } + + return Promise.resolve(this.provider.resolveTask(item, createToken())).then(value => { + if (value) { + return Converter.fromTask(value); + } + return undefined; + }); + } +} diff --git a/packages/plugin-ext/src/plugin/tasks/tasks.ts b/packages/plugin-ext/src/plugin/tasks/tasks.ts new file mode 100644 index 0000000000000..44b64450924a1 --- /dev/null +++ b/packages/plugin-ext/src/plugin/tasks/tasks.ts @@ -0,0 +1,78 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { + PLUGIN_RPC_CONTEXT, + TasksExt, + TasksMain, + TaskDto +} from '../../api/plugin-api'; +import * as theia from '@theia/plugin'; +import { Disposable } from '../types-impl'; +import { RPCProtocol } from '../../api/rpc-protocol'; +import { TaskProviderAdapter } from './task-provider'; + +export class TasksExtImpl implements TasksExt { + private proxy: TasksMain; + + private callId = 0; + private adaptersMap = new Map(); + + constructor(rpc: RPCProtocol) { + this.proxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.TASKS_MAIN); + } + + registerTaskProvider(type: string, provider: theia.TaskProvider): theia.Disposable { + const callId = this.addNewAdapter(new TaskProviderAdapter(provider)); + this.proxy.$registerTaskProvider(callId, type); + return this.createDisposable(callId); + } + + $provideTasks(handle: number): Promise { + const adapter = this.adaptersMap.get(handle); + if (adapter) { + return adapter.provideTasks(); + } else { + return Promise.reject(new Error('No adapter found to provide tasks')); + } + } + + $resolveTask(handle: number, task: TaskDto): Promise { + const adapter = this.adaptersMap.get(handle); + if (adapter) { + return adapter.resolveTask(task); + } else { + return Promise.reject(new Error('No adapter found to resolve task')); + } + } + + private addNewAdapter(adapter: TaskProviderAdapter): number { + const callId = this.nextCallId(); + this.adaptersMap.set(callId, adapter); + return callId; + } + + private nextCallId(): number { + return this.callId++; + } + + private createDisposable(callId: number): theia.Disposable { + return new Disposable(() => { + this.adaptersMap.delete(callId); + this.proxy.$unregister(callId); + }); + } +} diff --git a/packages/plugin-ext/src/plugin/type-converters.ts b/packages/plugin-ext/src/plugin/type-converters.ts index 4b978b5c4814f..05bb4e1c69d20 100644 --- a/packages/plugin-ext/src/plugin/type-converters.ts +++ b/packages/plugin-ext/src/plugin/type-converters.ts @@ -14,7 +14,7 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { EditorPosition, Selection, Position, DecorationOptions, WorkspaceEditDto, ResourceTextEditDto, ResourceFileEditDto } from '../api/plugin-api'; +import { EditorPosition, Selection, Position, DecorationOptions, WorkspaceEditDto, ResourceTextEditDto, ResourceFileEditDto, TaskDto, ProcessTaskDto } from '../api/plugin-api'; import * as model from '../api/model'; import * as theia from '@theia/plugin'; import * as types from './types-impl'; @@ -477,3 +477,103 @@ export function toWorkspaceFolder(folder: model.WorkspaceFolder): theia.Workspac index: folder.index }; } + +export function fromTask(task: theia.Task): TaskDto | undefined { + if (!task) { + return undefined; + } + + const taskDto = {} as TaskDto; + taskDto.label = task.name; + + const taskDefinition = task.definition; + if (!taskDefinition) { + return taskDto; + } + + taskDto.type = taskDefinition.type; + for (const key in taskDefinition) { + if (taskDefinition.hasOwnProperty(key)) { + taskDto[key] = taskDefinition[key]; + } + } + + const execution = task.execution; + if (!execution) { + return taskDto; + } + + const processTaskDto = taskDto as ProcessTaskDto; + if (taskDefinition.type === 'shell') { + return fromShellExecution(execution, processTaskDto); + } + + if (taskDefinition.type === 'process') { + return fromProcessExecution( execution, processTaskDto); + } + + return processTaskDto; +} + +export function fromProcessExecution(execution: theia.ProcessExecution, processTaskDto: ProcessTaskDto): ProcessTaskDto { + processTaskDto.command = execution.process; + processTaskDto.args = execution.args; + + const options = execution.options; + if (options) { + processTaskDto.cwd = options.cwd; + processTaskDto.options = options; + } + return processTaskDto; +} + +export function fromShellExecution(execution: theia.ShellExecution, processTaskDto: ProcessTaskDto): ProcessTaskDto { + const options = execution.options; + if (options) { + processTaskDto.cwd = options.cwd; + processTaskDto.args = options.shellArgs; + processTaskDto.options = options; + } + + const commandLine = execution.commandLine; + if (commandLine) { + const args = commandLine.split(' '); + const taskCommand = args.shift(); + + if (taskCommand) { + processTaskDto.command = taskCommand; + } + + processTaskDto.args = args; + return processTaskDto; + } + + const command = execution.command; + if (typeof command === 'string') { + processTaskDto.command = command; + processTaskDto.args = getShellArgs(execution.args); + return processTaskDto; + } else { + throw new Error('Converting ShellQuotedString command is not implemented'); + } +} + +export function getShellArgs(args: undefined | (string | theia.ShellQuotedString)[]): string[] { + if (!args || args.length === 0) { + return []; + } + + const element = args[0]; + if (typeof element === 'string') { + return args as string[]; + } + + const result: string[] = []; + const shellQuotedArgs = args as theia.ShellQuotedString[]; + + shellQuotedArgs.forEach(arg => { + result.push(arg.value); + }); + + return result; +} diff --git a/packages/plugin-ext/src/plugin/types-impl.ts b/packages/plugin-ext/src/plugin/types-impl.ts index 31e559abfa2e6..b69a39d26447a 100644 --- a/packages/plugin-ext/src/plugin/types-impl.ts +++ b/packages/plugin-ext/src/plugin/types-impl.ts @@ -16,6 +16,7 @@ import { illegalArgument } from '../common/errors'; import * as theia from '@theia/plugin'; +import * as crypto from 'crypto'; import URI from 'vscode-uri'; import { relative } from '../common/paths-util'; import { startsWithIgnoreCase } from '../common/strings'; @@ -1276,3 +1277,387 @@ export enum ProgressLocation { */ Notification = 15 } + +export class ProcessExecution { + private executionProcess: string; + private arguments: string[]; + private executionOptions: theia.ProcessExecutionOptions | undefined; + + constructor(process: string, options?: theia.ProcessExecutionOptions); + constructor(process: string, args: string[], options?: theia.ProcessExecutionOptions); + constructor(process: string, varg1?: string[] | theia.ProcessExecutionOptions, varg2?: theia.ProcessExecutionOptions) { + if (typeof process !== 'string') { + throw illegalArgument('process'); + } + this.executionProcess = process; + if (varg1 !== undefined) { + if (Array.isArray(varg1)) { + this.arguments = varg1; + this.executionOptions = varg2; + } else { + this.executionOptions = varg1; + } + } + if (this.arguments === undefined) { + this.arguments = []; + } + } + + get process(): string { + return this.executionProcess; + } + + set process(value: string) { + if (typeof value !== 'string') { + throw illegalArgument('process'); + } + this.executionProcess = value; + } + + get args(): string[] { + return this.arguments; + } + + set args(value: string[]) { + if (!Array.isArray(value)) { + value = []; + } + this.arguments = value; + } + + get options(): theia.ProcessExecutionOptions | undefined { + return this.executionOptions; + } + + set options(value: theia.ProcessExecutionOptions | undefined) { + this.executionOptions = value; + } + + public computeId(): string { + const hash = crypto.createHash('md5'); + hash.update('process'); + if (this.executionProcess !== undefined) { + hash.update(this.executionProcess); + } + if (this.arguments && this.arguments.length > 0) { + for (const arg of this.arguments) { + hash.update(arg); + } + } + return hash.digest('hex'); + } +} + +export enum ShellQuoting { + Escape = 1, + Strong = 2, + Weak = 3 +} + +export enum TaskPanelKind { + Shared = 1, + Dedicated = 2, + New = 3 +} + +export enum TaskRevealKind { + Always = 1, + Silent = 2, + Never = 3 +} + +export class ShellExecution { + private shellCommandLine: string; + private shellCommand: string | theia.ShellQuotedString; + private arguments: (string | theia.ShellQuotedString)[]; + private shellOptions: theia.ShellExecutionOptions | undefined; + + constructor(commandLine: string, options?: theia.ShellExecutionOptions); + constructor(command: string | theia.ShellQuotedString, args: (string | theia.ShellQuotedString)[], options?: theia.ShellExecutionOptions); + + constructor(arg0: string | theia.ShellQuotedString, arg1?: theia.ShellExecutionOptions | (string | theia.ShellQuotedString)[], arg2?: theia.ShellExecutionOptions) { + if (Array.isArray(arg1) || typeof arg1 === 'string') { + if (!arg0) { + throw illegalArgument('command can\'t be undefined or null'); + } + if (typeof arg0 !== 'string' && typeof arg0.value !== 'string') { + throw illegalArgument('command'); + } + this.shellCommand = arg0; + this.arguments = arg1 as (string | theia.ShellQuotedString)[]; + this.shellOptions = arg2; + } else { + if (typeof arg0 !== 'string') { + throw illegalArgument('commandLine'); + } + this.shellCommandLine = arg0; + this.shellOptions = arg1; + } + } + + get commandLine(): string { + return this.shellCommandLine; + } + + set commandLine(value: string) { + if (typeof value !== 'string') { + throw illegalArgument('commandLine'); + } + this.shellCommandLine = value; + } + + get command(): string | theia.ShellQuotedString { + return this.shellCommand; + } + + set command(value: string | theia.ShellQuotedString) { + if (typeof value !== 'string' && typeof value.value !== 'string') { + throw illegalArgument('command'); + } + this.shellCommand = value; + } + + get args(): (string | theia.ShellQuotedString)[] { + return this.arguments; + } + + set args(value: (string | theia.ShellQuotedString)[]) { + this.arguments = value || []; + } + + get options(): theia.ShellExecutionOptions | undefined { + return this.shellOptions; + } + + set options(value: theia.ShellExecutionOptions | undefined) { + this.shellOptions = value; + } + + public computeId(): string { + const hash = crypto.createHash('md5'); + hash.update('shell'); + if (this.shellCommandLine !== undefined) { + hash.update(this.shellCommandLine); + } + if (this.shellCommand !== undefined) { + hash.update(typeof this.shellCommand === 'string' ? this.shellCommand : this.shellCommand.value); + } + if (this.arguments && this.arguments.length > 0) { + for (const arg of this.arguments) { + hash.update(typeof arg === 'string' ? arg : arg.value); + } + } + return hash.digest('hex'); + } +} + +export class TaskGroup { + private groupId: string; + + public static Clean: TaskGroup = new TaskGroup('clean', 'Clean'); + public static Build: TaskGroup = new TaskGroup('build', 'Build'); + public static Rebuild: TaskGroup = new TaskGroup('rebuild', 'Rebuild'); + 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('id'); + } + if (typeof label !== 'string') { + throw illegalArgument('name'); + } + this.groupId = id; + } + + get id(): string { + return this.groupId; + } +} + +export enum TaskScope { + Global = 1, + Workspace = 2 +} + +export class Task { + private taskDefinition: theia.TaskDefinition | undefined; + private taskScope: theia.TaskScope.Global | theia.TaskScope.Workspace | theia.WorkspaceFolder | undefined; + private taskName: string; + private taskExecution: ProcessExecution | ShellExecution | undefined; + private taskProblemMatchers: string[]; + private hasTaskProblemMatchers: boolean; + private isTaskBackground: boolean; + private taskSource: string; + private taskGroup: TaskGroup | undefined; + private taskPresentationOptions: theia.TaskPresentationOptions | undefined; + + constructor(taskDefinition: theia.TaskDefinition, + scope: theia.WorkspaceFolder | theia.TaskScope.Global | theia.TaskScope.Workspace, + name: string, + source: string, + execution?: ProcessExecution | ShellExecution, + problemMatchers?: string | string[]) { + + this.definition = taskDefinition; + this.scope = scope; + this.name = name; + this.source = source; + this.execution = execution; + + if (typeof problemMatchers === 'string') { + this.taskProblemMatchers = [problemMatchers]; + this.hasTaskProblemMatchers = true; + } else if (Array.isArray(problemMatchers)) { + this.taskProblemMatchers = problemMatchers; + this.hasTaskProblemMatchers = true; + } else { + this.taskProblemMatchers = []; + this.hasTaskProblemMatchers = false; + } + this.isTaskBackground = false; + } + + get definition(): theia.TaskDefinition | undefined { + return this.taskDefinition; + } + + set definition(value: theia.TaskDefinition | undefined) { + if (value === undefined || value === null) { + throw illegalArgument('Kind can\'t be undefined or null'); + } + this.clear(); + this.taskDefinition = value; + } + + get scope(): theia.TaskScope.Global | theia.TaskScope.Workspace | theia.WorkspaceFolder | undefined { + return this.taskScope; + } + + set scope(value: theia.TaskScope.Global | theia.TaskScope.Workspace | theia.WorkspaceFolder | undefined) { + this.clear(); + this.taskScope = value; + } + + get name(): string { + return this.taskName; + } + + set name(value: string) { + if (typeof value !== 'string') { + throw illegalArgument('name'); + } + this.clear(); + this.taskName = value; + } + + get execution(): ProcessExecution | ShellExecution | undefined { + return this.taskExecution; + } + + set execution(value: ProcessExecution | ShellExecution | undefined) { + if (value === null) { + value = undefined; + } + this.clear(); + this.taskExecution = value; + } + + get problemMatchers(): string[] { + return this.taskProblemMatchers; + } + + set problemMatchers(value: string[]) { + if (!Array.isArray(value)) { + this.taskProblemMatchers = []; + this.hasTaskProblemMatchers = false; + return; + } + this.clear(); + this.taskProblemMatchers = value; + this.hasTaskProblemMatchers = true; + } + + get hasProblemMatchers(): boolean { + return this.hasTaskProblemMatchers; + } + + get isBackground(): boolean { + return this.isTaskBackground; + } + + set isBackground(value: boolean) { + if (value !== true && value !== false) { + value = false; + } + this.clear(); + this.isTaskBackground = value; + } + + get source(): string { + return this.taskSource; + } + + set source(value: string) { + if (typeof value !== 'string' || value.length === 0) { + throw illegalArgument('source must be a string of length > 0'); + } + this.clear(); + this.taskSource = value; + } + + get group(): TaskGroup | undefined { + return this.taskGroup; + } + + set group(value: TaskGroup | undefined) { + if (value === undefined || value === null) { + this.taskGroup = undefined; + return; + } + this.clear(); + this.taskGroup = value; + } + + get presentationOptions(): theia.TaskPresentationOptions | undefined { + return this.taskPresentationOptions; + } + + set presentationOptions(value: theia.TaskPresentationOptions | undefined) { + if (value === null) { + value = undefined; + } + this.clear(); + this.taskPresentationOptions = value; + } + + private clear(): void { + this.taskScope = undefined; + this.taskDefinition = undefined; + if (this.taskExecution instanceof ProcessExecution) { + this.taskDefinition = { + type: 'process', + id: this.taskExecution.computeId() + }; + } else if (this.taskExecution instanceof ShellExecution) { + this.taskDefinition = { + type: 'shell', + id: this.taskExecution.computeId() + }; + } + } +} diff --git a/packages/plugin/src/theia.d.ts b/packages/plugin/src/theia.d.ts index 23a419f3be653..320e07a086095 100644 --- a/packages/plugin/src/theia.d.ts +++ b/packages/plugin/src/theia.d.ts @@ -5965,4 +5965,380 @@ declare module '@theia/plugin' { */ selection?: Range; } + + export enum ShellQuoting { + + /** + * Character escaping should be used. This for example + * uses \ on bash and ` on PowerShell. + */ + Escape = 1, + + /** + * Strong string quoting should be used. This for example + * uses " for Windows cmd and ' for bash and PowerShell. + * Strong quoting treats arguments as literal strings. + * Under PowerShell echo 'The value is $(2 * 3)' will + * print `The value is $(2 * 3)` + */ + Strong = 2, + + /** + * Weak string quoting should be used. This for example + * uses " for Windows cmd, bash and PowerShell. Weak quoting + * still performs some kind of evaluation inside the quoted + * string. Under PowerShell echo "The value is $(2 * 3)" + * will print `The value is 6` + */ + Weak = 3 + } + + /** A string that will be quoted depending on the used shell. */ + export interface ShellQuotedString { + /** The actual string value */ + value: string; + + /** The quoting style to use */ + quoting: ShellQuoting; + } + + export interface ShellQuotingOptions { + + /** + * The character used to do character escaping. If a string is provided only spaces + * are escaped. If a `{ escapeChar, charsToEscape }` literal is provide all characters + * in `charsToEscape` are escaped using the `escapeChar`. + */ + escape?: string | { + /** The escape character */ + escapeChar: string; + + /** The characters to escape */ + charsToEscape: string; + }; + + /** The character used for strong quoting. The string's length must be 1 */ + strong?: string; + + /** The character used for weak quoting. The string's length must be 1 */ + weak?: string; + } + + export interface ShellExecutionOptions { + + /** The shell executable */ + executable?: string; + + /** + * The arguments to be passed to the shell executable used to run the task. Most shells + * require special arguments to execute a command. For example `bash` requires the `-c` + * argument to execute a command, `PowerShell` requires `-Command` and `cmd` requires both + * `/d` and `/c`. + */ + shellArgs?: string[]; + + /** The shell quotes supported by this shell */ + shellQuoting?: ShellQuotingOptions; + + /** + * The current working directory of the executed shell. + * If omitted the tools current workspace root is used. + */ + cwd?: string; + + /** + * The additional environment of the executed shell. If omitted + * the parent process' environment is used. If provided it is merged with + * the parent process' environment. + */ + env?: { [key: string]: string }; + } + + export class ShellExecution { + /** + * Creates a shell execution with a full command line. + * + * @param commandLine The command line to execute. + * @param options Optional options for the started the shell. + */ + constructor(commandLine: string, options?: ShellExecutionOptions); + + /** + * Creates a shell execution with a command and arguments. For the real execution VS Code will + * construct a command line from the command and the arguments. This is subject to interpretation + * especially when it comes to quoting. If full control over the command line is needed please + * use the constructor that creates a `ShellExecution` with the full command line. + * + * @param command The command to execute. + * @param args The command arguments. + * @param options Optional options for the started the shell. + */ + constructor(command: string | ShellQuotedString, args: (string | ShellQuotedString)[], options?: ShellExecutionOptions); + + /** + * The shell command line. Is `undefined` if created with a command and arguments. + */ + commandLine?: string; + + /** + * The shell options used when the command line is executed in a shell. + * Defaults to undefined. + */ + options?: ShellExecutionOptions; + + /** + * The shell command. Is `undefined` if created with a full command line. + */ + command?: string | ShellQuotedString; + + /** + * The shell args. Is `undefined` if created with a full command line. + */ + args?: (string | ShellQuotedString)[]; + } + + export interface ProcessExecutionOptions { + /** + * The current working directory of the executed program or shell. + * If omitted the tools current workspace root is used. + */ + cwd?: string; + + /** + * The additional environment of the executed program or shell. If omitted + * the parent process' environment is used. If provided it is merged with + * the parent process' environment. + */ + env?: { [key: string]: string }; + } + + export class ProcessExecution { + + /** + * Creates a process execution. + * + * @param process The process to start. + * @param options Optional options for the started process. + */ + constructor(process: string, options?: ProcessExecutionOptions); + + /** + * Creates a process execution. + * + * @param process The process to start. + * @param args Arguments to be passed to the process. + * @param options Optional options for the started process. + */ + constructor(process: string, args: string[], options?: ProcessExecutionOptions); + + /** The process to be executed. */ + process: string; + + /** The arguments passed to the process. Defaults to an empty array. */ + args: string[]; + + /** + * The process options used when the process is executed. + * Defaults to undefined. + */ + options?: ProcessExecutionOptions; + } + + export interface TaskDefinition { + /** + * The task definition describing the task provided by an extension. + * Usually a task provider defines more properties to identify + * a task. They need to be defined in the package.json of the + * extension under the 'taskDefinitions' extension point. The npm + * task definition for example looks like this + * ```typescript + * interface NpmTaskDefinition extends TaskDefinition { + * script: string; + * } + * ``` + * + * Note that type identifier starting with a '$' are reserved for internal + * usages and shouldn't be used by extensions. + */ + readonly type: string; + + /** Additional attributes of a concrete task definition. */ + [name: string]: any; + } + + export enum TaskScope { + /** The task is a global task */ + Global = 1, + + /** The task is a workspace task */ + Workspace = 2 + } + + export class TaskGroup { + + /** The clean task group */ + static Clean: TaskGroup; + + /** The build task group */ + static Build: TaskGroup; + + /** The rebuild all task group */ + static Rebuild: TaskGroup; + + /** The test all task group */ + static Test: TaskGroup; + + private constructor(id: string, label: string); + } + + /** Controls the behaviour of the terminal's visibility. */ + export enum TaskRevealKind { + /** Always brings the terminal to front if the task is executed. */ + Always = 1, + + /** + * Only brings the terminal to front if a problem is detected executing the task + * (e.g. the task couldn't be started because). + */ + Silent = 2, + + /** The terminal never comes to front when the task is executed. */ + Never = 3 + } + + /** Controls how the task channel is used between tasks */ + export enum TaskPanelKind { + + /** Shares a panel with other tasks. This is the default. */ + Shared = 1, + + /** + * Uses a dedicated panel for this tasks. The panel is not + * shared with other tasks. + */ + Dedicated = 2, + + /** Creates a new panel whenever this task is executed. */ + New = 3 + } + + export interface TaskPresentationOptions { + /** + * Controls whether the task output is reveal in the user interface. + * Defaults to `RevealKind.Always`. + */ + reveal?: TaskRevealKind; + + /** + * Controls whether the command associated with the task is echoed + * in the user interface. + */ + echo?: boolean; + + /** Controls whether the panel showing the task output is taking focus. */ + focus?: boolean; + + /** + * Controls if the task panel is used for this task only (dedicated), + * shared between tasks (shared) or if a new panel is created on + * every task execution (new). Defaults to `TaskInstanceKind.Shared` + */ + panel?: TaskPanelKind; + + /** Controls whether to show the "Terminal will be reused by tasks, press any key to close it" message. */ + showReuseMessage?: boolean; + } + + export class Task { + + /** + * Creates a new task. + * + * @param definition The task definition. + * @param scope Specifies the task's scope. + * @param name The task's name. Is presented in the user interface. + * @param source The task's source (e.g. 'gulp', 'npm', ...). Is presented in the user interface. + * @param execution The process or shell execution. + * @param problemMatchers the names of problem matchers to use, like '$tsc' + * or '$eslint'. Problem matchers can be contributed by an extension using + * the `problemMatchers` extension point. + */ + constructor(taskDefinition: TaskDefinition, + scope: WorkspaceFolder | TaskScope.Global | TaskScope.Workspace, + name: string, + source?: string, + execution?: ProcessExecution | ShellExecution, + problemMatchers?: string | string[]); + + /** The task's name */ + name: string; + + /** The task's definition. */ + definition: TaskDefinition; + + /** The task's scope. */ + scope?: TaskScope.Global | TaskScope.Workspace | WorkspaceFolder; + + /** The task's execution engine */ + execution?: ProcessExecution | ShellExecution; + + /** Whether the task is a background task or not. */ + isBackground?: boolean; + + /** + * A human-readable string describing the source of this + * shell task, e.g. 'gulp' or 'npm'. + */ + source?: string; + + /** + * The task group this tasks belongs to. See TaskGroup + * for a predefined set of available groups. + * Defaults to undefined meaning that the task doesn't + * belong to any special group. + */ + group?: TaskGroup; + + /** The presentation options. Defaults to an empty literal. */ + presentationOptions?: TaskPresentationOptions; + + /** + * The problem matchers attached to the task. Defaults to an empty + * array. + */ + problemMatchers?: string[]; + } + + export interface TaskProvider { + /** + * Provides tasks. + * @param token A cancellation token. + * @return an array of tasks + */ + provideTasks(token?: CancellationToken): ProviderResult; + + /** + * Resolves a task that has no [`execution`](#Task.execution) set. Tasks are + * often created from information found in the `tasks.json`-file. Such tasks miss + * the information on how to execute them and a task provider must fill in + * the missing information in the `resolveTask`-method. + * + * @param task The task to resolve. + * @param token A cancellation token. + * @return The resolved task + */ + resolveTask(task: Task, token?: CancellationToken): ProviderResult; + } + + export namespace tasks { + + /** + * Register a task provider. + * + * @param type The task kind type this provider is registered for. + * @param provider A task provider. + * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + */ + export function registerTaskProvider(type: string, provider: TaskProvider): Disposable; + } }